appview-less bluesky client
24
fork

Configure Feed

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

at main 208 lines 6.4 kB view raw
1<script lang="ts"> 2 import { needsReload, settings } from '$lib/settings'; 3 import { get } from 'svelte/store'; 4 import Tabs from './Tabs.svelte'; 5 import { portal } from 'svelte-portal'; 6 import { 7 router, 8 clients, 9 accountPreferences, 10 setAccountPreferences, 11 syncAccountPreferences, 12 loadAccountPreferences 13 } from '$lib/state.svelte'; 14 import { accounts as accountsStore, generateColorForDid } from '$lib/accounts'; 15 import AccountSelector from './AccountSelector.svelte'; 16 import Dropdown from './Dropdown.svelte'; 17 import SettingsAdvancedTab from './SettingsAdvancedTab.svelte'; 18 import SettingsStyleTab from './SettingsStyleTab.svelte'; 19 import SettingsModerationTab from './SettingsModerationTab.svelte'; 20 import SettingsFeedsTab from './SettingsFeedsTab.svelte'; 21 import type { Did } from '@atcute/lexicons'; 22 import type { AtprotoDid } from '@atcute/lexicons/syntax'; 23 import Icon from '@iconify/svelte'; 24 import type { FeedGenerator } from '$lib/at/feeds'; 25 26 interface Props { 27 tab: string; 28 } 29 30 let { tab }: Props = $props(); 31 32 let localSettings = $state(get(settings)); 33 let hasReloadChanges = $derived(needsReload($settings, localSettings)); 34 35 $effect(() => { 36 $settings.theme = localSettings.theme; 37 }); 38 39 const handleSave = () => { 40 settings.set(localSettings); 41 window.location.reload(); 42 }; 43 44 const handleReset = () => { 45 const confirmed = confirm('reset all settings to defaults?'); 46 if (!confirmed) return; 47 settings.reset(); 48 window.location.reload(); 49 }; 50 51 const onTabChange = (tab: string) => router.replace(`/settings/${tab}`); 52 53 let selectedAccount: AtprotoDid | null = $state(null); 54 let syncStatus = $state<'syncing' | 'synced' | null>(null); 55 let isAccountDropdownOpen = $state(false); 56 57 const accounts = $derived($accountsStore.filter((a) => clients.has(a.did))); 58 const selectedAccountData = $derived(accounts.find((a) => a.did === selectedAccount)); 59 const currentPrefs = $derived( 60 selectedAccount ? (accountPreferences.get(selectedAccount) ?? null) : null 61 ); 62 const mutes = $derived(currentPrefs?.mutes ?? []); 63 64 $effect(() => { 65 if (accounts.length > 0 && !selectedAccount) { 66 selectedAccount = accounts[0].did; 67 } 68 }); 69 70 let syncDebounceTimer: ReturnType<typeof setTimeout> | null = null; 71 const SYNC_DEBOUNCE_MS = 1000; 72 73 const scheduleSyncFor = (did: AtprotoDid) => { 74 if (syncDebounceTimer) clearTimeout(syncDebounceTimer); 75 syncDebounceTimer = setTimeout(async () => { 76 syncStatus = 'syncing'; 77 await syncAccountPreferences(did); 78 syncStatus = 'synced'; 79 setTimeout(() => (syncStatus = null), 2000); 80 }, SYNC_DEBOUNCE_MS); 81 }; 82 83 const handleAddMute = (did: Did) => { 84 if (!selectedAccount) return; 85 setAccountPreferences(selectedAccount, { mutes: [...mutes, did] }); 86 scheduleSyncFor(selectedAccount); 87 }; 88 89 const handleRemoveMute = (did: Did) => { 90 if (!selectedAccount) return; 91 setAccountPreferences(selectedAccount, { mutes: mutes.filter((m) => m !== did) }); 92 scheduleSyncFor(selectedAccount); 93 }; 94 95 const handleReload = async () => { 96 if (!selectedAccount) return; 97 syncStatus = 'syncing'; 98 await loadAccountPreferences({ did: selectedAccount, handle: null }); 99 syncStatus = 'synced'; 100 setTimeout(() => (syncStatus = null), 2000); 101 }; 102 103 const feeds = $derived(localSettings.feeds); 104 105 const handleAddFeed = (feed: FeedGenerator) => { 106 localSettings.feeds = [...localSettings.feeds, { feed, pinned: false }]; 107 settings.set(localSettings); 108 }; 109 110 const handleRemoveFeed = (uri: string) => { 111 localSettings.feeds = localSettings.feeds.filter((f) => f.feed.uri !== uri); 112 settings.set(localSettings); 113 }; 114 115 const handleTogglePin = (uri: string) => { 116 localSettings.feeds = localSettings.feeds.map((f) => 117 f.feed.uri === uri ? { ...f, pinned: !f.pinned } : f 118 ); 119 settings.set(localSettings); 120 }; 121</script> 122 123<div class="flex flex-col"> 124 <div class="mb-6 flex items-center justify-between p-4 pb-0"> 125 <div> 126 <h2 class="text-3xl font-bold">settings</h2> 127 <div class="mt-2 flex gap-2"> 128 <div class="h-1 w-8 rounded-full bg-(--nucleus-accent)"></div> 129 <div class="h-1 w-9.5 rounded-full bg-(--nucleus-accent2)"></div> 130 </div> 131 </div> 132 <div class="flex items-center gap-2"> 133 {#if tab === 'moderation'} 134 {#if syncStatus} 135 <span class="text-xs opacity-70">{syncStatus}</span> 136 {/if} 137 <Dropdown 138 class="min-w-48 rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) shadow-2xl" 139 bind:isOpen={isAccountDropdownOpen} 140 placement="bottom-end" 141 > 142 {#snippet trigger()} 143 <button 144 onclick={() => (isAccountDropdownOpen = !isAccountDropdownOpen)} 145 class="flex action-button items-center gap-1.5 text-sm" 146 style="color: {selectedAccountData 147 ? generateColorForDid(selectedAccountData.did) 148 : 'inherit'}" 149 > 150 <span>@{selectedAccountData?.handle ?? selectedAccount?.slice(0, 12)}</span> 151 <span class="opacity-50"></span> 152 </button> 153 {/snippet} 154 <AccountSelector 155 {accounts} 156 selectedDid={selectedAccount} 157 onSelect={(did) => { 158 selectedAccount = did; 159 isAccountDropdownOpen = false; 160 }} 161 /> 162 </Dropdown> 163 <button onclick={handleReload} class="action-button p-2" title="reload from pocket"> 164 <Icon 165 class={syncStatus === 'syncing' ? 'animate-spin' : ''} 166 icon="heroicons:arrow-path-16-solid" 167 width="20" 168 /> 169 </button> 170 {:else if hasReloadChanges} 171 <button onclick={handleSave} class="action-button animate-pulse shadow-lg"> 172 save &amp; reload 173 </button> 174 {/if} 175 </div> 176 </div> 177 178 <div class="flex-1"> 179 {#if tab === 'advanced'} 180 <SettingsAdvancedTab bind:localSettings onReset={handleReset} /> 181 {:else if tab === 'moderation'} 182 <SettingsModerationTab 183 {mutes} 184 onAddMute={handleAddMute} 185 onRemoveMute={handleRemoveMute} 186 {selectedAccount} 187 /> 188 {:else if tab === 'style'} 189 <SettingsStyleTab bind:localSettings /> 190 {:else if tab === 'feeds'} 191 <SettingsFeedsTab 192 {feeds} 193 onAddFeed={handleAddFeed} 194 onRemoveFeed={handleRemoveFeed} 195 onTogglePin={handleTogglePin} 196 /> 197 {/if} 198 </div> 199 200 <div 201 use:portal={'#footer-portal'} 202 class=" 203 z-20 w-full max-w-2xl bg-(--nucleus-bg) p-4 pt-2 pb-1 shadow-[0_-10px_20px_-5px_rgba(0,0,0,0.1)] 204 " 205 > 206 <Tabs tabs={['feeds', 'moderation', 'style', 'advanced']} activeTab={tab} {onTabChange} /> 207 </div> 208</div>