diff --git a/Mod Manager/src/electron.cjs b/Mod Manager/src/electron.cjs index 3d36192b..cbba7c8b 100644 --- a/Mod Manager/src/electron.cjs +++ b/Mod Manager/src/electron.cjs @@ -4,6 +4,7 @@ const { app, BrowserWindow, ipcMain, dialog } = require("electron") const serve = require("electron-serve") const fs = require("fs") const path = require("path") +const { spawn } = require('child_process'); try { require("electron-reloader")(module) @@ -124,30 +125,50 @@ if (!lock) { }) } -ipcMain.on("deploy", () => { - let deployProcess = require("child_process").spawn("Deploy.exe --doNotPause --colors", ["--doNotPause --colors"], { - shell: true, - cwd: ".." - }) - - let deployOutput = "" - - mainWindow.webContents.send("frameworkDeployModalOpen") - - deployProcess.stdout.on("data", (data) => { - deployOutput += String(data) - mainWindow.webContents.send("frameworkDeployOutput", deployOutput) - }) - - deployProcess.stderr.on("data", (data) => { - deployOutput += String(data) - mainWindow.webContents.send("frameworkDeployOutput", deployOutput) - }) - - deployProcess.on("close", (data) => { - mainWindow.webContents.send("frameworkDeployFinished") - }) -}) +ipcMain.on("deploy", async () => { + const deployProcess = spawn("Deploy.exe", ["--doNotPause", "--colors"], { + shell: true, + cwd: ".." + }); + + mainWindow.webContents.send("frameworkDeployModalOpen"); + + let partialLine = ''; + + const sendData = (data) => { + const lines = (partialLine + data.toString()).split('\n'); + partialLine = lines.pop(); + + lines.forEach(line => { + if (line.trim() !== '') { + mainWindow.webContents.send("frameworkDeployOutput", line); + } + }); + }; + + deployProcess.stdout.on('data', sendData); + deployProcess.stderr.on('data', sendData); + + await new Promise((resolve, reject) => { + deployProcess.on('close', () => { + if (partialLine.trim() !== '') { + mainWindow.webContents.send("frameworkDeployOutput", partialLine); + } + mainWindow.webContents.send("frameworkDeployFinished"); + resolve(); + }); + + deployProcess.on('error', (error) => { + mainWindow.webContents.send("frameworkDeployError", error.message); + reject(error); + }); + }); + + if (isProcessClosed) { + mainWindow.webContents.send("frameworkDeployFinished"); + } + +}); ipcMain.on("modFileOpenDialog", () => { mainWindow.webContents.send( diff --git a/Mod Manager/src/routes/modList/+page.svelte b/Mod Manager/src/routes/modList/+page.svelte index d91bcb56..ba92bc88 100644 --- a/Mod Manager/src/routes/modList/+page.svelte +++ b/Mod Manager/src/routes/modList/+page.svelte @@ -71,40 +71,135 @@ }) let changed = false - let showDropHint = false let dependencyCycleModalOpen = false let frameworkDeployModalOpen = false let deployOutput = "" - let deployOutputHTML = "" - let deployDiagnostics: string[] = [] + let deployOutputBuffer = "" let deployFinished = false + let ignoreScrollEvent = false + let autoScrollDeployOutput = true + let userHasScrolledDeployOutput = false + let autoScrollNotifications = true + let userHasScrolledNotifications = false + let notifications = [] + let outputLines: string | any[] = [] + + + $: outputLines = deployOutput.split(/\r?\n/).filter(line => line.trim() !== ''); + $: if (outputLines.length > 0 && autoScrollDeployOutput) { + requestAnimationFrame(() => { + const outputElement = document.getElementById('deployOutputElement'); + if (outputElement) { + outputElement.scrollTop = outputElement.scrollHeight; + } + }); + } + $: { let combinedNotifications = [...warnings, ...errors]; + if (combinedNotifications.length !== notifications.length) { + notifications = combinedNotifications; + scrollToBottom('notificationElement'); + } + } + $: if (outputLines.length > 0 && autoScrollDeployOutput) { + scrollToBottom('deployOutputElement'); + } + $: if (notifications.length > 0 && autoScrollNotifications) { + scrollToBottom('notificationElement'); + } + + const updateDeployOutput = throttle(() => { + deployOutput += deployOutputBuffer; + deployOutputBuffer = ""; + outputLines = deployOutput.split(/\r?\n/).filter(line => line.trim() !== ''); + + if (!userHasScrolledDeployOutput) { + scrollToBottom('deployOutputElement'); + } + }, 150); - window.ipc.receive("frameworkDeployModalOpen", () => { - frameworkDeployModalOpen = true - }) + function handleScroll(event: UIEvent & { currentTarget: EventTarget & HTMLDivElement }, elementId: string) { + if (ignoreScrollEvent) { + return; + } - const convertOutputToHTML = throttle(() => { - deployOutputHTML = convertAnsi.toHtml(deployOutput) + const element = event.target; + const nearBottom = element.scrollHeight - element.clientHeight <= element.scrollTop + 10; + // The VSC error is right, so do not add a non-null assertion for it. If you do, the user will not be able to scroll in the deploy and notification output anymore. - Knew - if (deployDiagnostics.length < 20) { - deployDiagnostics = deployOutput.split(/\r?\n/).filter((a) => a.match(/.*WARN.*?\t/) || a.match(/.*ERROR.*?\t/)) + if (elementId === 'deployOutputElement') { + if (!nearBottom) { + userHasScrolledDeployOutput = true; + autoScrollDeployOutput = false; + } + } else if (elementId === 'notificationElement') { + if (!nearBottom) { + userHasScrolledNotifications = true; + autoScrollNotifications = false; + } } + } + + function scrollToBottom(elementId: string) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { // The double requestAnimationFrame is intentional!! Without it, there is a chance the output will stop scrolling when the user didn't request it. - Knew + const element = document.getElementById(elementId); + if (element) { + element.scrollTop = element.scrollHeight; + } + setTimeout(() => ignoreScrollEvent = false, 100); + }); + }); + } - setTimeout(() => { - document.getElementById("deployOutputElement")?.children[0].scrollIntoView(false) - }, 100) - }, 500) + function enableAutoScrollDeployOutput() { + autoScrollDeployOutput = true; + userHasScrolledDeployOutput = false; + scrollToBottom('deployOutputElement'); + } + + function enableAutoScrollNotifications() { + autoScrollNotifications = true; + userHasScrolledNotifications = false; + scrollToBottom('notificationElement'); + } + + window.ipc.receive("frameworkDeployModalOpen", () => { + frameworkDeployModalOpen = true + }) window.ipc.receive("frameworkDeployOutput", (output: string) => { - deployOutput = output - convertOutputToHTML() - }) + ignoreScrollEvent = true; + deployOutputBuffer += output + "\n"; + updateDeployOutput(); + }); window.ipc.receive("frameworkDeployFinished", () => { deployFinished = true }) + let warnings: any[] = []; + let errors: any[] = []; + + $: { + let warningIndex = 1; + let errorIndex = 1; + warnings = []; + errors = []; + + deployOutput.split(/\r?\n/).forEach(line => { + if (line.match(/.*WARN.*?\t/)) { + warnings.push({ message: line.replace(/.*WARN.*?\t/, ""), count: warningIndex++ }); + } else if (line.match(/.*ERROR.*?\t/)) { + errors.push({ message: line.replace(/.*ERROR.*?\t/, ""), count: errorIndex++ }); + } + }); + } + + $: totalWarnings = warnings.length; + $: totalErrors = errors.length; + $: totalNotifications = totalWarnings + totalErrors; + document.addEventListener("drop", (event) => { event.preventDefault() event.stopPropagation() @@ -448,7 +543,6 @@ on:click={() => { if (sortMods()) { deployOutput = "" - deployOutputHTML = "" deployFinished = false window.ipc.send("deploy") } else { @@ -550,24 +644,83 @@ Your mods are being deployed. This may take a while - grab a coffee or something.
-
{@html deployOutputHTML}
- {#if deployOutput.split(/\r?\n/).some((a) => a.match(/.*WARN.*?\t/)) || deployOutput.split(/\r?\n/).some((a) => a.match(/.*ERROR.*?\t/))} -
-
- {#each deployDiagnostics as line} - -
- {line.includes("WARN") ? "Warning" : "Error"} -
-
{line.replace(/.*WARN.*?\t/, "").replace(/.*ERROR.*?\t/, "")}
-
- {/each} +
handleScroll(event, 'deployOutputElement')}> + + {#each outputLines as line} +
{@html convertAnsi.toHtml(line)}
+ {/each} + + {#if userHasScrolledDeployOutput} + + {/if} + +
+ {#if warnings.length > 0 || errors.length > 0} +
+
handleScroll(event, 'notificationElement')}> + + {#each warnings as warning} + +
Warning #{warning.count}
+
{warning.message}
+
+ {/each} + + {#each errors as error} + +
Error #{error.count}
+
{error.message}
+
+ {/each} + + {#if userHasScrolledNotifications} + + {/if} +
{/if} +
+ {#if totalNotifications > 0} + + + + + {totalWarnings} + + + + + + + {totalErrors} + + {/if} +
+ + {#if deployFinished}
@@ -577,7 +730,7 @@ .filter((a) => a.length) .at(-1) .match(/\tDone in .*/) && !deployOutput.split(/\r?\n/).some((a) => a.match(/.*WARN.*?\t/))} - + Deploy successful {:else if deployOutput .split(/\r?\n/) @@ -806,4 +959,36 @@ :global(.bx--snippet.bx--snippet--single) { background-color: #262626; } + + .overflow-y-auto { + color-scheme: dark !important; + } + + :global(.bx--inline-notification) { + margin-right: 10px !important; + margin-left: 8px !important; + background:#2F2F2F !important; + max-width: 100% !important; + margin-top: 0rem !important; + margin-bottom: 0rem !important; + } + + :global(.bx--modal-content) { + overflow-y: visible !important; + } + + @media (min-width: 42rem) {:global(.bx--modal-container) { + position: relative !important; + height: auto !important; + width: 90% !important; + min-height: 30rem !important; + }} + + :global(.bx--modal-content) { + margin-bottom:1.8rem !important; + } + + :global(.bx--modal-header) { + margin-bottom:0rem !important; + }