appview-less bluesky client
24
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor settings into separate components

dawn 3ddf6456 b42ee4d0

+202 -158
+12
src/app.css
··· 129 129 .post-dropdown { 130 130 @apply flex min-w-54 flex-col gap-1 rounded-sm border-2 p-1 shadow-2xl backdrop-blur-xl backdrop-brightness-60; 131 131 } 132 + 133 + .settings-box { 134 + @apply rounded-sm border-2 border-dashed border-(--nucleus-fg)/10 p-4; 135 + } 136 + 137 + .settings-header { 138 + @apply mb-2 text-lg font-bold; 139 + } 140 + 141 + .settings-desc { 142 + @apply mb-2 text-sm text-(--nucleus-fg)/80; 143 + }
+70
src/components/SettingsAdvancedTab.svelte
··· 1 + <script lang="ts"> 2 + import { defaultSettings, type Settings } from '$lib/settings'; 3 + import { cache } from '$lib/cache'; 4 + 5 + interface Props { 6 + localSettings: Settings; 7 + onReset: () => void; 8 + } 9 + 10 + let { localSettings = $bindable(), onReset }: Props = $props(); 11 + 12 + const handleClearCache = () => { 13 + cache.clear(); 14 + alert('cache cleared!'); 15 + }; 16 + </script> 17 + 18 + {#snippet _input(name: string, desc: string)} 19 + <div> 20 + <label for={name} class="settings-desc block"> 21 + {desc} 22 + </label> 23 + <input 24 + id={name} 25 + type="url" 26 + bind:value={localSettings.endpoints[name]} 27 + placeholder={defaultSettings.endpoints[name]} 28 + class="single-line-input" 29 + /> 30 + </div> 31 + {/snippet} 32 + 33 + <div class="space-y-3 p-4"> 34 + <div> 35 + <h3 class="settings-header">api endpoints</h3> 36 + <div class="settings-box space-y-4"> 37 + {@render _input('slingshot', 'slingshot url (for fetching records & resolving identity)')} 38 + {@render _input('spacedust', 'spacedust url (for notifications)')} 39 + {@render _input('constellation', 'constellation url (for backlinks)')} 40 + {@render _input('jetstream', 'jetstream url (for real-time updates)')} 41 + </div> 42 + </div> 43 + 44 + <div class="settings-box"> 45 + <label for="social-app-url" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80"> 46 + social-app url (for when copying links to posts / profiles) 47 + </label> 48 + <input 49 + id="social-app-url" 50 + type="url" 51 + bind:value={localSettings.socialAppUrl} 52 + placeholder={defaultSettings.socialAppUrl} 53 + class="single-line-input" 54 + /> 55 + </div> 56 + 57 + <h3 class="settings-header">cache management</h3> 58 + <div class="settings-box"> 59 + <p class="settings-desc">clears cached data (records, DID documents, handles, etc.)</p> 60 + <button onclick={handleClearCache} class="action-button"> clear cache </button> 61 + </div> 62 + 63 + <h3 class="settings-header">reset settings</h3> 64 + <div class="settings-box"> 65 + <p class="settings-desc">resets all settings to their default values</p> 66 + <button onclick={onReset} class="action-button border-red-600 text-red-600 hover:bg-red-600/20"> 67 + reset to defaults 68 + </button> 69 + </div> 70 + </div>
+65
src/components/SettingsModerationTab.svelte
··· 1 + <script lang="ts"> 2 + import MutedAccountItem from './MutedAccountItem.svelte'; 3 + import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'; 4 + import type { Did } from '@atcute/lexicons'; 5 + import type { Preferences } from '$lib/at/pocket'; 6 + 7 + interface Props { 8 + mutes: Did[]; 9 + currentPrefs: Preferences | null; 10 + onAddMute: (did: Did) => void; 11 + onRemoveMute: (did: Did) => void; 12 + } 13 + 14 + let { mutes, currentPrefs, onAddMute, onRemoveMute }: Props = $props(); 15 + 16 + let newMuteInput = $state(''); 17 + 18 + const handleAddMute = () => { 19 + if (!newMuteInput.trim()) return; 20 + const did = newMuteInput.trim() as Did; 21 + onAddMute(did); 22 + newMuteInput = ''; 23 + }; 24 + </script> 25 + 26 + <div class="space-y-4 p-4"> 27 + <div> 28 + <h3 class="settings-header">muted accounts</h3> 29 + <div class="settings-box space-y-2"> 30 + <div class="flex gap-2"> 31 + <input 32 + type="text" 33 + bind:value={newMuteInput} 34 + placeholder="did:plc:..." 35 + class="single-line-input flex-1" 36 + /> 37 + <button onclick={handleAddMute} class="action-button">add</button> 38 + </div> 39 + {#if mutes.length > 0} 40 + <div class="h-fit"> 41 + <VirtualList 42 + height={Math.min(mutes.length, 6) * 44} 43 + itemCount={mutes.length} 44 + itemSize={44} 45 + > 46 + {#snippet item({ index, style }: { index: number; style: string })} 47 + <MutedAccountItem 48 + {style} 49 + did={mutes[index]} 50 + onRemove={() => onRemoveMute(mutes[index])} 51 + /> 52 + {/snippet} 53 + </VirtualList> 54 + </div> 55 + {:else} 56 + <p class="py-2 text-center text-sm opacity-50">no muted accounts</p> 57 + {/if} 58 + </div> 59 + </div> 60 + {#if currentPrefs} 61 + <p class="text-xs opacity-50"> 62 + last synced: {new Date(currentPrefs.updatedAt).toLocaleString()} 63 + </p> 64 + {/if} 65 + </div>
+38
src/components/SettingsStyleTab.svelte
··· 1 + <script lang="ts"> 2 + import ColorPicker from 'svelte-awesome-color-picker'; 3 + import type { Settings } from '$lib/settings'; 4 + 5 + interface Props { 6 + localSettings: Settings; 7 + } 8 + 9 + let { localSettings = $bindable() }: Props = $props(); 10 + </script> 11 + 12 + {#snippet color(name: string, desc: string)} 13 + <div> 14 + <label for={name} class="settings-desc block"> 15 + {desc} 16 + </label> 17 + <div class="color-picker"> 18 + <ColorPicker 19 + bind:hex={localSettings.theme[name]} 20 + isAlpha={false} 21 + position="responsive" 22 + label={localSettings.theme[name]} 23 + /> 24 + </div> 25 + </div> 26 + {/snippet} 27 + 28 + <div class="space-y-5 p-4"> 29 + <div> 30 + <h3 class="settings-header">colors</h3> 31 + <div class="settings-box"> 32 + {@render color('fg', 'foreground color')} 33 + {@render color('bg', 'background color')} 34 + {@render color('accent', 'accent color')} 35 + {@render color('accent2', 'secondary accent color')} 36 + </div> 37 + </div> 38 + </div>
+17 -158
src/components/SettingsView.svelte
··· 1 1 <script lang="ts"> 2 - import { defaultSettings, needsReload, settings } from '$lib/settings'; 2 + import { needsReload, settings } from '$lib/settings'; 3 3 import { get } from 'svelte/store'; 4 - import ColorPicker from 'svelte-awesome-color-picker'; 5 4 import Tabs from './Tabs.svelte'; 6 5 import { portal } from 'svelte-portal'; 7 - import { cache } from '$lib/cache'; 8 6 import { 9 7 router, 10 8 clients, ··· 16 14 import { accounts as accountsStore, generateColorForDid } from '$lib/accounts'; 17 15 import AccountSelector from './AccountSelector.svelte'; 18 16 import Dropdown from './Dropdown.svelte'; 19 - import MutedAccountItem from './MutedAccountItem.svelte'; 20 - import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'; 17 + import SettingsAdvancedTab from './SettingsAdvancedTab.svelte'; 18 + import SettingsStyleTab from './SettingsStyleTab.svelte'; 19 + import SettingsModerationTab from './SettingsModerationTab.svelte'; 21 20 import type { Did } from '@atcute/lexicons'; 22 21 import type { AtprotoDid } from '@atcute/lexicons/syntax'; 23 22 import Icon from '@iconify/svelte'; ··· 47 46 window.location.reload(); 48 47 }; 49 48 50 - const handleClearCache = () => { 51 - cache.clear(); 52 - alert('cache cleared!'); 53 - }; 54 - 55 49 const onTabChange = (tab: string) => router.replace(`/settings/${tab}`); 56 50 57 51 let selectedAccount: AtprotoDid | null = $state(null); 58 - let newMuteInput = $state(''); 59 52 let syncStatus = $state<'syncing' | 'synced' | null>(null); 60 53 let isAccountDropdownOpen = $state(false); 61 54 62 55 const accounts = $derived($accountsStore.filter((a) => clients.has(a.did))); 63 56 const selectedAccountData = $derived(accounts.find((a) => a.did === selectedAccount)); 64 - const currentPrefs = $derived(selectedAccount ? accountPreferences.get(selectedAccount) : null); 57 + const currentPrefs = $derived( 58 + selectedAccount ? (accountPreferences.get(selectedAccount) ?? null) : null 59 + ); 65 60 const mutes = $derived(currentPrefs?.mutes ?? []); 66 61 67 62 $effect(() => { ··· 83 78 }, SYNC_DEBOUNCE_MS); 84 79 }; 85 80 86 - const handleAddMute = () => { 87 - if (!selectedAccount || !newMuteInput.trim()) return; 88 - const did = newMuteInput.trim() as Did; 81 + const handleAddMute = (did: Did) => { 82 + if (!selectedAccount) return; 89 83 setAccountPreferences(selectedAccount, { mutes: [...mutes, did] }); 90 84 scheduleSyncFor(selectedAccount); 91 - newMuteInput = ''; 92 85 }; 93 86 94 87 const handleRemoveMute = (did: Did) => { ··· 106 99 }; 107 100 </script> 108 101 109 - {#snippet advancedTab()} 110 - <div class="space-y-3 p-4"> 111 - <div> 112 - <h3 class="header">api endpoints</h3> 113 - <div class="borders space-y-4"> 114 - {#snippet _input(name: string, desc: string)} 115 - <div> 116 - <label for={name} class="header-desc block"> 117 - {desc} 118 - </label> 119 - <input 120 - id={name} 121 - type="url" 122 - bind:value={localSettings.endpoints[name]} 123 - placeholder={defaultSettings.endpoints[name]} 124 - class="single-line-input" 125 - /> 126 - </div> 127 - {/snippet} 128 - {@render _input('slingshot', 'slingshot url (for fetching records & resolving identity)')} 129 - {@render _input('spacedust', 'spacedust url (for notifications)')} 130 - {@render _input('constellation', 'constellation url (for backlinks)')} 131 - {@render _input('jetstream', 'jetstream url (for real-time updates)')} 132 - </div> 133 - </div> 134 - 135 - <div class="borders"> 136 - <label for="social-app-url" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80"> 137 - social-app url (for when copying links to posts / profiles) 138 - </label> 139 - <input 140 - id="social-app-url" 141 - type="url" 142 - bind:value={localSettings.socialAppUrl} 143 - placeholder={defaultSettings.socialAppUrl} 144 - class="single-line-input" 145 - /> 146 - </div> 147 - 148 - <h3 class="header">cache management</h3> 149 - <div class="borders"> 150 - <p class="header-desc">clears cached data (records, DID documents, handles, etc.)</p> 151 - <button onclick={handleClearCache} class="action-button"> clear cache </button> 152 - </div> 153 - 154 - <h3 class="header">reset settings</h3> 155 - <div class="borders"> 156 - <p class="header-desc">resets all settings to their default values</p> 157 - <button 158 - onclick={handleReset} 159 - class="action-button border-red-600 text-red-600 hover:bg-red-600/20" 160 - > 161 - reset to defaults 162 - </button> 163 - </div> 164 - </div> 165 - {/snippet} 166 - 167 - {#snippet styleTab()} 168 - <div class="space-y-5 p-4"> 169 - <div> 170 - <h3 class="header">colors</h3> 171 - <div class="borders"> 172 - {#snippet color(name: string, desc: string)} 173 - <div> 174 - <label for={name} class="header-desc block"> 175 - {desc} 176 - </label> 177 - <div class="color-picker"> 178 - <ColorPicker 179 - bind:hex={localSettings.theme[name]} 180 - isAlpha={false} 181 - position="responsive" 182 - label={localSettings.theme[name]} 183 - /> 184 - </div> 185 - </div> 186 - {/snippet} 187 - {@render color('fg', 'foreground color')} 188 - {@render color('bg', 'background color')} 189 - {@render color('accent', 'accent color')} 190 - {@render color('accent2', 'secondary accent color')} 191 - </div> 192 - </div> 193 - </div> 194 - {/snippet} 195 - 196 102 <div class="flex flex-col"> 197 103 <div class="mb-6 flex items-center justify-between p-4 pb-0"> 198 104 <div> ··· 250 156 251 157 <div class="flex-1"> 252 158 {#if tab === 'advanced'} 253 - {@render advancedTab()} 159 + <SettingsAdvancedTab bind:localSettings onReset={handleReset} /> 254 160 {:else if tab === 'moderation'} 255 - <div class="space-y-4 p-4"> 256 - <div> 257 - <h3 class="header">muted accounts</h3> 258 - <div class="borders space-y-2"> 259 - <div class="flex gap-2"> 260 - <input 261 - type="text" 262 - bind:value={newMuteInput} 263 - placeholder="did:plc:..." 264 - class="single-line-input flex-1" 265 - /> 266 - <button onclick={handleAddMute} class="action-button">add</button> 267 - </div> 268 - {#if mutes.length > 0} 269 - <div class="h-fit"> 270 - <VirtualList 271 - height={Math.min(mutes.length, 6) * 44} 272 - itemCount={mutes.length} 273 - itemSize={44} 274 - > 275 - {#snippet item({ index, style }: { index: number; style: string })} 276 - <MutedAccountItem 277 - {style} 278 - did={mutes[index]} 279 - onRemove={() => handleRemoveMute(mutes[index])} 280 - /> 281 - {/snippet} 282 - </VirtualList> 283 - </div> 284 - {:else} 285 - <p class="py-2 text-center text-sm opacity-50">no muted accounts</p> 286 - {/if} 287 - </div> 288 - </div> 289 - {#if currentPrefs} 290 - <p class="text-xs opacity-50"> 291 - last synced: {new Date(currentPrefs.updatedAt).toLocaleString()} 292 - </p> 293 - {/if} 294 - </div> 161 + <SettingsModerationTab 162 + {mutes} 163 + {currentPrefs} 164 + onAddMute={handleAddMute} 165 + onRemoveMute={handleRemoveMute} 166 + /> 295 167 {:else if tab === 'style'} 296 - {@render styleTab()} 168 + <SettingsStyleTab bind:localSettings /> 297 169 {/if} 298 170 </div> 299 171 ··· 306 178 <Tabs tabs={['moderation', 'style', 'advanced']} activeTab={tab} {onTabChange} /> 307 179 </div> 308 180 </div> 309 - 310 - <style> 311 - @reference "../app.css"; 312 - .borders { 313 - @apply rounded-sm border-2 border-dashed border-(--nucleus-fg)/10 p-4; 314 - } 315 - .header-desc { 316 - @apply mb-2 text-sm text-(--nucleus-fg)/80; 317 - } 318 - .header { 319 - @apply mb-2 text-lg font-bold; 320 - } 321 - </style>