logs
This commit is contained in:
@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Copy, Trash2 } from 'lucide-svelte';
|
import { Copy, Trash2, Search, Pause, Play } from 'lucide-svelte';
|
||||||
import { csound } from './csound';
|
import { csound } from './csound';
|
||||||
import type { LogEntry } from './csound';
|
import type { LogEntry } from './csound';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs?: LogEntry[];
|
logs?: LogEntry[];
|
||||||
@ -9,6 +10,32 @@
|
|||||||
|
|
||||||
let { logs = [] }: Props = $props();
|
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 {
|
function formatTime(date: Date): string {
|
||||||
return date.toLocaleTimeString('en-US', {
|
return date.toLocaleTimeString('en-US', {
|
||||||
hour12: false,
|
hour12: false,
|
||||||
@ -36,12 +63,73 @@
|
|||||||
function clearLogs() {
|
function clearLogs() {
|
||||||
csound.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>
|
</script>
|
||||||
|
|
||||||
<div class="log-panel">
|
<div class="log-panel">
|
||||||
<div class="log-header">
|
<div class="log-header">
|
||||||
<span class="log-title">Output</span>
|
<span class="log-title">Output</span>
|
||||||
<div class="log-actions">
|
<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
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
onclick={copyLogs}
|
onclick={copyLogs}
|
||||||
@ -60,11 +148,24 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-content">
|
{#if searchVisible}
|
||||||
{#if logs.length === 0}
|
<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>
|
<div class="empty-state">No output yet...</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each logs as log, i}
|
{#each filteredLogs as log, i}
|
||||||
<div class="log-entry" class:error={log.type === 'error'}>
|
<div class="log-entry" class:error={log.type === 'error'}>
|
||||||
<span class="log-timestamp">{formatTime(log.timestamp)}</span>
|
<span class="log-timestamp">{formatTime(log.timestamp)}</span>
|
||||||
<span class="log-message">{log.message}</span>
|
<span class="log-message">{log.message}</span>
|
||||||
@ -91,6 +192,40 @@
|
|||||||
border-bottom: 1px solid #3a3a3a;
|
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 {
|
.log-title {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -126,6 +261,26 @@
|
|||||||
cursor: not-allowed;
|
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 {
|
.log-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user