Search seed
diff --git a/src/components/Header.svelte b/src/components/Header.svelte
index 52e46639b..141bbc5fc 100644
--- a/src/components/Header.svelte
+++ b/src/components/Header.svelte
@@ -16,7 +16,7 @@
import { activeRouteStore, homeRoute } from "@app/lib/router";
import config from "@app/lib/config";
import { isMobile } from "@app/lib/media";
- import { determineSeed, selectedSeed } from "@app/views/nodes/SeedSelector";
+ import { determineSeed, searchSeed } from "@app/views/nodes/SeedSelector";
import Settings from "@app/App/Settings.svelte";
import Help from "@app/App/Help.svelte";
@@ -65,21 +65,20 @@
? ($activeRouteStore.params.baseUrl ?? determineSeed())
: undefined;
- // Search always queries the selected explore seed, regardless of which seed
- // the current page (e.g. a repo view) was served from.
- $: searchSeed = $selectedSeed ?? determineSeed();
+ // Search always queries the stored search seed, regardless of which seed the
+ // current page (e.g. a repo view) was served from.
// The explore routes already resolved /info during load; reuse it, but only
// when it describes the same seed we're about to search.
$: routeSearchAvailable =
($activeRouteStore.resource === "explore" ||
$activeRouteStore.resource === "explore.repos") &&
- seedKey($activeRouteStore.params.baseUrl) === seedKey(searchSeed)
+ seedKey($activeRouteStore.params.baseUrl) === seedKey($searchSeed)
? $activeRouteStore.params.searchAvailable
: undefined;
let searchAvailable = false;
- $: void resolveSearchAvailable(searchSeed, routeSearchAvailable);
+ $: void resolveSearchAvailable($searchSeed, routeSearchAvailable);
async function resolveSearchAvailable(
seed: BaseUrl,
@@ -103,7 +102,7 @@
// Older nodes without /info (or an unreachable seed) have no search.
searchAvailableCache[key] = false;
}
- if (seedKey(searchSeed) === key) {
+ if (seedKey($searchSeed) === key) {
searchAvailable = searchAvailableCache[key];
}
}
@@ -234,13 +233,13 @@
{#if showSearch && !collapsedSearch}
{/if}
{#if showSearch && collapsedSearch}
-
+
{/if}
{#if seedPickerBaseUrl}
diff --git a/src/components/NodeAvatar.svelte b/src/components/NodeAvatar.svelte
new file mode 100644
index 000000000..b82f67a25
--- /dev/null
+++ b/src/components/NodeAvatar.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+{#if avatarUrl}
+

+{:else}
+
+{/if}
diff --git a/src/marketing/Landing.svelte b/src/marketing/Landing.svelte
index 75bdc7793..bcc3178c2 100644
--- a/src/marketing/Landing.svelte
+++ b/src/marketing/Landing.svelte
@@ -4,7 +4,7 @@
import { HttpdClient } from "@http-client";
import { push, routeToPath, useDefaultNavigation } from "@app/lib/router";
- import { determineSeed, selectedSeed } from "@app/views/nodes/SeedSelector";
+ import { searchSeed } from "@app/views/nodes/SeedSelector";
import CopyCommand from "@app/marketing/CopyCommand.svelte";
import Meta from "@app/marketing/Meta.svelte";
@@ -44,18 +44,16 @@
}
}
- // Reload whenever the explore seed changes (e.g. picked in Settings) so the
- // carousel always reflects the currently selected seed, not just the one
- // resolved on first mount. `determineSeed()` applies the fallback logic; the
- // `$selectedSeed` reference is what makes this reactive.
+ // Reload whenever the search seed changes (e.g. picked in Settings) so the
+ // carousel always reflects the current seed, not just the one resolved on
+ // first mount.
let loadToken = 0;
// When the selected seed exposes a search backend the carousel shows the
// most-seeded repositories; otherwise it falls back to the repos pinned on
// the seed. Either way, links point at the repo on the current deployment.
- async function loadCarousel(_selected: BaseUrl | undefined) {
+ async function loadCarousel(seed: BaseUrl) {
const token = ++loadToken;
- const seed = determineSeed();
const api = new HttpdClient(seed);
loading = true;
try {
@@ -97,7 +95,7 @@
}
}
- $: void loadCarousel($selectedSeed);
+ $: void loadCarousel($searchSeed);
function onRepoClick(event: MouseEvent, route: Route) {
if (useDefaultNavigation(event)) {
diff --git a/src/views/NotFound.svelte b/src/views/NotFound.svelte
index d70411d1b..f32091600 100644
--- a/src/views/NotFound.svelte
+++ b/src/views/NotFound.svelte
@@ -36,7 +36,7 @@
-
+
{/if}
{#if description}
diff --git a/src/views/explore/SeedPicker.svelte b/src/views/explore/SeedPicker.svelte
index f1fd2d0a6..efa4af531 100644
--- a/src/views/explore/SeedPicker.svelte
+++ b/src/views/explore/SeedPicker.svelte
@@ -19,7 +19,7 @@
clearSeedFailure,
failedSeeds,
removeBookmark,
- selectedSeed,
+ explicitSeed,
} from "@app/views/nodes/SeedSelector";
import { closeFocused } from "@app/components/Popover.svelte";
@@ -38,9 +38,20 @@
export let mode: "node" | "search";
export let ariaLabel = "Seed selector";
export let title = "Switch seed";
+ // Where the picker sits. A breadcrumb sits on the left of the header, so
+ // its popover is anchored to the toggle's left edge instead of its right.
+ export let variant: "header" | "breadcrumb" | "panel" = "header";
const VALIDATION_TIMEOUT_MS = 10000;
+ // The seed currently in use isn't necessarily bookmarked or a default: it
+ // can be reached through a shared link or through failover. List it anyway,
+ // so it is always possible to open its node view from here.
+ $: isListed =
+ $bookmarkedSeeds.some(s => isEqual(s, baseUrl)) ||
+ config.preferredSeeds.some(s => isEqual(s, baseUrl));
+ $: customSeeds = isListed ? $bookmarkedSeeds : [...$bookmarkedSeeds, baseUrl];
+
let expanded: boolean = false;
let loading = false;
let addingNew = false;
@@ -147,7 +158,7 @@
// Either the picker is setting the search seed, or it's in node mode on a
// route whose URL names no node to switch (explore). Both come down to the
// same thing: the search seed is the only node such a page reads.
- selectedSeed.set(seed);
+ explicitSeed.set(seed);
// Leave the current page where it is; only the explore listing reloads,
// since it's the one view served by the search seed. Keyed off the
// unloaded route rather than the loaded one, because a failed listing
@@ -205,6 +216,10 @@
return failed.some(s => isEqual(s, seed));
}
+ function isBookmarked(seed: BaseUrl, bookmarks: BaseUrl[]) {
+ return bookmarks.some(s => isEqual(s, seed));
+ }
+
function handleEscape(event: KeyboardEvent) {
if (expanded && event.key === "Escape") {
closeFocused();
@@ -219,11 +234,17 @@
align-items: center;
gap: 0.375rem;
max-width: 14rem;
- padding: 0.375rem 0.5rem;
+ padding: 0.375rem 0;
border-radius: var(--border-radius-sm);
- color: var(--color-text-secondary);
cursor: pointer;
}
+ .target.breadcrumb {
+ padding: 0.375rem 0.5rem;
+ color: var(--color-text-primary);
+ }
+ .target.panel {
+ padding: 0.375rem 0.5rem;
+ }
.target:hover {
background-color: var(--color-surface-mid);
color: var(--color-text-primary);
@@ -296,13 +317,14 @@
-
+
{baseUrl.hostname}
-
+ {#if variant !== "breadcrumb"}
+
+ {/if}
Custom seeds
- {#if $bookmarkedSeeds.length > 0}
-
+ {#if customSeeds.length > 0}
+
openNodeView(item)}>
- removeBookmark(item)}>
-
-
+ {#if isBookmarked(item, $bookmarkedSeeds)}
+ removeBookmark(item)}>
+
+
+ {:else}
+ addBookmark(item)}>
+
+
+ {/if}
diff --git a/src/views/nodes/SeedSelector.ts b/src/views/nodes/SeedSelector.ts
index 78f0b069a..5f94c2c31 100644
--- a/src/views/nodes/SeedSelector.ts
+++ b/src/views/nodes/SeedSelector.ts
@@ -3,7 +3,7 @@ import type { BaseUrl } from "@http-client";
import isEqual from "lodash/isEqual";
import storedWritable from "@app/lib/localStore";
import * as z from "zod";
-import { get, writable } from "svelte/store";
+import { derived, get, writable } from "svelte/store";
import config from "@app/lib/config";
@@ -19,8 +19,10 @@ const HARDCODED_FALLBACK: BaseUrl = {
port: 443,
};
-// Seed that is opened on cold app start on the landing page.
-export const selectedSeed = storedWritable