Skip to content

Commit 533c2f8

Browse files
committed
Recurse into ~/Wallpapers subfolders and add search to local tab
Scan ~/Wallpapers recursively, auto-create it on first run, and skip hidden subdirectories. Files in subfolders carry their relative path as Name so the grid disambiguates duplicates. Add a Browse… picker, a substring search input, and a clear button in the local tab toolbar.
1 parent e6decd8 commit 533c2f8

2 files changed

Lines changed: 96 additions & 36 deletions

File tree

frontend/src/lib/components/local/LocalBrowser.svelte

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
let isLoading = $state(true);
2727
let sortBy = $state<'name' | 'date' | 'size'>('date');
2828
let filterTag = $state<string>('');
29+
let query = $state<string>('');
2930
let previewIndex = $state(-1);
3031
let previewSrc = $state<string>('');
3132
@@ -58,6 +59,11 @@
5859
wps = wps.filter(wp => allAssignments[wp.path] === filterTag);
5960
}
6061
62+
const q = query.trim().toLowerCase();
63+
if (q) {
64+
wps = wps.filter(wp => wp.name.toLowerCase().includes(q));
65+
}
66+
6167
wps.sort((a, b) => {
6268
if (sortBy === 'name') return a.name.localeCompare(b.name);
6369
if (sortBy === 'size') return b.size - a.size;
@@ -68,12 +74,24 @@
6874
})()
6975
);
7076
71-
function handleSelect(wp: Wallpaper) {
72-
setWallpaperPath(wp.path);
77+
function selectWallpaper(path: string) {
78+
setWallpaperPath(path);
7379
setActiveTab('editor');
7480
showToast('Wallpaper selected — click Extract to generate palette');
7581
}
7682
83+
async function handleBrowse() {
84+
try {
85+
const {OpenFileDialog} = await import(
86+
'../../../../wailsjs/go/main/App'
87+
);
88+
const path = await OpenFileDialog();
89+
if (path) selectWallpaper(path);
90+
} catch (err) {
91+
console.error('OpenFileDialog failed', err);
92+
}
93+
}
94+
7795
function handleAddExtra(path: string) {
7896
if (getAdditionalImages().includes(path)) {
7997
showToast('Already in additional images');
@@ -102,6 +120,32 @@
102120
<div
103121
class="bg-bg-secondary border-border flex flex-wrap items-center gap-1.5 border-b px-3 py-2"
104122
>
123+
<button
124+
class="bg-accent hover:bg-accent-hover px-2 py-0.5 text-[11px] font-medium text-[#111116] transition-colors"
125+
onclick={handleBrowse}
126+
title="Browse local files">Browse…</button
127+
>
128+
129+
<span class="bg-border mx-1 h-4 w-px"></span>
130+
131+
<input
132+
type="search"
133+
bind:value={query}
134+
placeholder="Search name or folder…"
135+
class="bg-bg-primary text-fg-primary border-border focus:border-border-focus placeholder:text-fg-dimmed w-44 border px-2 py-0.5 text-[11px] outline-none transition-colors"
136+
/>
137+
138+
{#if query}
139+
<button
140+
class="text-fg-dimmed hover:text-fg-secondary px-1 text-[11px]"
141+
onclick={() => (query = '')}
142+
title="Clear search"
143+
aria-label="Clear search">×</button
144+
>
145+
{/if}
146+
147+
<span class="bg-border mx-1 h-4 w-px"></span>
148+
105149
<!-- Sort -->
106150
<span class="text-fg-dimmed text-[10px] uppercase tracking-wider"
107151
>Sort</span
@@ -162,9 +206,13 @@
162206
{:else if filtered.length === 0}
163207
<div class="flex h-32 items-center justify-center">
164208
<span class="text-fg-dimmed text-[11px]">
165-
{filterTag
166-
? 'No wallpapers with this label'
167-
: 'No wallpapers found in ~/Wallpapers'}
209+
{#if query}
210+
No matches for "{query}"
211+
{:else if filterTag}
212+
No wallpapers with this label
213+
{:else}
214+
No wallpapers found in ~/Wallpapers
215+
{/if}
168216
</span>
169217
</div>
170218
{:else}
@@ -177,7 +225,7 @@
177225
>
178226
<button
179227
class="w-full text-left"
180-
onclick={() => handleSelect(wp)}
228+
onclick={() => selectWallpaper(wp.path)}
181229
>
182230
<div
183231
class="bg-bg-primary aspect-video overflow-hidden"
@@ -226,7 +274,8 @@
226274
>
227275
<button
228276
class="bg-accent hover:bg-accent-hover pointer-events-auto px-4 py-1.5 text-[11px] font-medium text-[#111116] transition-colors"
229-
onclick={() => handleSelect(wp)}>Use</button
277+
onclick={() => selectWallpaper(wp.path)}
278+
>Use</button
230279
>
231280
<button
232281
class="text-fg-primary bg-bg-elevated hover:bg-border-focus pointer-events-auto px-4 py-1.5 text-[11px] font-medium transition-colors"

internal/wallpaper/local.go

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package wallpaper
22

33
import (
4-
"os"
4+
"io/fs"
55
"path/filepath"
66
"strings"
77

88
"aether/internal/platform"
9+
"aether/internal/theme"
910
)
1011

1112
// WallpaperInfo describes a local wallpaper image file.
@@ -16,57 +17,67 @@ type WallpaperInfo struct {
1617
ModTime int64 `json:"modTime"`
1718
}
1819

19-
var imageExtensions = map[string]bool{
20-
".jpg": true,
21-
".jpeg": true,
22-
".png": true,
23-
".gif": true,
24-
".webp": true,
25-
".bmp": true,
26-
".mp4": true,
27-
".webm": true,
28-
}
29-
30-
// ScanDirectory scans a directory for image files and returns their info.
31-
// Only the immediate directory is scanned (no recursion).
20+
// ScanDirectory recursively scans a directory tree for image and video files.
21+
// Hidden directories (names starting with ".") and symlinked directories are
22+
// skipped. Files in subfolders are returned with a relative path as Name so
23+
// the UI can disambiguate files that share a basename across folders.
3224
func ScanDirectory(dir string) ([]WallpaperInfo, error) {
33-
entries, err := os.ReadDir(dir)
25+
root, err := filepath.Abs(dir)
3426
if err != nil {
35-
return nil, err
27+
root = dir
3628
}
3729

3830
var results []WallpaperInfo
39-
for _, entry := range entries {
40-
if entry.IsDir() {
41-
continue
31+
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
32+
if err != nil {
33+
// Ignore unreadable entries; keep walking the rest of the tree.
34+
if d != nil && d.IsDir() {
35+
return fs.SkipDir
36+
}
37+
return nil
4238
}
4339

44-
ext := strings.ToLower(filepath.Ext(entry.Name()))
45-
if !imageExtensions[ext] {
46-
continue
40+
if d.IsDir() {
41+
if path != root && strings.HasPrefix(d.Name(), ".") {
42+
return fs.SkipDir
43+
}
44+
return nil
4745
}
4846

49-
info, err := entry.Info()
47+
if !theme.IsImageFile(path) && !theme.IsVideoFile(path) {
48+
return nil
49+
}
50+
51+
info, err := d.Info()
5052
if err != nil {
51-
continue
53+
return nil
54+
}
55+
56+
name := d.Name()
57+
if rel, err := filepath.Rel(root, path); err == nil && rel != "." && !strings.HasPrefix(rel, "..") {
58+
name = rel
5259
}
5360

5461
results = append(results, WallpaperInfo{
55-
Path: filepath.Join(dir, entry.Name()),
56-
Name: entry.Name(),
62+
Path: path,
63+
Name: name,
5764
Size: info.Size(),
5865
ModTime: info.ModTime().Unix(),
5966
})
67+
return nil
68+
})
69+
if walkErr != nil {
70+
return nil, walkErr
6071
}
6172

6273
return results, nil
6374
}
6475

65-
// ScanDefaultDirs scans ~/Wallpapers for wallpaper images.
76+
// ScanDefaultDirs scans ~/Wallpapers (recursively), creating it if missing.
6677
func ScanDefaultDirs() ([]WallpaperInfo, error) {
6778
dir := platform.WallpaperDir()
68-
if _, err := os.Stat(dir); os.IsNotExist(err) {
69-
return nil, nil
79+
if err := platform.EnsureDir(dir); err != nil {
80+
return nil, err
7081
}
7182
return ScanDirectory(dir)
7283
}

0 commit comments

Comments
 (0)