Split Librarian.lua into 3 files

Orionik [09-25-22 - 01:52]
Split Librarian.lua into 3 files
- ESOUIModification.lua contains the code which modifies the LoreLibrary and LoreReader
- LibrarianData.lua contains the code which takes care of adding/removing books to the data (and the save)
- LibrarianUI.lua conatins the code that handle Librarian UI
- Librarian.lua contains some generic code Initialization flow, callbacks...
Filename
ESOUIModifications.lua
Librarian.lua
Librarian.txt
LibrarianData.lua
LibrarianUI.lua
diff --git a/ESOUIModifications.lua b/ESOUIModifications.lua
new file mode 100644
index 0000000..f256c03
--- /dev/null
+++ b/ESOUIModifications.lua
@@ -0,0 +1,229 @@
+
+function Librarian:AddLoreReaderUnreadToggle()
+    local function IsSameBook(book, title)
+        if book.title then
+            return book.title == title
+        end
+        local categoryIndex, collectionIndex, bookIndex = GetLoreBookIndicesFromBookId(book.bookId)
+        local bookTitle = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
+        return bookTitle == title
+    end
+
+    self.loreReaderKeybinds =
+    {
+        {
+            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
+            name = function()
+                local book = self:FindBook(self.lastShownBookId)
+                if not book or not IsSameBook(book, LORE_READER.titleText) then
+                    self.loreReaderUnreadIndicator:SetHidden(true)
+                    return ""
+                else
+                    if book.unread then
+                        if self.settings.showUnreadIndicatorInReader then
+                            self.loreReaderUnreadIndicator:SetHidden(false)
+                        else
+                            self.loreReaderUnreadIndicator:SetHidden(true)
+                        end
+                        return GetString(LIBRARIAN_MARK_READ)
+                    else
+                        self.loreReaderUnreadIndicator:SetHidden(true)
+                        return GetString(LIBRARIAN_MARK_UNREAD)
+                    end
+                end
+            end,
+            visible = function()
+                local book = self:FindBook(self.lastShownBookId)
+                return book and IsSameBook(book, LORE_READER.titleText)
+            end,
+            keybind = "UI_SHORTCUT_SECONDARY",
+            callback = function()
+                local book = self:FindBook(self.lastShownBookId)
+                if book and IsSameBook(book, LORE_READER.titleText) then
+                    self:ToggleReadBook(book)
+                    KEYBIND_STRIP:UpdateKeybindButtonGroup(self.loreReaderKeybinds)
+                end
+            end
+        }
+    }
+
+    local function OnSceneStateChange(oldState, newState)
+        -- Let's not add the keybind at all if this book is not registered in librarian
+        -- This way, other addon (like Librarium) can override the same keybind for something else
+        local book = self:FindBook(self.lastShownBookId)
+        if not book or not IsSameBook(book, LORE_READER.titleText) then
+            self.loreReaderUnreadIndicator:SetHidden(true)
+            return
+        end
+
+        if newState == SCENE_SHOWING then
+            KEYBIND_STRIP:AddKeybindButtonGroup(self.loreReaderKeybinds)
+        elseif newState == SCENE_HIDDEN then
+            KEYBIND_STRIP:RemoveKeybindButtonGroup(self.loreReaderKeybinds)
+        end
+    end
+
+    -- This list of scene should be the same as the list in esoui/ingame/lorereader/lorereader.lua in its Initialize function
+    LORE_READER_INVENTORY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
+    LORE_READER_LORE_LIBRARY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
+    LORE_READER_INTERACTION_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
+    GAMEPAD_LORE_READER_INVENTORY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
+    GAMEPAD_LORE_READER_LORE_LIBRARY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
+    GAMEPAD_LORE_READER_INTERACTION_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
+
+    self.loreReaderUnreadIndicator = WINDOW_MANAGER:CreateControl("LibrarianLoreReaderUnreadIndicator", ZO_LoreReaderBookContainer, CT_TEXTURE)
+    self.loreReaderUnreadIndicator:SetAnchor(TOPLEFT, ZO_LoreReaderBookContainerFirstPage, TOPLEFT, -32, 3)
+    self.loreReaderUnreadIndicator:SetAlpha(self.settings.unreadIndicatorTransparency)
+    self.loreReaderUnreadIndicator:SetDimensions(32, 32)
+    self.loreReaderUnreadIndicator:SetHidden(true)
+    self.loreReaderUnreadIndicator:SetTexture("esoui/art/miscellaneous/new_icon.dds")
+end
+
+function Librarian:AddLoreLibraryIcons()
+    if not self.settings.showUnreadIndicatorInLoreLibrary then
+        return
+    end
+
+    -- ADD UNREAD ICON ON EACH UNREAD ENTRY
+    local BOOK_DATA_TYPE = 1
+    local scrollList = LORE_LIBRARY.list:GetListControl()
+    local initalDataType = scrollList.dataTypes[BOOK_DATA_TYPE]
+    scrollList.dataTypes[BOOK_DATA_TYPE] = nil
+
+    local function SetUpBookEntry(control, data)
+        initalDataType.setupCallback(control, data)
+
+        local _, _, known, bookId = GetLoreBookInfo(data.categoryIndex, data.collectionIndex, data.bookIndex)
+        control.bookId = bookId
+        local shouldUnreadIconBeHidden = true
+        if known then
+            local book = self:FindBook(bookId)
+            if not book or book.unread then
+                shouldUnreadIconBeHidden = false
+            end
+        end
+        control:GetNamedChild("UnreadIcon"):SetHidden(shouldUnreadIconBeHidden)
+    end
+
+    ZO_ScrollList_AddDataType(scrollList, BOOK_DATA_TYPE, "Librarian_LoreLibrary_BookEntry", initalDataType.height, SetUpBookEntry)
+
+    -- WHEN READING BOOK FROM LORE LIBRARY, UPDATE LASTSHOWNBOOKID
+    local initial_ZO_LoreLibrary_ReadBook = ZO_LoreLibrary_ReadBook
+    ZO_LoreLibrary_ReadBook = function(categoryIndex, collectionIndex, bookIndex)
+        self.lastShownBookId = select(4, GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex))
+        initial_ZO_LoreLibrary_ReadBook(categoryIndex, collectionIndex, bookIndex)
+    end
+
+    -- ORDER BOOK LIST TO MAKE UNREAD BOOK FIRST
+    local function BookEntryComparator(leftScrollData, rightScrollData)
+        local leftData = leftScrollData.data
+        local rightData = rightScrollData.data
+        local leftTitle, _, leftKnown, leftBookId = GetLoreBookInfo(leftData.categoryIndex, leftData.collectionIndex, leftData.bookIndex)
+        local rightTitle, _, rightKnown, rightBookId = GetLoreBookInfo(rightData.categoryIndex, rightData.collectionIndex, rightData.bookIndex)
+
+        if leftKnown == rightKnown then
+            if leftKnown then
+                local leftBook = self:FindBook(leftBookId)
+                local rightBook = self:FindBook(rightBookId)
+                if (leftBook and not leftBook.unread) == (rightBook and not rightBook.unread) then
+                    return leftTitle < rightTitle
+                end
+
+                return leftBook and leftBook.unread
+            end
+        end
+
+        return leftKnown
+    end
+    LORE_LIBRARY.list.SortScrollList = function(loreLibrarySelf)
+        local scrollData = ZO_ScrollList_GetDataList(loreLibrarySelf.list)
+        table.sort(scrollData, BookEntryComparator)
+    end
+
+    -- ADD KEYBIND TO MARK BOOK AS READ FROM LORE LIBRARY
+    local loreLibraryKeybinds = LORE_LIBRARY.keybindStripDescriptor
+
+    -- Make current secondary tertiary
+    for i, keybindDescriptor in ipairs(loreLibraryKeybinds) do
+        if keybindDescriptor.keybind == "UI_SHORTCUT_SECONDARY" then
+            keybindDescriptor.keybind = "UI_SHORTCUT_TERTIARY"
+        end
+    end
+
+    local toggleKeybind =
+    {
+        alignment = KEYBIND_STRIP_ALIGN_RIGHT,
+        name = function()
+            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
+            if selectedRow and selectedRow.known then
+                local book = self:FindBook(selectedRow.bookId)
+                if not book or book.unread then
+                    return GetString(LIBRARIAN_MARK_READ)
+                else
+                    return GetString(LIBRARIAN_MARK_UNREAD)
+                end
+            end
+        end,
+        keybind = "UI_SHORTCUT_SECONDARY",
+        visible = function()
+            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
+            return selectedRow and selectedRow.known
+        end,
+        callback = function()
+            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
+            if selectedRow then
+                local book = self:FindBook(selectedRow.bookId)
+                if not book then
+                    book = { bookId = selectedRow.bookId, title = selectedRow.text:GetText() }
+                    self:AddBook(book, true)
+                end
+
+                if book then
+                    self:ToggleReadBook(book)
+                    KEYBIND_STRIP:UpdateKeybindButtonGroup(loreLibraryKeybinds)
+                end
+            end
+        end
+    }
+    table.insert(loreLibraryKeybinds, toggleKeybind)
+
+    -- ADD UNREAD ICON ON COLLECTION IF IT CONTAINS AN UNREAD BOOK OR ADD COMPLETED MARK IF ALL ARE KNOWN AND READ
+    local navigationEntryTemplateInfo = LORE_LIBRARY.navigationTree.templateInfo["ZO_LoreLibraryNavigationEntry"]
+    local previousSetupFunction = navigationEntryTemplateInfo.setupFunction
+
+    local function TreeEntrySetup(node, control, data, open)
+        previousSetupFunction(node, control, data, open)
+
+        local shouldHideIcon = true
+        local statusIcon = control:GetNamedChild("StatusIcon")
+        if data.numKnownBooks > 0 then
+            if self.unreadPerCollections[data.categoryIndex] and
+                self.unreadPerCollections[data.categoryIndex][data.collectionIndex] and
+                self.unreadPerCollections[data.categoryIndex][data.collectionIndex] > 0 then
+                shouldHideIcon = false
+                statusIcon:SetTexture("esoui/art/miscellaneous/new_icon.dds")
+            elseif data.numKnownBooks == data.totalBooks then
+                shouldHideIcon = false
+                statusIcon:SetTexture("esoui/art/miscellaneous/check.dds")
+            end
+        end
+        statusIcon:SetHidden(shouldHideIcon)
+    end
+
+    navigationEntryTemplateInfo.setupFunction = TreeEntrySetup
+    navigationEntryTemplateInfo.template = "Librarian_LoreLibraryNavigationEntry"
+    navigationEntryTemplateInfo.objectPool.templateName = "Librarian_LoreLibraryNavigationEntry"
+
+    -- Fix of ESO because a label header is equal to all of its entries with the current code (API 101033)
+    LORE_LIBRARY.navigationTree.templateInfo["ZO_LabelHeader"].equalityFunction = navigationEntryTemplateInfo.equalityFunction
+end
+
+function Librarian:RefreshLoreLibraryData()
+    if self.settings.showUnreadIndicatorInLoreLibrary then
+        local selectedCategoryIndex = LORE_LIBRARY:GetSelectedCategoryIndex()
+        local selectedCollectionIndex = LORE_LIBRARY:GetSelectedCollectionIndex()
+        local selectedCollectionId = select(7, GetLoreCollectionInfo(selectedCategoryIndex, selectedCollectionIndex))
+        LORE_LIBRARY:SetCollectionIdToSelect(selectedCollectionId)
+        LORE_LIBRARY:BuildCategoryList()
+    end
+end
diff --git a/Librarian.lua b/Librarian.lua
index 19126f9..b882729 100644
--- a/Librarian.lua
+++ b/Librarian.lua
@@ -9,18 +9,18 @@ Librarian.menuSettings = {
 }
 Librarian.slashCommandText = "/librarian"

