community absorbs the hub: search, transfers and browse ride the same pills, one entry in the sidebar
This commit is contained in:
1 parent
8c25f93afe
commit
64a7561750
7 files changed
+91
-22
No files matched your search
+6
-4
@@ -12,7 +12,6 @@
|
|||||||
import GlassBar from "./components/GlassBar.svelte";
|
import GlassBar from "./components/GlassBar.svelte";
|
||||||
import { previewUi } from "./lib/preview/previewSession.svelte.js";
|
import { previewUi } from "./lib/preview/previewSession.svelte.js";
|
||||||
import Home from "./views/Home.svelte";
|
import Home from "./views/Home.svelte";
|
||||||
import Soulseek from "./views/Soulseek.svelte";
|
|
||||||
import Community from "./views/Community.svelte";
|
import Community from "./views/Community.svelte";
|
||||||
import Library from "./views/Library.svelte";
|
import Library from "./views/Library.svelte";
|
||||||
import Settings from "./views/Settings.svelte";
|
import Settings from "./views/Settings.svelte";
|
||||||
@@ -48,9 +47,12 @@
|
|||||||
// their ids stay routable so the download ring, "get on soulseek"
|
// their ids stay routable so the download ring, "get on soulseek"
|
||||||
// hand-offs, history entries and restored sessions all land on the
|
// hand-offs, history entries and restored sessions all land on the
|
||||||
// hub's right tab (the alias → tab sync lives in Soulseek.svelte).
|
// hub's right tab (the alias → tab sync lives in Soulseek.svelte).
|
||||||
soulseek: Soulseek,
|
// The hub is fused into Community (2026-09-02); its ids still route
|
||||||
search: Soulseek,
|
// so the download ring, "get on soulseek" hand-offs and restored
|
||||||
downloads: Soulseek,
|
// sessions land on the right pill (Community reads the alias).
|
||||||
|
soulseek: Community,
|
||||||
|
search: Community,
|
||||||
|
downloads: Community,
|
||||||
community: Community,
|
community: Community,
|
||||||
library: Library,
|
library: Library,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
|||||||
@@ -190,15 +190,15 @@
|
|||||||
// lights up for them too.
|
// lights up for them too.
|
||||||
const items = [
|
const items = [
|
||||||
{ id: "home", label: "Home", icon: "home" },
|
{ id: "home", label: "Home", icon: "home" },
|
||||||
{ id: "soulseek", label: "Soulseek", icon: "globe" },
|
|
||||||
{ id: "community", label: "Community", icon: "users" },
|
{ id: "community", label: "Community", icon: "users" },
|
||||||
{ id: "library", label: "Your Library", icon: "library" },
|
{ id: "library", label: "Your Library", icon: "library" },
|
||||||
{ id: "settings", label: "Settings", icon: "settings" },
|
{ id: "settings", label: "Settings", icon: "settings" },
|
||||||
{ id: "playlists", label: "Playlists", icon: "playlist" },
|
{ id: "playlists", label: "Playlists", icon: "playlist" },
|
||||||
];
|
];
|
||||||
const HUB_ALIASES = new Set(["soulseek", "search", "downloads"]);
|
// The Soulseek hub is fused into Community; its ids light that entry.
|
||||||
|
const HUB_ALIASES = new Set(["community", "soulseek", "search", "downloads"]);
|
||||||
const itemActive = (item) =>
|
const itemActive = (item) =>
|
||||||
item.id === "soulseek" ? HUB_ALIASES.has(ui.view) : ui.view === item.id;
|
item.id === "community" ? HUB_ALIASES.has(ui.view) : ui.view === item.id;
|
||||||
|
|
||||||
// ---- the Soulseek status, moved here out of the title bar ----
|
// ---- the Soulseek status, moved here out of the title bar ----
|
||||||
function statusClick() {
|
function statusClick() {
|
||||||
@@ -373,13 +373,13 @@
|
|||||||
<button
|
<button
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
class:active={itemActive(item)}
|
class:active={itemActive(item)}
|
||||||
class:has-count={item.id === "downloads" && downloadCount > 0}
|
class:has-count={item.id === "community" && downloadCount > 0}
|
||||||
title={ui.sidebarCollapsed ? item.label : null}
|
title={ui.sidebarCollapsed ? item.label : null}
|
||||||
onclick={() => navigate(item.id)}
|
onclick={() => navigate(item.id)}
|
||||||
>
|
>
|
||||||
<span class="ic"><Icon name={item.icon} size={22} /></span>
|
<span class="ic"><Icon name={item.icon} size={22} /></span>
|
||||||
<span class="lbl">{item.label}</span>
|
<span class="lbl">{item.label}</span>
|
||||||
{#if item.id === "downloads" && downloadCount > 0}
|
{#if item.id === "community" && downloadCount > 0}
|
||||||
<span class="bdg">{downloadCount > 99 ? "99+" : downloadCount}</span>
|
<span class="bdg">{downloadCount > 99 ? "99+" : downloadCount}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.id === "playlists"}
|
{#if item.id === "playlists"}
|
||||||
|
|||||||
@@ -575,7 +575,7 @@
|
|||||||
title={dlFlash
|
title={dlFlash
|
||||||
? "Downloads finished"
|
? "Downloads finished"
|
||||||
: `${dl.active} downloading — open Downloads`}
|
: `${dl.active} downloading — open Downloads`}
|
||||||
onclick={() => (ui.view = "downloads")}
|
onclick={() => { ui.view = "downloads"; ui.hubJump += 1; }}
|
||||||
>
|
>
|
||||||
<svg width="26" height="26" viewBox="0 0 26 26">
|
<svg width="26" height="26" viewBox="0 0 26 26">
|
||||||
<circle class="track" cx="13" cy="13" r="9" />
|
<circle class="track" cx="13" cy="13" r="9" />
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function writeJson(key, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SECTIONS = ["inbox", "rooms", "people", "discover", "spaces", "you"];
|
export const SECTIONS = ["search", "transfers", "browse", "inbox", "rooms", "people", "discover", "spaces", "you"];
|
||||||
|
|
||||||
export const community = $state({
|
export const community = $state({
|
||||||
section: SECTIONS.includes(readJson(LS_SECTION, "")) ? readJson(LS_SECTION, "") : "inbox",
|
section: SECTIONS.includes(readJson(LS_SECTION, "")) ? readJson(LS_SECTION, "") : "inbox",
|
||||||
|
|||||||
+14
-6
@@ -293,6 +293,9 @@ export const ui = $state({
|
|||||||
windowActive: true,
|
windowActive: true,
|
||||||
// Fullscreen lyrics overlay (LyricsOverlay.svelte) over the content pane.
|
// Fullscreen lyrics overlay (LyricsOverlay.svelte) over the content pane.
|
||||||
lyricsFull: false,
|
lyricsFull: false,
|
||||||
|
// Bumped on every jump to a hub id (search/downloads) so the Community
|
||||||
|
// page switches its pill even when ui.view did not change.
|
||||||
|
hubJump: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function toggleNowPlaying() {
|
export function toggleNowPlaying() {
|
||||||
@@ -1714,6 +1717,9 @@ const identityWarmupDone = new Set();
|
|||||||
export function goToView(view, opts = {}) {
|
export function goToView(view, opts = {}) {
|
||||||
captureScroll();
|
captureScroll();
|
||||||
ui.view = view;
|
ui.view = view;
|
||||||
|
// The hub's routable ids are pills on the Community page; a repeat jump
|
||||||
|
// to one (already on that view) still has to switch the pill.
|
||||||
|
if (view === "search" || view === "downloads") ui.hubJump += 1;
|
||||||
if (opts.albumId !== undefined) ui.albumId = opts.albumId;
|
if (opts.albumId !== undefined) ui.albumId = opts.albumId;
|
||||||
if (opts.playlistId !== undefined) ui.playlistId = opts.playlistId;
|
if (opts.playlistId !== undefined) ui.playlistId = opts.playlistId;
|
||||||
recordVisit({ view, label: opts.label ?? VIEW_LABELS[view] ?? view, glyph: VIEW_GLYPHS[view] ?? null, ...opts });
|
recordVisit({ view, label: opts.label ?? VIEW_LABELS[view] ?? view, glyph: VIEW_GLYPHS[view] ?? null, ...opts });
|
||||||
@@ -1721,11 +1727,13 @@ export function goToView(view, opts = {}) {
|
|||||||
|
|
||||||
const VIEW_LABELS = {
|
const VIEW_LABELS = {
|
||||||
home: "Home",
|
home: "Home",
|
||||||
soulseek: "Soulseek",
|
// The hub is fused into Community (2026-09-02): its three routable ids
|
||||||
|
// land on that page's pills and read as that page in the trail.
|
||||||
|
soulseek: "Community",
|
||||||
community: "Community",
|
community: "Community",
|
||||||
search: "Search",
|
search: "Community",
|
||||||
library: "Your Library",
|
library: "Your Library",
|
||||||
downloads: "Downloads",
|
downloads: "Community",
|
||||||
settings: "Settings",
|
settings: "Settings",
|
||||||
playlists: "Playlists",
|
playlists: "Playlists",
|
||||||
playlist: "Playlist",
|
playlist: "Playlist",
|
||||||
@@ -1733,11 +1741,11 @@ const VIEW_LABELS = {
|
|||||||
};
|
};
|
||||||
const VIEW_GLYPHS = {
|
const VIEW_GLYPHS = {
|
||||||
home: "home",
|
home: "home",
|
||||||
soulseek: "globe",
|
soulseek: "users",
|
||||||
community: "users",
|
community: "users",
|
||||||
search: "search",
|
search: "users",
|
||||||
library: "library",
|
library: "library",
|
||||||
downloads: "download",
|
downloads: "users",
|
||||||
settings: "settings",
|
settings: "settings",
|
||||||
playlists: "playlist",
|
playlists: "playlist",
|
||||||
playlist: "music",
|
playlist: "music",
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
import { onMount, untrack } from "svelte";
|
import { onMount, untrack } from "svelte";
|
||||||
import Icon from "../components/Icon.svelte";
|
import Icon from "../components/Icon.svelte";
|
||||||
import Identicon, { hueOf } from "../components/Identicon.svelte";
|
import Identicon, { hueOf } from "../components/Identicon.svelte";
|
||||||
|
import Soulseek from "./Soulseek.svelte";
|
||||||
import * as api from "../lib/api.js";
|
import * as api from "../lib/api.js";
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
|
ui,
|
||||||
slsk,
|
slsk,
|
||||||
downloads,
|
downloads,
|
||||||
network,
|
network,
|
||||||
@@ -61,6 +63,30 @@
|
|||||||
if (app.connection === "online") void loadBuddies();
|
if (app.connection === "online") void loadBuddies();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- the fused hub -------------------------------------------------------
|
||||||
|
// Search, Transfers and Browse are the Soulseek hub rendered embedded;
|
||||||
|
// its old Chat / Users / Rooms tabs map onto this page's own sections.
|
||||||
|
const HUB_SECTIONS = new Set(["search", "transfers", "browse"]);
|
||||||
|
const hubOpen = $derived(HUB_SECTIONS.has(community.section));
|
||||||
|
function fromHub(tab) {
|
||||||
|
const map = { chat: "inbox", users: "people", rooms: "rooms" };
|
||||||
|
const s = map[tab] ?? tab;
|
||||||
|
if (s !== community.section) setSection(s);
|
||||||
|
}
|
||||||
|
// The hub's routable ids (the download ring, "get on soulseek", Home's
|
||||||
|
// CTA) arrive as ui.view; a repeat jump bumps ui.hubJump.
|
||||||
|
$effect(() => {
|
||||||
|
const v = ui.view;
|
||||||
|
void ui.hubJump;
|
||||||
|
const s = v === "search" ? "search" : v === "downloads" ? "transfers" : null;
|
||||||
|
if (s) untrack(() => setSection(s));
|
||||||
|
});
|
||||||
|
const TRANSFER_DONE = new Set(["complete", "failed", "cancelled"]);
|
||||||
|
const transfersActive = $derived(
|
||||||
|
Object.values(downloads.items).filter((d) => !TRANSFER_DONE.has(d.state)).length
|
||||||
|
+ Object.values(network.uploads).filter((u) => u.state === "transferring").length,
|
||||||
|
);
|
||||||
|
|
||||||
// ---- small helpers -------------------------------------------------------
|
// ---- small helpers -------------------------------------------------------
|
||||||
const key = $derived(personaKey());
|
const key = $derived(personaKey());
|
||||||
const profile = $derived(community.profile);
|
const profile = $derived(community.profile);
|
||||||
@@ -133,7 +159,7 @@
|
|||||||
// Hand-offs into the Soulseek hub: browse lives there.
|
// Hand-offs into the Soulseek hub: browse lives there.
|
||||||
function browseUser(u) {
|
function browseUser(u) {
|
||||||
slsk.browseRequest = u;
|
slsk.browseRequest = u;
|
||||||
goToView("soulseek");
|
setSection("browse");
|
||||||
}
|
}
|
||||||
function messageUser(u) {
|
function messageUser(u) {
|
||||||
openChat(u);
|
openChat(u);
|
||||||
@@ -442,6 +468,12 @@
|
|||||||
|
|
||||||
<!-- ── the pills ──────────────────────────────────────────────────── -->
|
<!-- ── the pills ──────────────────────────────────────────────────── -->
|
||||||
<div class="qtabs">
|
<div class="qtabs">
|
||||||
|
<button class="qtab" class:on={community.section === "search"} onclick={() => setSection("search")}>Search</button>
|
||||||
|
<button class="qtab" class:on={community.section === "transfers"} onclick={() => setSection("transfers")}>
|
||||||
|
Transfers{#if transfersActive}<span class="n">{transfersActive}</span>{/if}
|
||||||
|
</button>
|
||||||
|
<button class="qtab" class:on={community.section === "browse"} onclick={() => setSection("browse")}>Browse</button>
|
||||||
|
<span class="pillsep" aria-hidden="true"></span>
|
||||||
<button class="qtab" class:on={community.section === "inbox"} onclick={() => setSection("inbox")}>
|
<button class="qtab" class:on={community.section === "inbox"} onclick={() => setSection("inbox")}>
|
||||||
Inbox{#if unreadTotal}<span class="n">{unreadTotal}</span>{/if}
|
Inbox{#if unreadTotal}<span class="n">{unreadTotal}</span>{/if}
|
||||||
</button>
|
</button>
|
||||||
@@ -462,8 +494,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── SEARCH · TRANSFERS · BROWSE — the hub, embedded ─────────────── -->
|
||||||
|
{#if hubOpen}
|
||||||
|
<Soulseek embedded section={community.section} onsection={fromHub} />
|
||||||
|
|
||||||
<!-- ── INBOX ──────────────────────────────────────────────────────── -->
|
<!-- ── INBOX ──────────────────────────────────────────────────────── -->
|
||||||
{#if community.section === "inbox"}
|
{:else if community.section === "inbox"}
|
||||||
{#if !convos.length}
|
{#if !convos.length}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<Icon name="inbox" size={30} />
|
<Icon name="inbox" size={30} />
|
||||||
|
|||||||
@@ -42,12 +42,27 @@
|
|||||||
import Icon from "../components/Icon.svelte";
|
import Icon from "../components/Icon.svelte";
|
||||||
import PlaylistMenu from "../components/PlaylistMenu.svelte";
|
import PlaylistMenu from "../components/PlaylistMenu.svelte";
|
||||||
|
|
||||||
|
// Fused into the Community area (2026-09-02): the page renders this hub
|
||||||
|
// EMBEDDED for its Search / Transfers / Browse pills and owns the pill
|
||||||
|
// row itself; Rooms, Chat and Users below are the retired originals of
|
||||||
|
// Community's Rooms, Inbox and People (same stores), left in place for
|
||||||
|
// the standalone shape. `onsection` reports every tab change so a
|
||||||
|
// "message this user" from a search result lands on Community's Inbox.
|
||||||
|
let { embedded = false, section = null, onsection = null } = $props();
|
||||||
const aliasTab = (v) =>
|
const aliasTab = (v) =>
|
||||||
v === "downloads" ? "transfers" : v === "search" ? "search" : null;
|
v === "downloads" ? "transfers" : v === "search" ? "search" : null;
|
||||||
let tab = $state(aliasTab(ui.view) ?? "transfers");
|
let tab = $state(untrack(() => section) ?? aliasTab(ui.view) ?? "transfers");
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const t = aliasTab(ui.view);
|
const t = aliasTab(ui.view);
|
||||||
if (t) untrack(() => (tab = t));
|
if (t && !embedded) untrack(() => (tab = t));
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
const s = section;
|
||||||
|
if (embedded && s) untrack(() => (tab = s));
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
const t = tab;
|
||||||
|
if (embedded) untrack(() => onsection?.(t));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- transfers ----------------------------------------------------
|
// ---- transfers ----------------------------------------------------
|
||||||
@@ -348,7 +363,8 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="slsk">
|
<div class="slsk" class:embedded>
|
||||||
|
{#if !embedded}
|
||||||
<div class="qtabs">
|
<div class="qtabs">
|
||||||
<button class="qtab" class:on={tab === "transfers"} onclick={() => (tab = "transfers")}>
|
<button class="qtab" class:on={tab === "transfers"} onclick={() => (tab = "transfers")}>
|
||||||
Transfers
|
Transfers
|
||||||
@@ -372,6 +388,7 @@
|
|||||||
{#if slsk.buddies.length}<span class="n">{slsk.buddies.length}</span>{/if}
|
{#if slsk.buddies.length}<span class="n">{slsk.buddies.length}</span>{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- ═══ TRANSFERS ═══ -->
|
<!-- ═══ TRANSFERS ═══ -->
|
||||||
{#if tab === "transfers"}
|
{#if tab === "transfers"}
|
||||||
@@ -828,6 +845,12 @@
|
|||||||
padding-top: calc(var(--bar-h) + 18px);
|
padding-top: calc(var(--bar-h) + 18px);
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
/* Inside the Community page the shell is the page's; the hub is only
|
||||||
|
the tab body. */
|
||||||
|
.slsk.embedded {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── the Qt tab row ── */
|
/* ── the Qt tab row ── */
|
||||||
.qtabs {
|
.qtabs {
|
||||||
|
|||||||
Reference in new issue
Block a user