This commit is contained in:
bskjon 2025-03-26 23:17:35 +01:00
parent 749194f9e1
commit e02754f390
11 changed files with 611 additions and 58 deletions

View File

@ -93,10 +93,11 @@ class FfmpegProgressDecoder {
hasReadContinue = true
}
val results = Regex("Duration:\\s*([^,]+),").find(value)?.groupValues?.firstOrNull()
val results = Regex("Duration:\\s*([^,]+),").find(value)?.groupValues?.firstOrNull() ?: return
log.info { "Identified duration for estimation $results" }
val parsedDuration = Regex("[0-9]+:[0-9]+:[0-9]+.[0-9]+").find(results.toString())?.value ?: return
if (!parsedDurations.contains(parsedDuration)) {
parsedDurations.add(parsedDuration)
}

View File

@ -1,5 +1,6 @@
package no.iktdev.mediaprocessing.ui.socket
import com.google.gson.Gson
import no.iktdev.eventi.data.referenceId
import no.iktdev.eventi.database.toEpochSeconds
import no.iktdev.eventi.database.withDirtyRead
@ -32,24 +33,35 @@ class ProcesserTasksTopic(
@Autowired private val message: SimpMessagingTemplate?,
): SocketListener(message) {
private var referenceIds: List<String> = emptyList()
final val a2a = object : ProcesserListenerService.A2AProcesserListener {
override fun onExtractProgress(info: ProcesserEventInfo) {
if (referenceIds.none { it == info.referenceId }) {
updateTopicWithTasks()
}
log.info { "Forwarding extract progress ${Gson().toJson(info)}" }
message?.convertAndSend("/topic/processer/extract/progress", info)
pullAllTasks()
}
override fun onEncodeProgress(info: ProcesserEventInfo) {
if (referenceIds.none { it == info.referenceId }) {
updateTopicWithTasks()
}
log.info { "Forwarding encode progress ${Gson().toJson(info)}" }
message?.convertAndSend("/topic/processer/encode/progress", info)
pullAllTasks()
}
override fun onEncodeAssigned() {
pullAllTasks()
override fun onEncodeAssigned(task: Task) {
if (referenceIds.none { it == task.referenceId }) {
updateTopicWithTasks()
}
}
override fun onExtractAssigned() {
pullAllTasks()
override fun onExtractAssigned(task: Task) {
if (referenceIds.none { it == task.referenceId }) {
updateTopicWithTasks()
}
}
}
@ -78,7 +90,7 @@ class ProcesserTasksTopic(
@MessageMapping("/tasks/all")
fun pullAllTaskss() {
fun updateTopicWithTasks() {
val states = update()
template?.convertAndSend("/topic/tasks/all", states)
}
@ -107,7 +119,9 @@ class ProcesserTasksTopic(
val eventStates: MutableList<ContentEventState> = mutableListOf()
val tasks = pullAllTasks()
val availableEvents = pullAllEvents()
val availableEvents = pullAllEvents().also {
referenceIds = it.keys.toList()
}
for ((referenceId, events) in availableEvents) {
val startEvent = events.findFirstEventOf<MediaProcessStartEvent>() ?: continue

View File

@ -3,6 +3,7 @@ package no.iktdev.mediaprocessing.ui.socket.a2a
import com.google.gson.Gson
import mu.KotlinLogging
import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo
import no.iktdev.mediaprocessing.shared.common.task.Task
import no.iktdev.mediaprocessing.ui.UIEnv
import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService
import no.iktdev.mediaprocessing.ui.log
@ -35,6 +36,8 @@ class ProcesserListenerService(
socketClient?.subscribe("/topic/encode/progress", encodeProcessMessage)
socketClient?.subscribe("/topic/extract/progress", extractProcessFrameHandler)
socketClient?.subscribe("/topic/encode/assigned", encodeTaskAssignedMessage)
socketClient?.subscribe("/topic/extract/assigned", extractTaskAssignedMessage)
}
}
@ -45,6 +48,30 @@ class ProcesserListenerService(
}
}
private val encodeTaskAssignedMessage = object: SocketMessageHandler() {
override fun onMessage(socketMessage: String) {
super.onMessage(socketMessage)
val response = gson.fromJson(socketMessage, Task::class.java)
listeners.forEach { listener ->
run {
listener.onEncodeAssigned(response)
}
}
}
}
private val extractTaskAssignedMessage = object: SocketMessageHandler() {
override fun onMessage(socketMessage: String) {
super.onMessage(socketMessage)
val response = gson.fromJson(socketMessage, Task::class.java)
listeners.forEach { listener ->
run {
listener.onExtractAssigned(response)
}
}
}
}
private val encodeProcessMessage = object : SocketMessageHandler() {
override fun onMessage(socketMessage: String) {
@ -76,8 +103,8 @@ class ProcesserListenerService(
interface A2AProcesserListener {
fun onExtractProgress(info: ProcesserEventInfo)
fun onEncodeProgress(info: ProcesserEventInfo)
fun onEncodeAssigned()
fun onExtractAssigned()
fun onEncodeAssigned(task: Task)
fun onExtractAssigned(task: Task)
}
}

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
export default function ProgressbarWithLabel({indeterminateText, progress}: {indeterminateText?: string, progress?: number | undefined | null}) {
const variant = (progress) ? "determinate" : "indeterminate"
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant={variant} value={progress ?? 0} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography
variant="body2"
sx={{ color: 'text.secondary' }}>
{progress ? (`${Math.round(progress)}%`) :
indeterminateText}
</Typography>
</Box>
</Box>
);
}
/*export default function LinearWithValueLabel() {
const [progress, setProgress] = React.useState(10);
React.useEffect(() => {
const timer = setInterval(() => {
setProgress((prevProgress) => (prevProgress >= 100 ? 10 : prevProgress + 10));
}, 800);
return () => {
clearInterval(timer);
};
}, []);
return (
<Box sx={{ width: '100%' }}>
<ProgressbarWithLabel value={progress} />
</Box>
);
}*/

View File

@ -1,5 +1,5 @@
import { Box, TableContainer, Table, TableHead, TableRow, TableCell, Typography, TableBody, useTheme } from "@mui/material";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table";
import IconArrowUp from '@mui/icons-material/ArrowUpward';
import IconArrowDown from '@mui/icons-material/ArrowDownward';
@ -14,13 +14,19 @@ export interface ExpandableTableItem {
rowId: string
}
export default function ExpandableTable<T extends ExpandableTableItem>({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent }: { items: Array<T>, columns: Array<TablePropetyConfig>, cellCustomizer?: TableCellCustomizer<T>, expandableRender: ExpandableRender<T>, onRowClickedEvent?: TableRowActionEvents<T> }) {
export interface SortByAccessor {
accessor: string
order: 'asc' | 'desc'
}
export default function ExpandableTable<T extends ExpandableTableItem>({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent, defaultSort}: { items: Array<T>, columns: Array<TablePropetyConfig>, cellCustomizer?: TableCellCustomizer<T>, expandableRender: ExpandableRender<T>, onRowClickedEvent?: TableRowActionEvents<T>, defaultSort?: SortByAccessor }) {
const muiTheme = useTheme();
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
const [orderBy, setOrderBy] = useState<string>('');
const [expandedRowIds, setExpandedRowIds] = useState<Set<string>>(new Set());
const [selectedRow, setSelectedRow] = useState<T | null>(null);
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
const tableRowSingleClicked = (row: T | null) => {
if (row != null && 'rowId' in row) {
@ -37,8 +43,10 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
if (row === selectedRow) {
setSelectedRow(null);
setSelectedRowId(null);
} else {
setSelectedRow(row);
setSelectedRowId(row?.rowId ?? null);
if (row && onRowClickedEvent) {
onRowClickedEvent.click(row);
}
@ -74,18 +82,36 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
return 0;
};
const sortedData = items.slice().sort((a, b) => {
if (order === 'asc') {
return compareValues(a, b, orderBy);
} else {
return compareValues(b, a, orderBy);
}
});
const sortedData = useMemo(() => {
return items.slice().sort((a, b) => {
if (order === 'asc') {
return compareValues(a, b, orderBy);
} else {
return compareValues(b, a, orderBy);
}
});
}, [items, order, orderBy]);
useEffect(() => {
handleSort(columns[0].accessor)
}, [])
if (defaultSort) {
setOrder(defaultSort.order);
setOrderBy(defaultSort.accessor);
}
}, [defaultSort])
useEffect(() => {
if (selectedRowId) {
const matchingRow = items.find((item) => item.rowId === selectedRowId);
if (matchingRow) {
setSelectedRow(matchingRow);
} else {
setSelectedRow(null); // Hvis raden ikke finnes lenger, fjern valg
setSelectedRowId(null);
}
}
}, [items, selectedRowId]);
return (
<Box sx={{
@ -106,7 +132,7 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
top: 0,
backgroundColor: muiTheme.palette.background.paper,
}}>
<TableRow>
<TableRow key={`orderRow-${Math.random()}`}>
{columns.map((column) => (
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
<Box display="flex">
@ -124,37 +150,34 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
<TableBody sx={{
overflowY: "scroll"
}}>
{sortedData?.map((row: T, rowIndex: number) => (
<>
<TableRow key={rowIndex}
onClick={() => tableRowSingleClicked(row)}
onDoubleClick={() => tableRowDoubleClicked(row)}
onContextMenu={(e) => {
tableRowContextMenu(e, row);
tableRowSingleClicked(row);
}}
style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }}
>
{columns.map((column) => (
<TableCell key={column.accessor}>
{customizer && customizer(column.accessor, row) !== null
? customizer(column.accessor, row)
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
</TableCell>
))}
</TableRow>
{(expandedRowIds.has(row.rowId)) ?
(<TableRow key={rowIndex + "_1"}>
<TableCell colSpan={columns.length}>
{
expandableRender(row)?.expandElement
}
</TableCell>
</TableRow>): null
}
</>
))}
{sortedData?.map((row: T, rowIndex: number) => [
<TableRow key={row.rowId}
onClick={() => tableRowSingleClicked(row)}
onDoubleClick={() => tableRowDoubleClicked(row)}
onContextMenu={(e) => {
tableRowContextMenu(e, row);
tableRowSingleClicked(row);
}}
style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }}
>
{columns.map((column) => (
<TableCell key={column.accessor}>
{customizer && customizer(column.accessor, row) !== null
? customizer(column.accessor, row)
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
</TableCell>
))}
</TableRow>,
(expandedRowIds.has(row.rowId)) ?
(<TableRow key={row.rowId + "-expanded"}>
<TableCell colSpan={columns.length}>
{
expandableRender(row)?.expandElement
}
</TableCell>
</TableRow>): null
])}
</TableBody>
</Table>
</TableContainer>

View File

@ -0,0 +1,6 @@
export interface CoordinatorOperationRequest {
destination: string;
file: string;
source: string;
mode: "FLOW" | "MANUAL";
}

View File

@ -0,0 +1,52 @@
import React from "react";
import { useState, useCallback } from "react";
import ReactDOMServer from "react-dom/server";
export const useCenteredTree = (defaultTranslate = { x: 0, y: 0 }) => {
const [translate, setTranslate] = useState<{ x: number; y: number }>(defaultTranslate);
const [dimensions, setDimensions] = useState<{ width: number; height: number }>({ width: 0, height: 0 });
const containerRef = useCallback((containerElem: HTMLDivElement | null) => {
if (containerElem !== null) {
const { width, height } = containerElem.getBoundingClientRect();
setDimensions({ width, height });
setTranslate({ x: width / 3, y: height / 2 });
}
}, []);
return [dimensions, translate, containerRef] as const; // Merk: `as const` sikrer faste typer
};
export const extractSvgContent = (icon: JSX.Element | null): JSX.Element[] => {
if (icon === null) {
return [];
}
const svgMarkup = ReactDOMServer.renderToString(icon);
const parsedSvg = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml');
// Hent viewBox-attributtet fra SVG
const svgElement = parsedSvg.documentElement;
const viewBox = svgElement.getAttribute('viewBox');
if (!viewBox) {
throw new Error('SVG mangler viewBox-attributt');
}
// Ekstraher minX, minY, width, height fra viewBox
const [minX, minY, width, height] = viewBox.split(' ').map(Number);
// Beregn offset for senteret og sett det som negativt transform
const offsetX = -(minX + width / 2);
const offsetY = -(minY + height / 2);
return Array.from(parsedSvg.documentElement.children).map((child, index) => {
const props = {
key: index,
transform: `translate(${offsetX}, ${offsetY})`, // Juster transform med negativt offset
...Array.from(child.attributes).reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}),
};
return React.createElement(child.tagName, props, null);
});
};

View File

@ -0,0 +1,4 @@
.thicc-link {
stroke-width: 3;
stroke: black!important;
}

View File

@ -0,0 +1,288 @@
import Tree, { CustomNodeElementProps } from "react-d3-tree";
import { useWsSubscription } from "../ws/subscriptions"
import NotStartedIcon from '@mui/icons-material/NotStarted';
import ReactDOMServer from 'react-dom/server';
import React, { ReactNode, useCallback, useEffect, useState } from "react";
import { extractSvgContent, useCenteredTree } from "../features/util";
import './EventsPage.css';
import ClearIcon from '@mui/icons-material/Clear';
import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import SubtitlesIcon from '@mui/icons-material/Subtitles';
import MovieIcon from '@mui/icons-material/Movie';
import CheckIcon from '@mui/icons-material/Check';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
import { client } from "stompjs";
import { useStompClient } from "react-stomp-hooks";
import { useDispatch, useSelector } from "react-redux";
import { ContentEventState, ProcesserEventInfo, Status, update, updateEncodeProgress, WorkStatus } from "../store/work-slice";
import ExpandableTable, { ExpandableItem } from "../features/table/expandableTable";
import { RootState } from "../store";
import { TableCellCustomizer, TablePropetyConfig } from "../features/table/table";
import SimpleTable from "../features/table/sortableTable";
import { UnixTimestamp } from "../features/UxTc";
import ProgressbarWithLabel from "../features/components/ProgressbarWithLabel";
import { LinearProgress, Typography } from "@mui/material";
import { stat } from "fs";
interface RawNodeDatum {
name: string;
attributes?: Record<string, string | number | boolean>;
children?: RawNodeDatum[];
fill?: string;
}
interface EventNodeData {
statusIcon: JSX.Element | null
statusColor: string
}
function getEventNodeStatus(status: Status): EventNodeData {
const toData = (status: Status): EventNodeData => {
switch (status) {
case Status.NeedsApproval: {
return {
statusIcon: <NotStartedIcon />,
statusColor: "crimson"
} as EventNodeData
}
case Status.Completed: {
return {
statusIcon: <CheckIcon />,
statusColor: "forestgreen"
}
}
case Status.Skipped: {
return {
statusIcon: <KeyboardDoubleArrowRightIcon />,
statusColor: "#313131"
}
}
case Status.Awaiting: {
return {
statusIcon: <MoreHorizIcon />,
statusColor: "#313131"
}
}
case Status.Pending: {
return {
statusIcon: <HourglassEmptyIcon />,
statusColor: "#ffa000"
}
}
case Status.InProgress: {
return {
statusIcon: <HourglassEmptyIcon />,
statusColor: "dodgerblue"
}
}
case Status.Failed: {
return {
statusIcon: <ClearIcon />,
statusColor: "crimson"
}
}
}
}
return toData(status);
}
function renderCustomNodeElement(nodeData: CustomNodeElementProps): JSX.Element {
const attr = nodeData.nodeDatum.attributes;
let workIcon: JSX.Element | null = null
switch (attr?.type) {
case "encode": {
workIcon = <MovieIcon />
break;
}
case "extract": {
workIcon = <SubtitlesIcon />
break;
}
case "convert": {
workIcon = <AutoAwesomeMotionIcon />
break;
}
}
const status = getEventNodeStatus((attr?.status as Status) ?? Status.Awaiting)
return (<>
<g>
<circle r="20" strokeWidth={3} fill={status.statusColor} />
<g transform="scale(1.5)" stroke="none" fill="white">
{extractSvgContent(status.statusIcon)}
</g>
<g transform="translate(0, 45) scale(1)" stroke="none" fill="white">
{extractSvgContent(workIcon)}
</g>
</g>
</>);
}
const taskOperationStatusToStatus = (status: WorkStatus): Status | undefined => {
switch (status) {
case WorkStatus.Completed:
return Status.Completed;
case WorkStatus.Failed:
return Status.Failed;
case WorkStatus.Pending:
return Status.Pending;
case WorkStatus.Started:
case WorkStatus.Working:
return Status.InProgress;
}
}
const transformToSteps = (state: ContentEventState): RawNodeDatum => ({
name: state.referenceId,
attributes: {
type: "encode",
status: taskOperationStatusToStatus(state?.encodeWork?.status) ?? state.encode
},
children: [
{
name: state.referenceId,
attributes: {
type: "extract",
status: state.extract
},
children: [
{
name: state.referenceId,
attributes: {
type: "convert",
status: state.extract
},
}
]
},
]
});
export default function EventsPage() {
const client = useStompClient();
const dispatch = useDispatch();
const events = useSelector((state: RootState) => state.work);
useWsSubscription<Array<ContentEventState>>("/topic/tasks/all", (response) => {
console.log(response)
dispatch(update(response))
});
useWsSubscription<ProcesserEventInfo>("/topic/processer/encode/progress", (response) => {
console.log(response)
dispatch(updateEncodeProgress(response))
})
useEffect(() => {
client?.publish({
destination: "/app/tasks/all"
})
}, [client]);
const createCellTable: TableCellCustomizer<ContentEventState> = (accessor, data) => {
switch (accessor) {
case "runners": {
return (<>
<div id="treeWrapper" style={{
...linkThicc,
width: '150px', height: '90px'
}} ref={containerRef}>
<Tree
dimensions={dimensions}
translate={{
x: 24,
y: 24
}}
data={transformToSteps(data)}
orientation="horizontal"
separation={{
nonSiblings: 1,
siblings: 1,
}}
nodeSize={{
x: 50,
y: 50
}}
draggable={false}
zoomable={false}
renderCustomNodeElement={renderCustomNodeElement}
pathClassFunc={() => 'thicc-link'}
/>
</div>
</>)
};
case "created": {
if (typeof data[accessor] === "number") {
return UnixTimestamp({ timestamp: data[accessor] });
}
return null;
}
default: return null;
}
};
function renderExpandableItem(item: ContentEventState): ExpandableItem<ContentEventState> | null {
const progress = item.encodeWork?.progress?.progress ?? undefined;
const showProgressbar = [WorkStatus.Pending, WorkStatus.Started, WorkStatus.Working, WorkStatus.Completed].includes(item?.encodeWork?.status)
const processer = item.encodeWork // events.encodeWork[item.referenceId];
const showIndeterminate = processer?.status in [WorkStatus.Pending, WorkStatus.Started] || processer?.progress?.progress <= 0
console.log({
type: "info",
processer: processer,
showIndeterminate: showIndeterminate,
showProgressbar: showProgressbar,
isWorking: item.encodeWork?.status == WorkStatus.Working
});
return {
tag: item.referenceId,
expandElement: (() => {
//const data = transformEventGroups(item);
return (
<>
<Typography>{item.encodeWork?.progress?.timeLeft}</Typography>
{(showProgressbar) ?
<ProgressbarWithLabel indeterminateText={"Waiting"} progress={progress} /> : null
}
</>
);
})()
};
}
const columns: Array<TablePropetyConfig> = [
{ label: "Title", accessor: "title" },
{ label: "Started", accessor: "created" },
{ label: "", accessor: "runners" },
];
const [dimensions, translate, containerRef] = useCenteredTree();
const linkThicc = {
strokeWith: 5
}
return (
<>
<ExpandableTable items={events.items} columns={columns} cellCustomizer={createCellTable} expandableRender={renderExpandableItem} defaultSort={{
order: 'desc',
accessor: "created"
}} />
</>
)
}

View File

@ -63,10 +63,16 @@ function getPartFor(path: string, index: number): string | null {
let parts: string[];
if (isWindowsPath(path)) {
parts = [path.slice(0, 3), ...path.slice(3).split(separator)];
} else if (path.startsWith(separator)) {
parts = [separator, ...path.slice(1).split(separator)];
} else {
parts = path.split(separator);
if (path.length == 1 && index == 0) {
return "/"
}
if (path.startsWith(separator)) {
parts = [separator, ...path.slice(1).split(separator)];
} else {
parts = path.split(separator);
}
}
if (index < parts.length) {
@ -74,7 +80,10 @@ function getPartFor(path: string, index: number): string | null {
if (isWindowsPath(path) && index === 0) {
return parts[0];
}
return parts.slice(0, index + 1).join(separator);
const returningPath = parts.slice(0, index + 1).join(separator).replace(/\/\//g, "/");
return returningPath;
}
return null;
@ -115,7 +124,7 @@ function getSegments(absolutePath: string): Array<Segment> {
function getSegmentedNaviagatablePath(rootClick: () => void, navigateTo: (path: string | null) => void, path: string | null): JSX.Element {
const segments = getSegments("/src/input/completed")
const segments = getSegments(path!)
const utElements = segments.map((segment: Segment, index: number) => {
return (

View File

@ -0,0 +1,86 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { ExpandableTableItem } from "../features/table/expandableTable"
export enum WorkStatus {
Pending = "Pending",
Started = "Started",
Working = "Working",
Completed = "Completed",
Failed = "Failed"
}
export interface ProcesserProgress {
progress: number
speed: string
timeWorkedOn: string
timeLeft: string
}
export interface ProcesserEventInfo {
referenceId: string
eventId: string
status: WorkStatus
progress: ProcesserProgress
inputFile: string
outputFiles: Array<string>
}
export enum Status {
Skipped = 'Skipped',
Awaiting = 'Awaiting',
NeedsApproval = 'NeedsApproval',
Pending = 'Pending',
InProgress = 'InProgress',
Completed = 'Completed',
Failed = "Failed"
}
export interface ContentEventState extends ExpandableTableItem {
referenceId: string
title: string
encode: Status
extract: Status
convert: Status
created: number
encodeWork: ProcesserEventInfo
}
export interface ContentEventStateItems {
items: Array<ContentEventState>,
encodeWork: { [key: string]: ProcesserEventInfo }
}
const initialState: ContentEventStateItems = {
items: [],
encodeWork: {}
}
const workSlice = createSlice({
name: "Work",
initialState,
reducers: {
update(state, action: PayloadAction<Array<ContentEventState>>) {
state.items = action.payload.map(item => ({
...item,
rowId: item.referenceId, // Setter rowId lik referenceId
encodeWork: state.encodeWork[item.referenceId] ?? undefined // Reapply encodeWork hvis det finnes
}));
},
updateEncodeProgress(state, action: PayloadAction<ProcesserEventInfo>) {
state.encodeWork[action.payload.referenceId] = action.payload;
state.items = state.items.map(item =>
item.referenceId === action.payload.referenceId
? { ...item, encodeWork: action.payload }
: item
);
}
}
})
export const { update, updateEncodeProgress } = workSlice.actions;
export default workSlice.reducer;