-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
+Librarian.constants = {
+    LIBRARIAN_DATA = 1,
+    LIBRARIAN_SEARCH = 1,

-local ENTRY_SORT_KEYS =
-{
-    ["title"] = { },
-    ["unread"] = { tiebreaker = "timeStamp" },
-    ["timeStamp"] = { tiebreaker = "title" },
-    ["category"] = { tiebreaker = "title" },
-    ["wordCount"] = { tiebreaker = "title" }
+    ENTRY_SORT_KEYS =
+    {
+        ["title"] = { },
+        ["unread"] = { tiebreaker = "timeStamp" },
+        ["timeStamp"] = { tiebreaker = "title" },
+        ["category"] = { tiebreaker = "title" },
+        ["wordCount"] = { tiebreaker = "title" }
+    }
 }

 ESO_Dialogs["LIBRARIAN_DELETE_BOOK"] =
@@ -63,7 +63,7 @@ function Librarian:Initialize(...)
     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_AddDataType(self.list, self.constants.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)
@@ -94,9 +94,9 @@ function Librarian:Initialize(...)
     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.search:AddProcessor(self.constants.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.sortFunction = function(listEntry1, listEntry2) return ZO_TableOrderingFunction(listEntry1.data, listEntry2.data, self.currentSortKey, self.constants.ENTRY_SORT_KEYS, self.currentSortOrder) end

     LibrarianDeprecation:UpdateSavedVariables(self)

@@ -141,683 +141,6 @@ function Librarian:Initialize(...)
     self:ImportFromLoreLibrary()
 end

-function Librarian:AddLoreReaderUnreadToggle()
-    local function IsSameBook(book, title)
-        if book.title then
-            return book.title == title
-        end
-        local categoryIndex, collectionIndex, bookIndex = GetLoreBookIndicesFromBookId(book.bookId)
-        local bookTitle = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
-        return bookTitle == title
-    end
-
-    self.loreReaderKeybinds =
-    {
-        {
-            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
-            name = function()
-                local book = self:FindBook(self.lastShownBookId)
-                if not book or not IsSameBook(book, LORE_READER.titleText) then
-                    self.loreReaderUnreadIndicator:SetHidden(true)
-                    return ""
-                else
-                    if book.unread then
-                        if self.settings.showUnreadIndicatorInReader then
-                            self.loreReaderUnreadIndicator:SetHidden(false)
-                        else
-                            self.loreReaderUnreadIndicator:SetHidden(true)
-                        end
-                        return GetString(LIBRARIAN_MARK_READ)
-                    else
-                        self.loreReaderUnreadIndicator:SetHidden(true)
-                        return GetString(LIBRARIAN_MARK_UNREAD)
-                    end
-                end
-            end,
-            visible = function()
-                local book = self:FindBook(self.lastShownBookId)
-                return book and IsSameBook(book, LORE_READER.titleText)
-            end,
-            keybind = "UI_SHORTCUT_SECONDARY",
-            callback = function()
-                local book = self:FindBook(self.lastShownBookId)
-                if book and IsSameBook(book, LORE_READER.titleText) then
-                    self:ToggleReadBook(book)
-                    KEYBIND_STRIP:UpdateKeybindButtonGroup(self.loreReaderKeybinds)
-                end
-            end
-        }
-    }
-
-    local function OnSceneStateChange(oldState, newState)
-        -- Let's not add the keybind at all if this book is not registered in librarian
-        -- This way, other addon (like Librarium) can override the same keybind for something else
-        local book = self:FindBook(self.lastShownBookId)
-        if not book or not IsSameBook(book, LORE_READER.titleText) then
-            self.loreReaderUnreadIndicator:SetHidden(true)
-            return
-        end
-
-        if newState == SCENE_SHOWING then
-            KEYBIND_STRIP:AddKeybindButtonGroup(self.loreReaderKeybinds)
-        elseif newState == SCENE_HIDDEN then
-            KEYBIND_STRIP:RemoveKeybindButtonGroup(self.loreReaderKeybinds)
-        end
-    end
-
-    -- This list of scene should be the same as the list in esoui/ingame/lorereader/lorereader.lua in its Initialize function
-    LORE_READER_INVENTORY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
-    LORE_READER_LORE_LIBRARY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
-    LORE_READER_INTERACTION_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
-    GAMEPAD_LORE_READER_INVENTORY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
-    GAMEPAD_LORE_READER_LORE_LIBRARY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
-    GAMEPAD_LORE_READER_INTERACTION_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
-
-    self.loreReaderUnreadIndicator = WINDOW_MANAGER:CreateControl("LibrarianLoreReaderUnreadIndicator", ZO_LoreReaderBookContainer, CT_TEXTURE)
-    self.loreReaderUnreadIndicator:SetAnchor(TOPLEFT, ZO_LoreReaderBookContainerFirstPage, TOPLEFT, -32, 3)
-    self.loreReaderUnreadIndicator:SetAlpha(self.settings.unreadIndicatorTransparency)
-    self.loreReaderUnreadIndicator:SetDimensions(32, 32)
-    self.loreReaderUnreadIndicator:SetHidden(true)
-    self.loreReaderUnreadIndicator:SetTexture("esoui/art/miscellaneous/new_icon.dds")
-end
-
-function Librarian:AddLoreLibraryIcons()
-    if not self.settings.showUnreadIndicatorInLoreLibrary then
-        return
-    end
-
-    -- ADD UNREAD ICON ON EACH UNREAD ENTRY
-    local BOOK_DATA_TYPE = 1
-    local scrollList = LORE_LIBRARY.list:GetListControl()
-    local initalDataType = scrollList.dataTypes[BOOK_DATA_TYPE]
-    scrollList.dataTypes[BOOK_DATA_TYPE] = nil
-
-    local function SetUpBookEntry(control, data)
-        initalDataType.setupCallback(control, data)
-
-        local _, _, known, bookId = GetLoreBookInfo(data.categoryIndex, data.collectionIndex, data.bookIndex)
-        control.bookId = bookId
-        local shouldUnreadIconBeHidden = true
-        if known then
-            local book = self:FindBook(bookId)
-            if not book or book.unread then
-                shouldUnreadIconBeHidden = false
-            end
-        end
-        control:GetNamedChild("UnreadIcon"):SetHidden(shouldUnreadIconBeHidden)
-    end
-
-    ZO_ScrollList_AddDataType(scrollList, BOOK_DATA_TYPE, "Librarian_LoreLibrary_BookEntry", initalDataType.height, SetUpBookEntry)
-
-    -- WHEN READING BOOK FROM LORE LIBRARY, UPDATE LASTSHOWNBOOKID
-    local initial_ZO_LoreLibrary_ReadBook = ZO_LoreLibrary_ReadBook
-    ZO_LoreLibrary_ReadBook = function(categoryIndex, collectionIndex, bookIndex)
-        self.lastShownBookId = select(4, GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex))
-        initial_ZO_LoreLibrary_ReadBook(categoryIndex, collectionIndex, bookIndex)
-    end
-
-    -- ORDER BOOK LIST TO MAKE UNREAD BOOK FIRST
-    local function BookEntryComparator(leftScrollData, rightScrollData)
-        local leftData = leftScrollData.data
-        local rightData = rightScrollData.data
-        local leftTitle, _, leftKnown, leftBookId = GetLoreBookInfo(leftData.categoryIndex, leftData.collectionIndex, leftData.bookIndex)
-        local rightTitle, _, rightKnown, rightBookId = GetLoreBookInfo(rightData.categoryIndex, rightData.collectionIndex, rightData.bookIndex)
-
-        if leftKnown == rightKnown then
-            if leftKnown then
-                local leftBook = self:FindBook(leftBookId)
-                local rightBook = self:FindBook(rightBookId)
-                if (leftBook and not leftBook.unread) == (rightBook and not rightBook.unread) then
-                    return leftTitle < rightTitle
-                end
-
-                return leftBook and leftBook.unread
-            end
-        end
-
-        return leftKnown
-    end
-    LORE_LIBRARY.list.SortScrollList = function(loreLibrarySelf)
-        local scrollData = ZO_ScrollList_GetDataList(loreLibrarySelf.list)
-        table.sort(scrollData, BookEntryComparator)
-    end
-
-    -- ADD KEYBIND TO MARK BOOK AS READ FROM LORE LIBRARY
-    local loreLibraryKeybinds = LORE_LIBRARY.keybindStripDescriptor
-
-    -- Make current secondary tertiary
-    for i, keybindDescriptor in ipairs(loreLibraryKeybinds) do
-        if keybindDescriptor.keybind == "UI_SHORTCUT_SECONDARY" then
-            keybindDescriptor.keybind = "UI_SHORTCUT_TERTIARY"
-        end
-    end
-
-    local toggleKeybind =
-    {
-        alignment = KEYBIND_STRIP_ALIGN_RIGHT,
-        name = function()
-            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
-            if selectedRow and selectedRow.known then
-                local book = self:FindBook(selectedRow.bookId)
-                if not book or book.unread then
-                    return GetString(LIBRARIAN_MARK_READ)
-                else
-                    return GetString(LIBRARIAN_MARK_UNREAD)
-                end
-            end
-        end,
-        keybind = "UI_SHORTCUT_SECONDARY",
-        visible = function()
-            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
-            return selectedRow and selectedRow.known
-        end,
-        callback = function()
-            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
-            if selectedRow then
-                local book = self:FindBook(selectedRow.bookId)
-                if not book then
-                    book = { bookId = selectedRow.bookId, title = selectedRow.text:GetText() }
-                    self:AddBook(book, true)
-                end
-
-                if book then
-                    self:ToggleReadBook(book)
-                    KEYBIND_STRIP:UpdateKeybindButtonGroup(loreLibraryKeybinds)
-                end
-            end
-        end
-    }
-    table.insert(loreLibraryKeybinds, toggleKeybind)
-
-    -- ADD UNREAD ICON ON COLLECTION IF IT CONTAINS AN UNREAD BOOK OR ADD COMPLETED MARK IF ALL ARE KNOWN AND READ
-    local navigationEntryTemplateInfo = LORE_LIBRARY.navigationTree.templateInfo["ZO_LoreLibraryNavigationEntry"]
-    local previousSetupFunction = navigationEntryTemplateInfo.setupFunction
-
-    local function TreeEntrySetup(node, control, data, open)
-        previousSetupFunction(node, control, data, open)
-
-        local shouldHideIcon = true
-        local statusIcon = control:GetNamedChild("StatusIcon")
-        if data.numKnownBooks > 0 then
-            if self.unreadPerCollections[data.categoryIndex] and
-                self.unreadPerCollections[data.categoryIndex][data.collectionIndex] and
-                self.unreadPerCollections[data.categoryIndex][data.collectionIndex] > 0 then
-                shouldHideIcon = false
-                statusIcon:SetTexture("esoui/art/miscellaneous/new_icon.dds")
-            elseif data.numKnownBooks == data.totalBooks then
-                shouldHideIcon = false
-                statusIcon:SetTexture("esoui/art/miscellaneous/check.dds")
-            end
-        end
-        statusIcon:SetHidden(shouldHideIcon)
-    end
-
-    navigationEntryTemplateInfo.setupFunction = TreeEntrySetup
-    navigationEntryTemplateInfo.template = "Librarian_LoreLibraryNavigationEntry"
-    navigationEntryTemplateInfo.objectPool.templateName = "Librarian_LoreLibraryNavigationEntry"
-
-    -- Fix of ESO because a label header is equal to all of its entries with the current code (API 101033)
-    LORE_LIBRARY.navigationTree.templateInfo["ZO_LabelHeader"].equalityFunction = navigationEntryTemplateInfo.equalityFunction
-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)
-            end,
-        },
-        {
-            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
-            name = function()
-                if not self.mouseOverRow then return nil end
-                local book = self:FindBook(self.mouseOverRow.data.bookId)
-                if book.unread then
-                    return GetString(LIBRARIAN_MARK_READ)
-                else
-                    return GetString(LIBRARIAN_MARK_UNREAD)
-                end
-            end,
-            keybind = "UI_SHORTCUT_SECONDARY",
-            visible = function()
-                return self.mouseOverRow
-            end,
-            callback = function()
-                local book = self:FindBook(self.mouseOverRow.data.bookId)
-                self:ToggleReadBook(book)
-            end,
-        },
-        {
-            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
-            name = GetString(LIBRARIAN_GO_TO_CATEGORY),
-            keybind = "UI_SHORTCUT_TERTIARY",
-            visible = function()
-                return self.mouseOverRow and self.mouseOverRow.data.categoryIndex and self.mouseOverRow.data.collectionIndex
-            end,
-            callback = function()
-                local collectionId = select(7, GetLoreCollectionInfo(self.mouseOverRow.data.categoryIndex, self.mouseOverRow.data.collectionIndex))
-                LORE_LIBRARY:SetCollectionIdToSelect(collectionId)
-                MAIN_MENU_KEYBOARD:ShowScene("loreLibrary")
-            end,
-        },
-        {
-            alignment = KEYBIND_STRIP_ALIGN_LEFT,
-            name = GetString(LIBRARIAN_DELETE_BOOK),
-            keybind = "UI_SHORTCUT_NEGATIVE",
-            visible = function()
-                return self.mouseOverRow and (not self.mouseOverRow.data.categoryIndex or not self.mouseOverRow.data.collectionIndex)
-            end,
-            callback = function()
-                self.potentialBookIdToDelete = self.mouseOverRow.data.bookId
-                ZO_Dialogs_ShowPlatformDialog("LIBRARIAN_DELETE_BOOK", {bookId = self.mouseOverRow.data.bookId}, {mainTextParams = {self.mouseOverRow.data.title}})
-            end,
-        }
-    }
-end
-
-function Librarian:InitializeScene()
-    if not LIBRARIAN_SCENE then
-        LIBRARIAN_TITLE_FRAGMENT = ZO_SetTitleFragment:New(LIBRARIAN_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()
-    self.unreadPerCollections = {}
-    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 em = GetEventManager()
-    local function endSearch()
-        em:UnregisterForUpdate(identifier)
-        self.settings.chatEnabled = chatEnabled
-        self.settings.alertEnabled = alertEnabled
-        if hasImportedBooks then
-            self:RefreshAllData()
-        else
-            self:RefreshLoreLibraryData()
-        end
-    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
-                    local loreCategoryName
-                    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, _, known, bookId = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
-        if known then
-            local book = self:FindBook(bookId)
-            local characterBook = self:FindCharacterBook(bookId)
-            if book and characterBook and book.unread then
-                self:AddUnreadBookInCollection(categoryIndex, collectionIndex)
-            elseif not characterBook then
-                local newBook = { bookId = bookId, title = title }
-                self:AddBook(newBook, false)
-                hasImportedBooks = true
-            end
-        end
-        return true
-    end
-
-    local function steps()
-        local gettime = GetGameTimeMilliseconds
-        local start = gettime()
-        -- do as much as possible in 5ms
-        while ((gettime() - start) < 5) and step() do
-        end
-    end
-    em:RegisterForUpdate(identifier, 0, steps)
-end
-
-function Librarian:BuildMasterList()
-    local GetLoreBookIndicesFromBookId, GetLoreCollectionInfo = GetLoreBookIndicesFromBookId, GetLoreCollectionInfo
-    local GetLoreBookInfo = GetLoreBookInfo
-
-    local function ShouldDisplayBook(book)
-        if not self.settings.showHiddenBook then
-            local category, collection = GetLoreBookIndicesFromBookId(book.bookId)
-            if category and collection then
-                local hiddenCollection = select(5, GetLoreCollectionInfo(category, collection))
-                if hiddenCollection then
-                    return false
-                end
-            else
-                return false
-            end
-        end
-        return true
-    end
-
-    for i, book in ipairs(self.books) do
-        if ShouldDisplayBook(book) then
-            if not self.masterList[i] then
-                self.masterList[i] = {}
-            end
-
-            local data = self.masterList[i]
-            for k,v in pairs(book) do
-				if k == "body" then
-					data[k] = table.concat(book.body)
-				else
-					data[k] = v
-				end
-            end
-
-            -- because we reuse the same list between 2 refresh, the data may already be filled up so no need to recompute it, these won't change
-            if not data.categoryIndex or not data.collectionIndex or not data.bookIndex then
-                data.categoryIndex, data.collectionIndex, data.bookIndex = GetLoreBookIndicesFromBookId(book.bookId)
-            end
-            if data.categoryIndex and data.collectionIndex and data.bookIndex and (not data.title or not data.body or not data.category) then
-                data.title = GetLoreBookInfo(data.categoryIndex, data.collectionIndex, data.bookIndex)
-                data.body = ReadLoreBook(data.categoryIndex, data.collectionIndex, data.bookIndex)
-                data.category = GetLoreCollectionInfo(data.categoryIndex, data.collectionIndex)
-            end
-
-            if not data.category or data.category == "" then
-                data.category = GetString(LIBRARIAN_NO_CATEGORY)
-            end
-
-            if not data.wordCount then
-                local wordCount = 0
-                if data.body then
-                    for w in data.body:gmatch("%S+") do
-                        wordCount = wordCount + 1
-                    end
-                end
-                data.wordCount = wordCount
-            end
-
-            data.type = LIBRARIAN_SEARCH
-            local characterBook = self:FindCharacterBook(book.bookId)
-            if characterBook then
-                data.seenByCurrentCharacter = true
-                data.timeStamp = characterBook.timeStamp
-            else
-                data.seenByCurrentCharacter = false
-                data.timeStamp = book.timeStamp
-            end
-        else
-            self.masterList[i] = nil
-        end
-    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 _, data in pairs(self.masterList) do
-        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(LIBRARIAN_BOOK_COUNT), bookCount)
-    if unreadCount > 0 then message = string.format(GetString(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.category = GetControl(control, "Category")
-    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.category.normalColor = ZO_NORMAL_TEXT
-    control.category:SetText(data.category)
-
-    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 not self.searchOnlyTitle and data.body and zo_plainstrfind(data.body:lower(), lowerSearchTerm) then
-        return true
-    end
-
-    return false
-end
-
-function Librarian:FindCharacterBook(bookId)
-    if not self.characterBooks then
-        return nil
-    end
-
-    if self.characterBooksCache[bookId] then
-        return self.characterBooksCache[bookId]
-    end
-
-    for _,book in ipairs(self.characterBooks) do
-        if book.bookId == bookId then
-            self.characterBooksCache[bookId] = book
-            return book
-        end
-    end
-
-    return nil
-end
-
-function Librarian:FindBook(bookId)
-    for _,book in ipairs(self.books) do
-        if book.bookId == bookId then
-            return book
-        end
-    end
-end
-
-function Librarian:AddBookToGlobalSave(book)
-    book.timeStamp = GetTimeStamp()
-    book.unread = true
-
-    local categoryIndex, collectionIndex, bookIndex = GetLoreBookIndicesFromBookId(book.bookId)
-    if categoryIndex and collectionIndex and bookIndex then
-        -- if these indexes exist in the API, this mean that we will be able to get these info when needed (so no need to save them)
-        book.title = nil
-        book.body = nil
-        book.medium = nil
-        book.showTitle = nil
-    else
-        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
-
-    table.insert(self.books, book)
-
-    for index, bookIdToDelete in ipairs(self.globalSavedVars.deletedBooks) do
-        if bookIdToDelete == book.bookId then
-            table.remove(self.globalSavedVars.deletedBooks, index)
-            break
-        end
-    end
-end
-
-function Librarian:AddBook(book, refreshDataRightAway)
-    if not self:FindCharacterBook(book.bookId) then
-        local bookTitle = book.title -- storing it because AddBookToGlobalSave will delete it if we can retrieve it thanks to ESO API
-        local foundBook = self:FindBook(book.bookId)
-        if foundBook then
-            book = foundBook
-        else
-            self:AddBookToGlobalSave(book)
-        end
-
-        local characterBook = { bookId = book.bookId, timeStamp = GetTimeStamp() }
-        table.insert(self.characterBooks, characterBook)
-
-        if book.unread then
-            local categoryIndex, collectionIndex = GetLoreBookIndicesFromBookId(book.bookId)
-            if categoryIndex and collectionIndex then
-                self:AddUnreadBookInCollection(categoryIndex, collectionIndex)
-            end
-        end
-
-        if refreshDataRightAway then
-            self:RefreshAllData()
-        end
-
-        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))
-            local params = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_EVENT_LARGE_TEXT, SOUNDS.BOOK_ACQUIRED)
-            params:SetCSAType(CENTER_SCREEN_ANNOUNCE_TYPE_DISPLAY_ANNOUNCEMENT )
-            params:SetText(GetString(LIBRARIAN_NEW_BOOK_FOUND))
-            CENTER_SCREEN_ANNOUNCE:AddMessageWithParams(params)
-        end
-        if self.settings.chatEnabled then
-            d(string.format(GetString(LIBRARIAN_NEW_BOOK_FOUND_WITH_TITLE), bookTitle))
-        end
-
-        self.newBookCount = self.newBookCount + 1
-        if self.settings.reloadReminderBookCount and self.settings.reloadReminderBookCount > 0 and self.settings.reloadReminderBookCount <= self.newBookCount then
-            d(GetString(LIBRARIAN_RELOAD_REMINDER))
-        end
-    end
-
-end
-
-function Librarian:RemoveBook(bookId)
-    for _, bookIdToDelete in ipairs(self.globalSavedVars.deletedBooks) do
-        if bookIdToDelete == bookId then
-            d("Couldn't delete book, it is already beeing deleted")
-            return
-        end
-    end
-
-    table.insert(self.globalSavedVars.deletedBooks, bookId)
-
-    for index ,book in ipairs(self.books) do
-        if book.bookId == bookId then
-            table.remove(self.books, index)
-            break
-        end
-    end
-
-    self.characterBooksCache[bookId] = nil
-    self:RemoveCharacterBook(bookId)
-
-    -- Clean the master list to be sure the entry is removed from the display as well
-    self.masterList = {}
-    self:RefreshAllData()
-end
-
-function Librarian:RemoveCharacterBook(bookId)
-    for index ,book in ipairs(self.characterBooks) do
-        if book.bookId == bookId then
-            table.remove(self.characterBooks, index)
-            break
-        end
-    end
-end
-
-function Librarian:AddUnreadBookInCollection(categoryIndex, collectionIndex)
-    if not self.unreadPerCollections[categoryIndex] then self.unreadPerCollections[categoryIndex] = {} end
-    if not self.unreadPerCollections[categoryIndex][collectionIndex] then self.unreadPerCollections[categoryIndex][collectionIndex] = 0 end
-    self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] + 1
-end
-
-function Librarian:ToggleReadBook(book)
-    book.unread = not book.unread
-
-    local categoryIndex, collectionIndex = GetLoreBookIndicesFromBookId(book.bookId)
-    if categoryIndex and collectionIndex then
-        if not self.unreadPerCollections[categoryIndex] then self.unreadPerCollections[categoryIndex] = {} end
-        if not self.unreadPerCollections[categoryIndex][collectionIndex] then self.unreadPerCollections[categoryIndex][collectionIndex] = 0 end
-        if book.unread then
-            self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] + 1
-        elseif self.unreadPerCollections[categoryIndex][collectionIndex] > 0 then
-            self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] - 1
-        end
-    end
-
-    self:RefreshAllData()
-end
-
-function Librarian:Toggle()
-    if LibrarianFrame:IsControlHidden() then
-        SCENE_MANAGER:Show("librarian")
-    else
-        SCENE_MANAGER:Hide("librarian")
-    end
-end
-
 function Librarian:ReadBook(data)
     self.lastShownBookId = data.bookId

