This commit is contained in:
2025-10-15 01:00:54 +02:00
parent 4c28aa64f9
commit 8ac5ac0770

View File

@ -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;