Improve the outline panel (#15814)

*  实现大纲持久化
- 添加折叠状态变化的实时保存功能
- 在文档更新时恢复折叠状态
- 保存拖拽前的折叠状态并在拖拽后恢复
- 更新本地存储结构以支持折叠状态

* 🎨 clean code

*  在`data/storage`文件夹下创建`outline/${docID}.json`文件,存储标题outline展开信息

*  alt click折叠/展开统计标题

* 添加层级控制功能
- 新增层级控制滑条,允许用户展开指定层级

* 🌐 添加多语言支持的展开层级功能

*  f添加层级重置显示功能
- 新增 resetLevelDisplay 方法以重置层级显示状态
- 更新层级控制的初始化逻辑,默认不显示层级
- 在文档切换时重置层级显示状态

* 优化层级控制功能
- 添加用户主动层级控制标记
- 修改层级显示重置逻辑,仅在非用户操作时重置
- 更新层级控制滑条的点击事件处理

* ♻️ 重构大纲存储逻辑
- 合并大纲存储为单一文件 outline.json

* ♻️ outline.json 单行存储

* ♻️ outline.json参考recent-doc.json,只存储前1000个文档信息,每次新增信息会把数据放在最前面

* ♻️单行存储json

* ♻️ 增加outline.json存储限制至2000个

*  新增`保持当前标题展开`按钮,`保持全部展开`改为`全部展开`按钮

*  保持当前标题展开优化
- 超过两级折叠,也能都展开
- 如果父节点折叠,展开时自动折叠兄弟节点,只展开当前节点路径,如果父节点是展开状态,则不影响兄弟节点折叠状态

* 🔥 移除层级文本

*  右键点击toggle时展开所有子标题

*  右键click点击折叠图标,会折叠/展开所有子标题

*  大纲支持筛选功能

*  feat(大纲): 优化筛选功能以显示所有子标题
- 添加 showAllDescendants 函数以显示所有子标题
- 修改 processUL 函数以在父标题命中时显示所有子标题
- 确保未命中的子标题隐藏

*  优化大纲筛选

- 如果标题命中筛选,这个标题的所有父标题展开,以显示出这个标题位置
- 如果父标题命中筛选,子项都没有命中筛选,则折叠全部子项(依然可以展开显示)
- 如果父标题命中筛选,子项也有命中筛选,则展开命中的子项,其他无关子项隐藏

* 💄 展开层级改为按钮,原先的圆点样式碍眼

* 💄 展开层级改为按钮,原先的圆点样式碍眼

* 💄 style(菜单): 优化展开层级菜单的样式和位置
- 添加图标以增强可视化
- 调整菜单弹出位置以改善用户体验

* ♻️ refactor(大纲): 优化标题级别获取逻辑
- 调整展开到指定层级的逻辑,使用标题级别进行判断

* 🎨

*  feat(大纲): 添加右键菜单功能
- 实现右键点击标题时显示上下文菜单
- 增加标题升级、降级、插入、删除等操作
- 基于标题级别展开/折叠同级标题

*  feat(大纲): 添加子标题功能
- 在右键菜单中添加“添加子标题”选项
- 实现子标题的添加逻辑,支持最大级别为H6
- 使用当前标题作为父标题,插入新子标题

*  feat(大纲): "添加子标题"确保父标题展开状态

*  feat(大纲): 使用expandIds方式保存父标题展开状态
- 确保父标题保持展开状态
- 保存展开状态到持久化存储
- 移除冗余的状态保存逻辑

*  feat(大纲): 调整右键菜单顺序,将“全部折叠”功能移至“全部展开”之后

* 🌐 i18n optimization
This commit is contained in:
Achuan-2 2025-10-15 10:01:47 +08:00 committed by GitHub
parent 0211e04c2b
commit 7545c2517f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1768 additions and 55 deletions

View file

@ -977,6 +977,9 @@
"heading4": "عنوان 4", "heading4": "عنوان 4",
"heading5": "عنوان 5", "heading5": "عنوان 5",
"heading6": "عنوان 6", "heading6": "عنوان 6",
"outlineExpandLevel": "مستوى التوسيع",
"expandAll": "توسيع الكل",
"outlineKeepCurrentExpand": "الحفاظ على التوسيع الحالي",
"general": "عام", "general": "عام",
"list1": "قائمة", "list1": "قائمة",
"element": "عناصر", "element": "عناصر",

View file

@ -977,6 +977,9 @@
"heading4": "Überschrift 4", "heading4": "Überschrift 4",
"heading5": "Überschrift 5", "heading5": "Überschrift 5",
"heading6": "Überschrift 6", "heading6": "Überschrift 6",
"outlineExpandLevel": "Expansionsebene",
"expandAll": "Alle erweitern",
"outlineKeepCurrentExpand": "Aktuelle Erweiterung beibehalten",
"general": "Allgemein", "general": "Allgemein",
"list1": "Liste", "list1": "Liste",
"element": "Element", "element": "Element",

View file

@ -977,6 +977,9 @@
"heading4": "Heading 4", "heading4": "Heading 4",
"heading5": "Heading 5", "heading5": "Heading 5",
"heading6": "Heading 6", "heading6": "Heading 6",
"outlineExpandLevel": "Expand level",
"expandAll": "Expand all",
"outlineKeepCurrentExpand": "Keep current expand",
"general": "General", "general": "General",
"list1": "List", "list1": "List",
"element": "Element", "element": "Element",

View file

@ -977,6 +977,9 @@
"heading4": "Encabezado 4", "heading4": "Encabezado 4",
"heading5": "Encabezado 5", "heading5": "Encabezado 5",
"heading6": "Encabezado 6", "heading6": "Encabezado 6",
"outlineExpandLevel": "Nivel de expansión",
"expandAll": "Expandir todo",
"outlineKeepCurrentExpand": "Mantener la expansión actual",
"general": "General", "general": "General",
"list1": "Lista", "list1": "Lista",
"element": "elemento", "element": "elemento",

View file

@ -977,6 +977,9 @@
"heading4": "Titre 4", "heading4": "Titre 4",
"heading5": "Titre 5", "heading5": "Titre 5",
"heading6": "Titre 6", "heading6": "Titre 6",
"outlineExpandLevel": "Niveau d'expansion",
"expandAll": "Tout développer",
"outlineKeepCurrentExpand": "Maintenir le titre actuel développé",
"general": "Général", "general": "Général",
"list1": "Liste", "list1": "Liste",
"element": "élément", "element": "élément",

View file

@ -977,6 +977,9 @@
"heading4": "כותרת 4", "heading4": "כותרת 4",
"heading5": "כותרת 5", "heading5": "כותרת 5",
"heading6": "כותרת 6", "heading6": "כותרת 6",
"outlineExpandLevel": "רמת הרחבה",
"expandAll": "הרחב הכל",
"outlineKeepCurrentExpand": "שמור כותרת נוכחית מורחבת",
"general": "כללי", "general": "כללי",
"list1": "רשימה", "list1": "רשימה",
"element": "אלמנט", "element": "אלמנט",

View file

@ -977,6 +977,9 @@
"heading4": "Titolo 4", "heading4": "Titolo 4",
"heading5": "Titolo 5", "heading5": "Titolo 5",
"heading6": "Titolo 6", "heading6": "Titolo 6",
"outlineExpandLevel": "展開レベル",
"expandAll": "すべて展開",
"outlineKeepCurrentExpand": "現在の見出しを展開し続ける",
"general": "Generale", "general": "Generale",
"list1": "Lista", "list1": "Lista",
"element": "elemento", "element": "elemento",

View file

@ -977,6 +977,9 @@
"heading4": "見出し4", "heading4": "見出し4",
"heading5": "見出し5", "heading5": "見出し5",
"heading6": "見出し6", "heading6": "見出し6",
"outlineExpandLevel": "展開レベル",
"expandAll": "すべて展開",
"outlineKeepCurrentExpand": "現在の見出しを展開し続ける",
"general": "一般", "general": "一般",
"list1": "リスト", "list1": "リスト",
"element": "要素", "element": "要素",

View file

@ -977,6 +977,9 @@
"heading4": "Nagłówek 4", "heading4": "Nagłówek 4",
"heading5": "Nagłówek 5", "heading5": "Nagłówek 5",
"heading6": "Nagłówek 6", "heading6": "Nagłówek 6",
"outlineExpandLevel": "Poziom rozwinięcia",
"expandAll": "Rozwiń wszystko",
"outlineKeepCurrentExpand": "Utrzymuj bieżący tytuł rozwinięty",
"general": "Ogólne", "general": "Ogólne",
"list1": "Lista", "list1": "Lista",
"element": "element", "element": "element",

View file

@ -977,6 +977,9 @@
"heading4": "Título 4", "heading4": "Título 4",
"heading5": "Título 5", "heading5": "Título 5",
"heading6": "Título 6", "heading6": "Título 6",
"outlineExpandLevel": "Nível de expansão",
"expandAll": "Expandir tudo",
"outlineKeepCurrentExpand": "Manter título atual expandido",
"general": "Geral", "general": "Geral",
"list1": "Lista", "list1": "Lista",
"element": "Elemento", "element": "Elemento",

View file

@ -977,6 +977,9 @@
"heading4": "Заголовок 4", "heading4": "Заголовок 4",
"heading5": "Заголовок 5", "heading5": "Заголовок 5",
"heading6": "Заголовок 6", "heading6": "Заголовок 6",
"outlineExpandLevel": "Уровень развертывания",
"expandAll": "Развернуть все",
"outlineKeepCurrentExpand": "Сохранять текущий заголовок развернутым",
"general": "Общее", "general": "Общее",
"list1": "Список", "list1": "Список",
"element": "элемент", "element": "элемент",

View file

@ -977,6 +977,9 @@
"heading4": "四級標題", "heading4": "四級標題",
"heading5": "五級標題", "heading5": "五級標題",
"heading6": "六級標題", "heading6": "六級標題",
"outlineExpandLevel": "展開層級",
"expandAll": "全部展開",
"outlineKeepCurrentExpand": "保持當前標題展開",
"general": "通用", "general": "通用",
"list1": "列表", "list1": "列表",
"element": "元素", "element": "元素",

View file

@ -977,6 +977,9 @@
"heading4": "四级标题", "heading4": "四级标题",
"heading5": "五级标题", "heading5": "五级标题",
"heading6": "六级标题", "heading6": "六级标题",
"outlineExpandLevel": "展开层级",
"expandAll": "全部展开",
"outlineKeepCurrentExpand": "保持当前标题展开",
"general": "通用", "general": "通用",
"list1": "列表", "list1": "列表",
"element": "元素", "element": "元素",

File diff suppressed because it is too large Load diff

View file

@ -388,7 +388,7 @@ export const getLocalStorage = (cb: () => void) => {
defaultStorage[Constants.LOCAL_AI] = []; // {name: "", memo: ""} defaultStorage[Constants.LOCAL_AI] = []; // {name: "", memo: ""}
defaultStorage[Constants.LOCAL_PLUGIN_DOCKS] = {}; // { pluginName: {dockId: IPluginDockTab}} defaultStorage[Constants.LOCAL_PLUGIN_DOCKS] = {}; // { pluginName: {dockId: IPluginDockTab}}
defaultStorage[Constants.LOCAL_PLUGINTOPUNPIN] = []; defaultStorage[Constants.LOCAL_PLUGINTOPUNPIN] = [];
defaultStorage[Constants.LOCAL_OUTLINE] = {keepExpand: true}; defaultStorage[Constants.LOCAL_OUTLINE] = {keepExpand: true, expand: {}};
defaultStorage[Constants.LOCAL_FILEPOSITION] = {}; // {id: IScrollAttr} defaultStorage[Constants.LOCAL_FILEPOSITION] = {}; // {id: IScrollAttr}
defaultStorage[Constants.LOCAL_DIALOGPOSITION] = {}; // {id: IPosition} defaultStorage[Constants.LOCAL_DIALOGPOSITION] = {}; // {id: IPosition}
defaultStorage[Constants.LOCAL_HISTORY] = { defaultStorage[Constants.LOCAL_HISTORY] = {

View file

@ -16,8 +16,9 @@ export class Tree {
private ctrlClick: (element: HTMLElement) => void; private ctrlClick: (element: HTMLElement) => void;
private toggleClick: (element: Element) => void; private toggleClick: (element: Element) => void;
private shiftClick: (element: HTMLElement) => void; private shiftClick: (element: HTMLElement) => void;
private altClick: (element: HTMLElement) => void; private altClick: (element: HTMLElement, event?: MouseEvent) => void;
private rightClick: (element: HTMLElement, event: MouseEvent) => void; private rightClick: (element: HTMLElement, event: MouseEvent) => void;
public onToggleChange: () => void;
constructor(options: { constructor(options: {
element: HTMLElement, element: HTMLElement,
@ -26,10 +27,11 @@ export class Tree {
topExtHTML?: string, topExtHTML?: string,
click?(element: HTMLElement, event: MouseEvent): void click?(element: HTMLElement, event: MouseEvent): void
ctrlClick?(element: HTMLElement): void ctrlClick?(element: HTMLElement): void
altClick?(element: HTMLElement): void altClick?(element: HTMLElement, event?: MouseEvent): void
shiftClick?(element: HTMLElement): void shiftClick?(element: HTMLElement): void
toggleClick?(element: HTMLElement): void toggleClick?(element: HTMLElement): void
rightClick?(element: HTMLElement, event: MouseEvent): void rightClick?(element: HTMLElement, event: MouseEvent): void
onToggleChange?: () => void
}) { }) {
this.click = options.click; this.click = options.click;
this.ctrlClick = options.ctrlClick; this.ctrlClick = options.ctrlClick;
@ -37,6 +39,7 @@ export class Tree {
this.shiftClick = options.shiftClick; this.shiftClick = options.shiftClick;
this.rightClick = options.rightClick; this.rightClick = options.rightClick;
this.toggleClick = options.toggleClick; this.toggleClick = options.toggleClick;
this.onToggleChange = options.onToggleChange;
this.element = options.element; this.element = options.element;
this.blockExtHTML = options.blockExtHTML; this.blockExtHTML = options.blockExtHTML;
this.topExtHTML = options.topExtHTML; this.topExtHTML = options.topExtHTML;
@ -204,7 +207,18 @@ data-def-path="${item.defPath}">
this.element.addEventListener("contextmenu", (event) => { this.element.addEventListener("contextmenu", (event) => {
let target = event.target as HTMLElement; let target = event.target as HTMLElement;
while (target && !target.isEqualNode(this.element)) { while (target && !target.isEqualNode(this.element)) {
if (target.tagName === "LI" && this.rightClick) { if (target.classList.contains("b3-list-item__toggle") && !target.classList.contains("fn__hidden")) {
// 右键点击toggle时展开所有子标题
this.expandAllChildren(target.parentElement);
this.setCurrent(target.parentElement);
// 触发折叠状态变化事件
if (this.onToggleChange) {
this.onToggleChange();
}
event.preventDefault();
event.stopPropagation();
break;
} else if (target.tagName === "LI" && this.rightClick) {
this.rightClick(target, event); this.rightClick(target, event);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -219,6 +233,10 @@ data-def-path="${item.defPath}">
if (target.classList.contains("b3-list-item__toggle") && !target.classList.contains("fn__hidden")) { if (target.classList.contains("b3-list-item__toggle") && !target.classList.contains("fn__hidden")) {
this.toggleBlocks(target.parentElement); this.toggleBlocks(target.parentElement);
this.setCurrent(target.parentElement); this.setCurrent(target.parentElement);
// 触发折叠状态变化事件
if (this.onToggleChange) {
this.onToggleChange();
}
event.preventDefault(); event.preventDefault();
break; break;
} }
@ -237,7 +255,7 @@ data-def-path="${item.defPath}">
if (this.ctrlClick && window.siyuan.ctrlIsPressed) { if (this.ctrlClick && window.siyuan.ctrlIsPressed) {
this.ctrlClick(target); this.ctrlClick(target);
} else if (this.altClick && window.siyuan.altIsPressed) { } else if (this.altClick && window.siyuan.altIsPressed) {
this.altClick(target); this.altClick(target, event);
} else if (this.shiftClick && window.siyuan.shiftIsPressed) { } else if (this.shiftClick && window.siyuan.shiftIsPressed) {
this.shiftClick(target); this.shiftClick(target);
} else if (this.click) { } else if (this.click) {
@ -271,6 +289,86 @@ data-def-path="${item.defPath}">
}); });
} }
public expandAllChildren(liElement: Element) {
if (!liElement || !liElement.nextElementSibling) {
return;
}
// 获取当前项的子列表
const nextElement = liElement.nextElementSibling;
if (!nextElement || nextElement.tagName !== "UL") {
return;
}
// 检查子元素的展开状态,如果所有子元素都已展开,则折叠;否则展开所有
const areAllChildrenExpanded = this.areAllChildrenExpanded(nextElement);
// 确保当前元素保持展开状态
const svgElement = liElement.firstElementChild.firstElementChild;
if (!svgElement.classList.contains("b3-list-item__arrow--open")) {
svgElement.classList.add("b3-list-item__arrow--open");
nextElement.classList.remove("fn__none");
if (nextElement.nextElementSibling && nextElement.nextElementSibling.tagName === "UL") {
nextElement.nextElementSibling.classList.remove("fn__none");
}
}
if (areAllChildrenExpanded) {
// 折叠所有子元素,但保持当前元素展开
this.collapseAllChildren(nextElement);
} else {
// 展开所有子元素
this.expandAllChildrenRecursive(nextElement);
}
}
private areAllChildrenExpanded(ulElement: Element): boolean {
const childItems = ulElement.querySelectorAll(":scope > li");
for (const childLi of childItems) {
const arrow = childLi.querySelector(".b3-list-item__arrow");
if (arrow && !arrow.classList.contains("b3-list-item__arrow--open")) {
return false;
}
const childUl = childLi.nextElementSibling;
if (childUl && childUl.tagName === "UL") {
if (!this.areAllChildrenExpanded(childUl)) {
return false;
}
}
}
return true;
}
private expandAllChildrenRecursive(ulElement: Element) {
ulElement.classList.remove("fn__none");
const childItems = ulElement.querySelectorAll(":scope > li");
childItems.forEach(childLi => {
const arrow = childLi.querySelector(".b3-list-item__arrow");
if (arrow) {
arrow.classList.add("b3-list-item__arrow--open");
}
const childUl = childLi.nextElementSibling;
if (childUl && childUl.tagName === "UL") {
this.expandAllChildrenRecursive(childUl);
}
});
}
private collapseAllChildren(ulElement: Element) {
const childItems = ulElement.querySelectorAll(":scope > li");
childItems.forEach(childLi => {
const arrow = childLi.querySelector(".b3-list-item__arrow");
if (arrow) {
arrow.classList.remove("b3-list-item__arrow--open");
}
const childUl = childLi.nextElementSibling;
if (childUl && childUl.tagName === "UL") {
childUl.classList.add("fn__none");
this.collapseAllChildren(childUl);
}
});
}
public expandAll() { public expandAll() {
this.element.querySelectorAll("ul").forEach(item => { this.element.querySelectorAll("ul").forEach(item => {
if (!item.classList.contains("b3-list")) { if (!item.classList.contains("b3-list")) {

View file

@ -78,6 +78,9 @@ func ServeAPI(ginServer *gin.Engine) {
ginServer.Handle("POST", "/api/storage/getCriteria", model.CheckAuth, getCriteria) ginServer.Handle("POST", "/api/storage/getCriteria", model.CheckAuth, getCriteria)
ginServer.Handle("POST", "/api/storage/removeCriterion", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeCriterion) ginServer.Handle("POST", "/api/storage/removeCriterion", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeCriterion)
ginServer.Handle("POST", "/api/storage/getRecentDocs", model.CheckAuth, getRecentDocs) ginServer.Handle("POST", "/api/storage/getRecentDocs", model.CheckAuth, getRecentDocs)
ginServer.Handle("POST", "/api/storage/getOutlineStorage", model.CheckAuth, getOutlineStorage)
ginServer.Handle("POST", "/api/storage/setOutlineStorage", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setOutlineStorage)
ginServer.Handle("POST", "/api/storage/removeOutlineStorage", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeOutlineStorage)
ginServer.Handle("POST", "/api/account/login", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, login) ginServer.Handle("POST", "/api/account/login", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, login)
ginServer.Handle("POST", "/api/account/checkActivationcode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkActivationcode) ginServer.Handle("POST", "/api/account/checkActivationcode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkActivationcode)

View file

@ -180,3 +180,59 @@ func getLocalStorage(c *gin.Context) {
data := model.GetLocalStorage() data := model.GetLocalStorage()
ret.Data = data ret.Data = data
} }
func getOutlineStorage(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
docID := arg["docID"].(string)
data, err := model.GetOutlineStorage(docID)
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = data
}
func setOutlineStorage(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
docID := arg["docID"].(string)
val := arg["val"].(interface{})
err := model.SetOutlineStorage(docID, val)
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func removeOutlineStorage(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
docID := arg["docID"].(string)
err := model.RemoveOutlineStorage(docID)
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
}

View file

@ -37,6 +37,11 @@ type RecentDoc struct {
Title string `json:"title"` Title string `json:"title"`
} }
type OutlineDoc struct {
DocID string `json:"docID"`
Data map[string]interface{} `json:"data"`
}
var recentDocLock = sync.Mutex{} var recentDocLock = sync.Mutex{}
func RemoveRecentDoc(ids []string) { func RemoveRecentDoc(ids []string) {
@ -402,3 +407,124 @@ func getLocalStorage() (ret map[string]interface{}) {
} }
return return
} }
var outlineStorageLock = sync.Mutex{}
func GetOutlineStorage(docID string) (ret map[string]interface{}, err error) {
outlineStorageLock.Lock()
defer outlineStorageLock.Unlock()
ret = map[string]interface{}{}
outlineDocs, err := getOutlineDocs()
if err != nil {
return
}
for _, doc := range outlineDocs {
if doc.DocID == docID {
ret = doc.Data
break
}
}
return
}
func SetOutlineStorage(docID string, val interface{}) (err error) {
outlineStorageLock.Lock()
defer outlineStorageLock.Unlock()
outlineDoc := &OutlineDoc{
DocID: docID,
Data: make(map[string]interface{}),
}
if valMap, ok := val.(map[string]interface{}); ok {
outlineDoc.Data = valMap
}
outlineDocs, err := getOutlineDocs()
if err != nil {
return
}
// 如果文档已存在,先移除旧的
for i, doc := range outlineDocs {
if doc.DocID == docID {
outlineDocs = append(outlineDocs[:i], outlineDocs[i+1:]...)
break
}
}
// 将新的文档信息添加到最前面
outlineDocs = append([]*OutlineDoc{outlineDoc}, outlineDocs...)
// 限制为2000个文档
if 2000 < len(outlineDocs) {
outlineDocs = outlineDocs[:2000]
}
err = setOutlineDocs(outlineDocs)
return
}
func RemoveOutlineStorage(docID string) (err error) {
outlineStorageLock.Lock()
defer outlineStorageLock.Unlock()
outlineDocs, err := getOutlineDocs()
if err != nil {
return
}
for i, doc := range outlineDocs {
if doc.DocID == docID {
outlineDocs = append(outlineDocs[:i], outlineDocs[i+1:]...)
break
}
}
err = setOutlineDocs(outlineDocs)
return
}
func setOutlineDocs(outlineDocs []*OutlineDoc) (err error) {
dirPath := filepath.Join(util.DataDir, "storage")
if err = os.MkdirAll(dirPath, 0755); err != nil {
logging.LogErrorf("create storage [outline] dir failed: %s", err)
return
}
data, err := gulu.JSON.MarshalJSON(outlineDocs)
if err != nil {
logging.LogErrorf("marshal storage [outline] failed: %s", err)
return
}
lsPath := filepath.Join(dirPath, "outline.json")
err = filelock.WriteFile(lsPath, data)
if err != nil {
logging.LogErrorf("write storage [outline] failed: %s", err)
return
}
return
}
func getOutlineDocs() (ret []*OutlineDoc, err error) {
ret = []*OutlineDoc{}
dataPath := filepath.Join(util.DataDir, "storage/outline.json")
if !filelock.IsExist(dataPath) {
return
}
data, err := filelock.ReadFile(dataPath)
if err != nil {
logging.LogErrorf("read storage [outline] failed: %s", err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
logging.LogErrorf("unmarshal storage [outline] failed: %s", err)
return
}
return
}