@@ -843,44 +166,6 @@ function Librarian:RefreshAllData()
     self:RefreshLoreLibraryData()
 end

-function Librarian:RefreshLoreLibraryData()
-    if self.settings.showUnreadIndicatorInLoreLibrary then
-        local selectedCategoryIndex = LORE_LIBRARY:GetSelectedCategoryIndex()
-        local selectedCollectionIndex = LORE_LIBRARY:GetSelectedCollectionIndex()
-        local selectedCollectionId = select(7, GetLoreCollectionInfo(selectedCategoryIndex, selectedCollectionIndex))
-        LORE_LIBRARY:SetCollectionIdToSelect(selectedCollectionId)
-        LORE_LIBRARY:BuildCategoryList()
-    end
-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)
-
-    -- Let's wait a little bit before updating the filters if the player didn't have time to enter its full string yet
-    -- Because if we try to update the filters too much it sometimes crash
-    local TIME_BEFORE_UPDATING_FILTERS = 600 -- ms
-    self.lastTimeSearchModified = GetGameTimeMilliseconds()
-    local function TryRefreshFilters()
-        if self.lastTimeSearchModified + TIME_BEFORE_UPDATING_FILTERS <= GetGameTimeMilliseconds() then
-            self:RefreshFilters()
-        end
-    end
-    zo_callLater(TryRefreshFilters, TIME_BEFORE_UPDATING_FILTERS)
-end
-
 function Librarian.SlashCommand(args)
     Librarian:Toggle()
 end
