mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-16 14:40:12 +01:00
Merge 6419f5c7fb into db4fb41024
This commit is contained in:
commit
62402fe1a1
9 changed files with 390 additions and 212 deletions
|
|
@ -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 += `<li data-index="${index}" data-node-id="${item.rootID}" class="b3-list-item${index === 0 ? " b3-list-item--focus" : ""}">
|
||||
${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}
|
||||
<span class="b3-list-item__text">${escapeHtml(item.title)}</span>
|
||||
if (key) {
|
||||
data = data.filter((item) => {
|
||||
return item.title.toLowerCase().includes(key.toLowerCase());
|
||||
});
|
||||
}
|
||||
data.forEach((item) => {
|
||||
tabHtml += `<li data-index="${index}" data-node-id="${item.rootID}" class="b3-list-item${index === 0 ? " b3-list-item--focus" : ""}">
|
||||
${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file, "b3-list-item__graphic", true)}
|
||||
<span class="b3-list-item__text">${escapeHtml(item.title)}</span>
|
||||
</li>`;
|
||||
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 = '<ul class="b3-list b3-list--background" style="overflow: auto;width: 200px;">';
|
||||
let docIndex = 0;
|
||||
if (!key || window.siyuan.languages.riffCard.toLowerCase().includes(key.toLowerCase())) {
|
||||
dockHtml += `<li data-type="riffCard" data-index="0" class="b3-list-item${!switchPath ? " b3-list-item--focus" : ""}">
|
||||
<svg class="b3-list-item__graphic"><use xlink:href="#iconRiffCard"></use></svg>
|
||||
|
|
@ -57,30 +52,30 @@ ${unicode2Emoji(item.icon || window.siyuan.storage[Constants.LOCAL_IMAGES].file,
|
|||
if (!switchPath) {
|
||||
switchPath = window.siyuan.languages.riffCard;
|
||||
}
|
||||
docIndex++;
|
||||
}
|
||||
let docIndex = 1;
|
||||
getAllDocks().forEach((item) => {
|
||||
if (!key || item.title.toLowerCase().includes(key.toLowerCase())) {
|
||||
dockHtml += `<li data-type="${item.type}" data-index="${docIndex}" class="b3-list-item${!switchPath ? " b3-list-item--focus" : ""}">
|
||||
<svg class="b3-list-item__graphic"><use xlink:href="#${item.icon}"></use></svg>
|
||||
<span class="b3-list-item__text">${item.title}</span>
|
||||
<span class="b3-list-item__meta">${updateHotkeyTip(item.hotkey || "")}</span>
|
||||
<span class="b3-list-item__meta">${updateHotkeyTip(item.hotkey)}</span>
|
||||
</li>`;
|
||||
docIndex++;
|
||||
if (!switchPath) {
|
||||
switchPath = window.siyuan.languages.riffCard;
|
||||
switchPath = item.title;
|
||||
}
|
||||
docIndex++;
|
||||
}
|
||||
});
|
||||
dockHtml = dockHtml + "</ul>";
|
||||
dockHtml = '<ul class="b3-list b3-list--background" style="overflow: auto;width: 200px;">' + dockHtml + "</ul>";
|
||||
}
|
||||
|
||||
const pathElement = element.querySelector(".switch-doc__path");
|
||||
pathElement.innerHTML = switchPath;
|
||||
pathElement.previousElementSibling.innerHTML = `<div class="fn__flex fn__flex-1" style="overflow:auto;">
|
||||
${dockHtml}
|
||||
<ul style="${isWindow() ? "border-left:0;" : ""}min-width:360px;" class="b3-list b3-list--background fn__flex-1">${tabHtml}</ul>
|
||||
</div>`;
|
||||
pathElement.previousElementSibling.innerHTML = `<div class="fn__flex fn__flex-1" style="overflow: auto;">
|
||||
${dockHtml}
|
||||
<ul style="${isWindow() ? "border-left: 0;" : ""}min-width: 360px;" class="b3-list b3-list--background fn__flex-1">${tabHtml}</ul>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
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 = () => {
|
|||
<span class="fn__space"></span>
|
||||
<div class="fn__flex-center">
|
||||
<select class="b3-select" id="recentDocsSort">
|
||||
<option value="viewedAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "viewedAt" ? " selected" : ""}>${window.siyuan.languages.recentViewed}</option>
|
||||
<option value="updated"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "updated" ? " selected" : ""}>${window.siyuan.languages.recentModified}</option>
|
||||
<option value="openAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "openAt" ? " selected" : ""}>${window.siyuan.languages.recentOpened}</option>
|
||||
<option value="closedAt"${window.siyuan.storage[Constants.LOCAL_RECENT_DOCS].type === "closedAt" ? " selected" : ""}>${window.siyuan.languages.recentClosed}</option>
|
||||
<option value="viewedAt">${window.siyuan.languages.recentViewed}</option>
|
||||
<option value="updated">${window.siyuan.languages.recentModified}</option>
|
||||
<option value="openAt">${window.siyuan.languages.recentOpened}</option>
|
||||
<option value="closedAt">${window.siyuan.languages.recentClosed}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`,
|
||||
content: `<div class="fn__flex-column switch-doc">
|
||||
<div class="fn__flex fn__flex-1" style="overflow:auto;"></div>
|
||||
<div class="fn__flex fn__flex-1" style="overflow: auto;"></div>
|
||||
<div class="switch-doc__path"></div>
|
||||
</div>`,
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -779,7 +779,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) {
|
||||
|
|
@ -795,8 +795,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);
|
||||
|
|
@ -842,7 +844,7 @@ export class Wnd {
|
|||
}
|
||||
}
|
||||
});
|
||||
if (latestHeadElement && !closeAll) {
|
||||
if (latestHeadElement && !isBatchClose) {
|
||||
this.switchTab(latestHeadElement, true, true, false, false);
|
||||
this.showHeading();
|
||||
}
|
||||
|
|
@ -890,7 +892,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) {
|
||||
|
|
@ -899,9 +901,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -749,7 +749,6 @@ func GetDoc(startID, endID, id string, index int, query string, queryTypes map[s
|
|||
}
|
||||
keywords = gulu.Str.RemoveDuplicatedElem(keywords)
|
||||
|
||||
go setRecentDocByTree(tree)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1595,7 +1594,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) {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -113,8 +113,7 @@ func (box *Box) Unindex() {
|
|||
}
|
||||
|
||||
func unindex(boxID string) {
|
||||
ids := treenode.RemoveBlockTreesByBoxID(boxID)
|
||||
RemoveRecentDoc(ids)
|
||||
treenode.RemoveBlockTreesByBoxID(boxID)
|
||||
sql.DeleteBoxQueue(boxID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
err = setRecentDocs(recentDocs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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(), // 设置文档打开时间
|
||||
if len(deduplicated) <= 32 {
|
||||
// 清空 Title 和 Icon
|
||||
for _, doc := range deduplicated {
|
||||
doc.Title = ""
|
||||
doc.Icon = ""
|
||||
}
|
||||
return deduplicated
|
||||
}
|
||||
|
||||
recentDocLock.Lock()
|
||||
defer recentDocLock.Unlock()
|
||||
// 分别统计三种类型的记录
|
||||
var viewedDocs []*RecentDoc
|
||||
var openedDocs []*RecentDoc
|
||||
var closedDocs []*RecentDoc
|
||||
|
||||
recentDocs, err := getRecentDocs("")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i, c := range recentDocs {
|
||||
if c.RootID == recentDoc.RootID {
|
||||
recentDocs = append(recentDocs[:i], recentDocs[i+1:]...)
|
||||
break
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
recentDocs = append([]*RecentDoc{recentDoc}, recentDocs...)
|
||||
if 32 < len(recentDocs) {
|
||||
recentDocs = recentDocs[:32]
|
||||
// 分别按时间排序并截取 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]
|
||||
}
|
||||
|
||||
err = setRecentDocs(recentDocs)
|
||||
return
|
||||
// 合并三类记录
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]*RecentDoc, 0, len(docMap))
|
||||
for _, doc := range docMap {
|
||||
// 清空 Title 和 Icon
|
||||
doc.Title = ""
|
||||
doc.Icon = ""
|
||||
result = append(result, doc)
|
||||
}
|
||||
|
||||
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 "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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取文档标题
|
||||
title := ""
|
||||
if bt := bts[sqlBlock.ID]; nil != bt {
|
||||
title = path.Base(bt.HPath)
|
||||
}
|
||||
if title == "" {
|
||||
title = sqlBlock.Content
|
||||
if title == "" {
|
||||
title = sqlBlock.HPath
|
||||
if title == "" {
|
||||
title = sqlBlock.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
doc := &RecentDoc{
|
||||
RootID: sqlBlock.ID,
|
||||
Icon: icon,
|
||||
Title: title,
|
||||
}
|
||||
ret = append(ret, doc)
|
||||
}
|
||||
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
|
||||
filtered := []*RecentDoc{} // 初始化为空切片,确保 API 始终返回非 nil
|
||||
for _, doc := range ret {
|
||||
if doc.ClosedAt > 0 {
|
||||
filtered = append(filtered, doc)
|
||||
}
|
||||
if ret[i].ClosedAt == 0 {
|
||||
return false // 没有关闭时间的排在后面
|
||||
}
|
||||
if ret[j].ClosedAt == 0 {
|
||||
return true // 有关闭时间的排在前面
|
||||
}
|
||||
return ret[i].ClosedAt > ret[j].ClosedAt
|
||||
})
|
||||
}
|
||||
ret = filtered
|
||||
if 0 < len(ret) {
|
||||
sort.Slice(ret, func(i, j int) bool {
|
||||
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
|
||||
filtered := []*RecentDoc{} // 初始化为空切片,确保 API 始终返回非 nil
|
||||
for _, doc := range ret {
|
||||
if doc.OpenAt > 0 {
|
||||
filtered = append(filtered, doc)
|
||||
}
|
||||
if ret[i].OpenAt == 0 {
|
||||
return false // 没有打开时间的排在后面
|
||||
}
|
||||
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)
|
||||
}
|
||||
if ret[j].OpenAt == 0 {
|
||||
return true // 有打开时间的排在前面
|
||||
}
|
||||
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
|
||||
}
|
||||
if ret[i].ViewedAt == 0 {
|
||||
return false // 没有浏览时间的排在后面
|
||||
}
|
||||
if ret[j].ViewedAt == 0 {
|
||||
return true // 有浏览时间的排在前面
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue