Show FFmpeg logs after error, Improve icons in dark mode, Improve "Convert Selected" button

This commit is contained in:
2025-08-15 09:48:48 +07:00
parent 7ab5bf6bf6
commit 74f5257828
20 changed files with 449 additions and 310 deletions
+81 -35
View File
@@ -21,9 +21,11 @@ import {
} from "./util/ffmpeg";
import Neutralino from "@neutralinojs/lib";
import H264Options from "./components/H264Options";
import Configure from "./assets/breeze/actions/16/configure.svg";
import PlaybackStart from "./assets/breeze/actions/16/media-playback-start.svg";
import TrashEmpty from "./assets/breeze/actions/16/trash-empty.svg";
import { openFile } from "./util/oshelper";
import { getTemporaryFilePath } from "./util/path";
import { generateRandomString } from "./util/string";
import "./css/icons.css";
import BreezeIcon from "./components/BreezeIcon";
const commonCodecs = new Set(["h264", "hevc", "vp8", "vp9", "av1", "dnxhd"]);
@@ -50,6 +52,7 @@ function App() {
const [runningProcesses, setRunningProcesses] = createSignal<
RunningProcessInfo[]
>([]);
const logs: { [id: number]: string[] } = {};
let supportedCodecs: CodecInfo[] = [];
let ffmpegParams: FFmpegParams = { vcodec: "" };
let successfulCount = 0;
@@ -65,32 +68,45 @@ function App() {
}
function handleSpawnedProcessEvents(evt: CustomEvent) {
if (evt.detail.action !== "exit") return;
switch (evt.detail.action) {
case "stdErr":
logs[evt.detail.id].push(evt.detail.data);
break;
case "exit":
if (evt.detail.data === 0) {
successfulCount += 1;
} else {
unsuccessfulCount += 1;
if (evt.detail.data === 0) {
successfulCount += 1;
} else {
unsuccessfulCount += 1;
// If the exit code isn't 255 (the exit code of the program exiting because of cancellation)
if (evt.detail.data !== 255) {
Neutralino.os.showNotification(
"File Encoding Failed",
`Encoding for file "${runningProcesses()?.find((v) => v.process.id == evt.detail.id)?.file}" failed. Exit code ${evt.detail.data}.`,
);
if (evt.detail.data !== 255) {
Neutralino.os.showNotification(
"File Encoding Failed",
`Encoding for file "${runningProcesses()?.find((v) => v.process.id == evt.detail.id)?.file}" failed. Exit code ${evt.detail.data}.`,
);
}
const tempFilename = `${getTemporaryFilePath()}/vencoder-ffmpeg-${generateRandomString(8)}.log`;
Neutralino.filesystem.writeFile(
tempFilename,
logs[evt.detail.id].join("\n"),
);
openFile(tempFilename);
}
}
if (successfulCount + unsuccessfulCount === totalCount) {
Neutralino.os.showNotification(
"File(s) encoded.",
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed or cancelled.`,
);
successfulCount = 0;
unsuccessfulCount = 0;
totalCount = 0;
}
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
break;
}
if (successfulCount + unsuccessfulCount === totalCount) {
Neutralino.os.showNotification(
"File(s) encoded.",
`${successfulCount} files encoded successfully. ${unsuccessfulCount} failed or cancelled.`,
);
successfulCount = 0;
unsuccessfulCount = 0;
totalCount = 0;
}
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
}
onMount(async () => {
@@ -291,6 +307,8 @@ function App() {
setRunningProcesses(processes);
processes.forEach((v) => (logs[v.process.id] = []));
await Neutralino.storage.setData(
"filesBeingProcessed",
JSON.stringify(
@@ -315,9 +333,37 @@ function App() {
async function convertSelectedClicked() {
const result = await convertClip(selectedClip());
if (result !== undefined) {
setRunningProcesses([]);
if (result === undefined) {
return;
}
console.log(result);
totalCount = 1;
setRunningProcesses([result]);
logs[result.process.id] = [];
await Neutralino.storage.setData(
"filesBeingProcessed",
JSON.stringify([
{
id: result.process.id,
in: result.file,
len: result.length,
},
]),
);
await Neutralino.window.create(`${window.location.href}progress`, {
width: 600,
height: 400,
x: 120,
y: 120,
injectGlobals: true,
maximizable: false,
});
}
return (
@@ -370,8 +416,8 @@ function App() {
onclick={removeBtnClicked}
class="icon-button k-button"
>
<img
src={TrashEmpty}
<BreezeIcon
icon="b b-trash-empty"
alt="Remove Selected Video"
/>
</button>
@@ -380,8 +426,8 @@ function App() {
onclick={playBtnClicked}
class="icon-button k-button"
>
<img
src={PlaybackStart}
<BreezeIcon
icon="playback-start"
alt="Preview Selected Video"
/>
</button>
@@ -389,9 +435,9 @@ function App() {
class="icon-button k-button"
onclick={settingsBtnPressed}
>
<img
src={Configure}
alt="Configure Vencoder"
<BreezeIcon
icon="configure"
alt="Configure"
/>
</button>
</div>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 10 2.5 C 9.0680195 2.5 8.2844627 3.1373007 8.0625 4 L 2 4 L 2 5 L 8.0625 5 C 8.2844627 5.8626993 9.0680195 6.5 10 6.5 C 10.931981 6.5 11.715537 5.8626993 11.9375 5 L 14 5 L 14 4 L 11.9375 4 C 11.715537 3.1373007 10.931981 2.5 10 2.5 z M 5 9.5 C 4.0680191 9.5 3.2844626 10.137301 3.0625 11 L 2 11 L 2 12 L 3.0625 12 C 3.2844626 12.862699 4.0680191 13.5 5 13.5 C 5.9319809 13.5 6.7155374 12.862699 6.9375 12 L 7 12 L 9 12 L 14 12 L 14 11 L 9 11 L 7 11 L 6.9375 11 C 6.7155374 10.137301 5.9319809 9.5 5 9.5 z M 5 10.5 C 5.55228 10.5 6 10.94772 6 11.5 C 6 12.05228 5.55228 12.5 5 12.5 C 4.44772 12.5 4 12.05228 4 11.5 C 4 10.94772 4.44772 10.5 5 10.5 z " class="ColorScheme-Text"/>
</svg>

Before

Width:  |  Height:  |  Size: 983 B

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
<g class="ColorScheme-Text" fill="currentColor" fill-rule="evenodd">
<path d="m8 2a6 6 0 0 0 -6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0 -6-6zm0 1a5 5 0 0 1 5 5 5 5 0 0 1 -5 5 5 5 0 0 1 -5-5 5 5 0 0 1 5-5z"/>
<path d="m7 4h2v2h-2z"/>
<path d="m7 7h2v5h-2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 502 B

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
<path d="m2 2v12l12-6z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 282 B

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m5 2v2h1v-1h4v1h1v-2h-5zm-3 3v1h2v8h8v-8h2v-1zm3 1h6v7h-6z" class="ColorScheme-Text"/>
</svg>

Before

Width:  |  Height:  |  Size: 390 B

@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 10 2.5 C 9.0680195 2.5 8.2844627 3.1373007 8.0625 4 L 2 4 L 2 5 L 8.0625 5 C 8.2844627 5.8626993 9.0680195 6.5 10 6.5 C 10.931981 6.5 11.715537 5.8626993 11.9375 5 L 14 5 L 14 4 L 11.9375 4 C 11.715537 3.1373007 10.931981 2.5 10 2.5 z M 5 9.5 C 4.0680191 9.5 3.2844626 10.137301 3.0625 11 L 2 11 L 2 12 L 3.0625 12 C 3.2844626 12.862699 4.0680191 13.5 5 13.5 C 5.9319809 13.5 6.7155374 12.862699 6.9375 12 L 7 12 L 9 12 L 14 12 L 14 11 L 9 11 L 7 11 L 6.9375 11 C 6.7155374 10.137301 5.9319809 9.5 5 9.5 z M 5 10.5 C 5.55228 10.5 6 10.94772 6 11.5 C 6 12.05228 5.55228 12.5 5 12.5 C 4.44772 12.5 4 12.05228 4 11.5 C 4 10.94772 4.44772 10.5 5 10.5 z "
class="ColorScheme-Text"
/>
</svg>

Before

Width:  |  Height:  |  Size: 989 B

@@ -1,12 +0,0 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<g class="ColorScheme-Text" fill="currentColor" fill-rule="evenodd">
<path d="m8 2a6 6 0 0 0 -6 6 6 6 0 0 0 6 6 6 6 0 0 0 6-6 6 6 0 0 0 -6-6zm0 1a5 5 0 0 1 5 5 5 5 0 0 1 -5 5 5 5 0 0 1 -5-5 5 5 0 0 1 5-5z"/>
<path d="m7 4h2v2h-2z"/>
<path d="m7 7h2v5h-2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 495 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path d="m2 2v12l12-6z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 275 B

@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="m5 2v2h1v-1h4v1h1v-2h-5zm-3 3v1h2v8h8v-8h2v-1zm3 1h6v7h-6z"
class="ColorScheme-Text"
/>
</svg>

Before

Width:  |  Height:  |  Size: 394 B

+3
View File
@@ -0,0 +1,3 @@
export default function BreezeIcon(props: { icon: string; alt: string }) {
return <i class={`b b-${props.icon}`} role="img" aria-label={props.alt} />;
}
+10 -4
View File
@@ -1,7 +1,7 @@
import { createSignal, Show } from "solid-js";
import type { CodecInfo, FFmpegParams } from "../util/ffmpeg";
import HelpAbout from "../assets/breeze/actions/16/help-about.svg";
import { os } from "@neutralinojs/lib";
import BreezeIcon from "./BreezeIcon";
const information = {
h264: {
@@ -21,6 +21,10 @@ function H264Options(props: {
onParamChanged: (key: string, value: any) => void;
}) {
const [twopass, setTwopass] = createSignal(false);
const defaultCrf =
props.codec?.shortName === "h264"
? information.h264.defaultCrf
: information.hevc.defaultCrf;
return (
<section id="commonLossyOptions">
@@ -52,7 +56,7 @@ function H264Options(props: {
}
title="This will use the two-pass rate control mode instead of relying on a Constant Rate Factor (CRF) value."
>
<img src={HelpAbout} />
<BreezeIcon icon="help-about" alt="Help" />
</button>
</div>
<label>Preset</label>
@@ -91,7 +95,9 @@ function H264Options(props: {
type="number"
name="crf"
id="crf"
value={props.params.crf ?? "23"}
min="0"
max="51"
value={props.params.crf ?? defaultCrf}
oninput={(e) => {
props.params.crf = parseInt(e.target.value);
props.onParamChanged(
@@ -149,7 +155,7 @@ function H264Options(props: {
}
title="This will move some information to the beginning of your file and allow the video to begin playing before it is completely downloaded by the viewer, recommended for web videos. Click for more information."
>
<img src={HelpAbout} />
<BreezeIcon icon="help-about" alt="Help" />
</button>
</div>
</Show>
+38
View File
@@ -0,0 +1,38 @@
.b {
width: 1em;
height: 1em;
}
.b-trash-empty {
background-image: url("/breeze/actions/16/trash-empty.svg");
}
.b-configure {
background-image: url("/breeze/actions/16/configure.svg");
}
.b-playback-start {
background-image: url("/breeze/actions/16/media-playback-start.svg");
}
.b-help-about {
background-image: url("/breeze/actions/16/help-about.svg");
}
@media screen and (prefers-color-scheme: dark) {
.b-trash-empty {
background-image: url("/breeze-dark/actions/16/trash-empty.svg");
}
.b-configure {
background-image: url("/breeze-dark/actions/16/configure.svg");
}
.b-playback-start {
background-image: url("/breeze-dark/actions/16/media-playback-start.svg");
}
.b-help-about {
background-image: url("/breeze-dark/actions/16/help-about.svg");
}
}
+4 -2
View File
@@ -75,12 +75,14 @@ h2 {
justify-content: center;
align-items: center;
img {
img,
i {
width: 16px;
height: 16px;
}
&:disabled img {
&:disabled img,
&:disabled i {
filter: invert(50%);
}
}
+5
View File
@@ -64,12 +64,17 @@ function ProgressPage() {
percentage: (parseInt(info.out_time_us) / file.len) * 100,
};
if (Number.isNaN(progressObject[evt.detail.id].percentage)) {
progressObject[evt.detail.id].percentage = 0;
}
setProgressList(Object.values(progressObject));
break;
case "stdErr":
break;
case "exit":
console.log(`FFmpeg exited with code: ${evt.detail.data}`);
os.getSpawnedProcesses().then((processes) => {
if (processes.length === 0) {
setFinished(true);
+13
View File
@@ -0,0 +1,13 @@
import { os } from "@neutralinojs/lib";
const openFilePrograms: { [os: string]: string } = {
Linux: "xdg-open",
Windows: "explorer",
Darwin: "open",
};
export function openFile(path: string) {
let program = openFilePrograms[window.NL_OS] ?? "";
return os.execCommand(`${program} ${path}`);
}
+10
View File
@@ -0,0 +1,10 @@
export function getTemporaryFilePath() {
switch (window.NL_OS) {
case "Windows":
return "%temp%";
case "Linux":
return "/tmp";
default:
return ".";
}
}
+13
View File
@@ -0,0 +1,13 @@
// Code originally from: https://stackoverflow.com/a/1349426/14512055
export function generateRandomString(length: number) {
let result = "";
let characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(
Math.floor(Math.random() * charactersLength),
);
}
return result;
}