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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user