feat(frontend): notes list with tag chips and the tag in the url, from the menu and settings, capture status dot on the menu and a line in settings
This commit is contained in:
1 parent
171d8d922d
commit
9c556ff8b9
8 files changed
+596
-9
No files matched your search
@@ -5,10 +5,12 @@
|
||||
import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte";
|
||||
import Button from "#lib/components/ui/Button.svelte";
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import { captureLine } from "#lib/format/capture.js";
|
||||
import { accountName } from "#lib/format/peer.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||
import { capture } from "#lib/stores/capture.svelte.js";
|
||||
import { theme } from "#lib/stores/theme.svelte.js";
|
||||
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
||||
|
||||
@@ -23,6 +25,7 @@
|
||||
const entries: Entry[] = [
|
||||
{ icon: "eye", label: "settings.watches", panel: "watches" },
|
||||
{ icon: "unmute", label: "settings.alerts", panel: "alerts" },
|
||||
{ icon: "note", label: "notes.title", panel: "notes" },
|
||||
{ icon: "data", label: "settings.storage", panel: "storage" },
|
||||
{ icon: "play-story", label: "settings.stories", panel: "stories-all" },
|
||||
{ icon: "cloud-download", label: "settings.jobs", panel: "jobs" },
|
||||
@@ -31,6 +34,12 @@
|
||||
|
||||
let adding = $state(false);
|
||||
|
||||
const quiet = $derived(
|
||||
capture.status !== null && !capture.status.live
|
||||
? captureLine(capture.status, accounts.selected?.is_bot ?? false)
|
||||
: null
|
||||
);
|
||||
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
const match = DIGIT.exec(event.code);
|
||||
if (!(event.altKey && match)) {
|
||||
@@ -54,14 +63,18 @@
|
||||
<span class="menu-trigger">
|
||||
<Button
|
||||
{...props}
|
||||
aria-label={t("menu.open")}
|
||||
aria-label={quiet ? `${t("menu.open")}. ${quiet}` : t("menu.open")}
|
||||
round
|
||||
smaller
|
||||
title={quiet ?? undefined}
|
||||
variant="translucent"
|
||||
>
|
||||
<Icon name="menu" />
|
||||
</Button>
|
||||
<AlertBadge />
|
||||
{#if quiet}
|
||||
<span class="capture-dot"></span>
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
@@ -142,6 +155,21 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.capture-dot {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
right: 0.125rem;
|
||||
bottom: 0.125rem;
|
||||
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 50%;
|
||||
|
||||
background-color: var(--color-error);
|
||||
box-shadow: 0 0 0 2px var(--color-background);
|
||||
}
|
||||
|
||||
:global(.main-menu) {
|
||||
width: 17rem;
|
||||
max-height: calc(100dvh - 5rem);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AlertsPanel from "#lib/components/alerts/AlertsPanel.svelte";
|
||||
import AnnotationsPanel from "#lib/components/annotations/AnnotationsPanel.svelte";
|
||||
import NotesPanel from "#lib/components/annotations/NotesPanel.svelte";
|
||||
import BotsPanel from "#lib/components/bots/BotsPanel.svelte";
|
||||
import StoragePanel from "#lib/components/disk/StoragePanel.svelte";
|
||||
import TrashPanel from "#lib/components/disk/TrashPanel.svelte";
|
||||
@@ -25,6 +26,7 @@
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||
import { chats } from "#lib/stores/chats.svelte.js";
|
||||
import { notes } from "#lib/stores/notes.svelte.js";
|
||||
import { policyUi } from "#lib/stores/policy.svelte.js";
|
||||
import { storageUi } from "#lib/stores/storage.svelte.js";
|
||||
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
||||
@@ -37,6 +39,7 @@
|
||||
changes: "feed.changes",
|
||||
jobs: "settings.jobs",
|
||||
links: "panel.links",
|
||||
notes: "notes.title",
|
||||
policy: "settings.policy",
|
||||
presence: "chat.analytics",
|
||||
profile: "chat.profile",
|
||||
@@ -49,7 +52,6 @@
|
||||
"storage-trash": "panel.trash",
|
||||
stories: "settings.stories",
|
||||
"stories-all": "panel.allStories",
|
||||
versions: "panel.versions",
|
||||
watches: "settings.watches",
|
||||
webhooks: "watches.webhooks",
|
||||
};
|
||||
@@ -65,6 +67,9 @@
|
||||
if (ui.rightPanel === null) {
|
||||
return "";
|
||||
}
|
||||
if (ui.rightPanel === "notes" && notes.tag !== null) {
|
||||
return t("notes.tag", { tag: notes.tag });
|
||||
}
|
||||
const chat =
|
||||
ui.rightPanel === "storage-chat" && storageUi.chatId !== null
|
||||
? chats.byId(storageUi.chatId)
|
||||
@@ -139,6 +144,10 @@
|
||||
<AlertsPanel />
|
||||
{:else if ui.rightPanel === "annotations"}
|
||||
<AnnotationsPanel />
|
||||
{:else if ui.rightPanel === "notes"}
|
||||
{#key accounts.selectedId}
|
||||
<NotesPanel />
|
||||
{/key}
|
||||
{:else if ui.rightPanel === "shares"}
|
||||
<SharesPanel />
|
||||
{:else if ui.rightPanel === "bots"}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import Icon from "../ui/Icon.svelte";
|
||||
import AnnotationEditor from "./AnnotationEditor.svelte";
|
||||
import NoteText from "./NoteText.svelte";
|
||||
|
||||
interface Props {
|
||||
annotations: Annotation[];
|
||||
@@ -32,7 +33,7 @@
|
||||
submitLabel={t("common.save")}
|
||||
/>
|
||||
{:else}
|
||||
<p class="text">{annotation.text}</p>
|
||||
<NoteText text={annotation.text} />
|
||||
<div class="meta">
|
||||
<span class="time">{formatFull(annotation.updated_at)}</span>
|
||||
<button
|
||||
@@ -75,12 +76,6 @@
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||
import { notes } from "#lib/stores/notes.svelte.js";
|
||||
import { ui } from "#lib/stores/ui.svelte.js";
|
||||
import { page } from "$app/state";
|
||||
import AnnotationEditor from "./AnnotationEditor.svelte";
|
||||
@@ -41,6 +42,7 @@
|
||||
.then((rows) => {
|
||||
if (current === token) {
|
||||
items = rows;
|
||||
notes.mark(cid, mid, rows.length > 0);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -59,6 +61,7 @@
|
||||
try {
|
||||
const created = await createAnnotation(chatId, messageId, text);
|
||||
items = [created, ...items];
|
||||
notes.mark(chatId, messageId, true);
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
@@ -72,6 +75,7 @@
|
||||
async function remove(annotation: Annotation) {
|
||||
await deleteAnnotation(annotation.id);
|
||||
items = items.filter((item) => item.id !== annotation.id);
|
||||
notes.mark(annotation.chat_id, annotation.message_id, items.length > 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { noteParts, notes } from "#lib/stores/notes.svelte.js";
|
||||
|
||||
interface Props {
|
||||
interactive?: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const { interactive = true, text }: Props = $props();
|
||||
|
||||
const parts = $derived(noteParts(text));
|
||||
|
||||
function openTag(event: MouseEvent, tag: string) {
|
||||
event.stopPropagation();
|
||||
notes.openTag(tag);
|
||||
}
|
||||
</script>
|
||||
|
||||
<p class="note-text">
|
||||
{#each parts as part, index (index)}
|
||||
{#if "tag" in part && interactive}
|
||||
<button
|
||||
class="tag"
|
||||
onclick={(event) => openTag(event, part.tag)}
|
||||
type="button"
|
||||
class:active={notes.tag === part.tag}
|
||||
>
|
||||
{part.label}
|
||||
</button>
|
||||
{:else if "tag" in part}
|
||||
<span class="tag" class:active={notes.tag === part.tag}
|
||||
>{part.label}</span
|
||||
>
|
||||
{:else}
|
||||
{part.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</p>
|
||||
|
||||
<style lang="scss">
|
||||
.note-text {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline;
|
||||
|
||||
padding: 0 0.3125rem;
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
font: inherit;
|
||||
font-size: 0.875em;
|
||||
color: var(--color-primary);
|
||||
|
||||
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&.active {
|
||||
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:is(button):hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary) 20%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&:is(button) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,402 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { ripple } from "#lib/actions/ripple.js";
|
||||
import {
|
||||
listNotes,
|
||||
listNoteTags,
|
||||
type Note,
|
||||
type NoteTag,
|
||||
} from "#lib/api/notes.js";
|
||||
import NoteText from "#lib/components/annotations/NoteText.svelte";
|
||||
import Avatar from "#lib/components/ui/Avatar.svelte";
|
||||
import Button from "#lib/components/ui/Button.svelte";
|
||||
import EmptyState from "#lib/components/ui/EmptyState.svelte";
|
||||
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||
import { formatListDate } from "#lib/format/datetime.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import { replaceParam } from "#lib/params.js";
|
||||
import { NOTES_PARAM, notes } from "#lib/stores/notes.svelte.js";
|
||||
import { ui } from "#lib/stores/ui.svelte.js";
|
||||
import { isMobile } from "#lib/viewport.js";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
const PAGE = 50;
|
||||
|
||||
let items = $state<Note[]>([]);
|
||||
let tags = $state<NoteTag[]>([]);
|
||||
let loading = $state(true);
|
||||
let loadingMore = $state(false);
|
||||
let hasMore = $state(false);
|
||||
let failed = $state(false);
|
||||
let token = 0;
|
||||
|
||||
const chips = $derived.by(() => {
|
||||
const current = notes.tag;
|
||||
if (current === null || tags.some((item) => item.tag === current)) {
|
||||
return tags;
|
||||
}
|
||||
return [{ count: 0, tag: current }, ...tags];
|
||||
});
|
||||
|
||||
async function load(tag: string | null, offset = 0) {
|
||||
token += 1;
|
||||
const mine = token;
|
||||
if (offset === 0) {
|
||||
loading = true;
|
||||
} else {
|
||||
loadingMore = true;
|
||||
}
|
||||
try {
|
||||
const page = await listNotes(tag, offset, PAGE);
|
||||
if (mine !== token) {
|
||||
return;
|
||||
}
|
||||
items = offset === 0 ? page : [...items, ...page];
|
||||
hasMore = page.length === PAGE;
|
||||
failed = false;
|
||||
} catch {
|
||||
if (mine === token) {
|
||||
failed = true;
|
||||
}
|
||||
} finally {
|
||||
if (mine === token) {
|
||||
loading = false;
|
||||
loadingMore = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const { tag } = notes;
|
||||
untrack(() => {
|
||||
replaceParam(NOTES_PARAM, tag ?? "");
|
||||
load(tag);
|
||||
});
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
listNoteTags()
|
||||
.then((result) => {
|
||||
tags = result;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
notes.tag = null;
|
||||
replaceParam(NOTES_PARAM, null);
|
||||
};
|
||||
});
|
||||
|
||||
function pick(tag: string | null) {
|
||||
notes.tag = notes.tag === tag ? null : tag;
|
||||
}
|
||||
|
||||
function href(note: Note): string {
|
||||
return `/app/${note.chat_id}?message=${note.message_id}`;
|
||||
}
|
||||
|
||||
function open(event: MouseEvent, note: Note) {
|
||||
if (
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.button !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
ui.requestJump(note.chat_id, note.message_id);
|
||||
if (isMobile()) {
|
||||
ui.resetPanel();
|
||||
goto(href(note));
|
||||
return;
|
||||
}
|
||||
const tag = notes.tag === null ? "" : encodeURIComponent(notes.tag);
|
||||
goto(`${href(note)}&${NOTES_PARAM}=${tag}`);
|
||||
}
|
||||
|
||||
function chatTitle(note: Note): string {
|
||||
return note.chat_title ?? t("chat.title", { id: note.chat_id });
|
||||
}
|
||||
|
||||
function quote(note: Note): string {
|
||||
if (note.message_date === null) {
|
||||
return t("notes.messageGone");
|
||||
}
|
||||
return note.message_text ?? t("notes.mediaMessage");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="notes">
|
||||
{#if chips.length > 0}
|
||||
<div class="chips">
|
||||
<button
|
||||
aria-pressed={notes.tag === null}
|
||||
class="chip"
|
||||
onclick={() => pick(null)}
|
||||
type="button"
|
||||
class:on={notes.tag === null}
|
||||
>
|
||||
{t("notes.all")}
|
||||
</button>
|
||||
{#each chips as item (item.tag)}
|
||||
<button
|
||||
aria-pressed={notes.tag === item.tag}
|
||||
class="chip"
|
||||
onclick={() => pick(item.tag)}
|
||||
type="button"
|
||||
class:on={notes.tag === item.tag}
|
||||
>
|
||||
#{item.tag}
|
||||
{#if item.count > 0}
|
||||
<span class="value">{item.count}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading && items.length === 0}
|
||||
<div class="center"><Spinner /></div>
|
||||
{:else if failed && items.length === 0}
|
||||
<div class="state">
|
||||
<p>{t("notes.failed")}</p>
|
||||
<Button onclick={() => load(notes.tag)} variant="text">
|
||||
{t("notes.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
{:else if items.length === 0}
|
||||
<EmptyState
|
||||
description={notes.tag === null ? t("notes.empty") : undefined}
|
||||
title={notes.tag === null
|
||||
? t("notes.emptyTitle")
|
||||
: t("notes.emptyTag", { tag: notes.tag })}
|
||||
/>
|
||||
{:else}
|
||||
<ul class="list">
|
||||
{#each items as note (note.id)}
|
||||
<li>
|
||||
<a
|
||||
class="row"
|
||||
href={href(note)}
|
||||
onclick={(event) => open(event, note)}
|
||||
use:ripple
|
||||
>
|
||||
<Avatar
|
||||
avatar={{
|
||||
id: note.chat_id,
|
||||
kind: note.chat_id > 0 ? "peer" : "chat",
|
||||
}}
|
||||
colorKey={note.chat_id}
|
||||
hasAvatar={note.chat_has_avatar}
|
||||
name={chatTitle(note)}
|
||||
size={2.75}
|
||||
/>
|
||||
<div class="body">
|
||||
<div class="top">
|
||||
<span class="title">{chatTitle(note)}</span>
|
||||
<span class="date">{formatListDate(note.created_at)}</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
<NoteText interactive={false} text={note.text} />
|
||||
</div>
|
||||
<div class="quote" class:deleted={note.message_deleted}>
|
||||
{quote(note)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if hasMore}
|
||||
<div class="more">
|
||||
<Button
|
||||
loading={loadingMore}
|
||||
onclick={() => load(notes.tag, items.length)}
|
||||
variant="text"
|
||||
>
|
||||
{t("notes.more")}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
.chip {
|
||||
cursor: pointer;
|
||||
|
||||
display: inline-flex;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
|
||||
height: 1.75rem;
|
||||
padding: 0 0.5625rem;
|
||||
border: 0;
|
||||
border-radius: 0.875rem;
|
||||
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-text);
|
||||
|
||||
background-color: var(--color-chat-hover);
|
||||
|
||||
transition:
|
||||
background-color 0.15s ease-out,
|
||||
box-shadow 0.15s ease-out;
|
||||
|
||||
&.on {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary) 12%,
|
||||
transparent
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.75rem;
|
||||
border-radius: 1.125rem;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.625rem;
|
||||
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
--ripple-color: var(--color-interactive-element-hover);
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
font-size: 0.9375rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 0.125rem;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.quote {
|
||||
overflow: hidden;
|
||||
|
||||
margin-top: 0.375rem;
|
||||
padding-inline-start: 0.5rem;
|
||||
border-inline-start: 2px solid var(--color-primary);
|
||||
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
padding: 2rem 1rem;
|
||||
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { captureLine } from "#lib/format/capture.js";
|
||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||
import { capture } from "#lib/stores/capture.svelte.js";
|
||||
|
||||
const status = $derived(capture.status);
|
||||
</script>
|
||||
|
||||
{#if status}
|
||||
<div class="capture-line" role="status" class:quiet={!status.live}>
|
||||
<span class="mark"><span class="dot"></span></span>
|
||||
<span class="label"
|
||||
>{captureLine(status, accounts.selected?.is_bot ?? false)}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.capture-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
|
||||
min-height: 3.5rem;
|
||||
padding: 0 1.25rem;
|
||||
|
||||
font-size: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.mark {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-green);
|
||||
}
|
||||
|
||||
.quiet .dot {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
|
||||
import About from "#lib/components/settings/About.svelte";
|
||||
import Appearance from "#lib/components/settings/Appearance.svelte";
|
||||
import CaptureLine from "#lib/components/settings/CaptureLine.svelte";
|
||||
import Language from "#lib/components/settings/Language.svelte";
|
||||
import MyAccount from "#lib/components/settings/MyAccount.svelte";
|
||||
import SettingsItem from "#lib/components/settings/SettingsItem.svelte";
|
||||
@@ -39,6 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<CaptureLine />
|
||||
<SettingsItem
|
||||
icon="settings"
|
||||
label={t("settings.policy")}
|
||||
@@ -83,6 +85,11 @@
|
||||
<AlertBadge inline />
|
||||
{/snippet}
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
icon="note"
|
||||
label={t("notes.title")}
|
||||
onclick={() => ui.openPanel("notes")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<About />
|
||||
|
||||
Reference in new issue
Block a user