diff --git a/app/src/business/openRecentDocs.ts b/app/src/business/openRecentDocs.ts index 5e1f421afbc..f1e7b3805b2 100644 --- a/app/src/business/openRecentDocs.ts +++ b/app/src/business/openRecentDocs.ts @@ -10,44 +10,39 @@ import {focusByRange} from "../protyle/util/selection"; import {hasClosestByClassName} from "../protyle/util/hasClosest"; import {hideElements} from "../protyle/ui/hideElements"; -const getHTML = async (data: { +const renderRecentDocsContent = async (data: { rootID: string, icon: string, title: string, viewedAt?: number, closedAt?: number, openAt?: number, - updated?: number -}[], element: Element, key?: string, sortBy: TRecentDocsSort = "viewedAt") => { +}[], element: Element, key?: string) => { let tabHtml = ""; let index = 0; - // 根据排序字段对数据进行排序 - const sortedData = [...data].sort((a, b) => { - const aValue = a[sortBy] || 0; - const bValue = b[sortBy] || 0; - return bValue - aValue; // 降序排序 - }); - - sortedData.forEach((item) => { - if (!key || item.title.toLowerCase().includes(key.toLowerCase())) { - tabHtml += `
  • -${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)} -${escapeHtml(item.title)} + if (key) { + data = data.filter((item) => { + return item.title.toLowerCase().includes(key.toLowerCase()); + }); + } + data.forEach((item) => { + tabHtml += `
  • + ${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)} + ${escapeHtml(item.title)}
  • `; - index++; - } + index++; }); let switchPath = ""; if (tabHtml) { const pathResponse = await fetchSyncPost("/api/filetree/getFullHPathByID", { - id: data[0].rootID + id: data[0].rootID // 过滤后的第一个文档 ID }); switchPath = escapeHtml(pathResponse.data); } let dockHtml = ""; if (!isWindow()) { - dockHtml = '"; + dockHtml = '"; } const pathElement = element.querySelector(".switch-doc__path"); pathElement.innerHTML = switchPath; - pathElement.previousElementSibling.innerHTML = `
    - ${dockHtml} - -
    `; + pathElement.previousElementSibling.innerHTML = `
    + ${dockHtml} + +
    `; }; export const openRecentDocs = () => { @@ -93,7 +88,8 @@ export const openRecentDocs = () => { hideElements(["dialog"]); return; } - fetchPost("/api/storage/getRecentDocs", {sortBy: "viewedAt"}, (response) => { + const sortBy = window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type; + fetchPost("/api/storage/getRecentDocs", {sortBy}, (response) => { let range: Range; if (getSelection().rangeCount > 0) { range = getSelection().getRangeAt(0); @@ -110,15 +106,15 @@ export const openRecentDocs = () => {
    `, content: `
    -
    +
    `, height: "80vh", @@ -128,16 +124,18 @@ export const openRecentDocs = () => { } } }); + const sortSelect = dialog.element.querySelector("#recentDocsSort") as HTMLSelectElement; + sortSelect.value = sortBy; const searchElement = dialog.element.querySelector("input"); searchElement.focus(); searchElement.addEventListener("compositionend", () => { - getHTML(response.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort); + renderRecentDocsContent(response.data, dialog.element, searchElement.value); }); searchElement.addEventListener("input", (event: InputEvent) => { if (event.isComposing) { return; } - getHTML(response.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort); + renderRecentDocsContent(response.data, dialog.element, searchElement.value); }); dialog.element.setAttribute("data-key", Constants.DIALOG_RECENTDOCS); dialog.element.addEventListener("click", (event) => { @@ -152,45 +150,16 @@ export const openRecentDocs = () => { }); // 添加排序下拉框事件监听 - const sortSelect = dialog.element.querySelector("#recentDocsSort") as HTMLSelectElement; sortSelect.addEventListener("change", () => { - // 重新调用API获取排序后的数据 - if (sortSelect.value === "updated") { - // 使用SQL查询获取最近修改的文档 - const data = { - stmt: "SELECT * FROM blocks WHERE type = 'd' ORDER BY updated DESC LIMIT 33" - }; - fetchSyncPost("/api/query/sql", data).then((sqlResponse) => { - if (sqlResponse.data && sqlResponse.data.length > 0) { - // 转换SQL查询结果格式 - const recentModifiedDocs = sqlResponse.data.map((block: any) => { - // 从ial中解析icon - let icon = ""; - if (block.ial) { - const iconMatch = block.ial.match(/icon="([^"]*)"/); - if (iconMatch) { - icon = iconMatch[1]; - } - } - return { - rootID: block.id, - icon, - title: block.content, - updated: block.updated - }; - }); - getHTML(recentModifiedDocs, dialog.element, searchElement.value, "updated"); - } - }); - } else { - fetchPost("/api/storage/getRecentDocs", {sortBy: sortSelect.value}, (newResponse) => { - getHTML(newResponse.data, dialog.element, searchElement.value, sortSelect.value as TRecentDocsSort); - }); - } + // 重新调用 API 获取排序后的数据 + fetchPost("/api/storage/getRecentDocs", {sortBy: sortSelect.value}, (newResponse) => { + response = newResponse; + renderRecentDocsContent(newResponse.data, dialog.element, searchElement.value); + }); window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type = sortSelect.value; setStorageVal(Constants.LOCAL_RECENT_DOCS, window.siyuan.storage[Constants.LOCAL_RECENT_DOCS]); }); - getHTML(response.data, dialog.element); + renderRecentDocsContent(response.data, dialog.element); }); }; diff --git a/app/src/layout/Wnd.ts b/app/src/layout/Wnd.ts index 5e8e636c6af..878fb5d38c1 100644 --- a/app/src/layout/Wnd.ts +++ b/app/src/layout/Wnd.ts @@ -778,7 +778,7 @@ export class Wnd { model.send("closews", {}); } - private removeTabAction = (id: string, closeAll = false, animate = true, isSaveLayout = true) => { + private removeTabAction = (id: string, isBatchClose = false, animate = true, isSaveLayout = true) => { clearCounter(); this.children.find((item, index) => { if (item.id === id) { @@ -794,8 +794,10 @@ export class Wnd { } if (item.model instanceof Editor) { saveScroll(item.model.editor.protyle); - // 更新文档关闭时间 - fetchPost("/api/storage/updateRecentDocCloseTime", {rootID: item.model.editor.protyle.block.rootID}); + // 更新文档关闭时间(批量关闭页签时由 closeTabByType 批量处理,这里不单独调用) + if (!isBatchClose) { + fetchPost("/api/storage/updateRecentDocCloseTime", {rootID: item.model.editor.protyle.block.rootID}); + } } if (this.children.length === 1) { this.destroyModel(this.children[0].model); @@ -841,7 +843,7 @@ export class Wnd { } } }); - if (latestHeadElement && !closeAll) { + if (latestHeadElement && !isBatchClose) { this.switchTab(latestHeadElement, true, true, false, false); this.showHeading(); } @@ -889,7 +891,7 @@ export class Wnd { /// #endif }; - public removeTab(id: string, closeAll = false, animate = true, isSaveLayout = true) { + public removeTab(id: string, isBatchClose = false, animate = true, isSaveLayout = true) { for (let index = 0; index < this.children.length; index++) { const item = this.children[index]; if (item.id === id) { @@ -898,9 +900,9 @@ export class Wnd { showMessage(window.siyuan.languages.uploading); return; } - this.removeTabAction(id, closeAll, animate, isSaveLayout); + this.removeTabAction(id, isBatchClose, animate, isSaveLayout); } else { - this.removeTabAction(id, closeAll, animate, isSaveLayout); + this.removeTabAction(id, isBatchClose, animate, isSaveLayout); } return; } diff --git a/app/src/layout/tabUtil.ts b/app/src/layout/tabUtil.ts index eb4d229bdd3..c45d68e19c6 100644 --- a/app/src/layout/tabUtil.ts +++ b/app/src/layout/tabUtil.ts @@ -23,6 +23,7 @@ import {openHistory} from "../history/history"; import {newFile} from "../util/newFile"; import {mountHelp, newNotebook} from "../util/mount"; import {Constants} from "../constants"; +import {fetchPost} from "../util/fetch"; export const getActiveTab = (wndActive = true) => { const activeTabElement = document.querySelector(".layout__wnd--active .item--focus"); @@ -360,28 +361,58 @@ export const copyTab = (app: App, tab: Tab) => { }; export const closeTabByType = async (tab: Tab, type: "closeOthers" | "closeAll" | "other", tabs?: Tab[]) => { + const tabsToClose: Tab[] = []; if (type === "closeOthers") { - for (let index = 0; index < tab.parent.children.length; index++) { - if (tab.parent.children[index].id !== tab.id && !tab.parent.children[index].headElement.classList.contains("item--pin")) { - await tab.parent.children[index].parent.removeTab(tab.parent.children[index].id, true, false); - index--; + for (const item of tab.parent.children) { + if (item.id !== tab.id && !item.headElement.classList.contains("item--pin")) { + tabsToClose.push(item); } } } else if (type === "closeAll") { - for (let index = 0; index < tab.parent.children.length; index++) { - if (!tab.parent.children[index].headElement.classList.contains("item--pin")) { - await tab.parent.children[index].parent.removeTab(tab.parent.children[index].id, true); - index--; + for (const item of tab.parent.children) { + if (!item.headElement.classList.contains("item--pin")) { + tabsToClose.push(item); } } - } else if (tabs.length > 0) { - for (let index = 0; index < tabs.length; index++) { - if (!tabs[index].headElement.classList.contains("item--pin")) { - await tabs[index].parent.removeTab(tabs[index].id); + } else if (tabs && tabs.length > 0) { + for (const item of tabs) { + if (!item.headElement.classList.contains("item--pin")) { + tabsToClose.push(item); } } } + // 收集所有需要关闭的文档 rootID 并批量关闭页签 + const rootIDs: string[] = []; + for (const item of tabsToClose) { + let rootID; + if (item.model instanceof Editor) { + rootID = item.model.editor.protyle.block.rootID; + } else if (!item.model) { + const initTab = item.headElement.getAttribute("data-initdata"); + if (initTab) { + const initTabData = JSON.parse(initTab); + if (initTabData && initTabData.instance === "Editor" && initTabData.rootId) { + rootID = initTabData.rootId; + } + } + } + if (rootID) { + rootIDs.push(rootID); + } + + if (type === "closeOthers") { + item.parent.removeTab(item.id, true, false); + } else { + item.parent.removeTab(item.id, true); + } + } + + // 批量更新文档关闭时间 + if (rootIDs.length > 0) { + fetchPost("/api/storage/batchUpdateRecentDocCloseTime", {rootIDs}); + } + if (tab.headElement.parentElement && !tab.headElement.parentElement.querySelector(".item--focus")) { tab.parent.switchTab(tab.headElement, true); } else if (tab.parent.children.length > 0) { diff --git a/kernel/api/router.go b/kernel/api/router.go index 7b5f603d963..f41ba11703a 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -80,6 +80,7 @@ func ServeAPI(ginServer *gin.Engine) { ginServer.Handle("POST", "/api/storage/getRecentDocs", model.CheckAuth, getRecentDocs) ginServer.Handle("POST", "/api/storage/updateRecentDocViewTime", model.CheckAuth, updateRecentDocViewTime) ginServer.Handle("POST", "/api/storage/updateRecentDocCloseTime", model.CheckAuth, updateRecentDocCloseTime) + ginServer.Handle("POST", "/api/storage/batchUpdateRecentDocCloseTime", model.CheckAuth, batchUpdateRecentDocCloseTime) ginServer.Handle("POST", "/api/storage/updateRecentDocOpenTime", model.CheckAuth, updateRecentDocOpenTime) ginServer.Handle("POST", "/api/storage/getOutlineStorage", model.CheckAuth, getOutlineStorage) diff --git a/kernel/api/storage.go b/kernel/api/storage.go index fa223bc61b2..5f68e63d4bb 100644 --- a/kernel/api/storage.go +++ b/kernel/api/storage.go @@ -293,11 +293,11 @@ func updateRecentDocCloseTime(c *gin.Context) { return } - if nil == arg["rootID"] { + rootID, ok := arg["rootID"].(string) + if !ok || rootID == "" { return } - rootID := arg["rootID"].(string) err := model.UpdateRecentDocCloseTime(rootID) if err != nil { ret.Code = -1 @@ -305,3 +305,26 @@ func updateRecentDocCloseTime(c *gin.Context) { return } } + +func batchUpdateRecentDocCloseTime(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + rootIDsArg := arg["rootIDs"].([]interface{}) + var rootIDs []string + for _, id := range rootIDsArg { + rootIDs = append(rootIDs, id.(string)) + } + + err := model.BatchUpdateRecentDocCloseTime(rootIDs) + if err != nil { + ret.Code = -1 + ret.Msg = err.Error() + return + } +} diff --git a/kernel/model/file.go b/kernel/model/file.go index fe1ec9ba652..b66d97c99d8 100644 --- a/kernel/model/file.go +++ b/kernel/model/file.go @@ -745,7 +745,6 @@ func GetDoc(startID, endID, id string, index int, query string, queryTypes map[s } keywords = gulu.Str.RemoveDuplicatedElem(keywords) - go setRecentDocByTree(tree) return } @@ -1597,7 +1596,6 @@ func removeDoc(box *Box, p string, luteEngine *lute.Lute) { logging.LogInfof("removed doc [%s%s]", box.ID, p) box.removeSort(removeIDs) - RemoveRecentDoc(removeIDs) if "/" != dir { others, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, dir)) if err == nil && 1 > len(others) { diff --git a/kernel/model/heading.go b/kernel/model/heading.go index 89d94e5dc8f..1a62f647f0f 100644 --- a/kernel/model/heading.go +++ b/kernel/model/heading.go @@ -284,7 +284,6 @@ func Doc2Heading(srcID, targetID string, after bool) (srcTreeBox, srcTreePath st logging.LogWarnf("remove tree [%s] failed: %s", srcTree.Path, removeErr) } box.removeSort([]string{srcTree.ID}) - RemoveRecentDoc([]string{srcTree.ID}) evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast) evt.Data = map[string]interface{}{ "ids": []string{srcTree.ID}, diff --git a/kernel/model/index.go b/kernel/model/index.go index f937dbb9bf4..53a858a7313 100644 --- a/kernel/model/index.go +++ b/kernel/model/index.go @@ -113,8 +113,7 @@ func (box *Box) Unindex() { } func unindex(boxID string) { - ids := treenode.RemoveBlockTreesByBoxID(boxID) - RemoveRecentDoc(ids) + treenode.RemoveBlockTreesByBoxID(boxID) sql.DeleteBoxQueue(boxID) } diff --git a/kernel/model/storage.go b/kernel/model/storage.go index 25e3cffdf16..e798406bb4c 100644 --- a/kernel/model/storage.go +++ b/kernel/model/storage.go @@ -22,6 +22,7 @@ import ( "path" "path/filepath" "sort" + "strings" "sync" "time" @@ -29,17 +30,18 @@ import ( "github.com/88250/lute/parse" "github.com/siyuan-note/filelock" "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/sql" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" ) type RecentDoc struct { RootID string `json:"rootID"` - Icon string `json:"icon"` - Title string `json:"title"` - ViewedAt int64 `json:"viewedAt"` // 浏览时间字段 - ClosedAt int64 `json:"closedAt"` // 关闭时间字段 - OpenAt int64 `json:"openAt"` // 文档第一次从文档树加载到页签的时间 + Icon string `json:"icon,omitempty"` + Title string `json:"title,omitempty"` + ViewedAt int64 `json:"viewedAt,omitempty"` // 浏览时间字段 + ClosedAt int64 `json:"closedAt,omitempty"` // 关闭时间字段 + OpenAt int64 `json:"openAt,omitempty"` // 文档第一次从文档树加载到页签的时间 } type OutlineDoc struct { @@ -49,62 +51,89 @@ type OutlineDoc struct { var recentDocLock = sync.Mutex{} -func RemoveRecentDoc(ids []string) { - recentDocLock.Lock() - defer recentDocLock.Unlock() - - recentDocs, err := getRecentDocs("") - if err != nil { - return +// normalizeRecentDocs 规范化最近文档列表:去重、清空 Title/Icon、按类型截取 32 条记录 +func normalizeRecentDocs(recentDocs []*RecentDoc) []*RecentDoc { + // 去重 + seen := make(map[string]struct{}, len(recentDocs)) + deduplicated := make([]*RecentDoc, 0, len(recentDocs)) + for _, doc := range recentDocs { + if _, ok := seen[doc.RootID]; !ok { + seen[doc.RootID] = struct{}{} + deduplicated = append(deduplicated, doc) + } } - ids = gulu.Str.RemoveDuplicatedElem(ids) - for i, doc := range recentDocs { - if gulu.Str.Contains(doc.RootID, ids) { - recentDocs = append(recentDocs[:i], recentDocs[i+1:]...) - break + if len(deduplicated) <= 32 { + // 清空 Title 和 Icon + for _, doc := range deduplicated { + doc.Title = "" + doc.Icon = "" } + return deduplicated } - err = setRecentDocs(recentDocs) - if err != nil { - return - } - return -} + // 分别统计三种类型的记录 + var viewedDocs []*RecentDoc + var openedDocs []*RecentDoc + var closedDocs []*RecentDoc -func setRecentDocByTree(tree *parse.Tree) { - recentDoc := &RecentDoc{ - RootID: tree.Root.ID, - Icon: tree.Root.IALAttr("icon"), - Title: tree.Root.IALAttr("title"), - ViewedAt: time.Now().Unix(), // 使用当前时间作为浏览时间 - ClosedAt: 0, // 初始化关闭时间为0,表示未关闭 - OpenAt: time.Now().Unix(), // 设置文档打开时间 + for _, doc := range deduplicated { + if doc.ViewedAt > 0 { + viewedDocs = append(viewedDocs, doc) + } + if doc.OpenAt > 0 { + openedDocs = append(openedDocs, doc) + } + if doc.ClosedAt > 0 { + closedDocs = append(closedDocs, doc) + } } - recentDocLock.Lock() - defer recentDocLock.Unlock() - - recentDocs, err := getRecentDocs("") - if err != nil { - return + // 分别按时间排序并截取 32 条记录 + if len(viewedDocs) > 32 { + sort.Slice(viewedDocs, func(i, j int) bool { + return viewedDocs[i].ViewedAt > viewedDocs[j].ViewedAt + }) + viewedDocs = viewedDocs[:32] + } + if len(openedDocs) > 32 { + sort.Slice(openedDocs, func(i, j int) bool { + return openedDocs[i].OpenAt > openedDocs[j].OpenAt + }) + openedDocs = openedDocs[:32] + } + if len(closedDocs) > 32 { + sort.Slice(closedDocs, func(i, j int) bool { + return closedDocs[i].ClosedAt > closedDocs[j].ClosedAt + }) + closedDocs = closedDocs[:32] } - for i, c := range recentDocs { - if c.RootID == recentDoc.RootID { - recentDocs = append(recentDocs[:i], recentDocs[i+1:]...) - break + // 合并三类记录 + docMap := make(map[string]*RecentDoc, 64) + for _, doc := range viewedDocs { + docMap[doc.RootID] = doc + } + for _, doc := range openedDocs { + if _, ok := docMap[doc.RootID]; !ok { + docMap[doc.RootID] = doc + } + } + for _, doc := range closedDocs { + if _, ok := docMap[doc.RootID]; !ok { + docMap[doc.RootID] = doc } } - recentDocs = append([]*RecentDoc{recentDoc}, recentDocs...) - if 32 < len(recentDocs) { - recentDocs = recentDocs[:32] + result := make([]*RecentDoc, 0, len(docMap)) + for _, doc := range docMap { + // 清空 Title 和 Icon + doc.Title = "" + doc.Icon = "" + result = append(result, doc) } - err = setRecentDocs(recentDocs) - return + return result } // UpdateRecentDocOpenTime 更新文档打开时间(只在第一次从文档树加载到页签时调用) @@ -112,24 +141,35 @@ func UpdateRecentDocOpenTime(rootID string) (err error) { recentDocLock.Lock() defer recentDocLock.Unlock() - recentDocs, err := getRecentDocs("") + recentDocs, err := loadRecentDocsRaw() if err != nil { return } - // 查找文档并更新打开时间 + timeNow := time.Now().Unix() + // 查找文档并更新打开时间和浏览时间 found := false for _, doc := range recentDocs { if doc.RootID == rootID { - doc.OpenAt = time.Now().Unix() + doc.OpenAt = timeNow + doc.ViewedAt = timeNow + doc.ClosedAt = 0 found = true break } } - if found { - err = setRecentDocs(recentDocs) + // 如果文档不存在,创建新记录 + if !found { + recentDoc := &RecentDoc{ + RootID: rootID, + OpenAt: timeNow, + ViewedAt: timeNow, + } + recentDocs = append([]*RecentDoc{recentDoc}, recentDocs...) } + + err = setRecentDocs(recentDocs) return } @@ -138,52 +178,92 @@ func UpdateRecentDocViewTime(rootID string) (err error) { recentDocLock.Lock() defer recentDocLock.Unlock() - recentDocs, err := getRecentDocs("") + recentDocs, err := loadRecentDocsRaw() if err != nil { return } - // 查找文档并更新浏览时间 + timeNow := time.Now().Unix() + // 查找文档并更新浏览时间,保留原来的打开时间 found := false for _, doc := range recentDocs { if doc.RootID == rootID { - doc.ViewedAt = time.Now().Unix() + // OpenAt 保持不变,保留原来的打开时间 + doc.ViewedAt = timeNow + doc.ClosedAt = 0 found = true break } } - if found { - // 按浏览时间降序排序 - sort.Slice(recentDocs, func(i, j int) bool { - return recentDocs[i].ViewedAt > recentDocs[j].ViewedAt - }) - err = setRecentDocs(recentDocs) + // 如果文档不存在,创建新记录 + if !found { + recentDoc := &RecentDoc{ + RootID: rootID, + // 新创建的记录不设置 OpenAt,因为这是浏览而不是打开 + ViewedAt: timeNow, + } + recentDocs = append([]*RecentDoc{recentDoc}, recentDocs...) } + + err = setRecentDocs(recentDocs) return } // UpdateRecentDocCloseTime 更新文档关闭时间 func UpdateRecentDocCloseTime(rootID string) (err error) { + return BatchUpdateRecentDocCloseTime([]string{rootID}) +} + +// BatchUpdateRecentDocCloseTime 批量更新文档关闭时间 +func BatchUpdateRecentDocCloseTime(rootIDs []string) (err error) { + if len(rootIDs) == 0 { + return + } + recentDocLock.Lock() defer recentDocLock.Unlock() - recentDocs, err := getRecentDocs("") + recentDocs, err := loadRecentDocsRaw() if err != nil { return } - // 查找文档并更新关闭时间 - found := false + rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs) + rootIDsMap := make(map[string]bool, len(rootIDs)) + for _, id := range rootIDs { + rootIDsMap[id] = true + } + + closeTime := time.Now().Unix() + + // 更新已存在的文档 + updated := false for _, doc := range recentDocs { - if doc.RootID == rootID { - doc.ClosedAt = time.Now().Unix() - found = true - break + if rootIDsMap[doc.RootID] { + doc.ClosedAt = closeTime + updated = true + delete(rootIDsMap, doc.RootID) // 标记已处理 } } - if found { + // 为不存在的文档创建新记录 + for rootID := range rootIDsMap { + tree, loadErr := LoadTreeByBlockID(rootID) + if loadErr != nil { + continue + } + + recentDoc := &RecentDoc{ + RootID: tree.Root.ID, + ClosedAt: closeTime, // 设置关闭时间 + } + + recentDocs = append([]*RecentDoc{recentDoc}, recentDocs...) + updated = true + } + + if updated { err = setRecentDocs(recentDocs) } return @@ -196,6 +276,8 @@ func GetRecentDocs(sortBy string) (ret []*RecentDoc, err error) { } func setRecentDocs(recentDocs []*RecentDoc) (err error) { + recentDocs = normalizeRecentDocs(recentDocs) + dirPath := filepath.Join(util.DataDir, "storage") if err = os.MkdirAll(dirPath, 0755); err != nil { logging.LogErrorf("create storage [recent-doc] dir failed: %s", err) @@ -217,8 +299,7 @@ func setRecentDocs(recentDocs []*RecentDoc) (err error) { return } -func getRecentDocs(sortBy string) (ret []*RecentDoc, err error) { - tmp := []*RecentDoc{} +func loadRecentDocsRaw() (ret []*RecentDoc, err error) { dataPath := filepath.Join(util.DataDir, "storage/recent-doc.json") if !filelock.IsExist(dataPath) { return @@ -230,7 +311,7 @@ func getRecentDocs(sortBy string) (ret []*RecentDoc, err error) { return } - if err = gulu.JSON.UnmarshalJSON(data, &tmp); err != nil { + if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil { logging.LogErrorf("unmarshal storage [recent-doc] failed: %s", err) if err = setRecentDocs([]*RecentDoc{}); err != nil { logging.LogErrorf("reset storage [recent-doc] failed: %s", err) @@ -238,16 +319,40 @@ func getRecentDocs(sortBy string) (ret []*RecentDoc, err error) { ret = []*RecentDoc{} return } + return +} + +func getRecentDocs(sortBy string) (ret []*RecentDoc, err error) { + ret = []*RecentDoc{} // 初始化为空切片,确保 API 始终返回非 nil + recentDocs, err := loadRecentDocsRaw() + if err != nil { + return + } + + // 去重 + seen := make(map[string]struct{}, len(recentDocs)) + var deduplicated []*RecentDoc + for _, doc := range recentDocs { + if _, ok := seen[doc.RootID]; !ok { + seen[doc.RootID] = struct{}{} + deduplicated = append(deduplicated, doc) + } + } var rootIDs []string - for _, doc := range tmp { + for _, doc := range deduplicated { rootIDs = append(rootIDs, doc.RootID) } bts := treenode.GetBlockTrees(rootIDs) var notExists []string - for _, doc := range tmp { + for _, doc := range deduplicated { if bt := bts[doc.RootID]; nil != bt { + // 获取最新的文档标题和图标 doc.Title = path.Base(bt.HPath) // Recent docs not updated after renaming https://github.com/siyuan-note/siyuan/issues/7827 + ial := sql.GetBlockAttrs(doc.RootID) + if "" != ial["icon"] { + doc.Icon = ial["icon"] + } ret = append(ret, doc) } else { notExists = append(notExists, doc.RootID) @@ -255,53 +360,104 @@ func getRecentDocs(sortBy string) (ret []*RecentDoc, err error) { } if 0 < len(notExists) { - setRecentDocs(ret) + err = setRecentDocs(ret) + if err != nil { + return + } } // 根据排序参数进行排序 switch sortBy { - case "closedAt": // 按关闭时间排序 - sort.Slice(ret, func(i, j int) bool { - if ret[i].ClosedAt == 0 && ret[j].ClosedAt == 0 { - // 如果都没有关闭时间,按浏览时间排序 - return ret[i].ViewedAt > ret[j].ViewedAt - } - if ret[i].ClosedAt == 0 { - return false // 没有关闭时间的排在后面 - } - if ret[j].ClosedAt == 0 { - return true // 有关闭时间的排在前面 + case "updated": // 按更新时间排序 + // 从数据库查询最近修改的文档 + sqlBlocks := sql.SelectBlocksRawStmt("SELECT * FROM blocks WHERE type = 'd' ORDER BY updated DESC", 1, 32) + ret = []*RecentDoc{} + if 1 > len(sqlBlocks) { + return + } + + // 获取文档树信息 + var rootIDs []string + for _, sqlBlock := range sqlBlocks { + rootIDs = append(rootIDs, sqlBlock.ID) + } + bts := treenode.GetBlockTrees(rootIDs) + + for _, sqlBlock := range sqlBlocks { + // 解析 IAL 获取 icon + icon := "" + if sqlBlock.IAL != "" { + ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:") + ialStr = strings.TrimSuffix(ialStr, "}") + ial := parse.Tokens2IAL([]byte(ialStr)) + for _, kv := range ial { + if kv[0] == "icon" { + icon = kv[1] + break + } + } } - return ret[i].ClosedAt > ret[j].ClosedAt - }) - case "openAt": // 按打开时间排序 - sort.Slice(ret, func(i, j int) bool { - if ret[i].OpenAt == 0 && ret[j].OpenAt == 0 { - // 如果都没有打开时间,按ID时间排序(ID包含时间信息) - return ret[i].RootID > ret[j].RootID + // 获取文档标题 + title := "" + if bt := bts[sqlBlock.ID]; nil != bt { + title = path.Base(bt.HPath) } - if ret[i].OpenAt == 0 { - return false // 没有打开时间的排在后面 + if title == "" { + title = sqlBlock.Content + if title == "" { + title = sqlBlock.HPath + if title == "" { + title = sqlBlock.ID + } + } } - if ret[j].OpenAt == 0 { - return true // 有打开时间的排在前面 + doc := &RecentDoc{ + RootID: sqlBlock.ID, + Icon: icon, + Title: title, } - return ret[i].OpenAt > ret[j].OpenAt - }) - default: // 默认按浏览时间排序 - sort.Slice(ret, func(i, j int) bool { - if ret[i].ViewedAt == 0 && ret[j].ViewedAt == 0 { - // 如果都没有浏览时间,按ID时间排序(ID包含时间信息) - return ret[i].RootID > ret[j].RootID + ret = append(ret, doc) + } + case "closedAt": // 按关闭时间排序 + filtered := []*RecentDoc{} // 初始化为空切片,确保 API 始终返回非 nil + for _, doc := range ret { + if doc.ClosedAt > 0 { + filtered = append(filtered, doc) } - if ret[i].ViewedAt == 0 { - return false // 没有浏览时间的排在后面 + } + ret = filtered + if 0 < len(ret) { + sort.Slice(ret, func(i, j int) bool { + return ret[i].ClosedAt > ret[j].ClosedAt + }) + } + case "openAt": // 按打开时间排序 + filtered := []*RecentDoc{} // 初始化为空切片,确保 API 始终返回非 nil + for _, doc := range ret { + if doc.OpenAt > 0 { + filtered = append(filtered, doc) } - if ret[j].ViewedAt == 0 { - return true // 有浏览时间的排在前面 + } + ret = filtered + if 0 < len(ret) { + sort.Slice(ret, func(i, j int) bool { + return ret[i].OpenAt > ret[j].OpenAt + }) + } + case "viewedAt": // 按浏览时间排序 + default: + filtered := []*RecentDoc{} // 初始化为空切片,确保 API 始终返回非 nil + for _, doc := range ret { + if doc.ViewedAt > 0 { + filtered = append(filtered, doc) } - return ret[i].ViewedAt > ret[j].ViewedAt - }) + } + ret = filtered + if 0 < len(ret) { + sort.Slice(ret, func(i, j int) bool { + return ret[i].ViewedAt > ret[j].ViewedAt + }) + } } return }