diff --git a/README.md b/README.md index af789da7..6458279b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A visual theming application for [Omarchy](https://omarchy.org). Extract colors ### Wallpaper Tools - Search and download wallpapers from wallhaven.cc directly in the app +- Browse public GitHub repositories for wallpapers - Export favorite wallpapers as a ZIP archive with source metadata - Apply a blurred wallpaper variant while extraction uses the original image - Full wallpaper editor with blur, exposure, sharpen, vignette, grain, and color toning @@ -133,6 +134,7 @@ From `frontend/`, run `npm ci`, `npm run check`, `npm test`, and `npm run build` | [Base16 Schemes](docs/base16.md) | Import community color schemes | | [Wallpaper Editor](docs/wallpaper-editor.md) | Image filters and presets | | [Wallhaven](docs/wallhaven.md) | Browse online wallpapers | +| [GitHub Source](docs/github-source.md) | Browse repository wallpapers | | [Favorites](docs/favorites.md) | Save wallpapers and export a collection | | [Blueprints](docs/blueprints.md) | Save and restore themes | | [Custom Templates](docs/custom-templates.md) | Add support for your apps | diff --git a/app.go b/app.go index 82b55247..f2d6d6d3 100644 --- a/app.go +++ b/app.go @@ -17,6 +17,7 @@ import ( "aether/internal/extraction" "aether/internal/favexport" "aether/internal/favorites" + "aether/internal/githubsource" "aether/internal/icontheme" "aether/internal/omarchy" "aether/internal/platform" @@ -41,6 +42,7 @@ type App struct { favorites *favorites.Service favExport *favexport.Exporter wallhaven *wallhaven.Client + githubSource *githubsource.Client batch *batch.Processor iconThemes *icontheme.Catalog themeWatcher *theme.ThemeWatcher @@ -101,6 +103,7 @@ func NewApp() *App { favorites: favorites.NewService(), favExport: favexport.New(wh), wallhaven: wh, + githubSource: githubsource.NewClient(), batch: batch.NewProcessor(), iconThemes: icontheme.NewCatalog(), themeWatcher: theme.NewThemeWatcher(), @@ -480,6 +483,16 @@ func (a *App) ThemeFolderExists(name string) bool { return err == nil && info.IsDir() } +// ListGitHubImages lists a public repository directory. +func (a *App) ListGitHubImages(rawURL string) (*githubsource.ListContentsResult, error) { + return a.githubSource.ListImages(rawURL) +} + +// GetGitHubThumbnail returns a bounded, cached preview for a remote wallpaper. +func (a *App) GetGitHubThumbnail(rawURL string) (*githubsource.ThumbnailResult, error) { + return githubsource.DownloadThumbnail(rawURL) +} + // BlurWallpaper prepares a cached preview of the derived wallpaper. func (a *App) BlurWallpaper(path string) (string, error) { return wallpaper.CreateBlurredVariant(path, platform.BlurDir()) diff --git a/docs/github-source.md b/docs/github-source.md new file mode 100644 index 00000000..35543f6a --- /dev/null +++ b/docs/github-source.md @@ -0,0 +1,43 @@ +# GitHub Source + +Use GitHub as a wallpaper source for Aether themes. +The browser supports public repositories and the same still-image formats as Local. + +## Browse a repository + +1. Open the `GitHub` tab. +2. Enter a repository URL. +3. Click `Fetch`. +4. Open a directory or select `Use` on an image. + +Use the name filter to narrow the current results. +The heart saves an image in Favorites. +The additional-image control adds it to the current theme's image set. +On Omarchy, `Wallpaper only` changes the background and preserves the active theme. + +The command palette also provides `Go to GitHub`. +Saved repositories remain available between sessions. + +## URL formats + +| Source | Example | +| --- | --- | +| Repository | `https://github.com/owner/repo` | +| Branch | `https://github.com/owner/repo/tree/main` | +| Directory | `https://github.com/owner/repo/tree/main/wallpapers` | +| Image | `https://github.com/owner/repo/blob/main/image.png` | +| Raw image | `https://raw.githubusercontent.com/owner/repo/main/image.png` | + +Use the source repository URL for a GitHub Pages site. +Published Pages paths can differ from repository paths. +Encode a slash inside a branch name as `%2F`. + +## Downloads and previews + +Aether loads thumbnails when their cards approach the visible area. +Previews use the shared download and image limits. +The metadata cache retains up to 32 responses for five minutes. + +GitHub limits anonymous API requests. +If the limit is reached, wait before you retry. +The browser lists one directory at a time. diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 53e58c24..c351bea7 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -6,6 +6,7 @@ import TargetAppsStrip from '$lib/components/layout/TargetAppsStrip.svelte'; import ThemeEditor from '$lib/components/editor/ThemeEditor.svelte'; import WallhavenBrowser from '$lib/components/wallhaven/WallhavenBrowser.svelte'; + import GitHubBrowser from '$lib/components/github/GitHubBrowser.svelte'; import LocalBrowser from '$lib/components/local/LocalBrowser.svelte'; import FavoritesView from '$lib/components/favorites/FavoritesView.svelte'; import BlueprintsView from '$lib/components/blueprints/BlueprintsView.svelte'; @@ -46,6 +47,7 @@ const VALID_TABS: readonly Tab[] = [ 'editor', 'wallhaven', + 'github', 'local', 'favorites', 'blueprints', @@ -563,6 +565,8 @@ {:else if activeTab === 'wallhaven'} + {:else if activeTab === 'github'} + {:else if activeTab === 'local'} {:else if activeTab === 'favorites'} diff --git a/frontend/src/lib/commands/commands.svelte.ts b/frontend/src/lib/commands/commands.svelte.ts index fae77bc5..be0bf6b9 100644 --- a/frontend/src/lib/commands/commands.svelte.ts +++ b/frontend/src/lib/commands/commands.svelte.ts @@ -56,6 +56,13 @@ export function buildCommands(): Command[] { keywords: 'browse online wallpapers', run: () => setActiveTab('wallhaven'), }, + { + id: 'nav.github', + label: 'Go to GitHub', + category: 'Navigate', + keywords: 'repository wallpapers source', + run: () => setActiveTab('github'), + }, { id: 'nav.local', label: 'Go to Local', diff --git a/frontend/src/lib/components/ExternalImportDialog.svelte b/frontend/src/lib/components/ExternalImportDialog.svelte index 0772dbad..42730a8c 100644 --- a/frontend/src/lib/components/ExternalImportDialog.svelte +++ b/frontend/src/lib/components/ExternalImportDialog.svelte @@ -32,9 +32,7 @@ let isApplying = $state(false); // edit=true imports load into the editor instead of applying. let isEdit = $derived(!!preview?.edit); - let isOmarchyInstall = $derived( - !isEdit && !!preview?.omarchy_theme_name - ); + let isOmarchyInstall = $derived(!isEdit && !!preview?.omarchy_theme_name); onMount(() => { // The backend emits this on startup when a staged file is present, @@ -186,8 +184,7 @@ {#if isOmarchyInstall}

Install as: - {preview.omarchy_theme_name}{preview.omarchy_theme_name}

{/if} diff --git a/frontend/src/lib/components/favorites/FavoritesView.svelte b/frontend/src/lib/components/favorites/FavoritesView.svelte index f221d6d8..cd1d4944 100644 --- a/frontend/src/lib/components/favorites/FavoritesView.svelte +++ b/frontend/src/lib/components/favorites/FavoritesView.svelte @@ -1,9 +1,10 @@ @@ -320,6 +373,7 @@ : ''} open={previewIndex >= 0} onclose={() => { + previewSequence++; previewIndex = -1; previewSrc = ''; }} diff --git a/frontend/src/lib/components/github/GitHubBrowser.svelte b/frontend/src/lib/components/github/GitHubBrowser.svelte new file mode 100644 index 00000000..134b4306 --- /dev/null +++ b/frontend/src/lib/components/github/GitHubBrowser.svelte @@ -0,0 +1,453 @@ + + +
+ + GitHub URL + + +
+ + {#if savedReposOpen} +
+
+ SAVED REPOS + +
+ {#if savedRepos.length === 0} +
+ No saved repos yet +
+ {:else} + {#each savedRepos as repo} +
loadSavedRepo(repo.url)} + onkeydown={e => { + if ( + e.target === e.currentTarget && + (e.key === 'Enter' || e.key === ' ') + ) { + e.preventDefault(); + loadSavedRepo(repo.url); + } + }} + > + + + + {repo.name} + +
+ {/each} + {/if} +
+ {/if} +
+ + + {#if results.length > 0} + + {/if} +
+ + {#if results.length > 0} + {fileResults.length} / {results.length}{dirResults.length > + 0 + ? `, ${dirResults.length} dir${dirResults.length === 1 ? '' : 's'}` + : ''} + {/if} +
+
+ +
+ {#if isLoading} + + {:else if error} + + {:else if filteredResults.length === 0 && results.length === 0} + + {#snippet icon()} + + + + + {/snippet} + + {:else if filteredResults.length === 0 && results.length > 0} + (nameFilter = '')} + /> + {:else} +
+ {#each filteredResults as item, i (item.path)} + {#if item.type === 'dir'} + + {:else} + handlePreview(item)} + /> + {/if} + {/each} +
+ {/if} +
+
+ += 0 ? fileResults[previewIndex]?.name : ''} + open={previewIndex >= 0} + onclose={closePreview} + hasPrev={previewIndex > 0} + hasNext={previewIndex < fileResults.length - 1} + onprev={() => showPreview(previewIndex - 1)} + onnext={() => showPreview(previewIndex + 1)} +/> diff --git a/frontend/src/lib/components/github/GitHubCard.svelte b/frontend/src/lib/components/github/GitHubCard.svelte new file mode 100644 index 00000000..aa940bf7 --- /dev/null +++ b/frontend/src/lib/components/github/GitHubCard.svelte @@ -0,0 +1,152 @@ + + +
+ useImage()} + onaddextra={() => useImage(true)} + onfavorite={favorite} + onwallpaperonly={() => applyWallpaperOnly(image.url)} + {onpreview} + > + {#snippet thumb()} + {#if thumbSrc}{image.name} + {:else}{thumbError + ? 'Preview unavailable' + : 'Loading preview…'}{/if} + {/snippet} + +
+ {dimensions} + {#if thumbError}{/if} + {image.size > 0 + ? `${(image.size / 1024 / 1024).toFixed(1)} MB` + : ''} +
+
diff --git a/frontend/src/lib/components/layout/HeaderBar.svelte b/frontend/src/lib/components/layout/HeaderBar.svelte index f7bd4986..1a8ecc89 100644 --- a/frontend/src/lib/components/layout/HeaderBar.svelte +++ b/frontend/src/lib/components/layout/HeaderBar.svelte @@ -44,6 +44,11 @@ // Globe icon: '', }, + { + id: 'github', + label: 'GitHub', + icon: '', + }, { id: 'local', label: 'Local', @@ -124,7 +129,11 @@ {/if} -