Thread viewer for Bluesky
at master 203 lines 5.1 kB view raw
1<script lang="ts"> 2 import UserAutocomplete, { type AutocompleteUser } from '../components/UserAutocomplete.svelte'; 3 import PostingStatsTable, { type TableOptions } from '../components/PostingStatsTable.svelte'; 4 import { accountAPI } from '../api.js'; 5 import { PostingStats, type PostingStatsResult } from '../services/posting_stats.js'; 6 import { numberOfDays } from '../utils.js'; 7 8 const tabs = [ 9 { id: 'home', title: 'Home timeline' }, 10 { id: 'list', title: 'List feed' }, 11 { id: 'users', title: 'Selected users' }, 12 { id: 'you', title: 'Your profile' } 13 ] as const; 14 15 let lists: json[] = $state([]); 16 17 let timeRangeDays = $state(7); 18 let selectedTab: typeof tabs[number]['id'] = $state(tabs[0].id); 19 let selectedUsers: AutocompleteUser[] = $state([]); 20 let selectedList: string | undefined = $state(); 21 22 let scanInProgress = $state(false); 23 let requestedDays: number | undefined = $state(); 24 let progress: number | undefined = $state(); 25 let scanInfo = $state(); 26 27 let tableOptions: TableOptions = $state({}); 28 let results: PostingStatsResult | null = $state(null); 29 30 let scanner = new PostingStats((p) => { progress = Math.max(progress || 0, p) }); 31 32 $effect(() => { 33 fetchLists(); 34 }) 35 36 function onTabChange() { 37 results = null; 38 } 39 40 async function fetchLists() { 41 let result = await accountAPI.loadUserLists(); 42 43 lists = result.sort((a, b) => { 44 let aName = a.name.toLocaleLowerCase(); 45 let bName = b.name.toLocaleLowerCase(); 46 47 return aName.localeCompare(bName); 48 }); 49 50 selectedList = lists[0]?.uri; 51 } 52 53 async function onsubmit(e: Event) { 54 e.preventDefault(); 55 56 try { 57 if (!scanInProgress) { 58 await runScan(); 59 } else { 60 scanInProgress = false; 61 scanner.abortScan(); 62 } 63 } catch (error) { 64 if (error.name !== 'AbortError') { 65 throw error; 66 } 67 } 68 } 69 70 async function runScan() { 71 if ((selectedTab == 'list' && !selectedList) || (selectedTab == 'users' && selectedUsers.length == 0)) { 72 return; 73 } 74 75 scanInfo = undefined; 76 results = null; 77 requestedDays = timeRangeDays; 78 progress = 0; 79 scanInProgress = true; 80 81 let startTime = new Date().getTime(); 82 let data: PostingStatsResult | null; 83 let options: TableOptions; 84 85 if (selectedTab == 'home') { 86 options = {}; 87 data = await scanner.scanHomeTimeline(requestedDays); 88 } else if (selectedTab == 'list') { 89 options = { showReposts: false }; 90 data = await scanner.scanListTimeline(selectedList!, requestedDays); 91 } else if (selectedTab == 'users') { 92 options = { showTotal: false, showPercentages: false }; 93 data = await scanner.scanUserTimelines(selectedUsers, requestedDays); 94 } else { // selectedTab == 'you' 95 options = { showTotal: false, showPercentages: false }; 96 data = await scanner.scanYourTimeline(requestedDays); 97 } 98 99 let now = new Date().getTime(); 100 101 if (now - startTime < 150) { 102 // artificial UI delay in case scan finishes immediately 103 await new Promise(resolve => setTimeout(resolve, 150)); 104 } 105 106 tableOptions = options; 107 results = data; 108 scanInProgress = false; 109 } 110</script> 111 112<main> 113<h2>Bluesky posting statistics</h2> 114 115<form {onsubmit}> 116 <p> 117 Scan posts from: 118 119 {#each tabs as tab} 120 <input type="radio" name="scan_type" id="scan_type_{tab.id}" value="{tab.id}" bind:group={selectedTab} onclick={onTabChange}> 121 <label for="scan_type_{tab.id}">{tab.title}</label> 122 {/each} 123 </p> 124 125 <p> 126 Time range: <input id="posting_stats_range" type="range" min="1" max="60" bind:value={timeRangeDays}> 127 <label for="posting_stats_range">{numberOfDays(timeRangeDays)}</label> 128 </p> 129 130 {#if selectedTab == 'list'} 131 <p class="list-choice"> 132 <label for="posting_stats_list">Select list:</label> 133 <select id="posting_stats_list" name="scan_list" bind:value={selectedList}> 134 {#each lists as list} 135 <option value={list.uri}>{list.name} </option> 136 {/each} 137 </select> 138 </p> 139 {/if} 140 141 {#if selectedTab == 'users'} 142 <UserAutocomplete bind:selectedUsers /> 143 {/if} 144 145 <p> 146 <input type="submit" value="{!scanInProgress ? 'Start scan' : 'Cancel'}"> 147 148 {#if scanInProgress} 149 <progress max={requestedDays} value={progress}></progress> 150 {/if} 151 </p> 152</form> 153 154{#if scanInfo} 155 <p class="scan-info">{scanInfo}</p> 156{/if} 157 158{#if results} 159 <PostingStatsTable {...tableOptions} {...results} /> 160{/if} 161</main> 162 163<style> 164 input[type="radio"] { 165 position: relative; 166 top: -1px; 167 margin-left: 5px; 168 } 169 170 input[type="radio"] + label { 171 user-select: none; 172 -webkit-user-select: none; 173 margin-right: 4px; 174 } 175 176 input[type="range"] { 177 width: 250px; 178 vertical-align: middle; 179 } 180 181 input[type="submit"] { 182 font-size: 12pt; 183 margin: 5px 0px; 184 padding: 5px 10px; 185 } 186 187 select { 188 font-size: 12pt; 189 margin-left: 5px; 190 } 191 192 progress { 193 width: 300px; 194 margin-left: 10px; 195 vertical-align: middle; 196 } 197 198 .scan-info { 199 font-weight: 600; 200 line-height: 125%; 201 margin: 20px 0px; 202 } 203</style>