search and/or read your saved and liked bluesky posts
wails go svelte sqlite desktop bluesky
at main 182 lines 5.3 kB view raw
1<script lang="ts"> 2 import { Clear, GetEntries } from "../../../wailsjs/go/main/LogService"; 3 import { EventsOn } from "../../../wailsjs/runtime/runtime"; 4 import { onMount } from "svelte"; 5 6 type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"; 7 8 type LogEntry = { level: LogLevel; message: string; timestamp: string }; 9 10 type Props = { visible: boolean }; 11 12 let { visible }: Props = $props(); 13 14 let logs = $state<LogEntry[]>([]); 15 let scrollLock = $state(false); 16 let logContainer: HTMLDivElement | undefined = $state(undefined); 17 let filterLevel = $state<LogLevel | "ALL">("ALL"); 18 19 const levels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"]; 20 21 function getLevelColor(level: LogLevel): string { 22 switch (level) { 23 case "DEBUG": 24 return "text-gray-500"; 25 case "INFO": 26 return "text-primary"; 27 case "WARN": 28 return "text-yellow-400"; 29 case "ERROR": 30 return "text-red-400"; 31 } 32 } 33 34 function getLevelBgColor(level: LogLevel | "ALL"): string { 35 switch (level) { 36 case "DEBUG": 37 return "bg-gray-600"; 38 case "INFO": 39 return "bg-blue-600"; 40 case "WARN": 41 return "bg-yellow-600"; 42 case "ERROR": 43 return "bg-red-600"; 44 default: 45 return "bg-gray-600"; 46 } 47 } 48 49 function formatTimestamp(timestamp: string): string { 50 const date = new Date(timestamp); 51 return date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); 52 } 53 54 function scrollToBottom() { 55 if (logContainer && !scrollLock) { 56 logContainer.scrollTop = logContainer.scrollHeight; 57 } 58 } 59 60 function toggleScrollLock() { 61 scrollLock = !scrollLock; 62 } 63 64 function setFilterLevel(level: LogLevel | "ALL") { 65 filterLevel = level; 66 } 67 68 function clearLogs() { 69 logs = []; 70 void Clear(); 71 } 72 73 function filteredLogs() { 74 if (filterLevel === "ALL") { 75 return logs; 76 } 77 return logs.filter((log) => log.level === filterLevel); 78 } 79 80 onMount(() => { 81 GetEntries() 82 .then((entries) => { 83 logs = entries.map((entry) => ({ 84 level: entry.level as LogLevel, 85 message: entry.message, 86 timestamp: entry.timestamp, 87 })); 88 setTimeout(scrollToBottom, 0); 89 }) 90 .catch((err) => { 91 console.error("Failed to load logs:", err); 92 }); 93 94 EventsOn("log:line", (entry: LogEntry) => { 95 logs = [...logs, entry]; 96 97 if (logs.length > 1000) { 98 logs = logs.slice(logs.length - 1000); 99 } 100 101 setTimeout(scrollToBottom, 0); 102 }); 103 104 EventsOn("log:cleared", () => { 105 logs = []; 106 }); 107 }); 108 109 $effect(() => { 110 if (!scrollLock) { 111 setTimeout(scrollToBottom, 0); 112 } 113 }); 114</script> 115 116{#if visible} 117 <div class="border-outline flex flex-col border-t bg-black"> 118 <!-- Header --> 119 <div class="bg-surface border-outline flex items-center justify-between border-b px-4 py-2"> 120 <div class="flex items-center gap-2"> 121 <span class="text-bright font-mono text-sm">Logs</span> 122 <span class="text-muted font-mono text-xs">({logs.length})</span> 123 </div> 124 125 <div class="flex items-center gap-2"> 126 <!-- Level Filter Buttons --> 127 <div class="mr-4 flex items-center gap-1"> 128 {#each ["ALL", ...levels] as level} 129 <button 130 onclick={() => setFilterLevel(level as LogLevel | "ALL")} 131 class="rounded px-2 py-1 font-mono text-xs transition-colors {filterLevel === level 132 ? getLevelBgColor(level) + ' text-white' 133 : 'text-muted hover:text-bright bg-black'}"> 134 {level} 135 </button> 136 {/each} 137 </div> 138 139 <!-- Scroll Lock Toggle --> 140 <button 141 onclick={toggleScrollLock} 142 class="rounded px-2 py-1 font-mono text-xs transition-colors {scrollLock 143 ? 'bg-yellow-600 text-white' 144 : 'text-muted hover:text-bright bg-black'}" 145 title={scrollLock ? "Scroll locked" : "Auto-scroll enabled"}> 146 {#if scrollLock} 147 <span class="flex items-center"> 148 <i class="i-ri-lock-2-line"></i> 149 </span> 150 {:else} 151 <span class="flex items-center"> 152 <i class="i-ri-arrow-down-box-line"></i> 153 </span> 154 {/if} 155 </button> 156 157 <!-- Clear Button --> 158 <button 159 onclick={clearLogs} 160 class="text-muted rounded bg-black px-2 py-1 font-mono text-xs transition-colors hover:text-red-400"> 161 Clear 162 </button> 163 </div> 164 </div> 165 166 <!-- Log Container --> 167 <div 168 bind:this={logContainer} 169 class="flex-1 space-y-0.5 overflow-y-auto p-2 font-mono text-xs" 170 style="max-height: 200px;"> 171 {#each filteredLogs() as log} 172 <div class="flex items-start gap-2 rounded px-1 hover:bg-white/5"> 173 <span class="text-muted shrink-0">{formatTimestamp(log.timestamp)}</span> 174 <span class="{getLevelColor(log.level)} w-12 shrink-0">[{log.level}]</span> 175 <span class="text-bright break-all">{log.message}</span> 176 </div> 177 {:else} 178 <div class="text-muted text-center py-4">No logs</div> 179 {/each} 180 </div> 181 </div> 182{/if}