Files
Cody BrittainandGitHub bcd29944b7 Prettier update (#21722)
No player-facing changes.

Ports several PRs from tgstation, including:

https://github.com/tgstation/tgstation/pull/66862
https://github.com/tgstation/tgstation/pull/80189
https://github.com/tgstation/tgstation/pull/90317
https://github.com/tgstation/tgstation/pull/91379

In addition, removes IntelliCode from the recommended extensions. This
is deprecated, and we prefer not to force the replacement onto
contributors. Contributors may individually choose to use this instead.
2026-01-28 07:16:34 +00:00

452 lines
12 KiB
JavaScript

/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { toFixed } from 'common/math';
import { useLocalState } from 'tgui/backend';
import { useDispatch, useSelector } from 'common/redux';
import {
Box,
Button,
ColorBox,
Divider,
Dropdown,
Flex,
Input,
LabeledList,
NumberInput,
Section,
Stack,
Tabs,
TextArea,
} from 'tgui/components';
import { ChatPageSettings } from '../chat';
import {
rebuildChat,
saveChatToDisk,
clearChatMessages,
} from '../chat/actions';
import { THEMES } from '../themes';
import {
changeSettingsTab,
updateSettings,
addHighlightSetting,
removeHighlightSetting,
updateHighlightSetting,
} from './actions';
import { SETTINGS_TABS, FONTS, MAX_HIGHLIGHT_SETTINGS } from './constants';
import { resetPaneSplitters, setEditPaneSplitters } from './scaling';
import {
selectActiveTab,
selectSettings,
selectHighlightSettings,
selectHighlightSettingById,
} from './selectors';
import { IMPL_IFRAME_INDEXED_DB, storage } from 'common/storage';
export const SettingsPanel = (props, context) => {
const activeTab = useSelector(context, selectActiveTab);
const dispatch = useDispatch(context);
return (
<Stack fill>
<Stack.Item>
<Section fitted fill minHeight="8em">
<Tabs vertical>
{SETTINGS_TABS.map((tab) => (
<Tabs.Tab
key={tab.id}
selected={tab.id === activeTab}
onClick={() =>
dispatch(
changeSettingsTab({
tabId: tab.id,
}),
)
}
>
{tab.name}
</Tabs.Tab>
))}
</Tabs>
</Section>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
{activeTab === 'general' && <SettingsGeneral />}
{activeTab === 'chatPage' && <ChatPageSettings />}
{activeTab === 'textHighlight' && <TextHighlightSettings />}
</Stack.Item>
</Stack>
);
};
export const SettingsGeneral = (props, context) => {
const { theme, fontFamily, fontSize, lineHeight, maxMessages } = useSelector(
context,
selectSettings,
);
const dispatch = useDispatch(context);
const [freeFont, setFreeFont] = useLocalState(context, 'freeFont', false);
const [editingPanes, setEditingPanes] = useLocalState(
context,
'uiScaling',
false,
);
return (
<Section>
<LabeledList>
<LabeledList.Item label="Theme">
<Dropdown
selected={theme}
options={THEMES}
onSelected={(value) =>
dispatch(
updateSettings({
theme: value,
}),
)
}
/>
</LabeledList.Item>
<LabeledList.Item label="UI sizes">
<Stack>
<Stack.Item>
<Button
onClick={() =>
setEditingPanes((val) => {
setEditPaneSplitters(!val);
return !val;
})
}
color={editingPanes ? 'red' : undefined}
icon={editingPanes ? 'save' : undefined}
>
{editingPanes ? 'Save' : 'Adjust UI Sizes'}
</Button>
</Stack.Item>
<Stack.Item>
<Button onClick={resetPaneSplitters} icon="refresh" color="red">
Reset
</Button>
</Stack.Item>
</Stack>
</LabeledList.Item>
<LabeledList.Item label="Font style">
<Stack inline align="baseline">
<Stack.Item>
{(!freeFont && (
<Dropdown
selected={fontFamily}
options={FONTS}
onSelected={(value) =>
dispatch(
updateSettings({
fontFamily: value,
}),
)
}
/>
)) || (
<Input
value={fontFamily}
onChange={(e, value) =>
dispatch(
updateSettings({
fontFamily: value,
}),
)
}
/>
)}
</Stack.Item>
<Stack.Item>
<Button
content="Custom font"
icon={freeFont ? 'lock-open' : 'lock'}
color={freeFont ? 'good' : 'bad'}
ml={1}
onClick={() => {
setFreeFont(!freeFont);
}}
/>
</Stack.Item>
</Stack>
</LabeledList.Item>
<LabeledList.Item label="Font size">
<NumberInput
width="4em"
step={1}
stepPixelSize={10}
minValue={8}
maxValue={32}
value={fontSize}
unit="px"
format={(value) => toFixed(value)}
onChange={(e, value) =>
dispatch(
updateSettings({
fontSize: value,
}),
)
}
/>
</LabeledList.Item>
<LabeledList.Item label="Line height">
<NumberInput
width="4em"
step={0.01}
stepPixelSize={2}
minValue={0.8}
maxValue={5}
value={lineHeight}
format={(value) => toFixed(value, 2)}
onDrag={(e, value) =>
dispatch(
updateSettings({
lineHeight: value,
}),
)
}
/>
</LabeledList.Item>
<LabeledList.Item label="Max messages">
<NumberInput
width="4em"
step={50}
stepPixelSize={2}
minValue={2000}
maxValue={32000}
value={maxMessages}
format={(value) => toFixed(value)}
onChange={(e, value) => {
storage.backendPromise.then((promise) => {
if (promise.impl === IMPL_IFRAME_INDEXED_DB) {
promise.setNumberStored(value);
}
});
dispatch(
updateSettings({
maxMessages: value,
}),
);
}}
/>
</LabeledList.Item>
</LabeledList>
<Divider />
<Button icon="eraser" onClick={() => dispatch(clearChatMessages())}>
Clear chat log
</Button>
<Button icon="save" onClick={() => dispatch(saveChatToDisk())}>
Save chat log
</Button>
</Section>
);
};
const TextHighlightSettings = (props, context) => {
const highlightSettings = useSelector(context, selectHighlightSettings);
const settings = useSelector(context, selectSettings);
const dispatch = useDispatch(context);
return (
<Section fill scrollable height="200px">
<Section p={0}>
<Flex direction="column">
{highlightSettings.map((id, i) => (
<TextHighlightSetting
key={i}
id={id}
mb={i + 1 === highlightSettings.length ? 0 : '10px'}
/>
))}
{highlightSettings.length < MAX_HIGHLIGHT_SETTINGS && (
<Flex.Item>
<Button
color="transparent"
icon="plus"
content="Add Highlight Setting"
onClick={() => {
dispatch(addHighlightSetting());
}}
/>
</Flex.Item>
)}
</Flex>
</Section>
<Divider />
<Box>
<Button
icon="check"
onClick={() => dispatch(rebuildChat(settings.maxMessages))}
>
Apply now
</Button>
<Box inline fontSize="0.9em" ml={1} color="label">
Can freeze the chat for a while.
</Box>
</Box>
</Section>
);
};
const TextHighlightSetting = (props, context) => {
const { id, ...rest } = props;
const highlightSettingById = useSelector(context, selectHighlightSettingById);
const dispatch = useDispatch(context);
const {
highlightColor,
highlightText,
highlightWholeMessage,
backgroundHighlightColor,
backgroundHighlightOpacity,
matchWord,
matchCase,
} = highlightSettingById[id];
return (
<Flex.Item {...rest}>
<Flex mb={1} color="label" align="baseline">
<Flex.Item grow>
<Button
content="Delete"
color="transparent"
icon="times"
onClick={() =>
dispatch(
removeHighlightSetting({
id: id,
}),
)
}
/>
</Flex.Item>
<Flex.Item>
{highlightWholeMessage && (
<>
<label>Opacity: </label>
<NumberInput
width="4em"
step={1}
stepPixelSize={5}
minValue={1}
maxValue={100}
unit="%"
value={backgroundHighlightOpacity}
format={(value) => toFixed(value)}
onDrag={(e, value) =>
dispatch(
updateHighlightSetting({
id: id,
backgroundHighlightOpacity: value,
}),
)
}
/>
</>
)}
</Flex.Item>
<Flex.Item shrink={0}>
{highlightWholeMessage && (
<>
<ColorBox mr={1} color={backgroundHighlightColor} />
<Input
mr={1}
width="5em"
monospace
placeholder="#ffdd44"
value={backgroundHighlightColor}
onInput={(e, value) =>
dispatch(
updateHighlightSetting({
id: id,
backgroundHighlightColor: value,
}),
)
}
/>
</>
)}
</Flex.Item>
<Flex.Item>
<Button.Checkbox
checked={highlightWholeMessage}
content="Whole Message"
tooltip="If this option is selected, the entire message will be highlighted in the color defined to the left."
mr="5px"
onClick={() =>
dispatch(
updateHighlightSetting({
id: id,
highlightWholeMessage: !highlightWholeMessage,
}),
)
}
/>
</Flex.Item>
<Flex.Item>
<Button.Checkbox
content="Exact"
checked={matchWord}
tooltipPosition="bottom-start"
tooltip="If this option is selected, only exact matches (no extra letters before or after) will trigger. Not compatible with punctuation. Overriden if regex is used."
onClick={() =>
dispatch(
updateHighlightSetting({
id: id,
matchWord: !matchWord,
}),
)
}
/>
</Flex.Item>
<Flex.Item>
<Button.Checkbox
content="Case"
tooltip="If this option is selected, the highlight will be case-sensitive."
checked={matchCase}
onClick={() =>
dispatch(
updateHighlightSetting({
id: id,
matchCase: !matchCase,
}),
)
}
/>
</Flex.Item>
<Flex.Item shrink={0}>
<ColorBox mr={1} color={highlightColor} />
<Input
width="5em"
monospace
placeholder="#ffffff"
value={highlightColor}
onInput={(e, value) =>
dispatch(
updateHighlightSetting({
id: id,
highlightColor: value,
}),
)
}
/>
</Flex.Item>
</Flex>
<TextArea
height="3em"
value={highlightText}
placeholder="Put words to highlight here. Separate terms with commas, i.e. (term1, term2, term3)"
onChange={(e, value) =>
dispatch(
updateHighlightSetting({
id: id,
highlightText: value,
}),
)
}
/>
</Flex.Item>
);
};