1// Package usecase contains application use cases that orchestrate domain logic.
2package usecase
3
4import (
5 "context"
6 "fmt"
7 "net/url"
8 "strings"
9
10 "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/domain/entity"
12 "github.com/bnema/dumber/internal/domain/repository"
13 "github.com/bnema/dumber/internal/logging"
14)
15
16// ManageZoomUseCase handles per-domain zoom level operations.
17// It uses an LRU cache to avoid database queries on every navigation.
18type ManageZoomUseCase struct {
19 zoomRepo repository.ZoomRepository
20 defaultZoom float64
21 cache port.Cache[string, *entity.ZoomLevel]
22}
23
24// NewManageZoomUseCase creates a new zoom management use case.
25// defaultZoom is the zoom level to use when resetting (typically from config).
26// cache is an LRU cache for zoom levels to avoid database queries on repeat visits.
27func NewManageZoomUseCase(
28 zoomRepo repository.ZoomRepository,
29 defaultZoom float64,
30 cache port.Cache[string, *entity.ZoomLevel],
31) *ManageZoomUseCase {
32 if defaultZoom <= 0 {
33 defaultZoom = entity.ZoomDefault
34 }
35 return &ManageZoomUseCase{
36 zoomRepo: zoomRepo,
37 defaultZoom: defaultZoom,
38 cache: cache,
39 }
40}
41
42// DefaultZoom returns the configured default zoom level.
43func (uc *ManageZoomUseCase) DefaultZoom() float64 {
44 return uc.defaultZoom
45}
46
47// GetZoom retrieves the zoom level for a domain.
48// Returns the configured default zoom level if none is set.
49// Uses LRU cache to avoid database queries on repeat visits.
50func (uc *ManageZoomUseCase) GetZoom(ctx context.Context, domain string) (*entity.ZoomLevel, error) {
51 log := logging.FromContext(ctx)
52
53 // Check cache first (fast path - no I/O)
54 if uc.cache != nil {
55 if cached, ok := uc.cache.Get(domain); ok {
56 log.Debug().Str("domain", domain).Float64("zoom", cached.ZoomFactor).Msg("zoom level from cache")
57 return cached, nil
58 }
59 }
60
61 // Cache miss - fetch from database
62 log.Debug().Str("domain", domain).Msg("zoom cache miss, querying database")
63 zoom, err := uc.zoomRepo.Get(ctx, domain)
64 if err != nil {
65 return nil, fmt.Errorf("failed to get zoom level: %w", err)
66 }
67
68 if zoom == nil {
69 zoom = entity.NewZoomLevel(domain, uc.defaultZoom)
70 log.Debug().Str("domain", domain).Float64("zoom", zoom.ZoomFactor).Msg("using default zoom")
71 }
72
73 // Cache for future lookups
74 if uc.cache != nil {
75 uc.cache.Set(domain, zoom)
76 }
77
78 return zoom, nil
79}
80
81// SetZoom saves a zoom level for a domain.
82func (uc *ManageZoomUseCase) SetZoom(ctx context.Context, domain string, factor float64) error {
83 log := logging.FromContext(ctx)
84 log.Debug().Str("domain", domain).Float64("factor", factor).Msg("setting zoom level")
85
86 zoom := entity.NewZoomLevel(domain, factor)
87 if err := uc.zoomRepo.Set(ctx, zoom); err != nil {
88 return fmt.Errorf("failed to set zoom level: %w", err)
89 }
90
91 // Update cache
92 if uc.cache != nil {
93 uc.cache.Set(domain, zoom)
94 }
95
96 log.Info().Str("domain", domain).Float64("factor", zoom.ZoomFactor).Msg("zoom level saved")
97 return nil
98}
99
100// ResetZoom removes the custom zoom level for a domain.
101func (uc *ManageZoomUseCase) ResetZoom(ctx context.Context, domain string) error {
102 log := logging.FromContext(ctx)
103 log.Debug().Str("domain", domain).Msg("resetting zoom level")
104
105 if err := uc.zoomRepo.Delete(ctx, domain); err != nil {
106 return fmt.Errorf("failed to reset zoom level: %w", err)
107 }
108
109 // Invalidate cache
110 if uc.cache != nil {
111 uc.cache.Remove(domain)
112 }
113
114 log.Info().Str("domain", domain).Msg("zoom level reset to default")
115 return nil
116}
117
118// ZoomIn increases the zoom level by one step (0.1).
119func (uc *ManageZoomUseCase) ZoomIn(ctx context.Context, domain string, current float64) (*entity.ZoomLevel, error) {
120 log := logging.FromContext(ctx)
121
122 zoom := entity.NewZoomLevel(domain, current)
123 zoom.ZoomIn()
124
125 log.Debug().
126 Str("domain", domain).
127 Float64("from", current).
128 Float64("to", zoom.ZoomFactor).
129 Msg("zooming in")
130
131 if err := uc.zoomRepo.Set(ctx, zoom); err != nil {
132 return nil, fmt.Errorf("failed to save zoom level: %w", err)
133 }
134
135 // Update cache
136 if uc.cache != nil {
137 uc.cache.Set(domain, zoom)
138 }
139
140 return zoom, nil
141}
142
143// ZoomOut decreases the zoom level by one step (0.1).
144func (uc *ManageZoomUseCase) ZoomOut(ctx context.Context, domain string, current float64) (*entity.ZoomLevel, error) {
145 log := logging.FromContext(ctx)
146
147 zoom := entity.NewZoomLevel(domain, current)
148 zoom.ZoomOut()
149
150 log.Debug().
151 Str("domain", domain).
152 Float64("from", current).
153 Float64("to", zoom.ZoomFactor).
154 Msg("zooming out")
155
156 if err := uc.zoomRepo.Set(ctx, zoom); err != nil {
157 return nil, fmt.Errorf("failed to save zoom level: %w", err)
158 }
159
160 // Update cache
161 if uc.cache != nil {
162 uc.cache.Set(domain, zoom)
163 }
164
165 return zoom, nil
166}
167
168// ApplyToWebView loads the saved zoom level and applies it to a webview.
169func (uc *ManageZoomUseCase) ApplyToWebView(ctx context.Context, webview port.WebView, domain string) error {
170 log := logging.FromContext(ctx)
171
172 zoom, err := uc.GetZoom(ctx, domain)
173 if err != nil {
174 return err
175 }
176
177 log.Debug().
178 Str("domain", domain).
179 Float64("factor", zoom.ZoomFactor).
180 Msg("applying zoom to webview")
181
182 if err := webview.SetZoomLevel(ctx, zoom.ZoomFactor); err != nil {
183 return fmt.Errorf("failed to set zoom level: %w", err)
184 }
185 return nil
186}
187
188// GetAll retrieves all saved zoom levels.
189func (uc *ManageZoomUseCase) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) {
190 log := logging.FromContext(ctx)
191 log.Debug().Msg("getting all zoom levels")
192
193 levels, err := uc.zoomRepo.GetAll(ctx)
194 if err != nil {
195 return nil, fmt.Errorf("failed to get zoom levels: %w", err)
196 }
197
198 log.Debug().Int("count", len(levels)).Msg("retrieved zoom levels")
199 return levels, nil
200}
201
202// ExtractZoomKey extracts the persistent zoom storage key from a URL string.
203// Host-based URLs use their host; file:// URLs use the file URI without query
204// or fragment so local documents can persist zoom too.
205func ExtractZoomKey(rawURL string) (string, error) {
206 u, err := url.Parse(rawURL)
207 if err != nil {
208 return "", fmt.Errorf("invalid URL: %w", err)
209 }
210 if strings.EqualFold(u.Scheme, "file") {
211 if u.Path == "" {
212 return "", fmt.Errorf("file URL has no path: %s", rawURL)
213 }
214 u.RawQuery = ""
215 u.Fragment = ""
216 return u.String(), nil
217 }
218 if u.Host == "" {
219 return "", fmt.Errorf("URL has no host: %s", rawURL)
220 }
221 return u.Host, nil
222}
223
224// ExtractDomain extracts the host from a URL string.
225func ExtractDomain(rawURL string) (string, error) {
226 u, err := url.Parse(rawURL)
227 if err != nil {
228 return "", fmt.Errorf("invalid URL: %w", err)
229 }
230 if u.Host == "" {
231 return "", fmt.Errorf("URL has no host: %s", rawURL)
232 }
233 return u.Host, nil
234}