Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/ErrorFallback/FallbackRender.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { FallbackProps } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Code, Text } from '@mantine/core';
import i18next from 'i18next';

// This function is called by the npm package react-error-boundary when an
// error is thrown. The return JSX of this function is then displayed instead
// of the JSX code that is inside the Error Boundary. The resetErrorBoundary
// prop allows us to pass in a function where we can "recover" from the thrown
// error, reset the error, and retry rendering.
export function FallbackRender({ error, resetErrorBoundary }: FallbackProps) {
const { t } = useTranslation('components', { keyPrefix: 'error-fallback' });
export function fallbackRender({ error, resetErrorBoundary }: FallbackProps) {
const t = (key: string) => i18next.t(`components:error-fallback.${key}`);

return (
<Alert variant={'light'} color={'red'} title={t('alert-title')}>
Expand Down
16 changes: 10 additions & 6 deletions src/hooks/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,24 @@ export function useProperty<T extends PropertyTypeKey>(
);
}

useSubscribeToProperty(uri);
const dispatch = useAppDispatch();
// Subscribe to the property
const ThrottleMs = 1000 / 60;

const setValue = useThrottledCallback((value: PropertyOrPropertyGroup<T>['value']) => {
dispatch(setPropertyValue({ uri, value }));
}, ThrottleMs);

return [prop?.value, setValue, prop?.metaData];
}

export function useSubscribeToProperty(uri: Uri) {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(subscribeToProperty({ uri }));
return () => {
dispatch(unsubscribeToProperty({ uri }));
};
}, [dispatch, uri]);

const setValue = useThrottledCallback((value: PropertyOrPropertyGroup<T>['value']) => {
dispatch(setPropertyValue({ uri, value }));
}, ThrottleMs);

return [prop?.value, setValue, prop?.metaData];
}
4 changes: 2 additions & 2 deletions src/pages/ActionsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ErrorBoundary } from 'react-error-boundary';
import { ModalsProvider } from '@mantine/modals';

import { FallbackRender } from '@/components/ErrorFallback/FallbackRender';
import { fallbackRender } from '@/components/ErrorFallback/FallbackRender';
import { ConnectionErrorOverlay } from '@/windowmanagement/ConnectionErrorOverlay';
import { ActionsPanel } from '@/windowmanagement/data/LazyLoads';
import { Window } from '@/windowmanagement/Window/Window';

