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