@@ -896,18 +181,6 @@ function Librarian.OnShowBook(eventCode, title, body, medium, showTitle, bookId)
     KEYBIND_STRIP:AddKeybindButtonGroup(LIBRARIAN.loreReaderKeybinds)
 end

-function Librarian.OnMouseEnterRow(control)
-    LIBRARIAN:Row_OnMouseEnter(control)
-end
-
-function Librarian.OnMouseExitRow(control)
-    LIBRARIAN:Row_OnMouseExit(control)
-end
-
-function Librarian.OnMouseUpRow(control, button, upInside)
-    LIBRARIAN:ReadBook(control.data)
-end
-
 function Librarian.OnAddonLoaded(event, addon)
     if addon == Librarian.name then
         EVENT_MANAGER:UnregisterForEvent(Librarian.name, EVENT_ADD_ON_LOADED)
diff --git a/Librarian.txt b/Librarian.txt
index d7d5192..798f8b7 100644
--- a/Librarian.txt
+++ b/Librarian.txt
@@ -10,7 +10,12 @@
 /lang/$(language).lua

 Librarian.lua
+
+ESOUIModifications.lua
+LibrarianData.lua
 LibrarianSettings.lua
+LibrarianUI.lua
+
 Librarian.xml
 Bindings.xml

diff --git a/LibrarianData.lua b/LibrarianData.lua
new file mode 100644
index 0000000..38567d6
--- /dev/null
+++ b/LibrarianData.lua
@@ -0,0 +1,222 @@
+function Librarian:ImportFromLoreLibrary()
+    self.unreadPerCollections = {}
+    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 em = GetEventManager()
+    local function endSearch()
+        em:UnregisterForUpdate(identifier)
+        self.settings.chatEnabled = chatEnabled
+        self.settings.alertEnabled = alertEnabled
+        if hasImportedBooks then
+            self:RefreshAllData()
+        else
+            self:RefreshLoreLibraryData()
+        end
+    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
+                    local loreCategoryName
+                    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, _, known, bookId = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
+        if known then
+            local book = self:FindBook(bookId)
+            local characterBook = self:FindCharacterBook(bookId)
+            if book and characterBook and book.unread then
+                self:AddUnreadBookInCollection(categoryIndex, collectionIndex)
+            elseif not characterBook then
+                local newBook = { bookId = bookId, title = title }
+                self:AddBook(newBook, false)
+                hasImportedBooks = true
+            end
+        end
+        return true
+    end
+
+    local function steps()
+        local gettime = GetGameTimeMilliseconds
+        local start = gettime()
+        -- do as much as possible in 5ms
+        while ((gettime() - start) < 5) and step() do
+        end
+    end
+    em:RegisterForUpdate(identifier, 0, steps)
+end
+
+function Librarian:FindCharacterBook(bookId)
+    if not self.characterBooks then
+        return nil
+    end
+
+    if self.characterBooksCache[bookId] then
+        return self.characterBooksCache[bookId]
+    end
+
+    for _,book in ipairs(self.characterBooks) do
+        if book.bookId == bookId then
+            self.characterBooksCache[bookId] = book
+            return book
+        end
+    end
+
+    return nil
+end
+
+function Librarian:FindBook(bookId)
+    for _,book in ipairs(self.books) do
+        if book.bookId == bookId then
+            return book
+        end
+    end
+end
+
+function Librarian:AddBookToGlobalSave(book)
+    book.timeStamp = GetTimeStamp()
+    book.unread = true
+
+    local categoryIndex, collectionIndex, bookIndex = GetLoreBookIndicesFromBookId(book.bookId)
+    if categoryIndex and collectionIndex and bookIndex then
+        -- if these indexes exist in the API, this mean that we will be able to get these info when needed (so no need to save them)
+        book.title = nil
+        book.body = nil
+        book.medium = nil
+        book.showTitle = nil
+    else
+        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
+
+    table.insert(self.books, book)
+
+    for index, bookIdToDelete in ipairs(self.globalSavedVars.deletedBooks) do
+        if bookIdToDelete == book.bookId then
+            table.remove(self.globalSavedVars.deletedBooks, index)
+            break
+        end
+    end
+end
+
+function Librarian:AddBook(book, refreshDataRightAway)
+    if not self:FindCharacterBook(book.bookId) then
+        local bookTitle = book.title -- storing it because AddBookToGlobalSave will delete it if we can retrieve it thanks to ESO API
+        local foundBook = self:FindBook(book.bookId)
+        if foundBook then
+            book = foundBook
+        else
+            self:AddBookToGlobalSave(book)
+        end
+
+        local characterBook = { bookId = book.bookId, timeStamp = GetTimeStamp() }
+        table.insert(self.characterBooks, characterBook)
+
+        if book.unread then
+            local categoryIndex, collectionIndex = GetLoreBookIndicesFromBookId(book.bookId)
+            if categoryIndex and collectionIndex then
+                self:AddUnreadBookInCollection(categoryIndex, collectionIndex)
+            end
+        end
+
+        if refreshDataRightAway then
+            self:RefreshAllData()
+        end
+
+        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))
+            local params = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_EVENT_LARGE_TEXT, SOUNDS.BOOK_ACQUIRED)
+            params:SetCSAType(CENTER_SCREEN_ANNOUNCE_TYPE_DISPLAY_ANNOUNCEMENT )
+            params:SetText(GetString(LIBRARIAN_NEW_BOOK_FOUND))
+            CENTER_SCREEN_ANNOUNCE:AddMessageWithParams(params)
+        end
+        if self.settings.chatEnabled then
+            d(string.format(GetString(LIBRARIAN_NEW_BOOK_FOUND_WITH_TITLE), bookTitle))
+        end
+
+        self.newBookCount = self.newBookCount + 1
+        if self.settings.reloadReminderBookCount and self.settings.reloadReminderBookCount > 0 and self.settings.reloadReminderBookCount <= self.newBookCount then
+            d(GetString(LIBRARIAN_RELOAD_REMINDER))
+        end
+    end
+
+end
+
+function Librarian:RemoveBook(bookId)
+    for _, bookIdToDelete in ipairs(self.globalSavedVars.deletedBooks) do
+        if bookIdToDelete == bookId then
+            d("Couldn't delete book, it is already beeing deleted")
+            return
+        end
+    end
+
+    table.insert(self.globalSavedVars.deletedBooks, bookId)
+
+    for index ,book in ipairs(self.books) do
+        if book.bookId == bookId then
+            table.remove(self.books, index)
+            break
+        end
+    end
+
+    self.characterBooksCache[bookId] = nil
+    self:RemoveCharacterBook(bookId)
+
+    -- Clean the master list to be sure the entry is removed from the display as well
+    self.masterList = {}
+    self:RefreshAllData()
+end
+
+function Librarian:RemoveCharacterBook(bookId)
+    for index ,book in ipairs(self.characterBooks) do
+        if book.bookId == bookId then
+            table.remove(self.characterBooks, index)
+            break
+        end
+    end
+end
+
+function Librarian:AddUnreadBookInCollection(categoryIndex, collectionIndex)
+    if not self.unreadPerCollections[categoryIndex] then self.unreadPerCollections[categoryIndex] = {} end
+    if not self.unreadPerCollections[categoryIndex][collectionIndex] then self.unreadPerCollections[categoryIndex][collectionIndex] = 0 end
+    self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] + 1
+end
+
+function Librarian:ToggleReadBook(book)
+    book.unread = not book.unread
+
+    local categoryIndex, collectionIndex = GetLoreBookIndicesFromBookId(book.bookId)
+    if categoryIndex and collectionIndex then
+        if not self.unreadPerCollections[categoryIndex] then self.unreadPerCollections[categoryIndex] = {} end
+        if not self.unreadPerCollections[categoryIndex][collectionIndex] then self.unreadPerCollections[categoryIndex][collectionIndex] = 0 end
+        if book.unread then
+            self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] + 1
+        elseif self.unreadPerCollections[categoryIndex][collectionIndex] > 0 then
+            self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] - 1
+        end
+    end
+
+    self:RefreshAllData()
+end
diff --git a/LibrarianUI.lua b/LibrarianUI.lua
new file mode 100644
index 0000000..a7668b4
--- /dev/null
+++ b/LibrarianUI.lua
@@ -0,0 +1,274 @@
+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)
+            end,
+        },
+        {
+            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
+            name = function()
+                if not self.mouseOverRow then return nil end
+                local book = self:FindBook(self.mouseOverRow.data.bookId)
+                if book.unread then
+                    return GetString(LIBRARIAN_MARK_READ)
+                else
+                    return GetString(LIBRARIAN_MARK_UNREAD)
+                end
+            end,
+            keybind = "UI_SHORTCUT_SECONDARY",
+            visible = function()
+                return self.mouseOverRow
+            end,
+            callback = function()
+                local book = self:FindBook(self.mouseOverRow.data.bookId)
+                self:ToggleReadBook(book)
+            end,
+        },
+        {
+            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
+            name = GetString(LIBRARIAN_GO_TO_CATEGORY),
+            keybind = "UI_SHORTCUT_TERTIARY",
+            visible = function()
+                return self.mouseOverRow and self.mouseOverRow.data.categoryIndex and self.mouseOverRow.data.collectionIndex
+            end,
+            callback = function()
+                local collectionId = select(7, GetLoreCollectionInfo(self.mouseOverRow.data.categoryIndex, self.mouseOverRow.data.collectionIndex))
+                LORE_LIBRARY:SetCollectionIdToSelect(collectionId)
+                MAIN_MENU_KEYBOARD:ShowScene("loreLibrary")
+            end,
+        },
+        {
+            alignment = KEYBIND_STRIP_ALIGN_LEFT,
+            name = GetString(LIBRARIAN_DELETE_BOOK),
+            keybind = "UI_SHORTCUT_NEGATIVE",
+            visible = function()
+                return self.mouseOverRow and (not self.mouseOverRow.data.categoryIndex or not self.mouseOverRow.data.collectionIndex)
+            end,
+            callback = function()
+                self.potentialBookIdToDelete = self.mouseOverRow.data.bookId
+                ZO_Dialogs_ShowPlatformDialog("LIBRARIAN_DELETE_BOOK", {bookId = self.mouseOverRow.data.bookId}, {mainTextParams = {self.mouseOverRow.data.title}})
+            end,
+        }
+    }
+end
+
+function Librarian:InitializeScene()
+    if not LIBRARIAN_SCENE then
+        LIBRARIAN_TITLE_FRAGMENT = ZO_SetTitleFragment:New(LIBRARIAN_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:BuildMasterList()
+    local GetLoreBookIndicesFromBookId, GetLoreCollectionInfo = GetLoreBookIndicesFromBookId, GetLoreCollectionInfo
+    local GetLoreBookInfo = GetLoreBookInfo
+
+    local function ShouldDisplayBook(book)
+        if not self.settings.showHiddenBook then
+            local category, collection = GetLoreBookIndicesFromBookId(book.bookId)
+            if category and collection then
+                local hiddenCollection = select(5, GetLoreCollectionInfo(category, collection))
+                if hiddenCollection then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    end
+
+    for i, book in ipairs(self.books) do
+        if ShouldDisplayBook(book) then
+            if not self.masterList[i] then
+                self.masterList[i] = {}
+            end
+
+            local data = self.masterList[i]
+            for k,v in pairs(book) do
+				if k == "body" then
+					data[k] = table.concat(book.body)
+				else
+					data[k] = v
+				end
+            end
+
+            -- because we reuse the same list between 2 refresh, the data may already be filled up so no need to recompute it, these won't change
+            if not data.categoryIndex or not data.collectionIndex or not data.bookIndex then
+                data.categoryIndex, data.collectionIndex, data.bookIndex = GetLoreBookIndicesFromBookId(book.bookId)
+            end
+            if data.categoryIndex and data.collectionIndex and data.bookIndex and (not data.title or not data.body or not data.category) then
+                data.title = GetLoreBookInfo(data.categoryIndex, data.collectionIndex, data.bookIndex)
+                data.body = ReadLoreBook(data.categoryIndex, data.collectionIndex, data.bookIndex)
+                data.category = GetLoreCollectionInfo(data.categoryIndex, data.collectionIndex)
+            end
+
+            if not data.category or data.category == "" then
+                data.category = GetString(LIBRARIAN_NO_CATEGORY)
+            end
+
+            if not data.wordCount then
+                local wordCount = 0
+                if data.body then
+                    for w in data.body:gmatch("%S+") do
+                        wordCount = wordCount + 1
+                    end
+                end
+                data.wordCount = wordCount
+            end
+
+            data.type = self.constants.LIBRARIAN_SEARCH
+            local characterBook = self:FindCharacterBook(book.bookId)
+            if characterBook then
+                data.seenByCurrentCharacter = true
+                data.timeStamp = characterBook.timeStamp
+            else
+                data.seenByCurrentCharacter = false
+                data.timeStamp = book.timeStamp
+            end
+        else
+            self.masterList[i] = nil
+        end
+    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 _, data in pairs(self.masterList) do
+        if self.settings.showAllBooks or data.seenByCurrentCharacter then
+            if(searchTerm == "" or self.search:IsMatch(searchTerm, data)) then
+                table.insert(scrollData, ZO_ScrollList_CreateDataEntry(self.constants.LIBRARIAN_DATA, data))
+                bookCount = bookCount + 1
+                if data.unread then unreadCount = unreadCount + 1 end
+            end
+        end
+    end
+
+    local message = string.format(GetString(LIBRARIAN_BOOK_COUNT), bookCount)
+    if unreadCount > 0 then message = string.format(GetString(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.category = GetControl(control, "Category")
+    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.category.normalColor = ZO_NORMAL_TEXT
+    control.category:SetText(data.category)
+
+    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 not self.searchOnlyTitle and data.body and zo_plainstrfind(data.body:lower(), lowerSearchTerm) then
+        return true
+    end
+
+    return false
+end
+
+function Librarian:Toggle()
+    if LibrarianFrame:IsControlHidden() then
+        SCENE_MANAGER:Show("librarian")
+    else
+        SCENE_MANAGER:Hide("librarian")
+    end
+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)
+
+    -- Let's wait a little bit before updating the filters if the player didn't have time to enter its full string yet
+    -- Because if we try to update the filters too much it sometimes crash
+    local TIME_BEFORE_UPDATING_FILTERS = 600 -- ms
+    self.lastTimeSearchModified = GetGameTimeMilliseconds()
+    local function TryRefreshFilters()
+        if self.lastTimeSearchModified + TIME_BEFORE_UPDATING_FILTERS <= GetGameTimeMilliseconds() then
+            self:RefreshFilters()
+        end
+    end
+    zo_callLater(TryRefreshFilters, TIME_BEFORE_UPDATING_FILTERS)
+end
+
+function Librarian.OnMouseEnterRow(control)
+    LIBRARIAN:Row_OnMouseEnter(control)
+end
+
+function Librarian.OnMouseExitRow(control)
+    LIBRARIAN:Row_OnMouseExit(control)
+end
+
+function Librarian.OnMouseUpRow(control, button, upInside)
+    LIBRARIAN:ReadBook(control.data)
+end