export function ActionsPage() {
return (
<ErrorBoundary
fallbackRender={FallbackRender}
fallbackRender={fallbackRender}
onReset={() => window.location.reload()}
>
<ModalsProvider>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/GuiPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { ErrorBoundary } from 'react-error-boundary';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';

import { FallbackRender } from '@/components/ErrorFallback/FallbackRender';
import { fallbackRender } from '@/components/ErrorFallback/FallbackRender';
import { WindowLayout } from '@/windowmanagement/WindowLayout/WindowLayout';
import { WindowLayoutProvider } from '@/windowmanagement/WindowLayout/WindowLayoutProvider';

export function GuiPage() {
return (
<ErrorBoundary
fallbackRender={FallbackRender}
fallbackRender={fallbackRender}
onReset={() => window.location.reload()}
>
<Notifications autoClose={6000} />
Expand Down
6 changes: 3 additions & 3 deletions src/panels/LogPanel/ScriptLogPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export function ScriptLogPanel() {

const fetchScriptLogEntries = useCallback(async () => {
// eslint-disable-next-line no-template-curly-in-string
const fileName = await luaApi?.absPath('${LOGS}/ScriptLog.txt');
const filename = await luaApi?.absPath('${LOGS}/ScriptLog.txt');

if (!fileName) {
if (!filename) {
return;
}

const data = (await luaApi?.readFileLines(fileName)) as FileLines;
const data = (await luaApi?.readFileLines(filename)) as FileLines;

if (!data) {
return;
Expand Down
141 changes: 131 additions & 10 deletions src/panels/SessionRecordingPanel/PlaySession.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,108 @@
import { useState } from 'react';
import { Group, Select, Stack, Title } from '@mantine/core';
import { useEffect, useMemo, useState } from 'react';
import {
Alert,
CheckIcon,
Group,
Select,
SelectProps,
Stack,
Text,
Title
} from '@mantine/core';

import { BoolInput } from '@/components/Input/BoolInput';
import { NumericInput } from '@/components/Input/NumericInput/NumericInput';
import { useSubscribeToProperty } from '@/hooks/properties';
import { useSubscribeToSessionRecording } from '@/hooks/topicSubscriptions';
import { useAppSelector } from '@/redux/hooks';
import { useAppDispatch, useAppSelector } from '@/redux/hooks';
import { updateSessionRecordingSettings } from '@/redux/sessionrecording/sessionRecordingSlice';

import { KeybindButtons } from '../KeybindsPanel/KeybindButtons';

import { PlaybackPauseButton } from './Playback/PlaybackPauseButton';
import { PlaybackPlayButton } from './Playback/PlaybackPlayButton';
import { PlaybackResumeButton } from './Playback/PlaybackResumeButton';
import { PlaybackStopButton } from './Playback/PlaybackStopButton';
import { RecordingState } from './types';
import { parseFilename } from './util';

export function PlaySession() {
const [loopPlayback, setLoopPlayback] = useState(false);
const [shouldOutputFrames, setShouldOutputFrames] = useState(false);
const [outputFramerate, setOutputFramerate] = useState(60);
const [filenamePlayback, setFilenamePlayback] = useState<string | null>(null);
const { latestFile, hideGuiOnPlayback, hideDashboardsOnPlayback } = useAppSelector(
(state) => state.sessionRecording.settings
);
const [playbackFile, setPlaybackFile] = useState<string | null>(latestFile);
const keybinds = useAppSelector((state) => state.actions.keybinds);

const fileList = useAppSelector((state) => state.sessionRecording.files);
const recordingState = useSubscribeToSessionRecording();
const dispatch = useAppDispatch();

// Subscribe to Properties so that the middleware will be notified on updated values
// Important! These properties have to be kept in sync with the properties used in the
// middleware
useSubscribeToProperty('Modules.CefWebGui.Visible');
useSubscribeToProperty('RenderEngine.ShowLog');
useSubscribeToProperty('RenderEngine.ShowVersion');
useSubscribeToProperty('Dashboard.IsEnabled');

const isIdle = recordingState === RecordingState.Idle;
const isPlayingBack =
recordingState === RecordingState.Paused || recordingState === RecordingState.Playing;

const isSettingsCombinationDangerous = loopPlayback && hideGuiOnPlayback;
const toggleGuiKeybind = keybinds.find(
(keybind) => keybind.action === 'os.ToggleMainGui'
);

// Update the playback dropdown list to the latest recorded file
useEffect(() => {
if (latestFile) {
setPlaybackFile(latestFile);
}
}, [latestFile]);

// Store file duplicates in map for quick lookup
const fileCounts = useMemo(() => {
const map = new Map<string, number>();
fileList.forEach((file) => {
const name = file.substring(0, file.lastIndexOf('.'));
map.set(name, (map.get(name) || 0) + 1);
});

return map;
}, [fileList]);

const fileListSelectData = useMemo(() => {
return fileList.map((file) => {
const { filename, isFileDuplicate } = parseFilename(file, fileCounts);
const label = isFileDuplicate ? file : filename;

return { value: file, label: label };
});
}, [fileList, fileCounts]);

const renderSelectOption: SelectProps['renderOption'] = ({ option, checked }) => {
const file = option.value;
const { filename, extension, isFileDuplicate } = parseFilename(file, fileCounts);

return (
<Group gap={'xs'}>
{checked && <CheckIcon size={12} color={'gray'} />}
<Text size={'sm'}>
{filename}
{isFileDuplicate && (
<Text c={'dimmed'} size={'xs'} span>
{extension}
</Text>
)}
</Text>
</Group>
);
};

function onLoopPlaybackChange(shouldLoop: boolean): void {
if (shouldLoop) {
setLoopPlayback(true);
Expand Down Expand Up @@ -75,19 +153,62 @@ export function PlaySession() {
disabled={!shouldOutputFrames || !isIdle}
/>
</Group>
<BoolInput
label={'Hide GUI on playback'}
value={hideGuiOnPlayback}
onChange={(value) =>
dispatch(updateSessionRecordingSettings({ hideGuiOnPlayback: value }))
}
info={
'When checked, hides the GUI during playback. It will reappear after the recording ends.'
}
disabled={isPlayingBack}
/>
{isSettingsCombinationDangerous && (
<Alert variant={'light'} color={'orange'} title={'Warning'}>
<Text mb={'xs'}>
Caution: Enabling both{' '}
<Text fs={'italic'} span inherit>
Loop playback
</Text>{' '}
and{' '}
<Text fs={'italic'} span inherit>
Hide GUI
</Text>{' '}
will cause the interface to remain hidden indefinitely during playback. To
reveal the GUI again, press:
</Text>
Comment on lines +169 to +180
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why this text string is needed, and like that the settings are marked in italics, but this text will be a pain to localize using i18next. Did we come up with a good way to deal with italics?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will see what the solution will be in the other PR that handles the translation

<KeybindButtons
selectedKey={toggleGuiKeybind?.key}
modifiers={toggleGuiKeybind?.modifiers}
/>
</Alert>
)}
<BoolInput
label={'Hide dashboards on playback'}
value={hideDashboardsOnPlayback}
onChange={(value) =>
dispatch(updateSessionRecordingSettings({ hideDashboardsOnPlayback: value }))
}
info={
'When checked, hides the dashboard overlays during playback. They will reappear after the recording ends.'
}
disabled={isPlayingBack}
/>
<Group gap={'xs'} align={'flex-end'}>
<Select
value={filenamePlayback}
value={playbackFile}
label={'Playback file'}
placeholder={'Select playback file'}
data={fileList}
onChange={setFilenamePlayback}
data={fileListSelectData}
renderOption={renderSelectOption}
onChange={setPlaybackFile}
searchable
disabled={isPlayingBack}
disabled={!isIdle}
/>
<PlaybackPlayButton
disabled={isPlayingBack || filenamePlayback === null}
filename={filenamePlayback}
disabled={isPlayingBack || !playbackFile}
filename={playbackFile}
loopPlayback={loopPlayback}
shouldOutputFrames={shouldOutputFrames}
outputFramerate={outputFramerate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Button, ButtonProps } from '@mantine/core';

import { useOpenSpaceApi } from '@/api/hooks';
import { PlayIcon } from '@/icons/icons';
import { useAppDispatch } from '@/redux/hooks';
import { showGUI } from '@/redux/sessionrecording/sessionRecordingMiddleware';
import { RecordingsFolderKey } from '@/util/keys';

interface Props extends ButtonProps {
Expand All @@ -19,14 +21,18 @@ export function PlaybackPlayButton({
...props
}: Props) {
const luaApi = useOpenSpaceApi();
const dispatch = useAppDispatch();

async function startPlayback(): Promise<void> {
const shouldWaitForTiles = true;
const filePath = await luaApi?.absPath(`${RecordingsFolderKey}${filename}`);
const filePath = await luaApi?.absPath(`${RecordingsFolderKey}/${filename}`);
if (!filePath) {
// TODO anden88 2025-02-18: notification about error using mantine notification system?
return;
}

dispatch(showGUI(false));

if (shouldOutputFrames) {
luaApi?.sessionRecording.startPlayback(
filePath,
Expand Down
46 changes: 36 additions & 10 deletions src/panels/SessionRecordingPanel/Record/RecordingStopButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,49 @@ import { Button, ButtonProps } from '@mantine/core';

import { useOpenSpaceApi } from '@/api/hooks';
import { StopIcon } from '@/icons/icons';
import { useAppSelector } from '@/redux/hooks';
import { useAppDispatch, useAppSelector } from '@/redux/hooks';
import { handleNotificationLogging } from '@/redux/logging/loggingMiddleware';
import { updateSessionRecordingSettings } from '@/redux/sessionrecording/sessionRecordingSlice';
import { LogLevel } from '@/types/enums';
import { RecordingsFolderKey } from '@/util/keys';

interface Props extends ButtonProps {
filename: string;
}
import { sessionRecordingFilenameWithExtension } from '../util';

export function RecordingStopButton({ filename, ...props }: Props) {
export function RecordingStopButton({ ...props }: ButtonProps) {
const luaApi = useOpenSpaceApi();
const { format, overwriteFile } = useAppSelector(
const { format, overwriteFile, recordingFilename } = useAppSelector(
(state) => state.sessionRecording.settings
);
const dispatch = useAppDispatch();

async function stopRecording(): Promise<void> {
let file = recordingFilename.trim();
file = sessionRecordingFilenameWithExtension(file, format);

try {
const filePath = await luaApi?.absPath(`${RecordingsFolderKey}/${file}`);

function stopRecording(): void {
// prettier-ignore
luaApi?.absPath(`${RecordingsFolderKey}${filename}`)
.then((value) => luaApi?.sessionRecording.stopRecording(value, format, overwriteFile));
if (filePath) {
await luaApi?.sessionRecording.stopRecording(filePath, format, overwriteFile);
dispatch(updateSessionRecordingSettings({ latestFile: file }));
} else {
dispatch(
handleNotificationLogging(
'Error stopping session recording',
`Invalid filepath, can't find filepath '${filePath}' for file: '${file}'`,
LogLevel.Error
)
);
}
} catch (error) {
dispatch(
handleNotificationLogging(
'Error stopping session recording',
error,
LogLevel.Error
)
);
}
}

return (
Expand Down
Loading