logs
This commit is contained in:
@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Copy, Trash2 } from 'lucide-svelte';
|
||||
import { Copy, Trash2, Search, Pause, Play } from 'lucide-svelte';
|
||||
import { csound } from './csound';
|
||||
import type { LogEntry } from './csound';
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
logs?: LogEntry[];
|
||||
@ -9,6 +10,32 @@
|
||||
|
||||
let { logs = [] }: Props = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let autoFollow = $state(true);
|
||||
let searchVisible = $state(false);
|
||||
let logContentEl: HTMLDivElement;
|
||||
let previousLogsLength = 0;
|
||||
|
||||
function fuzzyMatch(text: string, query: string): boolean {
|
||||
const textLower = text.toLowerCase();
|
||||
const queryLower = query.toLowerCase();
|
||||
let queryIndex = 0;
|
||||
|
||||
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
||||
if (textLower[i] === queryLower[queryIndex]) {
|
||||
queryIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return queryIndex === queryLower.length;
|
||||
}
|
||||
|
||||
const filteredLogs = $derived(
|
||||
searchQuery.trim() === ''
|
||||
? logs
|
||||
: logs.filter(log => fuzzyMatch(log.message, searchQuery))
|
||||
);
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
@ -36,12 +63,73 @@
|
||||
function clearLogs() {
|
||||
csound.clearLogs();
|
||||
}
|
||||
|
||||
function toggleAutoFollow() {
|
||||
autoFollow = !autoFollow;
|
||||
if (autoFollow) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
searchVisible = !searchVisible;
|
||||
if (!searchVisible) {
|
||||
searchQuery = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollToBottom() {
|
||||
if (logContentEl) {
|
||||
await tick();
|
||||
logContentEl.scrollTop = logContentEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!logContentEl) return;
|
||||
|
||||
const isAtBottom =
|
||||
Math.abs(logContentEl.scrollHeight - logContentEl.scrollTop - logContentEl.clientHeight) < 5;
|
||||
|
||||
if (isAtBottom && !autoFollow) {
|
||||
autoFollow = true;
|
||||
} else if (!isAtBottom && autoFollow) {
|
||||
autoFollow = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (autoFollow && logs.length > previousLogsLength && logContentEl) {
|
||||
scrollToBottom();
|
||||
}
|
||||
previousLogsLength = logs.length;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="log-panel">
|
||||
<div class="log-header">
|
||||
<span class="log-title">Output</span>
|
||||
<div class="log-actions">
|
||||
<button
|
||||
class="action-button"
|
||||
class:search-active={searchVisible}
|
||||
onclick={toggleSearch}
|
||||
title={searchVisible ? 'Hide search' : 'Show search'}
|
||||
>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
class:pause-active={!autoFollow}
|
||||
onclick={toggleAutoFollow}
|
||||
title={autoFollow ? 'Pause auto-follow' : 'Resume auto-follow'}
|
||||
>
|
||||
{#if autoFollow}
|
||||
<Pause size={14} />
|
||||
{:else}
|
||||
<Play size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
onclick={copyLogs}
|
||||
@ -60,11 +148,24 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-content">
|
||||
{#if logs.length === 0}
|
||||
{#if searchVisible}
|
||||
<div class="search-bar">
|
||||
<Search size={14} class="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search logs (fuzzy)..."
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="log-content" bind:this={logContentEl} onscroll={handleScroll}>
|
||||
{#if filteredLogs.length === 0 && searchQuery.trim() !== ''}
|
||||
<div class="empty-state">No matching logs found...</div>
|
||||
{:else if logs.length === 0}
|
||||
<div class="empty-state">No output yet...</div>
|
||||
{:else}
|
||||
{#each logs as log, i}
|
||||
{#each filteredLogs as log, i}
|
||||
<div class="log-entry" class:error={log.type === 'error'}>
|
||||
<span class="log-timestamp">{formatTime(log.timestamp)}</span>
|
||||
<span class="log-message">{log.message}</span>
|
||||
@ -91,6 +192,40 @@
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #252525;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.search-bar :global(.search-icon) {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background-color: #1a1a1a;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
border: 1px solid #3a3a3a;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
@ -126,6 +261,26 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.pause-active {
|
||||
color: rgba(255, 200, 100, 0.9);
|
||||
background-color: rgba(255, 200, 100, 0.15);
|
||||
}
|
||||
|
||||
.action-button.pause-active:hover {
|
||||
color: rgba(255, 200, 100, 1);
|
||||
background-color: rgba(255, 200, 100, 0.25);
|
||||
}
|
||||
|
||||
.action-button.search-active {
|
||||
color: rgba(100, 200, 255, 0.9);
|
||||
background-color: rgba(100, 200, 255, 0.15);
|
||||
}
|
||||
|
||||
.action-button.search-active:hover {
|
||||
color: rgba(100, 200, 255, 1);
|
||||
background-color: rgba(100, 200, 255, 0.25);
|
||||
}
|
||||
|
||||
.log-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user