v0.30.0 eb3068f

manage_zoom.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close