Skip to content

Commit 94ac449

Browse files
author
techartdev
committed
v0.3.0: Desktop UX overhaul
- Branding: accent colors changed from purple to official blue (#3B82F6) - Sidebar uses real assets/icon.svg instead of generated placeholder - Background service (useBackgroundService): auto-polls peers, subs, communities, public shares - Dashboard: removed manual Refresh, data is live from background service - Discover: merged 2 sync/refresh buttons into one, subscribe icon is Bookmark, subscribed shares show BookmarkCheck indicator - Communities: removed Refresh button, scp:// deep link shown on create with copy button - Settings: removed Load Config, Save only enabled when dirty, success auto-hides after 3s, log path copyable, QUIC hint fixed, Add Peer adds both TCP+QUIC by default, default log level error - All pages use bg prop for shared state instead of local data fetching - Bumped all crates and app to 0.3.0
1 parent af78603 commit 94ac449

18 files changed

Lines changed: 393 additions & 298 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ resolver = "2"
1111
[workspace.package]
1212
edition = "2024"
1313
license = "MPL-2.0"
14-
version = "0.2.1"
14+
version = "0.3.0"
1515
repository = "https://github.com/techartdev/scp2p"
1616

1717
[workspace.dependencies]

app/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "scp2p-app",
33
"private": true,
4-
"version": "0.2.1",
4+
"version": "0.3.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

app/public/icon.svg

Lines changed: 25 additions & 0 deletions
Loading

app/public/scp2p.svg

Lines changed: 0 additions & 15 deletions
This file was deleted.

app/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri/schemas/tauri-conf-v2.json",
33
"productName": "SCP2P",
4-
"version": "0.2.1",
4+
"version": "0.3.0",
55
"identifier": "com.scp2p.desktop",
66
"build": {
77
"frontendDist": "../dist",

app/src/App.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MyShares } from "@/pages/MyShares";
88
import { Settings } from "@/pages/Settings";
99
import { DownloadQueue } from "@/components/DownloadQueue";
1010
import { useDownloadQueue } from "@/hooks/useDownloadQueue";
11+
import { useBackgroundService } from "@/hooks/useBackgroundService";
1112
import { GripHorizontal } from "lucide-react";
1213
import * as cmd from "@/lib/commands";
1314
import type { RuntimeStatus, PageId } from "@/lib/types";
@@ -19,6 +20,7 @@ export default function App() {
1920
const [status, setStatus] = useState<RuntimeStatus | null>(null);
2021
const autoStartAttempted = useRef(false);
2122
const downloadQueue = useDownloadQueue();
23+
const bg = useBackgroundService(status?.running ?? false);
2224

2325
// Resizable download queue panel
2426
const [queueHeight, setQueueHeight] = useState(180);
@@ -30,7 +32,6 @@ export default function App() {
3032
const s = await cmd.runtimeStatus();
3133
setStatus(s);
3234
} catch {
33-
// node may not be ready yet
3435
setStatus({
3536
running: false,
3637
app_version: "",
@@ -62,7 +63,6 @@ export default function App() {
6263

6364
useEffect(() => {
6465
refreshStatus();
65-
// Poll status every 5 seconds
6666
const interval = setInterval(refreshStatus, 5000);
6767
return () => clearInterval(interval);
6868
}, [refreshStatus]);
@@ -73,14 +73,15 @@ export default function App() {
7373
return (
7474
<Dashboard
7575
status={status}
76+
bg={bg}
7677
onRefresh={refreshStatus}
7778
onNavigate={setPage}
7879
/>
7980
);
8081
case "discover":
81-
return <Discover status={status} onNavigate={setPage} downloadQueue={downloadQueue} />;
82+
return <Discover status={status} bg={bg} onNavigate={setPage} downloadQueue={downloadQueue} />;
8283
case "communities":
83-
return <Communities status={status} onNavigate={setPage} />;
84+
return <Communities status={status} bg={bg} onNavigate={setPage} />;
8485
case "search":
8586
return <SearchPage status={status} onNavigate={setPage} />;
8687
case "my-shares":
@@ -91,6 +92,7 @@ export default function App() {
9192
return (
9293
<Dashboard
9394
status={status}
95+
bg={bg}
9496
onRefresh={refreshStatus}
9597
onNavigate={setPage}
9698
/>

app/src/components/layout/Sidebar.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
Search,
66
Package,
77
Settings,
8-
Radio,
98
HelpCircle,
109
ExternalLink,
1110
Download,
@@ -72,9 +71,7 @@ export function Sidebar({ currentPage, onNavigate, nodeRunning, appVersion, down
7271
<aside className="w-56 h-full bg-surface border-r border-border flex flex-col shrink-0">
7372
{/* Logo / Brand */}
7473
<div className="px-5 py-5 flex items-center gap-3">
75-
<div className="h-8 w-8 rounded-xl bg-gradient-to-br from-accent to-accent-cyan flex items-center justify-center shadow-glow">
76-
<Radio className="h-4 w-4 text-white" />
77-
</div>
74+
<img src="/icon.svg" alt="SCP2P" className="h-8 w-8 rounded-xl shadow-glow" />
7875
<div>
7976
<h1 className="text-sm font-bold text-text-primary tracking-tight">
8077
SCP2P
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Central background service hook.
3+
*
4+
* Polls peers, subscriptions, communities, and public shares on a
5+
* regular cadence and exposes reactive state to all pages. Any page
6+
* can call `refresh()` for an immediate one-shot update, but the
7+
* periodic poll ensures data stays fresh without manual intervention.
8+
*/
9+
10+
import { useState, useEffect, useCallback, useRef } from "react";
11+
import * as cmd from "@/lib/commands";
12+
import type {
13+
PeerView,
14+
SubscriptionView,
15+
CommunityView,
16+
PublicShareView,
17+
SyncResultView,
18+
} from "@/lib/types";
19+
20+
/** How often (ms) to poll peers, subs, communities. */
21+
const POLL_INTERVAL = 5_000;
22+
23+
/** How often (ms) to poll public shares from peers (heavier). */
24+
const DISCOVER_INTERVAL = 15_000;
25+
26+
/** How often (ms) to run a sync pass (fetch manifests). */
27+
const SYNC_INTERVAL = 30_000;
28+
29+
export interface BackgroundState {
30+
peers: PeerView[];
31+
subscriptions: SubscriptionView[];
32+
communities: CommunityView[];
33+
publicShares: PublicShareView[];
34+
/** Trigger an immediate full refresh. */
35+
refresh: () => Promise<void>;
36+
/** Trigger an immediate sync (manifest fetch). */
37+
syncNow: () => Promise<SyncResultView | null>;
38+
/** Replace subscriptions after a local mutation (subscribe/unsub). */
39+
setSubscriptions: (subs: SubscriptionView[]) => void;
40+
/** Replace communities after a local mutation (join/leave/create). */
41+
setCommunities: (c: CommunityView[]) => void;
42+
/** True while an automatic sync is running. */
43+
syncing: boolean;
44+
lastSyncMessage: string | null;
45+
}
46+
47+
export function useBackgroundService(nodeRunning: boolean): BackgroundState {
48+
const [peers, setPeers] = useState<PeerView[]>([]);
49+
const [subscriptions, setSubscriptions] = useState<SubscriptionView[]>([]);
50+
const [communities, setCommunities] = useState<CommunityView[]>([]);
51+
const [publicShares, setPublicShares] = useState<PublicShareView[]>([]);
52+
const [syncing, setSyncing] = useState(false);
53+
const [lastSyncMessage, setLastSyncMessage] = useState<string | null>(null);
54+
const mountedRef = useRef(true);
55+
56+
const refreshCore = useCallback(async () => {
57+
if (!nodeRunning) return;
58+
try {
59+
const [p, s, c] = await Promise.all([
60+
cmd.listPeers().catch(() => [] as PeerView[]),
61+
cmd.listSubscriptions().catch(() => [] as SubscriptionView[]),
62+
cmd.listCommunities().catch(() => [] as CommunityView[]),
63+
]);
64+
if (!mountedRef.current) return;
65+
setPeers(p);
66+
setSubscriptions(s);
67+
setCommunities(c);
68+
} catch {
69+
/* node shutting down */
70+
}
71+
}, [nodeRunning]);
72+
73+
const refreshDiscover = useCallback(async () => {
74+
if (!nodeRunning) return;
75+
try {
76+
const ps = await cmd.browsePublicShares().catch(() => [] as PublicShareView[]);
77+
if (!mountedRef.current) return;
78+
setPublicShares(ps);
79+
} catch {
80+
/* ignore */
81+
}
82+
}, [nodeRunning]);
83+
84+
const doSync = useCallback(async (): Promise<SyncResultView | null> => {
85+
if (!nodeRunning) return null;
86+
setSyncing(true);
87+
try {
88+
const result = await cmd.syncNow();
89+
if (!mountedRef.current) return result;
90+
setSubscriptions(result.subscriptions);
91+
if (result.updated_count > 0) {
92+
setLastSyncMessage(
93+
`${result.updated_count} subscription${result.updated_count !== 1 ? "s" : ""} updated`
94+
);
95+
} else {
96+
setLastSyncMessage("Up to date");
97+
}
98+
setTimeout(() => {
99+
if (mountedRef.current) setLastSyncMessage(null);
100+
}, 4000);
101+
return result;
102+
} catch {
103+
return null;
104+
} finally {
105+
if (mountedRef.current) setSyncing(false);
106+
}
107+
}, [nodeRunning]);
108+
109+
const refresh = useCallback(async () => {
110+
await Promise.all([refreshCore(), refreshDiscover()]);
111+
}, [refreshCore, refreshDiscover]);
112+
113+
// Initial load + periodic poll
114+
useEffect(() => {
115+
mountedRef.current = true;
116+
if (!nodeRunning) {
117+
setPeers([]);
118+
setSubscriptions([]);
119+
setCommunities([]);
120+
setPublicShares([]);
121+
return;
122+
}
123+
124+
// Kick off immediately
125+
refreshCore();
126+
refreshDiscover();
127+
128+
const coreTimer = setInterval(refreshCore, POLL_INTERVAL);
129+
const discoverTimer = setInterval(refreshDiscover, DISCOVER_INTERVAL);
130+
const syncTimer = setInterval(doSync, SYNC_INTERVAL);
131+
132+
return () => {
133+
mountedRef.current = false;
134+
clearInterval(coreTimer);
135+
clearInterval(discoverTimer);
136+
clearInterval(syncTimer);
137+
};
138+
}, [nodeRunning, refreshCore, refreshDiscover, doSync]);
139+
140+
return {
141+
peers,
142+
subscriptions,
143+
communities,
144+
publicShares,
145+
refresh,
146+
syncNow: doSync,
147+
setSubscriptions,
148+
setCommunities,
149+
syncing,
150+
lastSyncMessage,
151+
};
152+
}

app/src/index.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ body {
5555
/* ── Focus styles ────────────────────────────────────────────────────── */
5656

5757
*:focus-visible {
58-
outline: 2px solid rgba(99, 102, 241, 0.5);
58+
outline: 2px solid rgba(59, 130, 246, 0.5);
5959
outline-offset: 2px;
6060
border-radius: 4px;
6161
}
@@ -87,8 +87,8 @@ body {
8787
padding: 1px;
8888
background: linear-gradient(
8989
135deg,
90-
rgba(99, 102, 241, 0.3),
91-
rgba(34, 211, 238, 0.1)
90+
rgba(59, 130, 246, 0.3),
91+
rgba(147, 197, 253, 0.1)
9292
);
9393
-webkit-mask: linear-gradient(#fff 0 0) content-box,
9494
linear-gradient(#fff 0 0);
@@ -105,7 +105,7 @@ body {
105105
}
106106

107107
.text-gradient-accent {
108-
background: linear-gradient(135deg, #818cf8 0%, #22d3ee 100%);
108+
background: linear-gradient(135deg, #60A5FA 0%, #93C5FD 100%);
109109
-webkit-background-clip: text;
110110
-webkit-text-fill-color: transparent;
111111
background-clip: text;

0 commit comments

Comments
 (0)