Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
13 changes: 13 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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())
Expand Down
43 changes: 43 additions & 0 deletions docs/github-source.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -46,6 +47,7 @@
const VALID_TABS: readonly Tab[] = [
'editor',
'wallhaven',
'github',
'local',
'favorites',
'blueprints',
Expand Down Expand Up @@ -563,6 +565,8 @@
<ThemeEditor />
{:else if activeTab === 'wallhaven'}
<WallhavenBrowser />
{:else if activeTab === 'github'}
<GitHubBrowser />
{:else if activeTab === 'local'}
<LocalBrowser />
{:else if activeTab === 'favorites'}
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/lib/commands/commands.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/lib/components/ExternalImportDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -186,8 +184,7 @@
{#if isOmarchyInstall}
<p class="text-fg-secondary mb-2 text-[11px]">
Install as:
<span class="text-fg-primary"
>{preview.omarchy_theme_name}</span
<span class="text-fg-primary">{preview.omarchy_theme_name}</span
>
</p>
{/if}
Expand Down
64 changes: 59 additions & 5 deletions frontend/src/lib/components/favorites/FavoritesView.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script lang="ts">
import {onMount} from 'svelte';
import {onMount, onDestroy} from 'svelte';
import {
setWallpaperPath,
addAdditionalImage,
getAdditionalImages,
getWallpaperRevision,
} from '$lib/stores/theme.svelte';
import {setActiveTab, showToast} from '$lib/stores/ui.svelte';
import {
Expand Down Expand Up @@ -40,6 +41,12 @@
let filterTag = $state<string>('');
let previewIndex = $state(-1);
let previewSrc = $state<string>('');
let current = true;
let previewSequence = 0;
onDestroy(() => {
current = false;
previewSequence++;
});

let allLabels = $derived(getLabels());
let allAssignments = $derived(getAssignments());
Expand All @@ -66,6 +73,17 @@
}
}

async function loadRemoteThumb(path: string) {
try {
const {GetGitHubThumbnail} = await import(
'../../../../wailsjs/go/main/App'
);
const result = await GetGitHubThumbnail(path);
if (result?.dataURL)
setCachedImage('thumb:' + path, result.dataURL);
} catch {}
}

async function loadThumbnails() {
for (const fav of favorites) {
if (isThumbnailCached(fav.path)) continue;
Expand All @@ -76,12 +94,24 @@
continue;
}

// Remote GitHub URLs — use Go thumbnail generator (download +
// resize to 300px, cached on disk for subsequent loads). Fire and
// forget so the loop doesn't block on HTTP downloads.
if (
fav.path.startsWith('http://') ||
fav.path.startsWith('https://')
) {
loadRemoteThumb(fav.path);
continue;
}

// Local files — load thumbnail
loadThumbnail(fav.path);
}
}

async function handleSelect(fav: Favorite) {
const revision = getWallpaperRevision();
let localPath = fav.path;

if (
Expand All @@ -101,6 +131,7 @@
}
}

if (!current || revision !== getWallpaperRevision()) return;
setWallpaperPath(localPath);
setActiveTab('editor');
showToast('Wallpaper selected — click Extract to generate palette');
Expand All @@ -116,6 +147,7 @@
}

async function handleAddExtra(fav: Favorite) {
const revision = getWallpaperRevision();
let localPath = fav.path;

if (
Expand All @@ -135,6 +167,7 @@
}
}

if (!current || revision !== getWallpaperRevision()) return;
if (getAdditionalImages().includes(localPath)) {
showToast('Already in additional images');
return;
Expand All @@ -144,19 +177,39 @@
}

async function resolvePreviewSrc(fav: Favorite): Promise<string> {
if (fav.type === 'github') {
const {DownloadWallpaper} = await import(
'../../../../wailsjs/go/main/App'
);
return loadFullImage(await DownloadWallpaper(fav.path));
}
if (fav.path?.startsWith('http')) return fav.path;
const cached = getCachedFullImage(fav.path);
return cached || (await loadFullImage(fav.path));
}

async function handlePreview(index: number) {
previewSrc = await resolvePreviewSrc(filtered[index]);
previewIndex = index;
const selected = filtered[index];
if (!selected) return;
const sequence = ++previewSequence;
try {
const source = await resolvePreviewSrc(selected);
if (
!current ||
sequence !== previewSequence ||
filtered[index]?.path !== selected.path
)
return;
previewSrc = source;
previewIndex = index;
} catch {
if (current && sequence === previewSequence)
showToast('Could not load the wallpaper preview');
}
}

async function navigatePreview(index: number) {
previewSrc = await resolvePreviewSrc(filtered[index]);
previewIndex = index;
await handlePreview(index);
}
</script>

Expand Down Expand Up @@ -320,6 +373,7 @@
: ''}
open={previewIndex >= 0}
onclose={() => {
previewSequence++;
previewIndex = -1;
previewSrc = '';
}}
Expand Down
Loading
Loading