feat(domain): add repository interfaces and domain entities
- commit
- 8523928edad5f67d09e8e75bfa0ed5b1648a4e45
- author
- bnema · · 2025-12-09 21:32 UTC
- parent
- 513db12
- signature
- unsigned
10 files · +765 -0
A internal/domain/entity/favorite.go +117 -0
| @@ -0,0 +1,117 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // FavoriteID uniquely identifies a favorite/bookmark. | |
| 6 | + | type FavoriteID int64 | |
| 7 | + | ||
| 8 | + | // FolderID uniquely identifies a bookmark folder. | |
| 9 | + | type FolderID int64 | |
| 10 | + | ||
| 11 | + | // TagID uniquely identifies a tag. | |
| 12 | + | type TagID int64 | |
| 13 | + | ||
| 14 | + | // Favorite represents a bookmarked URL. | |
| 15 | + | type Favorite struct { | |
| 16 | + | ID FavoriteID | |
| 17 | + | URL string | |
| 18 | + | Title string | |
| 19 | + | FaviconURL string | |
| 20 | + | FolderID *FolderID // nil = root level | |
| 21 | + | ShortcutKey *int // 1-9 for quick access (Alt+1 through Alt+9) | |
| 22 | + | Position int // Order within folder | |
| 23 | + | Tags []Tag | |
| 24 | + | CreatedAt time.Time | |
| 25 | + | UpdatedAt time.Time | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // NewFavorite creates a new favorite for a URL. | |
| 29 | + | func NewFavorite(url, title string) *Favorite { | |
| 30 | + | now := time.Now() | |
| 31 | + | return &Favorite{ | |
| 32 | + | URL: url, | |
| 33 | + | Title: title, | |
| 34 | + | CreatedAt: now, | |
| 35 | + | UpdatedAt: now, | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // HasShortcut returns true if this favorite has a keyboard shortcut. | |
| 40 | + | func (f *Favorite) HasShortcut() bool { | |
| 41 | + | return f.ShortcutKey != nil && *f.ShortcutKey >= 1 && *f.ShortcutKey <= 9 | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // InFolder returns true if this favorite is in a folder. | |
| 45 | + | func (f *Favorite) InFolder() bool { | |
| 46 | + | return f.FolderID != nil | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // HasTag returns true if this favorite has the given tag. | |
| 50 | + | func (f *Favorite) HasTag(tagID TagID) bool { | |
| 51 | + | for _, t := range f.Tags { | |
| 52 | + | if t.ID == tagID { | |
| 53 | + | return true | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | return false | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Folder represents a container for organizing favorites. | |
| 60 | + | type Folder struct { | |
| 61 | + | ID FolderID | |
| 62 | + | Name string | |
| 63 | + | Icon string // Optional icon identifier | |
| 64 | + | ParentID *FolderID // nil = root level | |
| 65 | + | Position int // Order within parent | |
| 66 | + | CreatedAt time.Time | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // NewFolder creates a new folder. | |
| 70 | + | func NewFolder(name string) *Folder { | |
| 71 | + | return &Folder{ | |
| 72 | + | Name: name, | |
| 73 | + | CreatedAt: time.Now(), | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // IsRoot returns true if this folder is at root level. | |
| 78 | + | func (f *Folder) IsRoot() bool { | |
| 79 | + | return f.ParentID == nil | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Tag represents a label that can be applied to favorites. | |
| 83 | + | type Tag struct { | |
| 84 | + | ID TagID | |
| 85 | + | Name string | |
| 86 | + | Color string // Hex color code (e.g., "#FF5733") | |
| 87 | + | CreatedAt time.Time | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // NewTag creates a new tag with default color. | |
| 91 | + | func NewTag(name string) *Tag { | |
| 92 | + | return &Tag{ | |
| 93 | + | Name: name, | |
| 94 | + | Color: "#808080", // Default gray | |
| 95 | + | CreatedAt: time.Now(), | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // FavoriteTree represents a hierarchical view of folders and favorites. | |
| 100 | + | type FavoriteTree struct { | |
| 101 | + | RootFolders []*Folder | |
| 102 | + | RootFavorites []*Favorite | |
| 103 | + | FolderMap map[FolderID]*Folder // Quick lookup | |
| 104 | + | ChildFolders map[FolderID][]*Folder // Children of each folder | |
| 105 | + | ChildFavorites map[FolderID][]*Favorite // Favorites in each folder | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // NewFavoriteTree creates an empty favorite tree. | |
| 109 | + | func NewFavoriteTree() *FavoriteTree { | |
| 110 | + | return &FavoriteTree{ | |
| 111 | + | RootFolders: make([]*Folder, 0), | |
| 112 | + | RootFavorites: make([]*Favorite, 0), | |
| 113 | + | FolderMap: make(map[FolderID]*Folder), | |
| 114 | + | ChildFolders: make(map[FolderID][]*Folder), | |
| 115 | + | ChildFavorites: make(map[FolderID][]*Favorite), | |
| 116 | + | } | |
| 117 | + | } | |
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // FavoriteID uniquely identifies a favorite/bookmark. | |||
| 6 | + | type FavoriteID int64 | |||
| 7 | + | ||||
| 8 | + | // FolderID uniquely identifies a bookmark folder. | |||
| 9 | + | type FolderID int64 | |||
| 10 | + | ||||
| 11 | + | // TagID uniquely identifies a tag. | |||
| 12 | + | type TagID int64 | |||
| 13 | + | ||||
| 14 | + | // Favorite represents a bookmarked URL. | |||
| 15 | + | type Favorite struct { | |||
| 16 | + | ID FavoriteID | |||
| 17 | + | URL string | |||
| 18 | + | Title string | |||
| 19 | + | FaviconURL string | |||
| 20 | + | FolderID *FolderID // nil = root level | |||
| 21 | + | ShortcutKey *int // 1-9 for quick access (Alt+1 through Alt+9) | |||
| 22 | + | Position int // Order within folder | |||
| 23 | + | Tags []Tag | |||
| 24 | + | CreatedAt time.Time | |||
| 25 | + | UpdatedAt time.Time | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // NewFavorite creates a new favorite for a URL. | |||
| 29 | + | func NewFavorite(url, title string) *Favorite { | |||
| 30 | + | now := time.Now() | |||
| 31 | + | return &Favorite{ | |||
| 32 | + | URL: url, | |||
| 33 | + | Title: title, | |||
| 34 | + | CreatedAt: now, | |||
| 35 | + | UpdatedAt: now, | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // HasShortcut returns true if this favorite has a keyboard shortcut. | |||
| 40 | + | func (f *Favorite) HasShortcut() bool { | |||
| 41 | + | return f.ShortcutKey != nil && *f.ShortcutKey >= 1 && *f.ShortcutKey <= 9 | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // InFolder returns true if this favorite is in a folder. | |||
| 45 | + | func (f *Favorite) InFolder() bool { | |||
| 46 | + | return f.FolderID != nil | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // HasTag returns true if this favorite has the given tag. | |||
| 50 | + | func (f *Favorite) HasTag(tagID TagID) bool { | |||
| 51 | + | for _, t := range f.Tags { | |||
| 52 | + | if t.ID == tagID { | |||
| 53 | + | return true | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | return false | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // Folder represents a container for organizing favorites. | |||
| 60 | + | type Folder struct { | |||
| 61 | + | ID FolderID | |||
| 62 | + | Name string | |||
| 63 | + | Icon string // Optional icon identifier | |||
| 64 | + | ParentID *FolderID // nil = root level | |||
| 65 | + | Position int // Order within parent | |||
| 66 | + | CreatedAt time.Time | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // NewFolder creates a new folder. | |||
| 70 | + | func NewFolder(name string) *Folder { | |||
| 71 | + | return &Folder{ | |||
| 72 | + | Name: name, | |||
| 73 | + | CreatedAt: time.Now(), | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // IsRoot returns true if this folder is at root level. | |||
| 78 | + | func (f *Folder) IsRoot() bool { | |||
| 79 | + | return f.ParentID == nil | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Tag represents a label that can be applied to favorites. | |||
| 83 | + | type Tag struct { | |||
| 84 | + | ID TagID | |||
| 85 | + | Name string | |||
| 86 | + | Color string // Hex color code (e.g., "#FF5733") | |||
| 87 | + | CreatedAt time.Time | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | // NewTag creates a new tag with default color. | |||
| 91 | + | func NewTag(name string) *Tag { | |||
| 92 | + | return &Tag{ | |||
| 93 | + | Name: name, | |||
| 94 | + | Color: "#808080", // Default gray | |||
| 95 | + | CreatedAt: time.Now(), | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // FavoriteTree represents a hierarchical view of folders and favorites. | |||
| 100 | + | type FavoriteTree struct { | |||
| 101 | + | RootFolders []*Folder | |||
| 102 | + | RootFavorites []*Favorite | |||
| 103 | + | FolderMap map[FolderID]*Folder // Quick lookup | |||
| 104 | + | ChildFolders map[FolderID][]*Folder // Children of each folder | |||
| 105 | + | ChildFavorites map[FolderID][]*Favorite // Favorites in each folder | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // NewFavoriteTree creates an empty favorite tree. | |||
| 109 | + | func NewFavoriteTree() *FavoriteTree { | |||
| 110 | + | return &FavoriteTree{ | |||
| 111 | + | RootFolders: make([]*Folder, 0), | |||
| 112 | + | RootFavorites: make([]*Favorite, 0), | |||
| 113 | + | FolderMap: make(map[FolderID]*Folder), | |||
| 114 | + | ChildFolders: make(map[FolderID][]*Folder), | |||
| 115 | + | ChildFavorites: make(map[FolderID][]*Favorite), | |||
| 116 | + | } | |||
| 117 | + | } | |||
A internal/domain/entity/history.go +39 -0
| @@ -0,0 +1,39 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // HistoryEntry represents a visited URL in browsing history. | |
| 6 | + | type HistoryEntry struct { | |
| 7 | + | ID int64 | |
| 8 | + | URL string | |
| 9 | + | Title string | |
| 10 | + | FaviconURL string | |
| 11 | + | VisitCount int64 | |
| 12 | + | LastVisited time.Time | |
| 13 | + | CreatedAt time.Time | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // NewHistoryEntry creates a new history entry for a URL. | |
| 17 | + | func NewHistoryEntry(url, title string) *HistoryEntry { | |
| 18 | + | now := time.Now() | |
| 19 | + | return &HistoryEntry{ | |
| 20 | + | URL: url, | |
| 21 | + | Title: title, | |
| 22 | + | VisitCount: 1, | |
| 23 | + | LastVisited: now, | |
| 24 | + | CreatedAt: now, | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // IncrementVisit updates the entry for a new visit. | |
| 29 | + | func (h *HistoryEntry) IncrementVisit() { | |
| 30 | + | h.VisitCount++ | |
| 31 | + | h.LastVisited = time.Now() | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // HistoryMatch represents a history entry that matched a search query. | |
| 35 | + | // Used for fuzzy search results with scoring. | |
| 36 | + | type HistoryMatch struct { | |
| 37 | + | Entry *HistoryEntry | |
| 38 | + | Score float64 // Match score (higher is better) | |
| 39 | + | } | |
| @@ -0,0 +1,39 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // HistoryEntry represents a visited URL in browsing history. | |||
| 6 | + | type HistoryEntry struct { | |||
| 7 | + | ID int64 | |||
| 8 | + | URL string | |||
| 9 | + | Title string | |||
| 10 | + | FaviconURL string | |||
| 11 | + | VisitCount int64 | |||
| 12 | + | LastVisited time.Time | |||
| 13 | + | CreatedAt time.Time | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // NewHistoryEntry creates a new history entry for a URL. | |||
| 17 | + | func NewHistoryEntry(url, title string) *HistoryEntry { | |||
| 18 | + | now := time.Now() | |||
| 19 | + | return &HistoryEntry{ | |||
| 20 | + | URL: url, | |||
| 21 | + | Title: title, | |||
| 22 | + | VisitCount: 1, | |||
| 23 | + | LastVisited: now, | |||
| 24 | + | CreatedAt: now, | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // IncrementVisit updates the entry for a new visit. | |||
| 29 | + | func (h *HistoryEntry) IncrementVisit() { | |||
| 30 | + | h.VisitCount++ | |||
| 31 | + | h.LastVisited = time.Now() | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // HistoryMatch represents a history entry that matched a search query. | |||
| 35 | + | // Used for fuzzy search results with scoring. | |||
| 36 | + | type HistoryMatch struct { | |||
| 37 | + | Entry *HistoryEntry | |||
| 38 | + | Score float64 // Match score (higher is better) | |||
| 39 | + | } | |||
A internal/domain/entity/pane.go +158 -0
| @@ -0,0 +1,158 @@ | |||
| 1 | + | // Package entity contains domain entities representing core business concepts. | |
| 2 | + | // These entities are pure Go types with no infrastructure dependencies. | |
| 3 | + | package entity | |
| 4 | + | ||
| 5 | + | import "time" | |
| 6 | + | ||
| 7 | + | // PaneID uniquely identifies a pane within the browser. | |
| 8 | + | type PaneID string | |
| 9 | + | ||
| 10 | + | // SplitDirection indicates how a pane container splits its children. | |
| 11 | + | type SplitDirection int | |
| 12 | + | ||
| 13 | + | const ( | |
| 14 | + | SplitNone SplitDirection = iota // Leaf node or stacked container | |
| 15 | + | SplitHorizontal // Left/right split | |
| 16 | + | SplitVertical // Top/bottom split | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // WindowType indicates the type of browser window. | |
| 20 | + | type WindowType int | |
| 21 | + | ||
| 22 | + | const ( | |
| 23 | + | WindowMain WindowType = iota // Regular browser tab | |
| 24 | + | WindowPopup // Popup window (OAuth, feature-restricted) | |
| 25 | + | ) | |
| 26 | + | ||
| 27 | + | // Pane represents a single browsing context (a WebView container). | |
| 28 | + | // This is the leaf-level entity that holds navigation state. | |
| 29 | + | type Pane struct { | |
| 30 | + | ID PaneID | |
| 31 | + | URI string | |
| 32 | + | Title string | |
| 33 | + | FaviconURL string | |
| 34 | + | WindowType WindowType | |
| 35 | + | ZoomFactor float64 | |
| 36 | + | CanGoBack bool | |
| 37 | + | CanForward bool | |
| 38 | + | IsLoading bool | |
| 39 | + | CreatedAt time.Time | |
| 40 | + | ||
| 41 | + | // Popup-specific fields | |
| 42 | + | IsRelated bool // Shares context with parent | |
| 43 | + | ParentPaneID *PaneID // Parent pane if this is a related popup | |
| 44 | + | AutoClose bool // Auto-close on OAuth success | |
| 45 | + | RequestID string // Request ID for popup tracking | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // NewPane creates a new pane with default values. | |
| 49 | + | func NewPane(id PaneID) *Pane { | |
| 50 | + | return &Pane{ | |
| 51 | + | ID: id, | |
| 52 | + | WindowType: WindowMain, | |
| 53 | + | ZoomFactor: 1.0, | |
| 54 | + | CreatedAt: time.Now(), | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // PaneNode represents a node in the workspace pane tree structure. | |
| 59 | + | // It can be either: | |
| 60 | + | // - Leaf node: Contains a single Pane | |
| 61 | + | // - Split node: Contains two children (left/right or top/bottom) | |
| 62 | + | // - Stacked node: Contains multiple panes in a tabbed view | |
| 63 | + | type PaneNode struct { | |
| 64 | + | ID string | |
| 65 | + | Pane *Pane // Non-nil for leaf nodes | |
| 66 | + | Parent *PaneNode // nil for root | |
| 67 | + | Children []*PaneNode | |
| 68 | + | ||
| 69 | + | // Layout | |
| 70 | + | SplitDir SplitDirection | |
| 71 | + | SplitRatio float64 // 0.0-1.0, position of divider | |
| 72 | + | ||
| 73 | + | // Stacked panes (alternative to split) | |
| 74 | + | IsStacked bool | |
| 75 | + | ActiveStackIndex int | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // IsLeaf returns true if this node contains a pane (no children). | |
| 79 | + | func (n *PaneNode) IsLeaf() bool { | |
| 80 | + | return n.Pane != nil && len(n.Children) == 0 && !n.IsStacked | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // IsContainer returns true if this is a split or stacked container. | |
| 84 | + | func (n *PaneNode) IsContainer() bool { | |
| 85 | + | return !n.IsLeaf() | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // IsSplit returns true if this node splits into two children. | |
| 89 | + | func (n *PaneNode) IsSplit() bool { | |
| 90 | + | return n.SplitDir != SplitNone && len(n.Children) == 2 | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // Left returns the left/top child in a split node. | |
| 94 | + | func (n *PaneNode) Left() *PaneNode { | |
| 95 | + | if len(n.Children) > 0 { | |
| 96 | + | return n.Children[0] | |
| 97 | + | } | |
| 98 | + | return nil | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // Right returns the right/bottom child in a split node. | |
| 102 | + | func (n *PaneNode) Right() *PaneNode { | |
| 103 | + | if len(n.Children) > 1 { | |
| 104 | + | return n.Children[1] | |
| 105 | + | } | |
| 106 | + | return nil | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // StackedPanes returns the list of panes if this is a stacked container. | |
| 110 | + | func (n *PaneNode) StackedPanes() []*PaneNode { | |
| 111 | + | if n.IsStacked { | |
| 112 | + | return n.Children | |
| 113 | + | } | |
| 114 | + | return nil | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // ActivePane returns the currently visible pane in a stacked container. | |
| 118 | + | func (n *PaneNode) ActivePane() *PaneNode { | |
| 119 | + | if n.IsStacked && n.ActiveStackIndex >= 0 && n.ActiveStackIndex < len(n.Children) { | |
| 120 | + | return n.Children[n.ActiveStackIndex] | |
| 121 | + | } | |
| 122 | + | return nil | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // Walk traverses the tree calling fn for each node. Returns early if fn returns false. | |
| 126 | + | func (n *PaneNode) Walk(fn func(*PaneNode) bool) { | |
| 127 | + | if !fn(n) { | |
| 128 | + | return | |
| 129 | + | } | |
| 130 | + | for _, child := range n.Children { | |
| 131 | + | child.Walk(fn) | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // FindPane searches the tree for a pane with the given ID. | |
| 136 | + | func (n *PaneNode) FindPane(id PaneID) *PaneNode { | |
| 137 | + | var found *PaneNode | |
| 138 | + | n.Walk(func(node *PaneNode) bool { | |
| 139 | + | if node.Pane != nil && node.Pane.ID == id { | |
| 140 | + | found = node | |
| 141 | + | return false | |
| 142 | + | } | |
| 143 | + | return true | |
| 144 | + | }) | |
| 145 | + | return found | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // LeafCount returns the number of leaf nodes (panes) in the tree. | |
| 149 | + | func (n *PaneNode) LeafCount() int { | |
| 150 | + | count := 0 | |
| 151 | + | n.Walk(func(node *PaneNode) bool { | |
| 152 | + | if node.IsLeaf() { | |
| 153 | + | count++ | |
| 154 | + | } | |
| 155 | + | return true | |
| 156 | + | }) | |
| 157 | + | return count | |
| 158 | + | } | |
| @@ -0,0 +1,158 @@ | |||||
| 1 | + | // Package entity contains domain entities representing core business concepts. | |||
| 2 | + | // These entities are pure Go types with no infrastructure dependencies. | |||
| 3 | + | package entity | |||
| 4 | + | ||||
| 5 | + | import "time" | |||
| 6 | + | ||||
| 7 | + | // PaneID uniquely identifies a pane within the browser. | |||
| 8 | + | type PaneID string | |||
| 9 | + | ||||
| 10 | + | // SplitDirection indicates how a pane container splits its children. | |||
| 11 | + | type SplitDirection int | |||
| 12 | + | ||||
| 13 | + | const ( | |||
| 14 | + | SplitNone SplitDirection = iota // Leaf node or stacked container | |||
| 15 | + | SplitHorizontal // Left/right split | |||
| 16 | + | SplitVertical // Top/bottom split | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | // WindowType indicates the type of browser window. | |||
| 20 | + | type WindowType int | |||
| 21 | + | ||||
| 22 | + | const ( | |||
| 23 | + | WindowMain WindowType = iota // Regular browser tab | |||
| 24 | + | WindowPopup // Popup window (OAuth, feature-restricted) | |||
| 25 | + | ) | |||
| 26 | + | ||||
| 27 | + | // Pane represents a single browsing context (a WebView container). | |||
| 28 | + | // This is the leaf-level entity that holds navigation state. | |||
| 29 | + | type Pane struct { | |||
| 30 | + | ID PaneID | |||
| 31 | + | URI string | |||
| 32 | + | Title string | |||
| 33 | + | FaviconURL string | |||
| 34 | + | WindowType WindowType | |||
| 35 | + | ZoomFactor float64 | |||
| 36 | + | CanGoBack bool | |||
| 37 | + | CanForward bool | |||
| 38 | + | IsLoading bool | |||
| 39 | + | CreatedAt time.Time | |||
| 40 | + | ||||
| 41 | + | // Popup-specific fields | |||
| 42 | + | IsRelated bool // Shares context with parent | |||
| 43 | + | ParentPaneID *PaneID // Parent pane if this is a related popup | |||
| 44 | + | AutoClose bool // Auto-close on OAuth success | |||
| 45 | + | RequestID string // Request ID for popup tracking | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // NewPane creates a new pane with default values. | |||
| 49 | + | func NewPane(id PaneID) *Pane { | |||
| 50 | + | return &Pane{ | |||
| 51 | + | ID: id, | |||
| 52 | + | WindowType: WindowMain, | |||
| 53 | + | ZoomFactor: 1.0, | |||
| 54 | + | CreatedAt: time.Now(), | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // PaneNode represents a node in the workspace pane tree structure. | |||
| 59 | + | // It can be either: | |||
| 60 | + | // - Leaf node: Contains a single Pane | |||
| 61 | + | // - Split node: Contains two children (left/right or top/bottom) | |||
| 62 | + | // - Stacked node: Contains multiple panes in a tabbed view | |||
| 63 | + | type PaneNode struct { | |||
| 64 | + | ID string | |||
| 65 | + | Pane *Pane // Non-nil for leaf nodes | |||
| 66 | + | Parent *PaneNode // nil for root | |||
| 67 | + | Children []*PaneNode | |||
| 68 | + | ||||
| 69 | + | // Layout | |||
| 70 | + | SplitDir SplitDirection | |||
| 71 | + | SplitRatio float64 // 0.0-1.0, position of divider | |||
| 72 | + | ||||
| 73 | + | // Stacked panes (alternative to split) | |||
| 74 | + | IsStacked bool | |||
| 75 | + | ActiveStackIndex int | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // IsLeaf returns true if this node contains a pane (no children). | |||
| 79 | + | func (n *PaneNode) IsLeaf() bool { | |||
| 80 | + | return n.Pane != nil && len(n.Children) == 0 && !n.IsStacked | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // IsContainer returns true if this is a split or stacked container. | |||
| 84 | + | func (n *PaneNode) IsContainer() bool { | |||
| 85 | + | return !n.IsLeaf() | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // IsSplit returns true if this node splits into two children. | |||
| 89 | + | func (n *PaneNode) IsSplit() bool { | |||
| 90 | + | return n.SplitDir != SplitNone && len(n.Children) == 2 | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // Left returns the left/top child in a split node. | |||
| 94 | + | func (n *PaneNode) Left() *PaneNode { | |||
| 95 | + | if len(n.Children) > 0 { | |||
| 96 | + | return n.Children[0] | |||
| 97 | + | } | |||
| 98 | + | return nil | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // Right returns the right/bottom child in a split node. | |||
| 102 | + | func (n *PaneNode) Right() *PaneNode { | |||
| 103 | + | if len(n.Children) > 1 { | |||
| 104 | + | return n.Children[1] | |||
| 105 | + | } | |||
| 106 | + | return nil | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // StackedPanes returns the list of panes if this is a stacked container. | |||
| 110 | + | func (n *PaneNode) StackedPanes() []*PaneNode { | |||
| 111 | + | if n.IsStacked { | |||
| 112 | + | return n.Children | |||
| 113 | + | } | |||
| 114 | + | return nil | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // ActivePane returns the currently visible pane in a stacked container. | |||
| 118 | + | func (n *PaneNode) ActivePane() *PaneNode { | |||
| 119 | + | if n.IsStacked && n.ActiveStackIndex >= 0 && n.ActiveStackIndex < len(n.Children) { | |||
| 120 | + | return n.Children[n.ActiveStackIndex] | |||
| 121 | + | } | |||
| 122 | + | return nil | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // Walk traverses the tree calling fn for each node. Returns early if fn returns false. | |||
| 126 | + | func (n *PaneNode) Walk(fn func(*PaneNode) bool) { | |||
| 127 | + | if !fn(n) { | |||
| 128 | + | return | |||
| 129 | + | } | |||
| 130 | + | for _, child := range n.Children { | |||
| 131 | + | child.Walk(fn) | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // FindPane searches the tree for a pane with the given ID. | |||
| 136 | + | func (n *PaneNode) FindPane(id PaneID) *PaneNode { | |||
| 137 | + | var found *PaneNode | |||
| 138 | + | n.Walk(func(node *PaneNode) bool { | |||
| 139 | + | if node.Pane != nil && node.Pane.ID == id { | |||
| 140 | + | found = node | |||
| 141 | + | return false | |||
| 142 | + | } | |||
| 143 | + | return true | |||
| 144 | + | }) | |||
| 145 | + | return found | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | // LeafCount returns the number of leaf nodes (panes) in the tree. | |||
| 149 | + | func (n *PaneNode) LeafCount() int { | |||
| 150 | + | count := 0 | |||
| 151 | + | n.Walk(func(node *PaneNode) bool { | |||
| 152 | + | if node.IsLeaf() { | |||
| 153 | + | count++ | |||
| 154 | + | } | |||
| 155 | + | return true | |||
| 156 | + | }) | |||
| 157 | + | return count | |||
| 158 | + | } | |||
A internal/domain/entity/tab.go +147 -0
| @@ -0,0 +1,147 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // TabID uniquely identifies a tab. | |
| 6 | + | type TabID string | |
| 7 | + | ||
| 8 | + | // Tab represents a browser tab containing a workspace. | |
| 9 | + | // Tabs are the top-level container in the browser's tab bar. | |
| 10 | + | type Tab struct { | |
| 11 | + | ID TabID | |
| 12 | + | Name string // Display name (often derived from active pane title) | |
| 13 | + | Workspace *Workspace // The workspace this tab contains | |
| 14 | + | Position int // Position in the tab bar (0-indexed) | |
| 15 | + | IsPinned bool // Pinned tabs stay at the left | |
| 16 | + | CreatedAt time.Time | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // NewTab creates a new tab with an initial pane. | |
| 20 | + | func NewTab(tabID TabID, workspaceID WorkspaceID, initialPane *Pane) *Tab { | |
| 21 | + | return &Tab{ | |
| 22 | + | ID: tabID, | |
| 23 | + | Workspace: NewWorkspace(workspaceID, initialPane), | |
| 24 | + | Position: 0, | |
| 25 | + | CreatedAt: time.Now(), | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // Title returns the display title for the tab. | |
| 30 | + | // Uses the active pane's title, falling back to URL or "New Tab". | |
| 31 | + | func (t *Tab) Title() string { | |
| 32 | + | if t.Name != "" { | |
| 33 | + | return t.Name | |
| 34 | + | } | |
| 35 | + | if t.Workspace != nil { | |
| 36 | + | if active := t.Workspace.ActivePane(); active != nil && active.Pane != nil { | |
| 37 | + | if active.Pane.Title != "" { | |
| 38 | + | return active.Pane.Title | |
| 39 | + | } | |
| 40 | + | if active.Pane.URI != "" { | |
| 41 | + | return active.Pane.URI | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | return "New Tab" | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // PaneCount returns the number of panes in this tab's workspace. | |
| 49 | + | func (t *Tab) PaneCount() int { | |
| 50 | + | if t.Workspace == nil { | |
| 51 | + | return 0 | |
| 52 | + | } | |
| 53 | + | return t.Workspace.PaneCount() | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // TabList manages an ordered collection of tabs. | |
| 57 | + | type TabList struct { | |
| 58 | + | Tabs []*Tab | |
| 59 | + | ActiveTabID TabID | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // NewTabList creates an empty tab list. | |
| 63 | + | func NewTabList() *TabList { | |
| 64 | + | return &TabList{ | |
| 65 | + | Tabs: make([]*Tab, 0), | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // Add appends a tab to the list. | |
| 70 | + | func (tl *TabList) Add(tab *Tab) { | |
| 71 | + | tab.Position = len(tl.Tabs) | |
| 72 | + | tl.Tabs = append(tl.Tabs, tab) | |
| 73 | + | if tl.ActiveTabID == "" { | |
| 74 | + | tl.ActiveTabID = tab.ID | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // Remove removes a tab by ID and reindexes positions. | |
| 79 | + | func (tl *TabList) Remove(id TabID) bool { | |
| 80 | + | for i, tab := range tl.Tabs { | |
| 81 | + | if tab.ID == id { | |
| 82 | + | tl.Tabs = append(tl.Tabs[:i], tl.Tabs[i+1:]...) | |
| 83 | + | // Reindex positions | |
| 84 | + | for j := i; j < len(tl.Tabs); j++ { | |
| 85 | + | tl.Tabs[j].Position = j | |
| 86 | + | } | |
| 87 | + | // Update active tab if needed | |
| 88 | + | if tl.ActiveTabID == id && len(tl.Tabs) > 0 { | |
| 89 | + | if i < len(tl.Tabs) { | |
| 90 | + | tl.ActiveTabID = tl.Tabs[i].ID | |
| 91 | + | } else { | |
| 92 | + | tl.ActiveTabID = tl.Tabs[len(tl.Tabs)-1].ID | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | return true | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | return false | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // Find returns a tab by ID. | |
| 102 | + | func (tl *TabList) Find(id TabID) *Tab { | |
| 103 | + | for _, tab := range tl.Tabs { | |
| 104 | + | if tab.ID == id { | |
| 105 | + | return tab | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | return nil | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // ActiveTab returns the currently active tab. | |
| 112 | + | func (tl *TabList) ActiveTab() *Tab { | |
| 113 | + | return tl.Find(tl.ActiveTabID) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // Count returns the number of tabs. | |
| 117 | + | func (tl *TabList) Count() int { | |
| 118 | + | return len(tl.Tabs) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Move moves a tab to a new position. | |
| 122 | + | func (tl *TabList) Move(id TabID, newPos int) bool { | |
| 123 | + | if newPos < 0 || newPos >= len(tl.Tabs) { | |
| 124 | + | return false | |
| 125 | + | } | |
| 126 | + | var tab *Tab | |
| 127 | + | var oldPos int | |
| 128 | + | for i, t := range tl.Tabs { | |
| 129 | + | if t.ID == id { | |
| 130 | + | tab = t | |
| 131 | + | oldPos = i | |
| 132 | + | break | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | if tab == nil { | |
| 136 | + | return false | |
| 137 | + | } | |
| 138 | + | // Remove from old position | |
| 139 | + | tl.Tabs = append(tl.Tabs[:oldPos], tl.Tabs[oldPos+1:]...) | |
| 140 | + | // Insert at new position | |
| 141 | + | tl.Tabs = append(tl.Tabs[:newPos], append([]*Tab{tab}, tl.Tabs[newPos:]...)...) | |
| 142 | + | // Reindex all | |
| 143 | + | for i := range tl.Tabs { | |
| 144 | + | tl.Tabs[i].Position = i | |
| 145 | + | } | |
| 146 | + | return true | |
| 147 | + | } | |
| @@ -0,0 +1,147 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // TabID uniquely identifies a tab. | |||
| 6 | + | type TabID string | |||
| 7 | + | ||||
| 8 | + | // Tab represents a browser tab containing a workspace. | |||
| 9 | + | // Tabs are the top-level container in the browser's tab bar. | |||
| 10 | + | type Tab struct { | |||
| 11 | + | ID TabID | |||
| 12 | + | Name string // Display name (often derived from active pane title) | |||
| 13 | + | Workspace *Workspace // The workspace this tab contains | |||
| 14 | + | Position int // Position in the tab bar (0-indexed) | |||
| 15 | + | IsPinned bool // Pinned tabs stay at the left | |||
| 16 | + | CreatedAt time.Time | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // NewTab creates a new tab with an initial pane. | |||
| 20 | + | func NewTab(tabID TabID, workspaceID WorkspaceID, initialPane *Pane) *Tab { | |||
| 21 | + | return &Tab{ | |||
| 22 | + | ID: tabID, | |||
| 23 | + | Workspace: NewWorkspace(workspaceID, initialPane), | |||
| 24 | + | Position: 0, | |||
| 25 | + | CreatedAt: time.Now(), | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // Title returns the display title for the tab. | |||
| 30 | + | // Uses the active pane's title, falling back to URL or "New Tab". | |||
| 31 | + | func (t *Tab) Title() string { | |||
| 32 | + | if t.Name != "" { | |||
| 33 | + | return t.Name | |||
| 34 | + | } | |||
| 35 | + | if t.Workspace != nil { | |||
| 36 | + | if active := t.Workspace.ActivePane(); active != nil && active.Pane != nil { | |||
| 37 | + | if active.Pane.Title != "" { | |||
| 38 | + | return active.Pane.Title | |||
| 39 | + | } | |||
| 40 | + | if active.Pane.URI != "" { | |||
| 41 | + | return active.Pane.URI | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | return "New Tab" | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // PaneCount returns the number of panes in this tab's workspace. | |||
| 49 | + | func (t *Tab) PaneCount() int { | |||
| 50 | + | if t.Workspace == nil { | |||
| 51 | + | return 0 | |||
| 52 | + | } | |||
| 53 | + | return t.Workspace.PaneCount() | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // TabList manages an ordered collection of tabs. | |||
| 57 | + | type TabList struct { | |||
| 58 | + | Tabs []*Tab | |||
| 59 | + | ActiveTabID TabID | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // NewTabList creates an empty tab list. | |||
| 63 | + | func NewTabList() *TabList { | |||
| 64 | + | return &TabList{ | |||
| 65 | + | Tabs: make([]*Tab, 0), | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // Add appends a tab to the list. | |||
| 70 | + | func (tl *TabList) Add(tab *Tab) { | |||
| 71 | + | tab.Position = len(tl.Tabs) | |||
| 72 | + | tl.Tabs = append(tl.Tabs, tab) | |||
| 73 | + | if tl.ActiveTabID == "" { | |||
| 74 | + | tl.ActiveTabID = tab.ID | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // Remove removes a tab by ID and reindexes positions. | |||
| 79 | + | func (tl *TabList) Remove(id TabID) bool { | |||
| 80 | + | for i, tab := range tl.Tabs { | |||
| 81 | + | if tab.ID == id { | |||
| 82 | + | tl.Tabs = append(tl.Tabs[:i], tl.Tabs[i+1:]...) | |||
| 83 | + | // Reindex positions | |||
| 84 | + | for j := i; j < len(tl.Tabs); j++ { | |||
| 85 | + | tl.Tabs[j].Position = j | |||
| 86 | + | } | |||
| 87 | + | // Update active tab if needed | |||
| 88 | + | if tl.ActiveTabID == id && len(tl.Tabs) > 0 { | |||
| 89 | + | if i < len(tl.Tabs) { | |||
| 90 | + | tl.ActiveTabID = tl.Tabs[i].ID | |||
| 91 | + | } else { | |||
| 92 | + | tl.ActiveTabID = tl.Tabs[len(tl.Tabs)-1].ID | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | return true | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | return false | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // Find returns a tab by ID. | |||
| 102 | + | func (tl *TabList) Find(id TabID) *Tab { | |||
| 103 | + | for _, tab := range tl.Tabs { | |||
| 104 | + | if tab.ID == id { | |||
| 105 | + | return tab | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | return nil | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | // ActiveTab returns the currently active tab. | |||
| 112 | + | func (tl *TabList) ActiveTab() *Tab { | |||
| 113 | + | return tl.Find(tl.ActiveTabID) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // Count returns the number of tabs. | |||
| 117 | + | func (tl *TabList) Count() int { | |||
| 118 | + | return len(tl.Tabs) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // Move moves a tab to a new position. | |||
| 122 | + | func (tl *TabList) Move(id TabID, newPos int) bool { | |||
| 123 | + | if newPos < 0 || newPos >= len(tl.Tabs) { | |||
| 124 | + | return false | |||
| 125 | + | } | |||
| 126 | + | var tab *Tab | |||
| 127 | + | var oldPos int | |||
| 128 | + | for i, t := range tl.Tabs { | |||
| 129 | + | if t.ID == id { | |||
| 130 | + | tab = t | |||
| 131 | + | oldPos = i | |||
| 132 | + | break | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | if tab == nil { | |||
| 136 | + | return false | |||
| 137 | + | } | |||
| 138 | + | // Remove from old position | |||
| 139 | + | tl.Tabs = append(tl.Tabs[:oldPos], tl.Tabs[oldPos+1:]...) | |||
| 140 | + | // Insert at new position | |||
| 141 | + | tl.Tabs = append(tl.Tabs[:newPos], append([]*Tab{tab}, tl.Tabs[newPos:]...)...) | |||
| 142 | + | // Reindex all | |||
| 143 | + | for i := range tl.Tabs { | |||
| 144 | + | tl.Tabs[i].Position = i | |||
| 145 | + | } | |||
| 146 | + | return true | |||
| 147 | + | } | |||
A internal/domain/entity/workspace.go +66 -0
| @@ -0,0 +1,66 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // WorkspaceID uniquely identifies a workspace. | |
| 6 | + | type WorkspaceID string | |
| 7 | + | ||
| 8 | + | // Workspace represents a collection of panes arranged in a tree layout. | |
| 9 | + | // Each tab contains exactly one workspace. | |
| 10 | + | type Workspace struct { | |
| 11 | + | ID WorkspaceID | |
| 12 | + | Name string | |
| 13 | + | Root *PaneNode // Root of the pane tree | |
| 14 | + | ActivePaneID PaneID // Currently focused pane | |
| 15 | + | CreatedAt time.Time | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // NewWorkspace creates a new workspace with an initial pane. | |
| 19 | + | func NewWorkspace(id WorkspaceID, initialPane *Pane) *Workspace { | |
| 20 | + | root := &PaneNode{ | |
| 21 | + | ID: string(initialPane.ID), | |
| 22 | + | Pane: initialPane, | |
| 23 | + | } | |
| 24 | + | return &Workspace{ | |
| 25 | + | ID: id, | |
| 26 | + | Root: root, | |
| 27 | + | ActivePaneID: initialPane.ID, | |
| 28 | + | CreatedAt: time.Now(), | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // PaneCount returns the number of panes in the workspace. | |
| 33 | + | func (w *Workspace) PaneCount() int { | |
| 34 | + | if w.Root == nil { | |
| 35 | + | return 0 | |
| 36 | + | } | |
| 37 | + | return w.Root.LeafCount() | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // FindPane searches for a pane by ID in the workspace. | |
| 41 | + | func (w *Workspace) FindPane(id PaneID) *PaneNode { | |
| 42 | + | if w.Root == nil { | |
| 43 | + | return nil | |
| 44 | + | } | |
| 45 | + | return w.Root.FindPane(id) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // ActivePane returns the currently active pane node. | |
| 49 | + | func (w *Workspace) ActivePane() *PaneNode { | |
| 50 | + | return w.FindPane(w.ActivePaneID) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // AllPanes returns all leaf panes in the workspace. | |
| 54 | + | func (w *Workspace) AllPanes() []*Pane { | |
| 55 | + | var panes []*Pane | |
| 56 | + | if w.Root == nil { | |
| 57 | + | return panes | |
| 58 | + | } | |
| 59 | + | w.Root.Walk(func(node *PaneNode) bool { | |
| 60 | + | if node.Pane != nil { | |
| 61 | + | panes = append(panes, node.Pane) | |
| 62 | + | } | |
| 63 | + | return true | |
| 64 | + | }) | |
| 65 | + | return panes | |
| 66 | + | } | |
| @@ -0,0 +1,66 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // WorkspaceID uniquely identifies a workspace. | |||
| 6 | + | type WorkspaceID string | |||
| 7 | + | ||||
| 8 | + | // Workspace represents a collection of panes arranged in a tree layout. | |||
| 9 | + | // Each tab contains exactly one workspace. | |||
| 10 | + | type Workspace struct { | |||
| 11 | + | ID WorkspaceID | |||
| 12 | + | Name string | |||
| 13 | + | Root *PaneNode // Root of the pane tree | |||
| 14 | + | ActivePaneID PaneID // Currently focused pane | |||
| 15 | + | CreatedAt time.Time | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // NewWorkspace creates a new workspace with an initial pane. | |||
| 19 | + | func NewWorkspace(id WorkspaceID, initialPane *Pane) *Workspace { | |||
| 20 | + | root := &PaneNode{ | |||
| 21 | + | ID: string(initialPane.ID), | |||
| 22 | + | Pane: initialPane, | |||
| 23 | + | } | |||
| 24 | + | return &Workspace{ | |||
| 25 | + | ID: id, | |||
| 26 | + | Root: root, | |||
| 27 | + | ActivePaneID: initialPane.ID, | |||
| 28 | + | CreatedAt: time.Now(), | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // PaneCount returns the number of panes in the workspace. | |||
| 33 | + | func (w *Workspace) PaneCount() int { | |||
| 34 | + | if w.Root == nil { | |||
| 35 | + | return 0 | |||
| 36 | + | } | |||
| 37 | + | return w.Root.LeafCount() | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // FindPane searches for a pane by ID in the workspace. | |||
| 41 | + | func (w *Workspace) FindPane(id PaneID) *PaneNode { | |||
| 42 | + | if w.Root == nil { | |||
| 43 | + | return nil | |||
| 44 | + | } | |||
| 45 | + | return w.Root.FindPane(id) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // ActivePane returns the currently active pane node. | |||
| 49 | + | func (w *Workspace) ActivePane() *PaneNode { | |||
| 50 | + | return w.FindPane(w.ActivePaneID) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // AllPanes returns all leaf panes in the workspace. | |||
| 54 | + | func (w *Workspace) AllPanes() []*Pane { | |||
| 55 | + | var panes []*Pane | |||
| 56 | + | if w.Root == nil { | |||
| 57 | + | return panes | |||
| 58 | + | } | |||
| 59 | + | w.Root.Walk(func(node *PaneNode) bool { | |||
| 60 | + | if node.Pane != nil { | |||
| 61 | + | panes = append(panes, node.Pane) | |||
| 62 | + | } | |||
| 63 | + | return true | |||
| 64 | + | }) | |||
| 65 | + | return panes | |||
| 66 | + | } | |||
A internal/domain/entity/zoom.go +70 -0
| @@ -0,0 +1,70 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // ZoomLevel represents the zoom factor for a specific domain. | |
| 6 | + | // Allows users to set persistent zoom levels per-site. | |
| 7 | + | type ZoomLevel struct { | |
| 8 | + | Domain string // Domain name (e.g., "github.com") | |
| 9 | + | ZoomFactor float64 // Zoom factor (1.0 = 100%, 1.5 = 150%) | |
| 10 | + | UpdatedAt time.Time | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | // Default zoom constants | |
| 14 | + | const ( | |
| 15 | + | ZoomDefault = 1.0 | |
| 16 | + | ZoomMin = 0.25 // 25% | |
| 17 | + | ZoomMax = 5.0 // 500% | |
| 18 | + | ZoomStep = 0.1 // 10% increments | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // NewZoomLevel creates a new zoom level for a domain. | |
| 22 | + | func NewZoomLevel(domain string, factor float64) *ZoomLevel { | |
| 23 | + | return &ZoomLevel{ | |
| 24 | + | Domain: domain, | |
| 25 | + | ZoomFactor: clampZoom(factor), | |
| 26 | + | UpdatedAt: time.Now(), | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // SetFactor updates the zoom factor, clamping to valid range. | |
| 31 | + | func (z *ZoomLevel) SetFactor(factor float64) { | |
| 32 | + | z.ZoomFactor = clampZoom(factor) | |
| 33 | + | z.UpdatedAt = time.Now() | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // ZoomIn increases the zoom factor by one step. | |
| 37 | + | func (z *ZoomLevel) ZoomIn() { | |
| 38 | + | z.SetFactor(z.ZoomFactor + ZoomStep) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // ZoomOut decreases the zoom factor by one step. | |
| 42 | + | func (z *ZoomLevel) ZoomOut() { | |
| 43 | + | z.SetFactor(z.ZoomFactor - ZoomStep) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Reset restores the zoom factor to default. | |
| 47 | + | func (z *ZoomLevel) Reset() { | |
| 48 | + | z.SetFactor(ZoomDefault) | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // IsDefault returns true if the zoom is at default level. | |
| 52 | + | func (z *ZoomLevel) IsDefault() bool { | |
| 53 | + | return z.ZoomFactor == ZoomDefault | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // Percentage returns the zoom factor as a percentage (e.g., 150 for 1.5). | |
| 57 | + | func (z *ZoomLevel) Percentage() int { | |
| 58 | + | return int(z.ZoomFactor * 100) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // clampZoom constrains a zoom factor to the valid range. | |
| 62 | + | func clampZoom(factor float64) float64 { | |
| 63 | + | if factor < ZoomMin { | |
| 64 | + | return ZoomMin | |
| 65 | + | } | |
| 66 | + | if factor > ZoomMax { | |
| 67 | + | return ZoomMax | |
| 68 | + | } | |
| 69 | + | return factor | |
| 70 | + | } | |
| @@ -0,0 +1,70 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // ZoomLevel represents the zoom factor for a specific domain. | |||
| 6 | + | // Allows users to set persistent zoom levels per-site. | |||
| 7 | + | type ZoomLevel struct { | |||
| 8 | + | Domain string // Domain name (e.g., "github.com") | |||
| 9 | + | ZoomFactor float64 // Zoom factor (1.0 = 100%, 1.5 = 150%) | |||
| 10 | + | UpdatedAt time.Time | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | // Default zoom constants | |||
| 14 | + | const ( | |||
| 15 | + | ZoomDefault = 1.0 | |||
| 16 | + | ZoomMin = 0.25 // 25% | |||
| 17 | + | ZoomMax = 5.0 // 500% | |||
| 18 | + | ZoomStep = 0.1 // 10% increments | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // NewZoomLevel creates a new zoom level for a domain. | |||
| 22 | + | func NewZoomLevel(domain string, factor float64) *ZoomLevel { | |||
| 23 | + | return &ZoomLevel{ | |||
| 24 | + | Domain: domain, | |||
| 25 | + | ZoomFactor: clampZoom(factor), | |||
| 26 | + | UpdatedAt: time.Now(), | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // SetFactor updates the zoom factor, clamping to valid range. | |||
| 31 | + | func (z *ZoomLevel) SetFactor(factor float64) { | |||
| 32 | + | z.ZoomFactor = clampZoom(factor) | |||
| 33 | + | z.UpdatedAt = time.Now() | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // ZoomIn increases the zoom factor by one step. | |||
| 37 | + | func (z *ZoomLevel) ZoomIn() { | |||
| 38 | + | z.SetFactor(z.ZoomFactor + ZoomStep) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // ZoomOut decreases the zoom factor by one step. | |||
| 42 | + | func (z *ZoomLevel) ZoomOut() { | |||
| 43 | + | z.SetFactor(z.ZoomFactor - ZoomStep) | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Reset restores the zoom factor to default. | |||
| 47 | + | func (z *ZoomLevel) Reset() { | |||
| 48 | + | z.SetFactor(ZoomDefault) | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // IsDefault returns true if the zoom is at default level. | |||
| 52 | + | func (z *ZoomLevel) IsDefault() bool { | |||
| 53 | + | return z.ZoomFactor == ZoomDefault | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // Percentage returns the zoom factor as a percentage (e.g., 150 for 1.5). | |||
| 57 | + | func (z *ZoomLevel) Percentage() int { | |||
| 58 | + | return int(z.ZoomFactor * 100) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // clampZoom constrains a zoom factor to the valid range. | |||
| 62 | + | func clampZoom(factor float64) float64 { | |||
| 63 | + | if factor < ZoomMin { | |||
| 64 | + | return ZoomMin | |||
| 65 | + | } | |||
| 66 | + | if factor > ZoomMax { | |||
| 67 | + | return ZoomMax | |||
| 68 | + | } | |||
| 69 | + | return factor | |||
| 70 | + | } | |||
A internal/domain/repository/favorite.go +91 -0
| @@ -0,0 +1,91 @@ | |||
| 1 | + | package repository | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // FavoriteRepository defines operations for bookmark persistence. | |
| 10 | + | type FavoriteRepository interface { | |
| 11 | + | // Save creates or updates a favorite. | |
| 12 | + | Save(ctx context.Context, fav *entity.Favorite) error | |
| 13 | + | ||
| 14 | + | // FindByID retrieves a favorite by its ID. | |
| 15 | + | FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) | |
| 16 | + | ||
| 17 | + | // FindByURL retrieves a favorite by its URL. | |
| 18 | + | FindByURL(ctx context.Context, url string) (*entity.Favorite, error) | |
| 19 | + | ||
| 20 | + | // GetAll retrieves all favorites. | |
| 21 | + | GetAll(ctx context.Context) ([]*entity.Favorite, error) | |
| 22 | + | ||
| 23 | + | // GetByFolder retrieves favorites in a specific folder. | |
| 24 | + | // Pass nil for root-level favorites. | |
| 25 | + | GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) | |
| 26 | + | ||
| 27 | + | // GetByShortcut retrieves the favorite with the given shortcut key (1-9). | |
| 28 | + | GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) | |
| 29 | + | ||
| 30 | + | // UpdatePosition updates a favorite's position within its folder. | |
| 31 | + | UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error | |
| 32 | + | ||
| 33 | + | // SetFolder moves a favorite to a different folder. | |
| 34 | + | SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error | |
| 35 | + | ||
| 36 | + | // SetShortcut assigns a keyboard shortcut (1-9) or nil to remove. | |
| 37 | + | SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error | |
| 38 | + | ||
| 39 | + | // Delete removes a favorite by ID. | |
| 40 | + | Delete(ctx context.Context, id entity.FavoriteID) error | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // FolderRepository defines operations for bookmark folder persistence. | |
| 44 | + | type FolderRepository interface { | |
| 45 | + | // Save creates or updates a folder. | |
| 46 | + | Save(ctx context.Context, folder *entity.Folder) error | |
| 47 | + | ||
| 48 | + | // FindByID retrieves a folder by its ID. | |
| 49 | + | FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) | |
| 50 | + | ||
| 51 | + | // GetAll retrieves all folders. | |
| 52 | + | GetAll(ctx context.Context) ([]*entity.Folder, error) | |
| 53 | + | ||
| 54 | + | // GetChildren retrieves child folders of a parent. | |
| 55 | + | // Pass nil for root-level folders. | |
| 56 | + | GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) | |
| 57 | + | ||
| 58 | + | // UpdatePosition updates a folder's position within its parent. | |
| 59 | + | UpdatePosition(ctx context.Context, id entity.FolderID, position int) error | |
| 60 | + | ||
| 61 | + | // Delete removes a folder by ID. | |
| 62 | + | // Favorites in the folder should be moved to root. | |
| 63 | + | Delete(ctx context.Context, id entity.FolderID) error | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // TagRepository defines operations for tag persistence. | |
| 67 | + | type TagRepository interface { | |
| 68 | + | // Save creates or updates a tag. | |
| 69 | + | Save(ctx context.Context, tag *entity.Tag) error | |
| 70 | + | ||
| 71 | + | // FindByID retrieves a tag by its ID. | |
| 72 | + | FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) | |
| 73 | + | ||
| 74 | + | // FindByName retrieves a tag by its name. | |
| 75 | + | FindByName(ctx context.Context, name string) (*entity.Tag, error) | |
| 76 | + | ||
| 77 | + | // GetAll retrieves all tags. | |
| 78 | + | GetAll(ctx context.Context) ([]*entity.Tag, error) | |
| 79 | + | ||
| 80 | + | // AssignToFavorite associates a tag with a favorite. | |
| 81 | + | AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error | |
| 82 | + | ||
| 83 | + | // RemoveFromFavorite removes a tag association from a favorite. | |
| 84 | + | RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error | |
| 85 | + | ||
| 86 | + | // GetForFavorite retrieves all tags associated with a favorite. | |
| 87 | + | GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) | |
| 88 | + | ||
| 89 | + | // Delete removes a tag by ID. | |
| 90 | + | Delete(ctx context.Context, id entity.TagID) error | |
| 91 | + | } | |
| @@ -0,0 +1,91 @@ | |||||
| 1 | + | package repository | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // FavoriteRepository defines operations for bookmark persistence. | |||
| 10 | + | type FavoriteRepository interface { | |||
| 11 | + | // Save creates or updates a favorite. | |||
| 12 | + | Save(ctx context.Context, fav *entity.Favorite) error | |||
| 13 | + | ||||
| 14 | + | // FindByID retrieves a favorite by its ID. | |||
| 15 | + | FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) | |||
| 16 | + | ||||
| 17 | + | // FindByURL retrieves a favorite by its URL. | |||
| 18 | + | FindByURL(ctx context.Context, url string) (*entity.Favorite, error) | |||
| 19 | + | ||||
| 20 | + | // GetAll retrieves all favorites. | |||
| 21 | + | GetAll(ctx context.Context) ([]*entity.Favorite, error) | |||
| 22 | + | ||||
| 23 | + | // GetByFolder retrieves favorites in a specific folder. | |||
| 24 | + | // Pass nil for root-level favorites. | |||
| 25 | + | GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) | |||
| 26 | + | ||||
| 27 | + | // GetByShortcut retrieves the favorite with the given shortcut key (1-9). | |||
| 28 | + | GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) | |||
| 29 | + | ||||
| 30 | + | // UpdatePosition updates a favorite's position within its folder. | |||
| 31 | + | UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error | |||
| 32 | + | ||||
| 33 | + | // SetFolder moves a favorite to a different folder. | |||
| 34 | + | SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error | |||
| 35 | + | ||||
| 36 | + | // SetShortcut assigns a keyboard shortcut (1-9) or nil to remove. | |||
| 37 | + | SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error | |||
| 38 | + | ||||
| 39 | + | // Delete removes a favorite by ID. | |||
| 40 | + | Delete(ctx context.Context, id entity.FavoriteID) error | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // FolderRepository defines operations for bookmark folder persistence. | |||
| 44 | + | type FolderRepository interface { | |||
| 45 | + | // Save creates or updates a folder. | |||
| 46 | + | Save(ctx context.Context, folder *entity.Folder) error | |||
| 47 | + | ||||
| 48 | + | // FindByID retrieves a folder by its ID. | |||
| 49 | + | FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) | |||
| 50 | + | ||||
| 51 | + | // GetAll retrieves all folders. | |||
| 52 | + | GetAll(ctx context.Context) ([]*entity.Folder, error) | |||
| 53 | + | ||||
| 54 | + | // GetChildren retrieves child folders of a parent. | |||
| 55 | + | // Pass nil for root-level folders. | |||
| 56 | + | GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) | |||
| 57 | + | ||||
| 58 | + | // UpdatePosition updates a folder's position within its parent. | |||
| 59 | + | UpdatePosition(ctx context.Context, id entity.FolderID, position int) error | |||
| 60 | + | ||||
| 61 | + | // Delete removes a folder by ID. | |||
| 62 | + | // Favorites in the folder should be moved to root. | |||
| 63 | + | Delete(ctx context.Context, id entity.FolderID) error | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // TagRepository defines operations for tag persistence. | |||
| 67 | + | type TagRepository interface { | |||
| 68 | + | // Save creates or updates a tag. | |||
| 69 | + | Save(ctx context.Context, tag *entity.Tag) error | |||
| 70 | + | ||||
| 71 | + | // FindByID retrieves a tag by its ID. | |||
| 72 | + | FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) | |||
| 73 | + | ||||
| 74 | + | // FindByName retrieves a tag by its name. | |||
| 75 | + | FindByName(ctx context.Context, name string) (*entity.Tag, error) | |||
| 76 | + | ||||
| 77 | + | // GetAll retrieves all tags. | |||
| 78 | + | GetAll(ctx context.Context) ([]*entity.Tag, error) | |||
| 79 | + | ||||
| 80 | + | // AssignToFavorite associates a tag with a favorite. | |||
| 81 | + | AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error | |||
| 82 | + | ||||
| 83 | + | // RemoveFromFavorite removes a tag association from a favorite. | |||
| 84 | + | RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error | |||
| 85 | + | ||||
| 86 | + | // GetForFavorite retrieves all tags associated with a favorite. | |||
| 87 | + | GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) | |||
| 88 | + | ||||
| 89 | + | // Delete removes a tag by ID. | |||
| 90 | + | Delete(ctx context.Context, id entity.TagID) error | |||
| 91 | + | } | |||
A internal/domain/repository/filter.go +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | package repository | |
| 2 | + | ||
| 3 | + | import "context" | |
| 4 | + | ||
| 5 | + | // ContentWhitelistRepository defines operations for content filter whitelist persistence. | |
| 6 | + | // Whitelisted domains bypass content filtering. | |
| 7 | + | type ContentWhitelistRepository interface { | |
| 8 | + | // Add adds a domain to the whitelist. | |
| 9 | + | Add(ctx context.Context, domain string) error | |
| 10 | + | ||
| 11 | + | // Remove removes a domain from the whitelist. | |
| 12 | + | Remove(ctx context.Context, domain string) error | |
| 13 | + | ||
| 14 | + | // Contains checks if a domain is whitelisted. | |
| 15 | + | Contains(ctx context.Context, domain string) (bool, error) | |
| 16 | + | ||
| 17 | + | // GetAll retrieves all whitelisted domains. | |
| 18 | + | GetAll(ctx context.Context) ([]string, error) | |
| 19 | + | } | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | package repository | |||
| 2 | + | ||||
| 3 | + | import "context" | |||
| 4 | + | ||||
| 5 | + | // ContentWhitelistRepository defines operations for content filter whitelist persistence. | |||
| 6 | + | // Whitelisted domains bypass content filtering. | |||
| 7 | + | type ContentWhitelistRepository interface { | |||
| 8 | + | // Add adds a domain to the whitelist. | |||
| 9 | + | Add(ctx context.Context, domain string) error | |||
| 10 | + | ||||
| 11 | + | // Remove removes a domain from the whitelist. | |||
| 12 | + | Remove(ctx context.Context, domain string) error | |||
| 13 | + | ||||
| 14 | + | // Contains checks if a domain is whitelisted. | |||
| 15 | + | Contains(ctx context.Context, domain string) (bool, error) | |||
| 16 | + | ||||
| 17 | + | // GetAll retrieves all whitelisted domains. | |||
| 18 | + | GetAll(ctx context.Context) ([]string, error) | |||
| 19 | + | } | |||
A internal/domain/repository/history.go +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | package repository | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // HistoryRepository defines operations for browsing history persistence. | |
| 11 | + | type HistoryRepository interface { | |
| 12 | + | // Save creates or updates a history entry (upsert). | |
| 13 | + | Save(ctx context.Context, entry *entity.HistoryEntry) error | |
| 14 | + | ||
| 15 | + | // FindByURL retrieves a history entry by its URL. | |
| 16 | + | FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) | |
| 17 | + | ||
| 18 | + | // Search performs a fuzzy search on history entries. | |
| 19 | + | Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) | |
| 20 | + | ||
| 21 | + | // GetRecent retrieves recent history entries with pagination. | |
| 22 | + | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | |
| 23 | + | ||
| 24 | + | // IncrementVisitCount increments the visit count for a URL. | |
| 25 | + | IncrementVisitCount(ctx context.Context, url string) error | |
| 26 | + | ||
| 27 | + | // Delete removes a single history entry by ID. | |
| 28 | + | Delete(ctx context.Context, id int64) error | |
| 29 | + | ||
| 30 | + | // DeleteOlderThan removes entries older than the given time. | |
| 31 | + | DeleteOlderThan(ctx context.Context, before time.Time) error | |
| 32 | + | ||
| 33 | + | // DeleteAll removes all history entries. | |
| 34 | + | DeleteAll(ctx context.Context) error | |
| 35 | + | } | |
| @@ -0,0 +1,35 @@ | |||||
| 1 | + | package repository | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // HistoryRepository defines operations for browsing history persistence. | |||
| 11 | + | type HistoryRepository interface { | |||
| 12 | + | // Save creates or updates a history entry (upsert). | |||
| 13 | + | Save(ctx context.Context, entry *entity.HistoryEntry) error | |||
| 14 | + | ||||
| 15 | + | // FindByURL retrieves a history entry by its URL. | |||
| 16 | + | FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) | |||
| 17 | + | ||||
| 18 | + | // Search performs a fuzzy search on history entries. | |||
| 19 | + | Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) | |||
| 20 | + | ||||
| 21 | + | // GetRecent retrieves recent history entries with pagination. | |||
| 22 | + | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | |||
| 23 | + | ||||
| 24 | + | // IncrementVisitCount increments the visit count for a URL. | |||
| 25 | + | IncrementVisitCount(ctx context.Context, url string) error | |||
| 26 | + | ||||
| 27 | + | // Delete removes a single history entry by ID. | |||
| 28 | + | Delete(ctx context.Context, id int64) error | |||
| 29 | + | ||||
| 30 | + | // DeleteOlderThan removes entries older than the given time. | |||
| 31 | + | DeleteOlderThan(ctx context.Context, before time.Time) error | |||
| 32 | + | ||||
| 33 | + | // DeleteAll removes all history entries. | |||
| 34 | + | DeleteAll(ctx context.Context) error | |||
| 35 | + | } | |||
A internal/domain/repository/zoom.go +23 -0
| @@ -0,0 +1,23 @@ | |||
| 1 | + | package repository | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // ZoomRepository defines operations for per-domain zoom level persistence. | |
| 10 | + | type ZoomRepository interface { | |
| 11 | + | // Get retrieves the zoom level for a domain. | |
| 12 | + | // Returns nil if no custom zoom is set. | |
| 13 | + | Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) | |
| 14 | + | ||
| 15 | + | // Set saves or updates the zoom level for a domain. | |
| 16 | + | Set(ctx context.Context, level *entity.ZoomLevel) error | |
| 17 | + | ||
| 18 | + | // Delete removes the custom zoom level for a domain. | |
| 19 | + | Delete(ctx context.Context, domain string) error | |
| 20 | + | ||
| 21 | + | // GetAll retrieves all custom zoom levels. | |
| 22 | + | GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) | |
| 23 | + | } | |
| @@ -0,0 +1,23 @@ | |||||
| 1 | + | package repository | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // ZoomRepository defines operations for per-domain zoom level persistence. | |||
| 10 | + | type ZoomRepository interface { | |||
| 11 | + | // Get retrieves the zoom level for a domain. | |||
| 12 | + | // Returns nil if no custom zoom is set. | |||
| 13 | + | Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) | |||
| 14 | + | ||||
| 15 | + | // Set saves or updates the zoom level for a domain. | |||
| 16 | + | Set(ctx context.Context, level *entity.ZoomLevel) error | |||
| 17 | + | ||||
| 18 | + | // Delete removes the custom zoom level for a domain. | |||
| 19 | + | Delete(ctx context.Context, domain string) error | |||
| 20 | + | ||||
| 21 | + | // GetAll retrieves all custom zoom levels. | |||
| 22 | + | GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) | |||
| 23 | + | } | |||