tauri-app: settings view + profile polish

- App.svelte: new 'settings' view with account/actions/about sections,
  sign-out button, pdsBase()/appviewBase() helpers, showError wired
  into logout, profile gains open-in-browser + sign-out + recent-posts
  heading
- NavRail: add settings item + gear icon, View union extended to 5
- tests: NavRail.test.ts now expects 5 buttons + covers settings click,
  NavRailHarness View type extended
This commit is contained in:
tomdebone
2026-07-07 18:52:10 +02:00
parent 550b89673d
commit 43fc889d47
4 changed files with 209 additions and 8 deletions
+194 -4
View File
@@ -7,6 +7,8 @@
fetchProfile,
fetchSearch,
fetchPost,
openExternalUrl,
showError,
type Session,
type Post,
type ProfileResponse,
@@ -19,7 +21,7 @@
import Terminal from "./lib/components/Terminal.svelte";
import Skeleton from "./lib/components/Skeleton.svelte";
type View = "home" | "compose" | "profile" | "search";
type View = "home" | "compose" | "profile" | "search" | "settings";
let view: View = $state("home");
let currentUser: Session | null = $state(null);
@@ -351,6 +353,19 @@
await refreshTimeline(true);
}
async function handleLogout() {
try {
await session.logout();
view = "home";
profile = null;
searchResults = [];
threadRoot = null;
threadParent = null;
userPosts = [];
} catch (e) {
showError(`logout failed: ${String(e)}`);
}
}
// Derive a display handle. The session already gives us the user's
// real handle (e.g. "alice.bsky.social"). When the AppView decorates
// posts that have empty handles it falls back to a synthetic
@@ -360,6 +375,24 @@
if (h.startsWith("@")) return h;
return `@${h}`;
}
// Mirror the URLs the Rust shell reads from MAARCADETWEET_PDS_URL /
// MAARCADETWEET_APPVIEW_URL (see `crates/tauri-app/src-tauri/src/lib.rs`).
// Used in the Settings view to show which backends the client is
// talking to. Kept as plain helpers so they can be swapped for a
// `pds_describe`/`appview_describe` Tauri command later.
function pdsBase(): string {
if (typeof import.meta !== "undefined" && (import.meta as any).env?.VITE_PDS_URL) {
return (import.meta as any).env.VITE_PDS_URL as string;
}
return "http://127.0.0.1:2583";
}
function appviewBase(): string {
if (typeof import.meta !== "undefined" && (import.meta as any).env?.VITE_APPVIEW_URL) {
return (import.meta as any).env.VITE_APPVIEW_URL as string;
}
return "http://127.0.0.1:2584";
}
</script>
{#if !currentUser}
@@ -446,9 +479,10 @@
{:else if profile}
<section class="profile">
<header class="profile__head">
<span class="profile__handle">{displayHandle(profile.handle)}</span>
<span class="profile__did" title={profile.did}>{profile.did}</span>
<div class="profile__handle">{displayHandle(profile.handle)}</div>
<div class="profile__did" title={profile.did}>{profile.did}</div>
</header>
<div class="profile__actions">
<button
class="btn btn--ghost"
@@ -463,7 +497,23 @@
onclick={() =>
copyToClipboard(`at://${profile!.did}/app.twi.post`)}
>copy at-uri</button>
<button
class="btn btn--ghost"
type="button"
title="Open profile in your default browser"
onclick={() =>
openExternalUrl(
`https://bsky.app/profile/${profile!.handle}`,
)}
>open in browser</button>
<button
class="btn btn--ghost"
type="button"
title="Sign out of this app"
onclick={handleLogout}
>sign out</button>
</div>
<dl class="counts">
<div>
<dt>followers</dt>
@@ -478,15 +528,90 @@
<dd>{profile.posts.length}</dd>
</div>
</dl>
{#if profile.posts.length === 0}
<div class="empty">// no posts yet</div>
<div class="empty">// no posts yet — compose your first one</div>
{:else}
<h3 class="profile__h3">// recent posts</h3>
{#each profile.posts as p (p.uri)}
<PostCard post={p} on_thread_click={openThread} />
{/each}
{/if}
</section>
{/if}
{:else if view === "settings"}
<div class="head">
<span class="prompt">$</span>
<span class="title">// settings</span>
</div>
<section class="settings">
<h3 class="settings__h3">// account</h3>
<dl class="settings__rows">
<div>
<dt>handle</dt>
<dd>@{currentUser?.handle ?? "?"}</dd>
</div>
<div>
<dt>did</dt>
<dd class="did-cell">{currentUser?.did ?? "?"}</dd>
</div>
<div>
<dt>posts in cache</dt>
<dd>{userPosts.length}</dd>
</div>
</dl>
<h3 class="settings__h3">// actions</h3>
<div class="settings__actions">
<button
class="btn btn--ghost"
type="button"
onclick={() =>
currentUser && copyToClipboard(currentUser.did)}
>copy my did</button>
<button
class="btn btn--ghost"
type="button"
onclick={() =>
openExternalUrl(
`https://bsky.app/profile/${currentUser?.handle ?? ""}`,
)}
>open profile in browser</button>
<button
class="btn btn--ghost"
type="button"
onclick={() => (view = "home")}
>← back to timeline</button>
</div>
<h3 class="settings__h3">// about</h3>
<dl class="settings__rows">
<div>
<dt>app</dt>
<dd>maarcadetweet</dd>
</div>
<div>
<dt>version</dt>
<dd>0.1.0</dd>
</div>
<div>
<dt>backend</dt>
<dd>{pdsBase()}</dd>
</div>
<div>
<dt>appview</dt>
<dd>{appviewBase()}</dd>
</div>
</dl>
<div class="settings__signout">
<button
class="btn btn--ghost btn--danger"
type="button"
onclick={handleLogout}
>sign out</button>
</div>
</section>
{:else if view === "search"}
<div class="head">
<span class="prompt">$</span>
@@ -753,4 +878,69 @@
border-color: var(--red);
background: rgba(255, 59, 48, 0.08);
}
.btn--danger {
color: var(--red);
border-color: var(--red);
}
.btn--danger:hover:not(:disabled) {
background: rgba(255, 59, 48, 0.08);
color: var(--red);
border-color: var(--red);
}
.profile__h3,
.settings__h3 {
font-family: var(--font-mono);
font-size: var(--fs-50);
color: var(--text-dim);
letter-spacing: 0.04em;
margin: var(--s-4) 0 var(--s-2);
font-weight: 400;
}
.did-cell {
word-break: break-all;
font-size: var(--fs-50);
}
.settings {
padding: 0 var(--s-3);
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.settings__rows {
display: flex;
flex-direction: column;
gap: var(--s-1);
padding: var(--s-2) var(--s-4);
margin: 0 0 var(--s-4);
font-family: var(--font-mono);
font-size: var(--fs-50);
}
.settings__rows > div {
display: flex;
gap: var(--s-3);
}
.settings__rows dt {
color: var(--text-dim);
letter-spacing: 0.04em;
min-width: 9rem;
}
.settings__rows dd {
margin: 0;
color: var(--text);
}
.settings__actions {
display: flex;
flex-wrap: wrap;
gap: var(--s-2);
margin: 0 0 var(--s-4);
}
.settings__signout {
margin-top: var(--s-4);
padding-top: var(--s-4);
border-top: 1px dashed var(--line);
}
</style>