Librarian = ZO_SortFilterList:Subclass() Librarian.defaults = {} ZO_CreateStringId("SI_BINDING_NAME_TOGGLE_LIBRARIAN", "Toggle Librarian") ZO_CreateStringId("SI_BINDING_NAME_RELOAD_UI", "Reload UI") ZO_CreateStringId("SI_WINDOW_TITLE_LIBRARIAN", "Librarian") ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_UNREAD", "Unread") ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_FOUND", "Found") ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_TITLE", "Title") ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_WORD_COUNT", "Words") ZO_CreateStringId("SI_LIBRARIAN_MARK_UNREAD", "Mark as Unread") ZO_CreateStringId("SI_LIBRARIAN_MARK_READ", "Mark as Read") ZO_CreateStringId("SI_LIBRARIAN_BOOK_COUNT", "%d Books") ZO_CreateStringId("SI_LIBRARIAN_UNREAD_COUNT", "%s (%d Unread)") ZO_CreateStringId("SI_LIBRARIAN_SHOW_ALL_BOOKS", "Show books for all characters") ZO_CreateStringId("SI_LIBRARIAN_NEW_BOOK_FOUND", "Book added to librarian") ZO_CreateStringId("SI_LIBRARIAN_NEW_BOOK_FOUND_WITH_TITLE", "Book added to librarian: %s") ZO_CreateStringId("SI_LIBRARIAN_FULLTEXT_SEARCH", "Full-text Search:") ZO_CreateStringId("SI_LIBRARIAN_SEARCH_HINT", "Enter text to search for.") ZO_CreateStringId("SI_LIBRARIAN_RELOAD_REMINDER", "ReloadUI suggested to update Librarian database.") ZO_CreateStringId("SI_LIBRARIAN_BACKUP_REMINDER", "Remember to backup your Librarian SavedVariables regularly. Look up Librarian on ESOUI for instructions.") ZO_CreateStringId("SI_LIBRARIAN_EMPTY_LIBRARY_IMPORT_PROMPT", [[It appears your Library is empty. Patch 1.3 fixed a bug in the storage of addon data which now needs to be moved back to the correct place. Please click the "Import from before patch" button in the Librarian setting menu to perform this migration. It is recommended that you backup your Librarian SavedVariables before performing this step as a precaution. Librarian settings can be opened by clicking the settings button in the top right of the window.]]) local SORT_ARROW_UP = "EsoUI/Art/Miscellaneous/list_sortUp.dds" local SORT_ARROW_DOWN = "EsoUI/Art/Miscellaneous/list_sortDown.dds" local LIBRARIAN_DATA = 1 local LIBRARIAN_SEARCH = 1 local ENTRY_SORT_KEYS = { ["title"] = { }, ["unread"] = { tiebreaker = "timeStamp" }, ["timeStamp"] = { tiebreaker = "title" }, ["wordCount"] = { tiebreaker = "title" } } function Librarian:New() local librarian = ZO_SortFilterList.New(self, LibrarianFrame) librarian:Initialise() return librarian end function Librarian:Initialise() self.masterList = {} self.newBookCount = 0 self.sortHeaderGroup:SelectHeaderByKey("timeStamp") ZO_ScrollList_AddDataType(self.list, LIBRARIAN_DATA, "LibrarianBookRow", 30, function(control, data) self:SetupBookRow(control, data) end) ZO_ScrollList_EnableHighlight(self.list, "ZO_ThinListHighlight") self.localSavedVars = ZO_SavedVars:New("Librarian_SavedVariables", 1, nil, self.defaults, nil) self.globalSavedVars = ZO_SavedVars:NewAccountWide("Librarian_SavedVariables", 1, nil, self.defaults, nil) if not self.globalSavedVars.settings then self.globalSavedVars.settings = {} end self.settings = self.globalSavedVars.settings if not self.globalSavedVars.books then self.globalSavedVars.books = {} end self.books = self.globalSavedVars.books if not self.localSavedVars.characterBooks then self.localSavedVars.characterBooks = {} end self.characterBooks = self.localSavedVars.characterBooks self.searchBox = GetControl(LibrarianFrame, "SearchBox") self.searchBox:SetHandler("OnTextChanged", function() self:OnSearchTextChanged() end) self.search = ZO_StringSearch:New() self.search:AddProcessor(LIBRARIAN_SEARCH, function(stringSearch, data, searchTerm, cache) return self:ProcessBookEntry(stringSearch, data, searchTerm, cache) end) self.sortFunction = function(listEntry1, listEntry2) return ZO_TableOrderingFunction(listEntry1.data, listEntry2.data, self.currentSortKey, ENTRY_SORT_KEYS, self.currentSortOrder) end self:UpdateSavedVariables() local settings = LibrarianSettings:New(self.settings) local function OnShowAllBooksClicked(checkButton, isChecked) self.settings.showAllBooks = isChecked self:RefreshFilters() end local function GetShowAllBooks() return self.settings.showAllBooks end local showAllBooks = LibrarianFrameShowAllBooks ZO_CheckButton_SetToggleFunction(showAllBooks, OnShowAllBooksClicked) ZO_CheckButton_SetCheckState(showAllBooks, GetShowAllBooks()) --self:ImportFromLoreLibrary() self:RefreshData() self:InitializeKeybindStripDescriptors() self:InitializeScene() self:AddLoreReaderUnreadToggle() end function Librarian:AddLoreReaderUnreadToggle() local readerKeybinds if LORE_READER.keybindStripDescriptor then readerKeybinds = LORE_READER.keybindStripDescriptor else readerKeybinds = LORE_READER.PCKeybindStripDescriptor end if readerKeybinds then -- Special exit button local exitKeybind = { alignment = KEYBIND_STRIP_ALIGN_RIGHT, name = GetString(SI_EXIT_BUTTON), keybind = "UI_SHORTCUT_EXIT", callback = function() SCENE_MANAGER:HideCurrentScene() end, } table.insert(readerKeybinds, exitKeybind) local toggleKeybind = { alignment = KEYBIND_STRIP_ALIGN_RIGHT, name = function() local book = self:FindBook(LORE_READER.titleText) if not book or book.unread then if self.settings.showUnreadIndicatorInReader then self.unreadIndicator:SetHidden(false) else self.unreadIndicator:SetHidden(true) end return GetString(SI_LIBRARIAN_MARK_READ) else self.unreadIndicator:SetHidden(true) return GetString(SI_LIBRARIAN_MARK_UNREAD) end end, keybind = "UI_SHORTCUT_SECONDARY", callback = function() local book = self:FindBook(LORE_READER.titleText) if not book then return end book.unread = not book.unread KEYBIND_STRIP:UpdateKeybindButtonGroup(readerKeybinds) self:RefreshData() end } table.insert(readerKeybinds, toggleKeybind) end self.unreadIndicator = WINDOW_MANAGER:CreateControl("LibrarianUnreadIndicator", ZO_LoreReaderBookContainer, CT_TEXTURE) self.unreadIndicator:SetAnchor(TOPLEFT, ZO_LoreReaderBookContainerFirstPage, TOPLEFT, -32, 3) self.unreadIndicator:SetDimensions(32, 32) self.unreadIndicator:SetHidden(true) self.unreadIndicator:SetTexture([[EsoUI/Art/Inventory/newitem_icon.dds]]) local function OnSceneStateChange(oldState, newState) if(newState == SCENE_SHOWING) then if KEYBIND_STRIP.defaultExit then KEYBIND_STRIP:RemoveKeybindButton(KEYBIND_STRIP.defaultExit) else KEYBIND_STRIP:RemoveDefaultExit() end KEYBIND_STRIP:AddKeybindButtonGroup(readerKeybinds) elseif(newState == SCENE_HIDDEN) then KEYBIND_STRIP:RemoveKeybindButtonGroup(readerKeybinds) if KEYBIND_STRIP.defaultExit then KEYBIND_STRIP:AddKeybindButton(KEYBIND_STRIP.defaultExit) else KEYBIND_STRIP:RestoreDefaultExit() end end end LORE_READER_INTERACTION_SCENE.callbackRegistry.StateChange[1][3] = true LORE_READER_INTERACTION_SCENE:Clean("StateChange") LORE_READER_INTERACTION_SCENE:RegisterCallback("StateChange", OnSceneStateChange) LORE_READER_INVENTORY_SCENE.callbackRegistry.StateChange[1][3] = true LORE_READER_INVENTORY_SCENE:Clean("StateChange") LORE_READER_INVENTORY_SCENE:RegisterCallback("StateChange", OnSceneStateChange) LORE_READER_LORE_LIBRARY_SCENE.callbackRegistry.StateChange[1][3] = true LORE_READER_LORE_LIBRARY_SCENE:Clean("StateChange") LORE_READER_LORE_LIBRARY_SCENE:RegisterCallback("StateChange", OnSceneStateChange) end function Librarian:UpdateSavedVariables() -- Version 1.0.4 - Settings moved to global variables. if self.localSavedVars.setting_time_format then self.globalSavedVars.settings.time_format = self.localSavedVars.setting_time_format self.localSavedVars.setting_time_format = nil end -- Version 1.0.4 - Book data moved to global variables if self.localSavedVars.books then for _,book in ipairs(self.localSavedVars.books) do local timeStamp = book.timeStamp local unread = book.unread self:AddBook(book) local characterBook = self:FindCharacterBook(book.title) if characterBook then characterBook.timeStamp = timeStamp end local globalBook = self:FindBook(book.title) if globalBook then globalBook.timeStamp = timeStamp globalBook.unread = unread end end self.localSavedVars.books = nil self:RefreshData() end -- Version 1.0.16 - Fixed a couple of settings names. if self.globalSavedVars.settings.alert_style then self.globalSavedVars.settings.alertStyle = self.globalSavedVars.settings.alert_style self.globalSavedVars.settings.alert_style = nil end if self.globalSavedVars.settings.time_format then self.globalSavedVars.settings.timeFormat = self.globalSavedVars.settings.time_format self.globalSavedVars.settings.time_format = nil end -- Version 1.1.3 - SavedVariable hell! for _,account in pairs(Librarian_SavedVariables["Default"]) do for _,book in pairs(account["$AccountWide"].books) do if type(book.body) == "string" then local newBody = book.body book.body = {} while string.len(newBody) > 1024 do table.insert(book.body, string.sub(newBody, 0, 1024)) newBody = string.sub(newBody, 1025) end table.insert(book.body, newBody) end end end end function Librarian:InitializeKeybindStripDescriptors() self.keybindStripDescriptor = { { alignment = KEYBIND_STRIP_ALIGN_RIGHT, name = GetString(SI_LORE_LIBRARY_READ), keybind = "UI_SHORTCUT_PRIMARY", visible = function() return self.mouseOverRow end, callback = function() self:ReadBook(self.mouseOverRow.data.title) end, }, { alignment = KEYBIND_STRIP_ALIGN_RIGHT, name = function() if not self.mouseOverRow then return nil end local book = self:FindBook(self.mouseOverRow.data.title) if book.unread then return GetString(SI_LIBRARIAN_MARK_READ) else return GetString(SI_LIBRARIAN_MARK_UNREAD) end end, keybind = "UI_SHORTCUT_SECONDARY", visible = function() return self.mouseOverRow end, callback = function() local book = self:FindBook(self.mouseOverRow.data.title) book.unread = not book.unread self:RefreshData() end, } } end function Librarian:InitializeScene() if not LIBRARIAN_SCENE then LIBRARIAN_TITLE_FRAGMENT = ZO_SetTitleFragment:New(SI_WINDOW_TITLE_LIBRARIAN) LIBRARIAN_SCENE = ZO_Scene:New("librarian", SCENE_MANAGER) LIBRARIAN_SCENE:AddFragmentGroup(FRAGMENT_GROUP.MOUSE_DRIVEN_UI_WINDOW) if self.settings.enableCharacterSpin then LIBRARIAN_SCENE:AddFragmentGroup(FRAGMENT_GROUP.FRAME_TARGET_STANDARD_RIGHT_PANEL) end LIBRARIAN_SCENE:AddFragment(ZO_FadeSceneFragment:New(LibrarianFrame)) LIBRARIAN_SCENE:AddFragment(RIGHT_BG_FRAGMENT) LIBRARIAN_SCENE:AddFragment(TITLE_FRAGMENT) LIBRARIAN_SCENE:AddFragment(LIBRARIAN_TITLE_FRAGMENT) LIBRARIAN_SCENE:AddFragment(CODEX_WINDOW_SOUNDS) LIBRARIAN_SCENE:RegisterCallback("StateChange", function(oldState, newState) if(newState == SCENE_SHOWING) then KEYBIND_STRIP:AddKeybindButtonGroup(self.keybindStripDescriptor) elseif(newState == SCENE_HIDDEN) then KEYBIND_STRIP:RemoveKeybindButtonGroup(self.keybindStripDescriptor) end end) end end function Librarian:ImportFromLoreLibrary() local hasImportedBooks = false local chatEnabled = self.settings.chatEnabled local alertEnabled = self.settings.alertEnabled self.settings.chatEnabled = true self.settings.alertEnabled = false local identifier = "LibrarianLoreImport" local GetLoreBookInfo, ReadLoreBook = GetLoreBookInfo, ReadLoreBook local em = GetEventManager() local function endSearch() em:UnregisterForUpdate(identifier) self.settings.chatEnabled = chatEnabled self.settings.alertEnabled = alertEnabled end local categoryIndex, collectionIndex, bookIndex = 0, 0, 0 local numCollections, totalBooks = 0, 0 local function step() if bookIndex >= totalBooks then if collectionIndex >= numCollections then if categoryIndex >= GetNumLoreCategories() then endSearch() return false else categoryIndex = categoryIndex + 1 numCollections = select(2, GetLoreCategoryInfo(categoryIndex)) collectionIndex = 0 end end collectionIndex = collectionIndex + 1 totalBooks = select(4, GetLoreCollectionInfo(categoryIndex, collectionIndex)) bookIndex = 0 end bookIndex = bookIndex + 1 local title, icon, known = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex) if known then if not self:FindCharacterBook(title) then local body, medium, showTitle = ReadLoreBook(categoryIndex, collectionIndex, bookIndex) local book = { title = title, body = body, medium = medium, showTitle = showTitle } self:AddBook(book) end end return true end local function steps() -- do as much as possible in 5ms local gettime = GetGameTimeMilliseconds local start = gettime() while true do if ((gettime() - start) >= 5) or not step() then break end end end em:RegisterForUpdate(identifier, 0, steps) end function Librarian:CanImportFromEmptyAccount() return Librarian_SavedVariables["Default"][""] ~= nil end function Librarian:ImportFromEmptyAccount() if Librarian_SavedVariables["Default"][""] ~= nil then Librarian_SavedVariables["Default"][GetDisplayName()] = Librarian_SavedVariables["Default"][""] end Librarian_SavedVariables["Default"][""] = nil SLASH_COMMANDS["/reloadui"]() end function Librarian:BuildMasterList() if self:CanImportFromEmptyAccount() and #self.books == 0 then LibrarianFrameMessage:SetHidden(false) end for i, book in ipairs(self.books) do local data = {} for k,v in pairs(book) do if k == "body" then data[k] = table.concat(book.body) else data[k] = v end end data.type = LIBRARIAN_SEARCH local characterBook = self:FindCharacterBook(book.title) if characterBook then data.seenByCurrentCharacter = true data.timeStamp = characterBook.timeStamp else data.seenByCurrentCharacter = false data.timeStamp = book.timeStamp end self.masterList[i] = data end end function Librarian:FilterScrollList() local scrollData = ZO_ScrollList_GetDataList(self.list) ZO_ClearNumericallyIndexedTable(scrollData) local bookCount = 0 local unreadCount = 0 local searchTerm = self.searchBox:GetText() for i = 1, #self.masterList do local data = self.masterList[i] if self.settings.showAllBooks or data.seenByCurrentCharacter then if(searchTerm == "" or self.search:IsMatch(searchTerm, data)) then table.insert(scrollData, ZO_ScrollList_CreateDataEntry(LIBRARIAN_DATA, data)) bookCount = bookCount + 1 if data.unread then unreadCount = unreadCount + 1 end end end end local message = string.format(GetString(SI_LIBRARIAN_BOOK_COUNT), bookCount) if unreadCount > 0 then message = string.format(GetString(SI_LIBRARIAN_UNREAD_COUNT), message, unreadCount) end LibrarianFrameBookCount:SetText(message) end function Librarian:SortScrollList() local scrollData = ZO_ScrollList_GetDataList(self.list) table.sort(scrollData, self.sortFunction) end function Librarian:SetupBookRow(control, data) control.data = data control.unread = GetControl(control, "Unread") control.found = GetControl(control, "Found") control.title = GetControl(control, "Title") control.wordCount = GetControl(control, "WordCount") control.unread.nonRecolorable = true if data.unread then control.unread:SetAlpha(1) else control.unread:SetAlpha(0) end control.found.normalColor = ZO_NORMAL_TEXT control.found:SetText(self:FormatClockTime(data.timeStamp)) control.title.normalColor = ZO_NORMAL_TEXT control.title:SetText(data.title) control.wordCount.normalColor = ZO_NORMAL_TEXT control.wordCount:SetText(data.wordCount) ZO_SortFilterList.SetupRow(self, control, data) end function Librarian:ProcessBookEntry(stringSearch, data, searchTerm, cache) local lowerSearchTerm = searchTerm:lower() if data.title and zo_plainstrfind(data.title:lower(), lowerSearchTerm) then return true end if data.body and zo_plainstrfind(data.body:lower(), lowerSearchTerm) then return true end return false end function Librarian:FindCharacterBook(title) if not self.characterBooks then return nil end for _,book in pairs(self.characterBooks) do if book.title == title then return book end end end function Librarian:FindBook(title) for _,book in pairs(self.books) do if book.title == title then return book end end end function Librarian:AddBook(book) if not self:FindCharacterBook(book.title) then if not self:FindBook(book.title) then book.timeStamp = GetTimeStamp() book.unread = true local wordCount = 0 for w in book.body:gmatch("%S+") do wordCount = wordCount + 1 end book.wordCount = wordCount local newBody = book.body book.body = {} while string.len(newBody) > 1024 do table.insert(book.body, string.sub(newBody, 0, 1024)) newBody = string.sub(newBody, 1025) end table.insert(book.body, newBody) table.insert(self.books, book) end local characterBook = { title = book.title, timeStamp = GetTimeStamp() } table.insert(self.characterBooks, characterBook) self:RefreshData() if self.settings.alertEnabled then CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, CSA_EVENT_LARGE_TEXT, SOUNDS.BOOK_ACQUIRED, GetString(SI_LIBRARIAN_NEW_BOOK_FOUND)) end if self.settings.chatEnabled then d(string.format(GetString(SI_LIBRARIAN_NEW_BOOK_FOUND_WITH_TITLE), book.title)) end self.newBookCount = self.newBookCount + 1 if self.settings.reloadReminderBookCount and self.settings.reloadReminderBookCount > 0 and self.settings.reloadReminderBookCount <= self.newBookCount then d(GetString(SI_LIBRARIAN_RELOAD_REMINDER)) end end end function Librarian:Toggle() if LibrarianFrame:IsControlHidden() then SCENE_MANAGER:Show("librarian") else SCENE_MANAGER:Hide("librarian") end end function Librarian:ReadBook(title) local book = self:FindBook(title) LORE_READER:SetupBook(book.title, book.body and table.concat(book.body), book.medium, book.showTitle) SCENE_MANAGER:Push("loreReaderInteraction") PlaySound(LORE_READER.OpenSound) end function Librarian:FormatClockTime(time) local midnightSeconds = GetSecondsSinceMidnight() local utcSeconds = GetTimeStamp() % 86400 local offset = midnightSeconds - utcSeconds if offset < -43200 then offset = offset + 86400 end local dateString = GetDateStringFromTimestamp(time) local timeString = ZO_FormatTime((time + offset) % 86400, TIME_FORMAT_STYLE_CLOCK_TIME, self.settings.timeFormat) return string.format("%s %s", dateString, timeString) end function Librarian:OnSearchTextChanged() ZO_EditDefaultText_OnTextChanged(self.searchBox) self:RefreshFilters() end local function SlashCommand(args) Librarian:Toggle() end local function OnAddonLoaded(event, addon) if addon == "Librarian" then LIBRARIAN = Librarian:New() end end local function OnShowBook(eventCode, title, body, medium, showTitle) local book = { title = title, body = body, medium = medium, showTitle = showTitle } LIBRARIAN:AddBook(book) end function LibrarianRow_OnMouseEnter(control) LIBRARIAN:Row_OnMouseEnter(control) end function LibrarianRow_OnMouseExit(control) LIBRARIAN:Row_OnMouseExit(control) end function LibrarianRow_OnMouseUp(control, button, upInside) LIBRARIAN:ReadBook(control.data.title) end SLASH_COMMANDS["/librarian"] = SlashCommand EVENT_MANAGER:RegisterForEvent("Librarian", EVENT_ADD_ON_LOADED, OnAddonLoaded) EVENT_MANAGER:RegisterForEvent("Librarian", EVENT_SHOW_BOOK, OnShowBook)