search and/or read your saved and liked bluesky posts
wails go svelte sqlite desktop bluesky
4
fork

Configure Feed

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

feat: rich-text facet rendering

+282 -137
+138 -1
README.md
··· 1 - # BlueSky Browser (Desktop) 1 + <!-- markdownlint-disable MD033 --> 2 + # bsky-browser 3 + 4 + Desktop app for searching your Bluesky bookmarks and likes with a local SQLite index, full-text search, rich-text facet rendering, and a built-in log viewer. 5 + 6 + ## What It Does 7 + 8 + - Authenticates with Bluesky using loopback OAuth. 9 + - Stores session state, tokens, and DPoP metadata in a shared SQLite database. 10 + - Indexes bookmarks and likes into a local FTS5-backed search index. 11 + - Lets you browse recent indexed posts or search by text. 12 + - Renders rich-text facets for links, mentions, and hashtags. 13 + - Includes desktop-side refresh progress events and a frontend log viewer. 14 + 15 + ## Screenshots 16 + 17 + ![Placeholder for the login screen showing the Bluesky handle field, authentication button, and dark desktop styling](https://placehold.co/1600x1000/111111/EAEAEA?text=Login+Screen) 18 + 19 + ![Placeholder for the main search view showing indexed post count, search bar, source filter, and results table after a successful refresh](https://placehold.co/1600x1000/111111/EAEAEA?text=Main+Search+View) 20 + 21 + ![Placeholder for a results table row with rendered rich-text facets such as links, mentions, and hashtags inside post text](https://placehold.co/1600x1000/111111/EAEAEA?text=Facet+Rendering) 22 + 23 + ![Placeholder for the bottom progress bar and log viewer during an active indexing run with info, warn, and error lines visible](https://placehold.co/1600x1000/111111/EAEAEA?text=Logs+and+Progress) 24 + 25 + ## Usage 26 + 27 + 1. Launch the app. 28 + 2. Enter your Bluesky handle and complete OAuth in the browser. 29 + 3. Click `Refresh` to index bookmarks and likes. 30 + 4. Use the search box to run FTS queries, or leave it empty to browse recent posts. 31 + 5. Filter results by `All`, `Saved`, or `Liked`. 32 + 6. Click a row to open the original post on `bsky.app`. 33 + 34 + ## Keyboard Shortcuts 35 + 36 + - `Cmd+K` or `Ctrl+K`: focus the search input 37 + - `Cmd+R` or `Ctrl+R`: refresh indexed data 38 + - `Cmd+L` or `Ctrl+L`: toggle the log viewer 39 + 40 + ## Project 41 + 42 + ### Requirements 43 + 44 + - Go 45 + - [Wails v2](https://wails.io/) 46 + - Node.js 47 + - `pnpm` 48 + 49 + ### Install 50 + 51 + ```bash 52 + git clone <your-repo-url> 53 + cd bsky-browser-gui 54 + pnpm --dir frontend install 55 + ``` 56 + 57 + If you prefer `task`, the same setup is available through: 58 + 59 + ```bash 60 + task init 61 + ``` 62 + 63 + ### Development 64 + 65 + Start the desktop app with hot reload: 66 + 67 + ```bash 68 + wails dev 69 + ``` 70 + 71 + Or: 72 + 73 + ```bash 74 + task dev 75 + ``` 76 + 77 + Useful checks: 78 + 79 + ```bash 80 + go test ./... 81 + pnpm --dir frontend check 82 + ``` 83 + 84 + <details> 85 + <summary>OAuth and Local Data</summary> 86 + 87 + - OAuth callback URL: `http://127.0.0.1:8787/callback` 88 + - Default database path: `~/.config/bsky-browser/bsky-browser.db` 89 + - Default log directory: `~/.config/bsky-browser/logs/` 90 + 91 + You can override paths with: 92 + 93 + - `BSKY_BROWSER_DATA` 94 + - `BSKY_BROWSER_LOG` 95 + - `XDG_CONFIG_HOME` 96 + 97 + </details> 98 + 99 + <details> 100 + <summary>Project Structure</summary> 101 + 102 + - [app.go](/Users/owais/Desktop/bsky-browser-gui/app.go): app startup/shutdown wiring 103 + - [auth_service.go](/Users/owais/Desktop/bsky-browser-gui/auth_service.go): Bluesky OAuth flow and session refresh 104 + - [database.go](/Users/owais/Desktop/bsky-browser-gui/database.go): SQLite access, migrations, FTS search 105 + - [index_service.go](/Users/owais/Desktop/bsky-browser-gui/index_service.go): bookmark/like indexing 106 + - [search_service.go](/Users/owais/Desktop/bsky-browser-gui/search_service.go): Wails search bindings 107 + - [log_service.go](/Users/owais/Desktop/bsky-browser-gui/log_service.go): log event streaming 108 + - [frontend/src/App.svelte](/Users/owais/Desktop/bsky-browser-gui/frontend/src/App.svelte): main UI shell 109 + 110 + </details> 111 + 112 + <details> 113 + <summary>Notes</summary> 114 + 115 + - Session state is persisted so token refreshes and DPoP nonce updates survive app restarts. 116 + - Empty searches intentionally return recent posts instead of sending an invalid FTS wildcard query. 117 + 118 + </details> 119 + 120 + ### Production Build 121 + 122 + Create a macOS app bundle: 123 + 124 + ```bash 125 + wails build 126 + ``` 127 + 128 + Verified output: 129 + 130 + ```text 131 + build/bin/bsky-browser-gui.app 132 + ``` 133 + 134 + Equivalent task: 135 + 136 + ```bash 137 + task build 138 + ```
+3 -3
TODO.md
··· 65 65 - [x] Empty state: show "No posts indexed" with prompt to refresh 66 66 - [x] Error handling: toast/notification for network failures, auth expiry 67 67 - [x] Keyboard shortcuts: `Cmd+K` focus search, `Cmd+R` refresh, `Cmd+L` toggle log viewer 68 - - [ ] Window title and app icon (`build/appicon.png`) 69 - - [ ] Production build verification (`wails build` → macOS `.app` bundle) 70 - - [ ] README with build instructions, screenshots, and usage 68 + - [x] Window title and app icon (`build/appicon.png`) 69 + - [x] Production build verification (`wails build` → macOS `.app` bundle) 70 + - [~] README with build instructions, screenshots, and usage
+2 -2
Taskfile.yml
··· 24 24 init: 25 25 desc: Initialize the project (install dependencies) 26 26 cmds: 27 - - cd frontend && npm install 27 + - cd frontend && pnpm install 28 28 29 29 check: 30 30 desc: Check TypeScript and Svelte 31 31 cmds: 32 - - cd frontend && npm run check 32 + - cd frontend && pnpm check
+3 -2
app.go
··· 32 32 func (a *App) startup(ctx context.Context) { 33 33 a.ctx = ctx 34 34 35 - a.indexService.SetContext(ctx) 36 - a.logService.SetContext(ctx) 35 + a.authService.setContext(ctx) 36 + a.indexService.setContext(ctx) 37 + a.logService.setContext(ctx) 37 38 38 39 // Initialize log service first 39 40 if err := a.logService.Initialize(); err != nil {
+10 -3
auth_service.go
··· 12 12 "github.com/bluesky-social/indigo/atproto/auth/oauth" 13 13 "github.com/bluesky-social/indigo/atproto/identity" 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 + "github.com/wailsapp/wails/v2/pkg/runtime" 15 16 ) 16 17 17 18 // AuthService provides authentication functionality via Wails bindings 18 19 type AuthService struct { 20 + ctx context.Context 19 21 app *oauth.ClientApp 20 22 server *http.Server 21 23 listener net.Listener ··· 30 32 codeChan: make(chan string, 1), 31 33 errChan: make(chan error, 1), 32 34 } 35 + } 36 + 37 + func (s *AuthService) setContext(ctx context.Context) { 38 + s.ctx = ctx 33 39 } 34 40 35 41 // Login initiates OAuth login flow for the given handle ··· 151 157 return nil 152 158 } 153 159 154 - // Whoami returns the current authenticated user, optionally resolving handle from DID 155 - // 156 - // TODO: store [context.Context] in [AuthService] to be able to use wails' runtime.LogWarningf 160 + // Whoami returns the current authenticated user, optionally resolving handle from DID. 157 161 func (s *AuthService) Whoami(force bool) (*Auth, error) { 158 162 auth, err := GetAuth() 159 163 if err != nil { ··· 172 176 dir := &identity.BaseDirectory{} 173 177 ident, err := dir.LookupDID(context.Background(), did) 174 178 if err != nil { 179 + if s.ctx != nil { 180 + runtime.LogWarningf(s.ctx, "failed to resolve handle for %s: %v", auth.DID, err) 181 + } 175 182 return auth, nil 176 183 } 177 184
build/appicon.png

