From b2e1c383d76e0dadae6e63eee71032851220f91f Mon Sep 17 00:00:00 2001 From: mrzhou Date: Fri, 13 Feb 2026 12:37:01 +0800 Subject: [PATCH] first --- main.go | 508 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..b2427a0 --- /dev/null +++ b/main.go @@ -0,0 +1,508 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// Note 笔记数据结构 +type Note struct { + ID string `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// NoteStore 笔记存储管理 +type NoteStore struct { + notes []Note + path string + app fyne.App + window fyne.Window +} + +func NewNoteStore(app fyne.App, window fyne.Window) *NoteStore { + home, _ := os.UserHomeDir() + path := filepath.Join(home, ".goknow", "notes.json") + os.MkdirAll(filepath.Dir(path), 0755) + + store := &NoteStore{ + app: app, + window: window, + path: path, + } + store.Load() + return store +} + +func (s *NoteStore) Load() { + data, err := os.ReadFile(s.path) + if err != nil { + s.notes = []Note{} + return + } + json.Unmarshal(data, &s.notes) +} + +func (s *NoteStore) Save() { + data, _ := json.MarshalIndent(s.notes, "", " ") + os.WriteFile(s.path, data, 0644) +} + +func (s *NoteStore) Add(note Note) { + s.notes = append(s.notes, note) + s.Save() +} + +func (s *NoteStore) Update(id string, title, content string, tags []string) { + for i := range s.notes { + if s.notes[i].ID == id { + s.notes[i].Title = title + s.notes[i].Content = content + s.notes[i].Tags = tags + s.notes[i].UpdatedAt = time.Now() + s.Save() + return + } + } +} + +func (s *NoteStore) Delete(id string) { + for i := range s.notes { + if s.notes[i].ID == id { + s.notes = append(s.notes[:i], s.notes[i+1:]...) + s.Save() + return + } + } +} + +func (s *NoteStore) Search(query string) []Note { + if query == "" { + // 按更新时间排序 + sort.Slice(s.notes, func(i, j int) bool { + return s.notes[i].UpdatedAt.After(s.notes[j].UpdatedAt) + }) + return s.notes + } + + var results []Note + query = strings.ToLower(query) + for _, note := range s.notes { + if strings.Contains(strings.ToLower(note.Title), query) || + strings.Contains(strings.ToLower(note.Content), query) || + containsTag(note.Tags, query) { + results = append(results, note) + } + } + return results +} + +func containsTag(tags []string, query string) bool { + for _, tag := range tags { + if strings.Contains(strings.ToLower(tag), query) { + return true + } + } + return false +} + +// UI 组件 +type NoteApp struct { + store *NoteStore + window fyne.Window + noteList *widget.List + searchBox *widget.Entry + titleEntry *widget.Entry + content *widget.Entry + tagsEntry *widget.Entry + preview *widget.RichText + currentID string + notes []Note +} + +func NewNoteApp(app fyne.App) *NoteApp { + window := app.NewWindow("GoKnow - 知识管理") + window.Resize(fyne.NewSize(1200, 800)) + window.CenterOnScreen() + + noteApp := &NoteApp{ + window: window, + } + noteApp.store = NewNoteStore(app, window) + noteApp.setupUI() + return noteApp +} + +func (a *NoteApp) setupUI() { + // 左侧边栏 - 搜索和列表 + a.searchBox = widget.NewEntry() + a.searchBox.SetPlaceHolder("🔍 搜索笔记、标签...") + a.searchBox.OnChanged = func(s string) { + a.refreshList() + } + + // 新建按钮 + newBtn := widget.NewButtonWithIcon("新建笔记", theme.ContentAddIcon(), func() { + a.createNewNote() + }) + newBtn.Importance = widget.HighImportance + + // 笔记列表 + a.noteList = widget.NewList( + func() int { return len(a.notes) }, + func() fyne.CanvasObject { + title := widget.NewLabel("Title") + timeLabel := widget.NewLabel("Time") + return container.NewBorder( + nil, nil, + widget.NewIcon(theme.DocumentIcon()), + timeLabel, + title, + ) + }, + func(i widget.ListItemID, o fyne.CanvasObject) { + note := a.notes[i] + box := o.(*fyne.Container) + + // The structure from NewBorder with (top, bottom, left, right, center) is: + // Objects[0]: center (title), Objects[1]: left (icon), Objects[2]: right (timeLabel) + title := box.Objects[0].(*widget.Label) + title.SetText(note.Title) + if note.Title == "" { + title.SetText("无标题") + } + + timeLabel := box.Objects[2].(*widget.Label) + timeLabel.SetText(formatTime(note.UpdatedAt)) + timeLabel.TextStyle = fyne.TextStyle{Italic: true} + }, + ) + + a.noteList.OnSelected = func(id widget.ListItemID) { + if id < len(a.notes) { + a.loadNote(a.notes[id]) + } + } + + sidebar := container.NewBorder( + container.NewVBox(a.searchBox, newBtn), + nil, nil, nil, + a.noteList, + ) + + // 右侧编辑器 + a.titleEntry = widget.NewEntry() + a.titleEntry.SetPlaceHolder("笔记标题") + a.titleEntry.TextStyle = fyne.TextStyle{Bold: true} + + a.tagsEntry = widget.NewEntry() + a.tagsEntry.SetPlaceHolder("标签: 用逗号分隔, 如: go, 编程, 笔记") + + a.content = widget.NewMultiLineEntry() + a.content.SetPlaceHolder("在此输入 Markdown 内容...\n\n支持:\n- 标题\n- **粗体**\n- *斜体*\n- `代码`\n- [链接](url)") + a.content.Wrapping = fyne.TextWrapWord + + // 预览区域 + a.preview = widget.NewRichText() + a.preview.Wrapping = fyne.TextWrapWord + + // 工具栏 + saveBtn := widget.NewButtonWithIcon("保存", theme.DocumentSaveIcon(), func() { + a.saveCurrentNote() + }) + saveBtn.Importance = widget.HighImportance + + deleteBtn := widget.NewButtonWithIcon("删除", theme.DeleteIcon(), func() { + a.deleteCurrentNote() + }) + + toolbar := container.NewHBox(saveBtn, deleteBtn, layout.NewSpacer()) + + // 编辑器容器 + editor := container.NewBorder( + container.NewVBox( + a.titleEntry, + widget.NewSeparator(), + a.tagsEntry, + widget.NewSeparator(), + toolbar, + ), + nil, nil, nil, + container.NewHSplit( + a.content, + container.NewScroll(a.preview), + ), + ) + + // 主布局 + split := container.NewHSplit(sidebar, editor) + split.Offset = 0.3 + + a.window.SetContent(split) + + // 实时预览 + a.content.OnChanged = func(s string) { + a.updatePreview(s) + } + + // 初始加载 + a.refreshList() + if len(a.notes) > 0 { + a.noteList.Select(0) + } else { + a.createNewNote() + } +} + +func (a *NoteApp) createNewNote() { + note := Note{ + ID: fmt.Sprintf("%d", time.Now().UnixNano()), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + a.store.Add(note) + a.refreshList() + a.selectNoteByID(note.ID) + a.titleEntry.SetText("") + a.content.SetText("") + a.tagsEntry.SetText("") + a.currentID = note.ID + a.updatePreview("") +} + +func (a *NoteApp) loadNote(note Note) { + a.currentID = note.ID + a.titleEntry.SetText(note.Title) + a.content.SetText(note.Content) + a.tagsEntry.SetText(strings.Join(note.Tags, ", ")) + a.updatePreview(note.Content) +} + +func (a *NoteApp) saveCurrentNote() { + if a.currentID == "" { + return + } + + tags := []string{} + for _, t := range strings.Split(a.tagsEntry.Text, ",") { + t = strings.TrimSpace(t) + if t != "" { + tags = append(tags, t) + } + } + + a.store.Update(a.currentID, a.titleEntry.Text, a.content.Text, tags) + a.refreshList() + + // 显示保存动画提示 + a.showToast("已保存") +} + +func (a *NoteApp) deleteCurrentNote() { + if a.currentID == "" { + return + } + + dialog.ShowConfirm("确认删除", "确定要删除这条笔记吗?", func(confirm bool) { + if confirm { + a.store.Delete(a.currentID) + a.refreshList() + if len(a.notes) > 0 { + a.noteList.Select(0) + } else { + a.createNewNote() + } + } + }, a.window) +} + +func (a *NoteApp) refreshList() { + a.notes = a.store.Search(a.searchBox.Text) + a.noteList.Refresh() + if len(a.notes) == 0 && a.searchBox.Text == "" { + // 如果没有笔记且不是搜索状态,创建默认笔记 + a.createNewNote() + } +} + +func (a *NoteApp) selectNoteByID(id string) { + for i, note := range a.notes { + if note.ID == id { + a.noteList.Select(i) + return + } + } +} + +func (a *NoteApp) updatePreview(markdown string) { + // 简单的 Markdown 渲染 + segments := parseMarkdown(markdown) + a.preview.Segments = segments + a.preview.Refresh() +} + +func (a *NoteApp) showToast(message string) { + // 创建临时提示 + toast := canvas.NewText(message, theme.PrimaryColor()) + toast.TextSize = 16 + toast.TextStyle = fyne.TextStyle{Bold: true} + + // 添加到窗口右上角 + go func() { + time.Sleep(2 * time.Second) + }() +} + +func formatTime(t time.Time) string { + now := time.Now() + diff := now.Sub(t) + + if diff < time.Hour { + return fmt.Sprintf("%d分钟前", int(diff.Minutes())) + } else if diff < 24*time.Hour { + return fmt.Sprintf("%d小时前", int(diff.Hours())) + } else if diff < 7*24*time.Hour { + return fmt.Sprintf("%d天前", int(diff.Hours()/24)) + } + return t.Format("01-02") +} + +// 简单的 Markdown 解析器 +func parseMarkdown(text string) []widget.RichTextSegment { + var segments []widget.RichTextSegment + lines := strings.Split(text, "\n") + + for i, line := range lines { + // 标题 + if strings.HasPrefix(line, "# ") { + seg := &widget.TextSegment{ + Text: strings.TrimPrefix(line, "# "), + Style: widget.RichTextStyleHeading, + } + seg.Style.TextStyle = fyne.TextStyle{Bold: true} + seg.Style.SizeName = theme.SizeNameHeadingText + segments = append(segments, seg) + } else if strings.HasPrefix(line, "## ") { + seg := &widget.TextSegment{ + Text: strings.TrimPrefix(line, "## "), + Style: widget.RichTextStyleSubHeading, + } + seg.Style.TextStyle = fyne.TextStyle{Bold: true} + segments = append(segments, seg) + } else { + // 处理行内格式 + parts := parseInlineFormat(line) + segments = append(segments, parts...) + } + + // 添加换行(除了最后一行) + if i < len(lines)-1 { + segments = append(segments, &widget.TextSegment{ + Text: "\n", + Style: widget.RichTextStyleParagraph, + }) + } + } + + return segments +} + +func parseInlineFormat(line string) []widget.RichTextSegment { + var segments []widget.RichTextSegment + remaining := line + + for len(remaining) > 0 { + // 查找粗体 **text** + if idx := strings.Index(remaining, "**"); idx != -1 { + if idx > 0 { + segments = append(segments, &widget.TextSegment{ + Text: remaining[:idx], + Style: widget.RichTextStyleInline, + }) + } + end := strings.Index(remaining[idx+2:], "**") + if end != -1 { + segments = append(segments, &widget.TextSegment{ + Text: remaining[idx+2 : idx+2+end], + Style: widget.RichTextStyleStrong, + }) + remaining = remaining[idx+2+end+2:] + continue + } + } + + // 查找斜体 *text* + if idx := strings.Index(remaining, "*"); idx != -1 && !strings.HasPrefix(remaining, "**") { + if idx > 0 { + segments = append(segments, &widget.TextSegment{ + Text: remaining[:idx], + Style: widget.RichTextStyleInline, + }) + } + end := strings.Index(remaining[idx+1:], "*") + if end != -1 { + segments = append(segments, &widget.TextSegment{ + Text: remaining[idx+1 : idx+1+end], + Style: widget.RichTextStyleEmphasis, + }) + remaining = remaining[idx+1+end+1:] + continue + } + } + + // 查找代码 `text` + if idx := strings.Index(remaining, "`"); idx != -1 { + if idx > 0 { + segments = append(segments, &widget.TextSegment{ + Text: remaining[:idx], + Style: widget.RichTextStyleInline, + }) + } + end := strings.Index(remaining[idx+1:], "`") + if end != -1 { + seg := &widget.TextSegment{ + Text: remaining[idx+1 : idx+1+end], + Style: widget.RichTextStyleCodeBlock, + } + seg.Style.ColorName = theme.ColorNamePrimary + segments = append(segments, seg) + remaining = remaining[idx+1+end+1:] + continue + } + } + + // 普通文本 + segments = append(segments, &widget.TextSegment{ + Text: remaining, + Style: widget.RichTextStyleInline, + }) + break + } + + return segments +} + +func main() { + myApp := app.NewWithID("cn.miw.goknow") + myApp.Settings().SetTheme(theme.DarkTheme()) + + noteApp := NewNoteApp(myApp) + noteApp.window.ShowAndRun() +}