first
This commit is contained in:
508
main.go
Normal file
508
main.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user