This is a binary file and will not be displayed.

+8 -38
frontend/src/App.svelte
··· 14 14 import Toaster from "./lib/components/Toast.svelte"; 15 15 import { toaster } from "./lib/stores/toast.svelte"; 16 16 import EmptyState from "./lib/components/EmptyState.svelte"; 17 + import ProgressBar from "./lib/components/ProgressBar.svelte"; 17 18 import type { main } from "../wailsjs/go/models"; 19 + import type { IndexStats } from "./lib/types"; 18 20 19 21 type AuthInfo = { handle: string; did: string }; 20 - 21 - type IndexStats = { fetched: number; inserted: number; errors: number; total: number }; 22 22 23 23 let handle = $state(""); 24 24 let isLoading = $state(false); ··· 197 197 function handleGlobalKeydown(event: KeyboardEvent) { 198 198 if ((event.metaKey || event.ctrlKey) && event.key === "k") { 199 199 event.preventDefault(); 200 - const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement; 200 + const searchInput = document.getElementById("search-posts") as HTMLInputElement | null; 201 201 if (searchInput) { 202 202 searchInput.focus(); 203 + searchInput.select(); 203 204 } 204 205 } 205 206 ··· 321 322 </div> 322 323 </header> 323 324 324 - <!-- Search Bar --> 325 - <div class="px-6 py-4 border-b border-outline"> 325 + <div class="px-6 py-4 border-b border-secondary"> 326 326 <SearchBar bind:query={searchQuery} bind:source={searchSource} onSearch={performSearch} /> 327 327 </div> 328 328 329 - <!-- Main Content --> 330 - <div class="flex-1 p-6 overflow-hidden"> 329 + <main class="flex-1 p-6 overflow-hidden"> 331 330 {#if isSearching} 332 331 <div class="flex items-center justify-center h-full"> 333 332 <span class="font-sans text-muted animate-pulse">Searching...</span> ··· 337 336 {:else} 338 337 <DataTable posts={searchResults} {sortColumn} {sortDirection} onSort={handleSort} /> 339 338 {/if} 340 - </div> 339 + </main> 341 340 342 - <!-- Log Viewer --> 343 341 {#if showLogs} 344 342 <div transition:slide={{ duration: 300 }}> 345 343 <LogViewer visible={showLogs} /> 346 344 </div> 347 345 {/if} 348 346 349 - <!-- Progress Bar (bottom pinned) --> 350 347 {#if showProgress} 351 - <div class="border-t border-outline bg-surface px-6 py-3" transition:slide={{ duration: 300 }}> 352 - <div class="flex items-center justify-between mb-2"> 353 - <span class="font-sans text-sm text-muted"> 354 - <!-- {isIndexing ? "Indexing..." : "Indexing complete"} --> 355 - {#if isIndexing} 356 - <span class="animate-pulse">Indexing...</span> 357 - {:else} 358 - <span class="flex items-center gap-2"> 359 - <i class="i-ri-check-line text-emerald-400"></i> 360 - <span>Indexing complete</span> 361 - </span> 362 - {/if} 363 - </span> 364 - <span class="font-mono text-xs text-muted"> 365 - {indexStats.inserted} inserted / {indexStats.fetched} fetched 366 - {#if indexStats.errors > 0} 367 - <span class="text-red-500">({indexStats.errors} errors)</span> 368 - {/if} 369 - </span> 370 - </div> 371 - 372 - <div class="w-full h-1 bg-black rounded-full overflow-hidden"> 373 - <div 374 - class="h-full bg-[#333] transition-all duration-300 ease-out" 375 - style="width: {indexStats.fetched > 0 ? (indexStats.inserted / indexStats.fetched) * 100 : 0}%"> 376 - </div> 377 - </div> 378 - </div> 348 + <ProgressBar {isIndexing} {indexStats} /> 379 349 {/if} 380 350 </div> 381 351 {/if}
+1
frontend/src/index.css
··· 11 11 --color-primary: #33b1ff; 12 12 --color-primary-bright: #0f62fe; 13 13 --color-secondary: #ee5396; 14 + --color-secondary-bright: #ff7eb6; 14 15 --color-bright: #e5e5e5; 15 16 } 16 17
+2 -2
frontend/src/lib/components/DataTable.svelte
··· 111 111 <td class="px-4 py-3"> 112 112 <span 113 113 class="font-sans text-xs px-2 py-0.5 rounded-full {post.source === 'saved' 114 - ? 'bg-blue-500/20 text-blue-400' 115 - : 'bg-pink-500/20 text-pink-400'}"> 114 + ? 'bg-primary/20 text-primary' 115 + : 'bg-secondary/20 text-secondary'}"> 116 116 {post.source} 117 117 </span> 118 118 </td>
+32 -19
frontend/src/lib/components/LogViewer.svelte
··· 4 4 5 5 type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"; 6 6 7 - interface LogEntry { 7 + type LogEntry = { 8 8 level: LogLevel; 9 9 message: string; 10 10 timestamp: string; 11 - } 11 + }; 12 12 13 - interface Props { 13 + type Props = { 14 14 visible: boolean; 15 - } 15 + }; 16 16 17 17 let { visible }: Props = $props(); 18 18 ··· 23 23 24 24 const levels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"]; 25 25 26 - const levelColors: Record<LogLevel, string> = { 27 - DEBUG: "text-gray-500", 28 - INFO: "text-blue-400", 29 - WARN: "text-yellow-400", 30 - ERROR: "text-red-400", 31 - }; 26 + function getLevelColor(level: LogLevel): string { 27 + switch (level) { 28 + case "DEBUG": 29 + return "text-gray-500"; 30 + case "INFO": 31 + return "text-primary"; 32 + case "WARN": 33 + return "text-yellow-400"; 34 + case "ERROR": 35 + return "text-red-400"; 36 + } 37 + } 32 38 33 - const levelBgColors: Record<LogLevel | "ALL", string> = { 34 - ALL: "bg-gray-700", 35 - DEBUG: "bg-gray-600", 36 - INFO: "bg-blue-600", 37 - WARN: "bg-yellow-600", 38 - ERROR: "bg-red-600", 39 - }; 39 + function getLevelBgColor(level: LogLevel | "ALL"): string { 40 + switch (level) { 41 + case "DEBUG": 42 + return "bg-gray-600"; 43 + case "INFO": 44 + return "bg-blue-600"; 45 + case "WARN": 46 + return "bg-yellow-600"; 47 + case "ERROR": 48 + return "bg-red-600"; 49 + default: 50 + return "bg-gray-600"; 51 + } 52 + } 40 53 41 54 function formatTimestamp(timestamp: string): string { 42 55 const date = new Date(timestamp); ··· 112 125 <button 113 126 onclick={() => setFilterLevel(level as LogLevel | "ALL")} 114 127 class="font-mono text-xs px-2 py-1 rounded transition-colors {filterLevel === level 115 - ? levelBgColors[level] + ' text-white' 128 + ? getLevelBgColor(level) + ' text-white' 116 129 : 'bg-black text-muted hover:text-bright'}"> 117 130 {level} 118 131 </button> ··· 154 167 {#each filteredLogs() as log} 155 168 <div class="flex items-start gap-2 hover:bg-white/5 px-1 rounded"> 156 169 <span class="text-muted shrink-0">{formatTimestamp(log.timestamp)}</span> 157 - <span class="{levelColors[log.level]} shrink-0 w-12">[{log.level}]</span> 170 + <span class="{getLevelColor(log.level)} shrink-0 w-12">[{log.level}]</span> 158 171 <span class="text-bright break-all">{log.message}</span> 159 172 </div> 160 173 {:else}
+23 -25
frontend/src/lib/components/PostText.svelte
··· 1 1 <script lang="ts"> 2 - import { parseFacets, renderFacets, truncateRenderedFacets, type Facet } from "../facets"; 2 + import { parseFacets, renderFacets, truncateRenderedFacets } from "../facets"; 3 3 4 - interface Props { 4 + type Props = { 5 5 text: string; 6 6 facetsJson?: string; 7 7 maxLength?: number; 8 8 class?: string; 9 - } 9 + }; 10 10 11 11 let { text, facetsJson = "", maxLength = 0, class: className = "" }: Props = $props(); 12 12 13 - function getRenderedFacets(text: string, facetsJson: string, maxLength: number) { 13 + let renderedFacets = $derived.by(() => { 14 14 const facets = parseFacets(facetsJson); 15 15 const rendered = renderFacets(text, facets); 16 - 16 + 17 17 if (maxLength > 0 && maxLength < text.length) { 18 18 const { facets: truncated } = truncateRenderedFacets(rendered, maxLength); 19 19 return truncated; 20 20 } 21 - 22 - return rendered; 23 - } 24 21 25 - let renderedFacets = $derived(getRenderedFacets(text, facetsJson, maxLength)); 22 + return rendered; 23 + }); 26 24 </script> 27 25 28 - <span class="{className}"> 26 + <span class={className}> 29 27 {#each renderedFacets as facet} 30 - {#if facet.type === 'link'} 31 - <a 32 - href={facet.href} 33 - target="_blank" 28 + {#if facet.type === "link"} 29 + <a 30 + href={facet.href} 31 + target="_blank" 34 32 rel="noopener noreferrer" 35 - class="text-blue-400 hover:text-blue-300 hover:underline" 33 + class="text-primary hover:text-primary-bright hover:underline" 36 34 onclick={(e) => e.stopPropagation()}> 37 35 {facet.text} 38 36 </a> 39 - {:else if facet.type === 'mention'} 40 - <a 41 - href={facet.href} 42 - target="_blank" 37 + {:else if facet.type === "mention"} 38 + <a 39 + href={facet.href} 40 + target="_blank" 43 41 rel="noopener noreferrer" 44 - class="text-blue-400 hover:text-blue-300 hover:underline" 42 + class="text-primary hover:text-primary-bright hover:underline" 45 43 onclick={(e) => e.stopPropagation()}> 46 44 {facet.text} 47 45 </a> 48 - {:else if facet.type === 'tag'} 49 - <a 50 - href={facet.href} 51 - target="_blank" 46 + {:else if facet.type === "tag"} 47 + <a 48 + href={facet.href} 49 + target="_blank" 52 50 rel="noopener noreferrer" 53 - class="text-pink-400 hover:text-pink-300 hover:underline" 51 + class="text-secondary hover:text-secondary-bright hover:underline" 54 52 onclick={(e) => e.stopPropagation()}> 55 53 {facet.text} 56 54 </a>
+34
frontend/src/lib/components/ProgressBar.svelte
··· 1 + <script lang="ts"> 2 + import { slide } from "svelte/transition"; 3 + import type { IndexStats } from "../types"; 4 + 5 + const { isIndexing, indexStats } = $props<{ isIndexing: boolean; indexStats: IndexStats }>(); 6 + </script> 7 + 8 + <div class="border-t border-outline bg-surface px-6 py-3" transition:slide={{ duration: 300 }}> 9 + <div class="flex items-center justify-between mb-2"> 10 + <span class="font-sans text-sm text-muted"> 11 + {#if isIndexing} 12 + <span class="animate-pulse">Indexing...</span> 13 + {:else} 14 + <span class="flex items-center gap-2"> 15 + <i class="i-ri-check-line text-emerald-400"></i> 16 + <span>Indexing complete</span> 17 + </span> 18 + {/if} 19 + </span> 20 + <span class="font-mono text-xs text-muted"> 21 + {indexStats.inserted} inserted / {indexStats.fetched} fetched 22 + {#if indexStats.errors > 0} 23 + <span class="text-red-500">({indexStats.errors} errors)</span> 24 + {/if} 25 + </span> 26 + </div> 27 + 28 + <div class="w-full h-1 bg-black rounded-full overflow-hidden"> 29 + <div 30 + class="h-full bg-[#333] transition-all duration-300 ease-out" 31 + style="width: {indexStats.fetched > 0 ? (indexStats.inserted / indexStats.fetched) * 100 : 0}%"> 32 + </div> 33 + </div> 34 + </div>
+3 -8
frontend/src/lib/components/SearchBar.svelte
··· 18 18 onSearch(query, source); 19 19 } 20 20 21 - function handleKeydown(e: KeyboardEvent) { 22 - if (e.key === "Enter") { 23 - onSearch(query, source); 24 - } 25 - } 26 - 27 21 function handleClick(src: string) { 28 22 source = src; 29 23 onSearch(query, source); ··· 33 27 <form onsubmit={handleSubmit} class="flex items-center gap-3"> 34 28 <div class="flex-1 relative"> 35 29 <input 36 - type="text" 30 + id="search-posts" 31 + type="search" 37 32 placeholder="Search posts..." 38 33 bind:value={query} 39 - onkeydown={handleKeydown} 34 + enterkeyhint="search" 40 35 class="w-full bg-black border border-outline rounded-lg px-4 py-2.5 font-mono text-sm text-muted placeholder-[#333] focus:outline-none focus:border-[#333]" /> 41 36 <div class="absolute right-3 top-1/2 -translate-y-1/2"> 42 37 <kbd class="font-mono text-xs text-muted bg-surface px-2 py-0.5 rounded border border-outline">⌘K</kbd>
+1 -1
frontend/src/lib/components/Toast.svelte
··· 6 6 function getTypeStyle(kind: ToastKind) { 7 7 switch (kind) { 8 8 case "info": 9 - return "bg-blue-500/20 border-blue-500/50 text-blue-400"; 9 + return "bg-primary/20 border-primary/50 text-primary"; 10 10 case "success": 11 11 return "bg-green-500/20 border-green-500/50 text-green-400"; 12 12 case "warning":
+17 -12
frontend/src/lib/facets.ts
··· 76 76 const facetStart = byteOffsetToCharIndex(text, facet.index.byteStart); 77 77 const facetEnd = byteOffsetToCharIndex(text, facet.index.byteEnd); 78 78 const facetText = text.slice(facetStart, facetEnd); 79 + let renderedFacet: RenderedFacet = { type: "text", text: facetText }; 79 80 80 81 for (const feature of facet.features) { 81 82 const type = feature.$type; 82 83 83 84 if (type === "app.bsky.richtext.facet#link") { 84 - result.push({ type: "link", text: facetText, href: feature.uri }); 85 - } else if (type === "app.bsky.richtext.facet#mention") { 86 - result.push({ 85 + renderedFacet = { type: "link", text: facetText, href: feature.uri }; 86 + break; 87 + } 88 + 89 + if (type === "app.bsky.richtext.facet#mention") { 90 + renderedFacet = { 87 91 type: "mention", 88 92 text: facetText, 89 93 did: feature.did, 90 94 href: `https://bsky.app/profile/${feature.did}`, 91 - }); 92 - } else if (type === "app.bsky.richtext.facet#tag") { 93 - result.push({ 95 + }; 96 + break; 97 + } 98 + 99 + if (type === "app.bsky.richtext.facet#tag") { 100 + renderedFacet = { 94 101 type: "tag", 95 102 text: facetText, 96 103 tag: feature.tag, 97 104 href: `https://bsky.app/search?q=%23${encodeURIComponent(feature.tag)}`, 98 - }); 99 - } else { 100 - result.push({ type: "text", text: facetText }); 105 + }; 106 + break; 101 107 } 108 + } 102 109 103 - // TODO: parse ALL features 104 - break; 105 - } 110 + result.push(renderedFacet); 106 111 107 112 lastByteEnd = facet.index.byteEnd; 108 113 }
+1
frontend/src/lib/types.ts
··· 1 + export type IndexStats = { fetched: number; inserted: number; errors: number; total: number };
-3
frontend/wailsjs/go/main/IndexService.d.ts
··· 1 1 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 2 // This file is automatically generated. DO NOT EDIT 3 - import {context} from '../models'; 4 3 5 4 export function IsIndexing():Promise<boolean>; 6 5 7 6 export function Refresh(arg1:number):Promise<void>; 8 - 9 - export function SetContext(arg1:context.Context):Promise<void>;
-4
frontend/wailsjs/go/main/IndexService.js
··· 9 9 export function Refresh(arg1) { 10 10 return window['go']['main']['IndexService']['Refresh'](arg1); 11 11 } 12 - 13 - export function SetContext(arg1) { 14 - return window['go']['main']['IndexService']['SetContext'](arg1); 15 - }
+1 -2
index_service.go
··· 49 49 return &IndexService{} 50 50 } 51 51 52 - // SetContext sets the Wails context for event emission 53 - func (s *IndexService) SetContext(ctx context.Context) { 52 + func (s *IndexService) setContext(ctx context.Context) { 54 53 s.ctx = ctx 55 54 } 56 55
+1 -8
log_service.go
··· 59 59 continue 60 60 } 61 61 62 - // Parse the log level from the line 63 62 level := w.parseLevel(line) 64 63 65 64 entry := LogEntry{ ··· 98 97 } 99 98 } 100 99 101 - // SetContext sets the Wails context for event emission 102 - func (s *LogService) SetContext(ctx context.Context) { 100 + func (s *LogService) setContext(ctx context.Context) { 103 101 s.ctx = ctx 104 102 } 105 103 106 104 // Initialize sets up the log service with a file writer 107 105 func (s *LogService) Initialize() error { 108 - // Open log file 109 106 logPath := s.getLogPath() 110 107 logDir := filepath.Dir(logPath) 111 108 if err := os.MkdirAll(logDir, 0755); err != nil { ··· 118 115 } 119 116 s.file = file 120 117 121 - // Create the log writer 122 118 s.writer = &LogWriter{service: s} 123 - 124 119 return nil 125 120 } 126 121 ··· 201 196 s.mu.Lock() 202 197 defer s.mu.Unlock() 203 198 204 - // Only add if level is >= current level 205 199 if !s.shouldLog(entry.Level) { 206 200 return 207 201 } 208 202 209 203 s.entries = append(s.entries, entry) 210 204 211 - // Trim if exceeding max entries 212 205 if len(s.entries) > s.maxEntries { 213 206 s.entries = s.entries[len(s.entries)-s.maxEntries:] 214 207 }
-2
logger.go
··· 22 22 service: service, 23 23 } 24 24 25 - // Redirect standard log to our logger 26 25 log.SetOutput(multiWriter) 27 26 } 28 27 29 28 // GetLogger returns the global app logger 30 29 func GetLogger() *AppLogger { 31 30 if appLogger == nil { 32 - // Fallback to stdout if not initialized 33 31 return &AppLogger{ 34 32 stdLogger: log.New(os.Stdout, "", log.LstdFlags), 35 33 }
+2 -2
main.go
··· 15 15 func main() { 16 16 app := NewApp() 17 17 err := wails.Run(&options.App{ 18 - Title: "bsky-browser-gui", 18 + Title: "bsky-browser", 19 19 Width: 1024, 20 20 Height: 768, 21 21 AssetServer: &assetserver.Options{Assets: assets}, 22 22 BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 1}, 23 23 OnStartup: app.startup, 24 24 OnShutdown: app.shutdown, 25 - Bind: []any{app, app.authService, app.indexService, app.searchService, app.logService}, 25 + Bind: []any{app, app.authService, app.indexService, app.searchService}, 26 26 }) 27 27 28 28 if err != nil {