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}
+ {: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}
-