refactor settings into separate components
127 unmodified linesExpand all128129130131132133134135136137138139140141142143127 unmodified linesExpand all
.post-dropdown { @apply flex min-w-54 flex-col gap-1 rounded-sm border-2 p-1 shadow-2xl backdrop-blur-xl backdrop-brightness-60;}
.settings-box { @apply rounded-sm border-2 border-dashed border-(--nucleus-fg)/10 p-4;}
.settings-header { @apply mb-2 text-lg font-bold;}
.settings-desc { @apply mb-2 text-sm text-(--nucleus-fg)/80;}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970<script lang="ts"> import { defaultSettings, type Settings } from '$lib/settings'; import { cache } from '$lib/cache';
interface Props { localSettings: Settings; onReset: () => void; }
let { localSettings = $bindable(), onReset }: Props = $props();
const handleClearCache = () => { cache.clear(); alert('cache cleared!'); };</script>
{#snippet _input(name: string, desc: string)} <div> <label for={name} class="settings-desc block"> {desc} </label> <input id={name} type="url" bind:value={localSettings.endpoints[name]} placeholder={defaultSettings.endpoints[name]} class="single-line-input" /> </div>{/snippet}
<div class="space-y-3 p-4"> <div> <h3 class="settings-header">api endpoints</h3> <div class="settings-box space-y-4"> {@render _input('slingshot', 'slingshot url (for fetching records & resolving identity)')} {@render _input('spacedust', 'spacedust url (for notifications)')} {@render _input('constellation', 'constellation url (for backlinks)')} {@render _input('jetstream', 'jetstream url (for real-time updates)')} </div> </div>
<div class="settings-box"> <label for="social-app-url" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80"> social-app url (for when copying links to posts / profiles) </label> <input id="social-app-url" type="url" bind:value={localSettings.socialAppUrl} placeholder={defaultSettings.socialAppUrl} class="single-line-input" /> </div>
<h3 class="settings-header">cache management</h3> <div class="settings-box"> <p class="settings-desc">clears cached data (records, DID documents, handles, etc.)</p> <button onclick={handleClearCache} class="action-button"> clear cache </button> </div>
<h3 class="settings-header">reset settings</h3> <div class="settings-box"> <p class="settings-desc">resets all settings to their default values</p> <button onclick={onReset} class="action-button border-red-600 text-red-600 hover:bg-red-600/20"> reset to defaults </button> </div></div>1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465<script lang="ts"> import MutedAccountItem from './MutedAccountItem.svelte'; import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'; import type { Did } from '@atcute/lexicons'; import type { Preferences } from '$lib/at/pocket';
interface Props { mutes: Did[]; currentPrefs: Preferences | null; onAddMute: (did: Did) => void; onRemoveMute: (did: Did) => void; }
let { mutes, currentPrefs, onAddMute, onRemoveMute }: Props = $props();
let newMuteInput = $state('');
const handleAddMute = () => { if (!newMuteInput.trim()) return; const did = newMuteInput.trim() as Did; onAddMute(did); newMuteInput = ''; };</script>
<div class="space-y-4 p-4"> <div> <h3 class="settings-header">muted accounts</h3> <div class="settings-box space-y-2"> <div class="flex gap-2"> <input type="text" bind:value={newMuteInput} placeholder="did:plc:..." class="single-line-input flex-1" /> <button onclick={handleAddMute} class="action-button">add</button> </div> {#if mutes.length > 0} <div class="h-fit"> <VirtualList height={Math.min(mutes.length, 6) * 44} itemCount={mutes.length} itemSize={44} > {#snippet item({ index, style }: { index: number; style: string })} <MutedAccountItem {style} did={mutes[index]} onRemove={() => onRemoveMute(mutes[index])} /> {/snippet} </VirtualList> </div> {:else} <p class="py-2 text-center text-sm opacity-50">no muted accounts</p> {/if} </div> </div> {#if currentPrefs} <p class="text-xs opacity-50"> last synced: {new Date(currentPrefs.updatedAt).toLocaleString()} </p> {/if}</div>1234567891011121314151617181920212223242526272829303132333435363738<script lang="ts"> import ColorPicker from 'svelte-awesome-color-picker'; import type { Settings } from '$lib/settings';
interface Props { localSettings: Settings; }
let { localSettings = $bindable() }: Props = $props();</script>
{#snippet color(name: string, desc: string)} <div> <label for={name} class="settings-desc block"> {desc} </label> <div class="color-picker"> <ColorPicker bind:hex={localSettings.theme[name]} isAlpha={false} position="responsive" label={localSettings.theme[name]} /> </div> </div>{/snippet}
<div class="space-y-5 p-4"> <div> <h3 class="settings-header">colors</h3> <div class="settings-box"> {@render color('fg', 'foreground color')} {@render color('bg', 'background color')} {@render color('accent', 'accent color')} {@render color('accent2', 'secondary accent color')} </div> </div></div>1223445767893 unmodified lines1314151619201718192021222321 unmodified lines454647485051525354495051585253545556645758596061626313 unmodified lines777879808687888182838491858687889 unmodified lines989910010110911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419510210310410549 unmodified lines1551561571582531591602552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932941611621631641651661672961681691701711724 unmodified lines177178179180309310311312313314315316317318319320321<script lang="ts"> import { defaultSettings, needsReload, settings } from '$lib/settings'; import { needsReload, settings } from '$lib/settings'; import { get } from 'svelte/store'; import ColorPicker from 'svelte-awesome-color-picker'; import Tabs from './Tabs.svelte'; import { portal } from 'svelte-portal'; import { cache } from '$lib/cache'; import { router, clients, accountPreferences,3 unmodified lines } from '$lib/state.svelte'; import { accounts as accountsStore, generateColorForDid } from '$lib/accounts'; import AccountSelector from './AccountSelector.svelte'; import Dropdown from './Dropdown.svelte'; import MutedAccountItem from './MutedAccountItem.svelte'; import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'; import SettingsAdvancedTab from './SettingsAdvancedTab.svelte'; import SettingsStyleTab from './SettingsStyleTab.svelte'; import SettingsModerationTab from './SettingsModerationTab.svelte'; import type { Did } from '@atcute/lexicons'; import type { AtprotoDid } from '@atcute/lexicons/syntax'; import Icon from '@iconify/svelte';
21 unmodified lines settings.reset(); window.location.reload(); };
const handleClearCache = () => { cache.clear(); alert('cache cleared!'); };
const onTabChange = (tab: string) => router.replace(`/settings/${tab}`);
let selectedAccount: AtprotoDid | null = $state(null); let newMuteInput = $state(''); let syncStatus = $state<'syncing' | 'synced' | null>(null); let isAccountDropdownOpen = $state(false);
const accounts = $derived($accountsStore.filter((a) => clients.has(a.did))); const selectedAccountData = $derived(accounts.find((a) => a.did === selectedAccount)); const currentPrefs = $derived(selectedAccount ? accountPreferences.get(selectedAccount) : null); const currentPrefs = $derived( selectedAccount ? (accountPreferences.get(selectedAccount) ?? null) : null ); const mutes = $derived(currentPrefs?.mutes ?? []);
$effect(() => { if (accounts.length > 0 && !selectedAccount) {13 unmodified lines setTimeout(() => (syncStatus = null), 2000); }, SYNC_DEBOUNCE_MS); };
const handleAddMute = () => { if (!selectedAccount || !newMuteInput.trim()) return; const did = newMuteInput.trim() as Did; const handleAddMute = (did: Did) => { if (!selectedAccount) return; setAccountPreferences(selectedAccount, { mutes: [...mutes, did] }); scheduleSyncFor(selectedAccount); newMuteInput = ''; };
const handleRemoveMute = (did: Did) => { if (!selectedAccount) return;9 unmodified lines setTimeout(() => (syncStatus = null), 2000); };</script>
{#snippet advancedTab()} <div class="space-y-3 p-4"> <div> <h3 class="header">api endpoints</h3> <div class="borders space-y-4"> {#snippet _input(name: string, desc: string)} <div> <label for={name} class="header-desc block"> {desc} </label> <input id={name} type="url" bind:value={localSettings.endpoints[name]} placeholder={defaultSettings.endpoints[name]} class="single-line-input" /> </div> {/snippet} {@render _input('slingshot', 'slingshot url (for fetching records & resolving identity)')} {@render _input('spacedust', 'spacedust url (for notifications)')} {@render _input('constellation', 'constellation url (for backlinks)')} {@render _input('jetstream', 'jetstream url (for real-time updates)')} </div> </div>
<div class="borders"> <label for="social-app-url" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80"> social-app url (for when copying links to posts / profiles) </label> <input id="social-app-url" type="url" bind:value={localSettings.socialAppUrl} placeholder={defaultSettings.socialAppUrl} class="single-line-input" /> </div>
<h3 class="header">cache management</h3> <div class="borders"> <p class="header-desc">clears cached data (records, DID documents, handles, etc.)</p> <button onclick={handleClearCache} class="action-button"> clear cache </button> </div>
<h3 class="header">reset settings</h3> <div class="borders"> <p class="header-desc">resets all settings to their default values</p> <button onclick={handleReset} class="action-button border-red-600 text-red-600 hover:bg-red-600/20" > reset to defaults </button> </div> </div>{/snippet}
{#snippet styleTab()} <div class="space-y-5 p-4"> <div> <h3 class="header">colors</h3> <div class="borders"> {#snippet color(name: string, desc: string)} <div> <label for={name} class="header-desc block"> {desc} </label> <div class="color-picker"> <ColorPicker bind:hex={localSettings.theme[name]} isAlpha={false} position="responsive" label={localSettings.theme[name]} /> </div> </div> {/snippet} {@render color('fg', 'foreground color')} {@render color('bg', 'background color')} {@render color('accent', 'accent color')} {@render color('accent2', 'secondary accent color')} </div> </div> </div>{/snippet}
<div class="flex flex-col"> <div class="mb-6 flex items-center justify-between p-4 pb-0"> <div> <h2 class="text-3xl font-bold">settings</h2>49 unmodified lines </div>
<div class="flex-1"> {#if tab === 'advanced'} {@render advancedTab()} <SettingsAdvancedTab bind:localSettings onReset={handleReset} /> {:else if tab === 'moderation'} <div class="space-y-4 p-4"> <div> <h3 class="header">muted accounts</h3> <div class="borders space-y-2"> <div class="flex gap-2"> <input type="text" bind:value={newMuteInput} placeholder="did:plc:..." class="single-line-input flex-1" /> <button onclick={handleAddMute} class="action-button">add</button> </div> {#if mutes.length > 0} <div class="h-fit"> <VirtualList height={Math.min(mutes.length, 6) * 44} itemCount={mutes.length} itemSize={44} > {#snippet item({ index, style }: { index: number; style: string })} <MutedAccountItem {style} did={mutes[index]} onRemove={() => handleRemoveMute(mutes[index])} /> {/snippet} </VirtualList> </div> {:else} <p class="py-2 text-center text-sm opacity-50">no muted accounts</p> {/if} </div> </div> {#if currentPrefs} <p class="text-xs opacity-50"> last synced: {new Date(currentPrefs.updatedAt).toLocaleString()} </p> {/if} </div> <SettingsModerationTab {mutes} {currentPrefs} onAddMute={handleAddMute} onRemoveMute={handleRemoveMute} /> {:else if tab === 'style'} {@render styleTab()} <SettingsStyleTab bind:localSettings /> {/if} </div>
<div4 unmodified lines > <Tabs tabs={['moderation', 'style', 'advanced']} activeTab={tab} {onTabChange} /> </div></div>
<style> @reference "../app.css"; .borders { @apply rounded-sm border-2 border-dashed border-(--nucleus-fg)/10 p-4; } .header-desc { @apply mb-2 text-sm text-(--nucleus-fg)/80; } .header { @apply mb-2 text-lg font-bold; }</style>