|
| 1 | +<svelte:window onresize={render} /> |
| 2 | + |
1 | 3 | <div |
2 | 4 | style="display: flex; flex-direction: column; justify-content: space-between; height: 100%; gap: 1em" |
3 | 5 | > |
|
9 | 11 | variant="outlined" |
10 | 12 | style="flex-basis: 0; flex-grow: 1; overflow: auto; background-color: #111; color-scheme: dark;" |
11 | 13 | bind:this={output} |
| 14 | + onscroll={render} |
12 | 15 | > |
13 | | - <Content> |
14 | | - <pre style="margin: 0;">{#each logs as log, i (i)}<div |
15 | | - style="color: {log[0]};">{log[1]}</div>{/each}</pre> |
| 16 | + <Content style="position: relative;"> |
| 17 | + <div |
| 18 | + style="height: {logSpacerHeight}px; width: {logSpacerWidth}px;" |
| 19 | + ></div> |
| 20 | + <div |
| 21 | + bind:this={logViewer} |
| 22 | + class="mdc-typography--body2" |
| 23 | + style="position: absolute; font-family: monospace; margin: 0; top: {start * |
| 24 | + lineHeight}px;" |
| 25 | + > |
| 26 | + {#each logs.slice(start, end) as line} |
| 27 | + <!-- prettier-ignore --> |
| 28 | + <div style="white-space: pre; color: {calculateColor(line)};">{line}</div> |
| 29 | + {/each} |
| 30 | + </div> |
16 | 31 | </Content> |
17 | 32 | </Paper> |
18 | 33 | </div> |
|
23 | 38 | <Button variant="outlined" onclick={scrollToBottom}> |
24 | 39 | <Label>Scroll to Bottom</Label> |
25 | 40 | </Button> |
26 | | - <Button variant="outlined" onclick={() => (logs = [])}> |
| 41 | + <Button |
| 42 | + variant="outlined" |
| 43 | + onclick={() => { |
| 44 | + logs = []; |
| 45 | + logSpacerWidth = 1; |
| 46 | + }} |
| 47 | + > |
27 | 48 | <Label>Clear Log</Label> |
28 | 49 | </Button> |
29 | 50 | <Button |
|
37 | 58 | </div> |
38 | 59 |
|
39 | 60 | <script lang="ts"> |
40 | | - import { tick } from 'svelte'; |
| 61 | + import { onMount, tick } from 'svelte'; |
41 | 62 | import Paper, { Content } from '@smui/paper'; |
42 | 63 | import Button, { Label } from '@smui/button'; |
43 | 64 | import type { ElectronAPI } from '../server/preload.js'; |
|
49 | 70 | }: { |
50 | 71 | electronAPI: ElectronAPI; |
51 | 72 | logging: boolean | undefined; |
52 | | - logs: [string, string][]; |
| 73 | + logs: string[]; |
53 | 74 | } = $props(); |
54 | 75 |
|
55 | 76 | let output: Paper; |
| 77 | + let logViewer: HTMLDivElement; |
| 78 | + let logSpacerHeight = $state(0); |
| 79 | + let logSpacerWidth = $state(0); |
| 80 | + let lineHeight = $state(20); |
| 81 | + let start = $state(0); |
| 82 | + let end = $state(0); |
56 | 83 |
|
57 | | - $effect(() => { |
| 84 | + onMount(() => { |
| 85 | + lineHeight = parseFloat(window.getComputedStyle(logViewer).lineHeight); |
| 86 | + if (isNaN(lineHeight)) { |
| 87 | + lineHeight = 20; |
| 88 | + } |
| 89 | + }); |
| 90 | +
|
| 91 | + $effect.pre(() => { |
58 | 92 | if (logs.length && output) { |
59 | 93 | const el = output.getElement(); |
60 | 94 | const isHidden = el.clientHeight < 60; |
61 | 95 | const isScrolledDown = |
62 | 96 | el.scrollTop >= el.scrollHeight - el.clientHeight - 48; |
63 | 97 |
|
| 98 | + logSpacerHeight = logs.length * lineHeight; |
| 99 | +
|
64 | 100 | if (isHidden || isScrolledDown) { |
65 | 101 | tick().then(() => { |
66 | 102 | scrollToBottom(); |
| 103 | + render(); |
67 | 104 | }); |
68 | 105 | } |
| 106 | + } else { |
| 107 | + logSpacerHeight = 0; |
69 | 108 | } |
70 | 109 | }); |
71 | 110 |
|
| 111 | + function render() { |
| 112 | + const el = output.getElement(); |
| 113 | +
|
| 114 | + const visibleLines = el.clientHeight / lineHeight; |
| 115 | + start = Math.max(Math.floor(el.scrollTop / lineHeight) - 4, 0); |
| 116 | + end = Math.min(start + visibleLines + 8, logs.length); |
| 117 | +
|
| 118 | + tick().then(() => { |
| 119 | + logSpacerWidth = Math.max(logViewer.clientWidth, logSpacerWidth); |
| 120 | + }); |
| 121 | + } |
| 122 | +
|
72 | 123 | function scrollToBottom() { |
73 | 124 | if (output) { |
74 | 125 | const el = output.getElement(); |
|
77 | 128 | } |
78 | 129 | } |
79 | 130 | } |
| 131 | +
|
| 132 | + function calculateColor(line: string) { |
| 133 | + const match = line.match(/^\[([^\]]+)\]/); |
| 134 | + if (!match) { |
| 135 | + return '#fff'; |
| 136 | + } |
| 137 | + const redId = match[1]; |
| 138 | + // Shuffle the chars for the other colors. |
| 139 | + const greenId = match[1].slice(-1) + match[1].slice(0, 1); |
| 140 | + const blueId = match[1].slice(-2) + match[1].slice(0, 2); |
| 141 | +
|
| 142 | + // Convert IDs to numbers. |
| 143 | + const redNum = [...redId].reduce<number>( |
| 144 | + (tot, char) => tot + char.charCodeAt(0), |
| 145 | + 0, |
| 146 | + ); |
| 147 | + const greenNum = [...greenId].reduce<number>( |
| 148 | + (tot, char) => tot + char.charCodeAt(0), |
| 149 | + 0, |
| 150 | + ); |
| 151 | + const blueNum = [...blueId].reduce<number>( |
| 152 | + (tot, char) => tot + char.charCodeAt(0), |
| 153 | + 0, |
| 154 | + ); |
| 155 | +
|
| 156 | + // Convert numbers to light colors. |
| 157 | + const red = ((redNum % 156) + 100).toString(16); |
| 158 | + const green = ((greenNum % 156) + 100).toString(16); |
| 159 | + const blue = ((blueNum % 156) + 100).toString(16); |
| 160 | +
|
| 161 | + // And convert RGB to a hex color. |
| 162 | + return `#${('00' + red).slice(-2)}${('00' + green).slice(-2)}${( |
| 163 | + '00' + blue |
| 164 | + ).slice(-2)}`; |
| 165 | + } |
80 | 166 | </script> |
0 commit comments