1
0
forked from askmiw/goknow
This commit is contained in:
2026-02-13 12:37:01 +08:00
parent 9f6a098b9a
commit b2e1c383d7

508
main.go Normal file
View File

@@ -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()
}