Merge pull request #13106 from ItsSelis/tgui-panel-removal

Removes tgui-panel
This commit is contained in:
Casey
2022-06-18 18:50:11 -04:00
committed by GitHub
59 changed files with 0 additions and 5070 deletions
-41
View File
@@ -1,41 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Flex } from 'tgui/components';
export const Notifications = props => {
const { children } = props;
return (
<div className="Notifications">
{children}
</div>
);
};
const NotificationsItem = props => {
const {
rightSlot,
children,
} = props;
return (
<Flex
align="center"
className="Notification">
<Flex.Item
className="Notification__content"
grow={1}>
{children}
</Flex.Item>
{rightSlot && (
<Flex.Item className="Notification__rightSlot">
{rightSlot}
</Flex.Item>
)}
</Flex>
);
};
Notifications.Item = NotificationsItem;
-138
View File
@@ -1,138 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Button, Section, Stack } from 'tgui/components';
import { Pane } from 'tgui/layouts';
import { NowPlayingWidget, useAudio } from './audio';
import { ChatPanel, ChatTabs } from './chat';
import { useGame } from './game';
import { Notifications } from './Notifications';
import { PingIndicator } from './ping';
import { SettingsPanel, useSettings } from './settings';
export const Panel = (props, context) => {
// IE8-10: Needs special treatment due to missing Flex support
if (Byond.IS_LTE_IE10) {
return (
<HoboPanel />
);
}
const audio = useAudio(context);
const settings = useSettings(context);
const game = useGame(context);
if (process.env.NODE_ENV !== 'production') {
const { useDebug, KitchenSink } = require('tgui/debug');
const debug = useDebug(context);
if (debug.kitchenSink) {
return (
<KitchenSink panel />
);
}
}
return (
<Pane theme={settings.theme}>
<Stack fill vertical>
<Stack.Item>
<Section fitted>
<Stack mr={1} align="center">
<Stack.Item grow overflowX="auto">
<ChatTabs />
</Stack.Item>
<Stack.Item>
<PingIndicator />
</Stack.Item>
<Stack.Item>
<Button
color="grey"
selected={audio.visible}
icon="music"
tooltip="Music player"
tooltipPosition="bottom-start"
onClick={() => audio.toggle()} />
</Stack.Item>
<Stack.Item>
<Button
icon={settings.visible ? 'times' : 'cog'}
selected={settings.visible}
tooltip={settings.visible
? 'Close settings'
: 'Open settings'}
tooltipPosition="bottom-start"
onClick={() => settings.toggle()} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
{audio.visible && (
<Stack.Item>
<Section>
<NowPlayingWidget />
</Section>
</Stack.Item>
)}
{settings.visible && (
<Stack.Item>
<SettingsPanel />
</Stack.Item>
)}
<Stack.Item grow>
<Section fill fitted position="relative">
<Pane.Content scrollable>
<ChatPanel lineHeight={settings.lineHeight} />
</Pane.Content>
<Notifications>
{game.connectionLostAt && (
<Notifications.Item
rightSlot={(
<Button
color="white"
onClick={() => Byond.command('.reconnect')}>
Reconnect
</Button>
)}>
You are either AFK, experiencing lag or the connection
has closed.
</Notifications.Item>
)}
{game.roundRestartedAt && (
<Notifications.Item>
The connection has been closed because the server is
restarting. Please wait while you automatically reconnect.
</Notifications.Item>
)}
</Notifications>
</Section>
</Stack.Item>
</Stack>
</Pane>
);
};
const HoboPanel = (props, context) => {
const settings = useSettings(context);
return (
<Pane theme={settings.theme}>
<Pane.Content scrollable>
<Button
style={{
position: 'fixed',
top: '1em',
right: '2em',
'z-index': 1000,
}}
selected={settings.visible}
onClick={() => settings.toggle()}>
Settings
</Button>
{settings.visible && (
<SettingsPanel />
) || (
<ChatPanel lineHeight={settings.lineHeight} />
)}
</Pane.Content>
</Pane>
);
};
@@ -1,68 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { toFixed } from 'common/math';
import { useDispatch, useSelector } from 'common/redux';
import { Button, Flex, Knob } from 'tgui/components';
import { useSettings } from '../settings';
import { selectAudio } from './selectors';
export const NowPlayingWidget = (props, context) => {
const audio = useSelector(context, selectAudio);
const dispatch = useDispatch(context);
const settings = useSettings(context);
const title = audio.meta?.title;
return (
<Flex align="center">
{audio.playing && (
<>
<Flex.Item
shrink={0}
mx={0.5}
color="label">
Now playing:
</Flex.Item>
<Flex.Item
mx={0.5}
grow={1}
style={{
'white-space': 'nowrap',
'overflow': 'hidden',
'text-overflow': 'ellipsis',
}}>
{title || 'Unknown Track'}
</Flex.Item>
</>
) || (
<Flex.Item grow={1} color="label">
Nothing to play.
</Flex.Item>
)}
{audio.playing && (
<Flex.Item mx={0.5} fontSize="0.9em">
<Button
tooltip="Stop"
icon="stop"
onClick={() => dispatch({
type: 'audio/stopMusic',
})} />
</Flex.Item>
)}
<Flex.Item mx={0.5} fontSize="0.9em">
<Knob
minValue={0}
maxValue={1}
value={settings.adminMusicVolume}
step={0.0025}
stepPixelSize={1}
format={value => toFixed(value * 100) + '%'}
onDrag={(e, value) => settings.update({
adminMusicVolume: value,
})} />
</Flex.Item>
</Flex>
);
};
-17
View File
@@ -1,17 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useSelector, useDispatch } from 'common/redux';
import { selectAudio } from './selectors';
export const useAudio = context => {
const state = useSelector(context, selectAudio);
const dispatch = useDispatch(context);
return {
...state,
toggle: () => dispatch({ type: 'audio/toggle' }),
};
};
-10
View File
@@ -1,10 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { useAudio } from './hooks';
export { audioMiddleware } from './middleware';
export { NowPlayingWidget } from './NowPlayingWidget';
export { audioReducer } from './reducer';
@@ -1,37 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { AudioPlayer } from './player';
export const audioMiddleware = store => {
const player = new AudioPlayer();
player.onPlay(() => {
store.dispatch({ type: 'audio/playing' });
});
player.onStop(() => {
store.dispatch({ type: 'audio/stopped' });
});
return next => action => {
const { type, payload } = action;
if (type === 'audio/playMusic') {
const { url, ...options } = payload;
player.play(url, options);
return next(action);
}
if (type === 'audio/stopMusic') {
player.stop();
return next(action);
}
if (type === 'settings/update' || type === 'settings/load') {
const volume = payload?.adminMusicVolume;
if (typeof volume === 'number') {
player.setVolume(volume);
}
return next(action);
}
return next(action);
};
};
-117
View File
@@ -1,117 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createLogger } from 'tgui/logging';
const logger = createLogger('AudioPlayer');
export class AudioPlayer {
constructor() {
// Doesn't support HTMLAudioElement
if (Byond.IS_LTE_IE9) {
return;
}
// Set up the HTMLAudioElement node
this.node = document.createElement('audio');
this.node.style.setProperty('display', 'none');
document.body.appendChild(this.node);
// Set up other properties
this.playing = false;
this.volume = 1;
this.options = {};
this.onPlaySubscribers = [];
this.onStopSubscribers = [];
// Listen for playback start events
this.node.addEventListener('canplaythrough', () => {
logger.log('canplaythrough');
this.playing = true;
this.node.playbackRate = this.options.pitch || 1;
this.node.currentTime = this.options.start || 0;
this.node.volume = this.volume;
this.node.play();
for (let subscriber of this.onPlaySubscribers) {
subscriber();
}
});
// Listen for playback stop events
this.node.addEventListener('ended', () => {
logger.log('ended');
this.stop();
});
// Listen for playback errors
this.node.addEventListener('error', e => {
if (this.playing) {
logger.log('playback error', e.error);
this.stop();
}
});
// Check every second to stop the playback at the right time
this.playbackInterval = setInterval(() => {
if (!this.playing) {
return;
}
const shouldStop = this.options.end > 0
&& this.node.currentTime >= this.options.end;
if (shouldStop) {
this.stop();
}
}, 1000);
}
destroy() {
if (!this.node) {
return;
}
this.node.stop();
document.removeChild(this.node);
clearInterval(this.playbackInterval);
}
play(url, options = {}) {
if (!this.node) {
return;
}
logger.log('playing', url, options);
this.options = options;
this.node.src = url;
}
stop() {
if (!this.node) {
return;
}
if (this.playing) {
for (let subscriber of this.onStopSubscribers) {
subscriber();
}
}
logger.log('stopping');
this.playing = false;
this.node.src = '';
}
setVolume(volume) {
if (!this.node) {
return;
}
this.volume = volume;
this.node.volume = volume;
}
onPlay(subscriber) {
if (!this.node) {
return;
}
this.onPlaySubscribers.push(subscriber);
}
onStop(subscriber) {
if (!this.node) {
return;
}
this.onStopSubscribers.push(subscriber);
}
}
-50
View File
@@ -1,50 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const initialState = {
visible: false,
playing: false,
track: null,
};
export const audioReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === 'audio/playing') {
return {
...state,
visible: true,
playing: true,
};
}
if (type === 'audio/stopped') {
return {
...state,
visible: false,
playing: false,
};
}
if (type === 'audio/playMusic') {
return {
...state,
meta: payload,
};
}
if (type === 'audio/stopMusic') {
return {
...state,
visible: false,
playing: false,
meta: null,
};
}
if (type === 'audio/toggle') {
return {
...state,
visible: !state.visible,
};
}
return state;
};
@@ -1,7 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const selectAudio = state => state.audio;
@@ -1,75 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useDispatch, useSelector } from 'common/redux';
import { Button, Collapsible, Divider, Input, Section, Stack } from 'tgui/components';
import { removeChatPage, toggleAcceptedType, updateChatPage } from './actions';
import { MESSAGE_TYPES } from './constants';
import { selectCurrentChatPage } from './selectors';
export const ChatPageSettings = (props, context) => {
const page = useSelector(context, selectCurrentChatPage);
const dispatch = useDispatch(context);
return (
<Section>
<Stack align="center">
<Stack.Item grow={1}>
<Input
fluid
value={page.name}
onChange={(e, value) => dispatch(updateChatPage({
pageId: page.id,
name: value,
}))} />
</Stack.Item>
<Stack.Item>
<Button
icon="times"
color="red"
onClick={() => dispatch(removeChatPage({
pageId: page.id,
}))}>
Remove
</Button>
</Stack.Item>
</Stack>
<Divider />
<Section title="Messages to display" level={2}>
{MESSAGE_TYPES
.filter(typeDef => !typeDef.important && !typeDef.admin)
.map(typeDef => (
<Button.Checkbox
key={typeDef.type}
checked={page.acceptedTypes[typeDef.type]}
onClick={() => dispatch(toggleAcceptedType({
pageId: page.id,
type: typeDef.type,
}))}>
{typeDef.name}
</Button.Checkbox>
))}
<Collapsible
mt={1}
color="transparent"
title="Admin stuff">
{MESSAGE_TYPES
.filter(typeDef => !typeDef.important && typeDef.admin)
.map(typeDef => (
<Button.Checkbox
key={typeDef.type}
checked={page.acceptedTypes[typeDef.type]}
onClick={() => dispatch(toggleAcceptedType({
pageId: page.id,
type: typeDef.type,
}))}>
{typeDef.name}
</Button.Checkbox>
))}
</Collapsible>
</Section>
</Section>
);
};
@@ -1,71 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { shallowDiffers } from 'common/react';
import { Component, createRef } from 'inferno';
import { Button } from 'tgui/components';
import { chatRenderer } from './renderer';
export class ChatPanel extends Component {
constructor() {
super();
this.ref = createRef();
this.state = {
scrollTracking: true,
};
this.handleScrollTrackingChange = value => this.setState({
scrollTracking: value,
});
}
componentDidMount() {
chatRenderer.mount(this.ref.current);
chatRenderer.events.on('scrollTrackingChanged',
this.handleScrollTrackingChange);
this.componentDidUpdate();
}
componentWillUnmount() {
chatRenderer.events.off('scrollTrackingChanged',
this.handleScrollTrackingChange);
}
componentDidUpdate(prevProps) {
requestAnimationFrame(() => {
chatRenderer.ensureScrollTracking();
});
const shouldUpdateStyle = (
!prevProps || shallowDiffers(this.props, prevProps)
);
if (shouldUpdateStyle) {
chatRenderer.assignStyle({
'width': '100%',
'white-space': 'pre-wrap',
'font-size': this.props.fontSize,
'line-height': this.props.lineHeight,
});
}
}
render() {
const {
scrollTracking,
} = this.state;
return (
<>
<div className="Chat" ref={this.ref} />
{!scrollTracking && (
<Button
className="Chat__scrollButton"
icon="arrow-down"
onClick={() => chatRenderer.scrollToBottom()}>
Scroll to bottom
</Button>
)}
</>
);
}
}
-61
View File
@@ -1,61 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useDispatch, useSelector } from 'common/redux';
import { Box, Tabs, Flex, Button } from 'tgui/components';
import { changeChatPage, addChatPage } from './actions';
import { selectChatPages, selectCurrentChatPage } from './selectors';
import { openChatSettings } from '../settings/actions';
const UnreadCountWidget = ({ value }) => (
<Box
style={{
'font-size': '0.7em',
'border-radius': '0.25em',
'width': '1.7em',
'line-height': '1.55em',
'background-color': 'crimson',
'color': '#fff',
}}>
{Math.min(value, 99)}
</Box>
);
export const ChatTabs = (props, context) => {
const pages = useSelector(context, selectChatPages);
const currentPage = useSelector(context, selectCurrentChatPage);
const dispatch = useDispatch(context);
return (
<Flex align="center">
<Flex.Item>
<Tabs textAlign="center">
{pages.map(page => (
<Tabs.Tab
key={page.id}
selected={page === currentPage}
rightSlot={page.unreadCount > 0 && (
<UnreadCountWidget value={page.unreadCount} />
)}
onClick={() => dispatch(changeChatPage({
pageId: page.id,
}))}>
{page.name}
</Tabs.Tab>
))}
</Tabs>
</Flex.Item>
<Flex.Item ml={1}>
<Button
color="transparent"
icon="plus"
onClick={() => {
dispatch(addChatPage());
dispatch(openChatSettings());
}} />
</Flex.Item>
</Flex>
);
};
-21
View File
@@ -1,21 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createAction } from 'common/redux';
import { createPage } from './model';
export const loadChat = createAction('chat/load');
export const rebuildChat = createAction('chat/rebuild');
export const updateMessageCount = createAction('chat/updateMessageCount');
export const addChatPage = createAction('chat/addPage', () => ({
payload: createPage(),
}));
export const changeChatPage = createAction('chat/changePage');
export const updateChatPage = createAction('chat/updatePage');
export const toggleAcceptedType = createAction('chat/toggleAcceptedType');
export const removeChatPage = createAction('chat/removePage');
export const changeScrollTracking = createAction('chat/changeScrollTracking');
export const saveChatToDisk = createAction('chat/saveToDisk');
-138
View File
@@ -1,138 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const MAX_VISIBLE_MESSAGES = 2500;
export const MAX_PERSISTED_MESSAGES = 1000;
export const MESSAGE_SAVE_INTERVAL = 10000;
export const MESSAGE_PRUNE_INTERVAL = 60000;
export const COMBINE_MAX_MESSAGES = 5;
export const COMBINE_MAX_TIME_WINDOW = 5000;
export const IMAGE_RETRY_DELAY = 250;
export const IMAGE_RETRY_LIMIT = 10;
export const IMAGE_RETRY_MESSAGE_AGE = 60000;
// Default message type
export const MESSAGE_TYPE_UNKNOWN = 'unknown';
// Internal message type
export const MESSAGE_TYPE_INTERNAL = 'internal';
// Must match the set of defines in code/__DEFINES/chat.dm
export const MESSAGE_TYPE_SYSTEM = 'system';
export const MESSAGE_TYPE_LOCALCHAT = 'localchat';
export const MESSAGE_TYPE_RADIO = 'radio';
export const MESSAGE_TYPE_INFO = 'info';
export const MESSAGE_TYPE_WARNING = 'warning';
export const MESSAGE_TYPE_DEADCHAT = 'deadchat';
export const MESSAGE_TYPE_OOC = 'ooc';
export const MESSAGE_TYPE_ADMINPM = 'adminpm';
export const MESSAGE_TYPE_COMBAT = 'combat';
export const MESSAGE_TYPE_ADMINCHAT = 'adminchat';
export const MESSAGE_TYPE_MODCHAT = 'modchat';
export const MESSAGE_TYPE_EVENTCHAT = 'eventchat';
export const MESSAGE_TYPE_ADMINLOG = 'adminlog';
export const MESSAGE_TYPE_ATTACKLOG = 'attacklog';
export const MESSAGE_TYPE_DEBUG = 'debug';
// Metadata for each message type
export const MESSAGE_TYPES = [
// Always-on types
{
type: MESSAGE_TYPE_SYSTEM,
name: 'System Messages',
description: 'Messages from your client, always enabled',
selector: '.boldannounce',
important: true,
},
// Basic types
{
type: MESSAGE_TYPE_LOCALCHAT,
name: 'Local',
description: 'In-character local messages (say, emote, etc)',
selector: '.say, .emote',
},
{
type: MESSAGE_TYPE_RADIO,
name: 'Radio',
description: 'All departments of radio messages',
selector: '.alert, .minorannounce, .syndradio, .centcomradio, .aiprivradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .suppradio, .servradio, .radio, .deptradio, .binarysay, .newscaster, .resonate',
},
{
type: MESSAGE_TYPE_INFO,
name: 'Info',
description: 'Non-urgent messages from the game and items',
selector: '.notice:not(.pm), .adminnotice, .info, .sinister, .cult, .infoplain, .announce, .hear, .smallnotice, .holoparasite',
},
{
type: MESSAGE_TYPE_WARNING,
name: 'Warnings',
description: 'Urgent messages from the game and items',
selector: '.warning:not(.pm), .critical, .userdanger, .italics, .alertsyndie, .warningplain',
},
{
type: MESSAGE_TYPE_DEADCHAT,
name: 'Deadchat',
description: 'All of deadchat',
selector: '.deadsay, .ghostalert',
},
{
type: MESSAGE_TYPE_OOC,
name: 'OOC',
description: 'The bluewall of global OOC messages',
selector: '.ooc, .adminooc, .oocplain',
},
{
type: MESSAGE_TYPE_ADMINPM,
name: 'Admin PMs',
description: 'Messages to/from admins (adminhelp)',
selector: '.pm, .adminhelp',
},
{
type: MESSAGE_TYPE_COMBAT,
name: 'Combat Log',
description: 'Urist McTraitor has stabbed you with a knife!',
selector: '.danger',
},
{
type: MESSAGE_TYPE_UNKNOWN,
name: 'Unsorted',
description: 'Everything we could not sort, always enabled',
},
// Admin stuff
{
type: MESSAGE_TYPE_ADMINCHAT,
name: 'Admin Chat',
description: 'ASAY messages',
selector: '.admin_channel, .adminsay',
admin: true,
},
{
type: MESSAGE_TYPE_MODCHAT,
name: 'Mod Chat',
description: 'MSAY messages',
selector: '.mod_channel',
admin: true,
},
{
type: MESSAGE_TYPE_ADMINLOG,
name: 'Admin Log',
description: 'ADMIN LOG: Urist McAdmin has jumped to coordinates X, Y, Z',
selector: '.log_message',
admin: true,
},
{
type: MESSAGE_TYPE_ATTACKLOG,
name: 'Attack Log',
description: 'Urist McTraitor has shot John Doe',
admin: true,
},
{
type: MESSAGE_TYPE_DEBUG,
name: 'Debug Log',
description: 'DEBUG: SSPlanets subsystem Recover().',
admin: true,
},
];
-11
View File
@@ -1,11 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { ChatPageSettings } from './ChatPageSettings';
export { ChatPanel } from './ChatPanel';
export { ChatTabs } from './ChatTabs';
export { chatMiddleware } from './middleware';
export { chatReducer } from './reducer';
-134
View File
@@ -1,134 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import DOMPurify from 'dompurify';
import { storage } from 'common/storage';
import { loadSettings, updateSettings } from '../settings/actions';
import { selectSettings } from '../settings/selectors';
import { addChatPage, changeChatPage, changeScrollTracking, loadChat, rebuildChat, removeChatPage, saveChatToDisk, toggleAcceptedType, updateMessageCount } from './actions';
import { MAX_PERSISTED_MESSAGES, MESSAGE_SAVE_INTERVAL } from './constants';
import { createMessage, serializeMessage } from './model';
import { chatRenderer } from './renderer';
import { selectChat, selectCurrentChatPage } from './selectors';
// List of blacklisted tags
const FORBID_TAGS = [
'a',
'iframe',
'link',
'video',
];
const saveChatToStorage = async store => {
const state = selectChat(store.getState());
const fromIndex = Math.max(0,
chatRenderer.messages.length - MAX_PERSISTED_MESSAGES);
const messages = chatRenderer.messages
.slice(fromIndex)
.map(message => serializeMessage(message));
storage.set('chat-state', state);
storage.set('chat-messages', messages);
};
const loadChatFromStorage = async store => {
const [state, messages] = await Promise.all([
storage.get('chat-state'),
storage.get('chat-messages'),
]);
// Discard incompatible versions
if (state && state.version <= 4) {
store.dispatch(loadChat());
return;
}
if (messages) {
for (let message of messages) {
if (message.html) {
message.html = DOMPurify.sanitize(message.html, {
FORBID_TAGS,
});
}
}
const batch = [
...messages,
createMessage({
type: 'internal/reconnected',
}),
];
chatRenderer.processBatch(batch, {
prepend: true,
});
}
store.dispatch(loadChat(state));
};
export const chatMiddleware = store => {
let initialized = false;
let loaded = false;
chatRenderer.events.on('batchProcessed', countByType => {
// Use this flag to workaround unread messages caused by
// loading them from storage. Side effect of that, is that
// message count can not be trusted, only unread count.
if (loaded) {
store.dispatch(updateMessageCount(countByType));
}
});
chatRenderer.events.on('scrollTrackingChanged', scrollTracking => {
store.dispatch(changeScrollTracking(scrollTracking));
});
setInterval(() => saveChatToStorage(store), MESSAGE_SAVE_INTERVAL);
return next => action => {
const { type, payload } = action;
if (!initialized) {
initialized = true;
loadChatFromStorage(store);
}
if (type === 'chat/message') {
// Normalize the payload
const batch = Array.isArray(payload) ? payload : [payload];
chatRenderer.processBatch(batch);
return;
}
if (type === loadChat.type) {
next(action);
const page = selectCurrentChatPage(store.getState());
chatRenderer.changePage(page);
chatRenderer.onStateLoaded();
loaded = true;
return;
}
if (type === changeChatPage.type
|| type === addChatPage.type
|| type === removeChatPage.type
|| type === toggleAcceptedType.type) {
next(action);
const page = selectCurrentChatPage(store.getState());
chatRenderer.changePage(page);
return;
}
if (type === rebuildChat.type) {
chatRenderer.rebuildChat();
return next(action);
}
if (type === updateSettings.type || type === loadSettings.type) {
next(action);
const settings = selectSettings(store.getState());
chatRenderer.setHighlight(
settings.highlightText,
settings.highlightColor);
return;
}
if (type === 'roundrestart') {
// Save chat as soon as possible
saveChatToStorage(store);
return next(action);
}
if (type === saveChatToDisk.type) {
chatRenderer.saveToDisk();
return;
}
return next(action);
};
};
-50
View File
@@ -1,50 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createUuid } from 'common/uuid';
import { MESSAGE_TYPES, MESSAGE_TYPE_INTERNAL } from './constants';
export const canPageAcceptType = (page, type) => (
type.startsWith(MESSAGE_TYPE_INTERNAL) || page.acceptedTypes[type]
);
export const createPage = obj => ({
id: createUuid(),
name: 'New Tab',
acceptedTypes: {},
unreadCount: 0,
createdAt: Date.now(),
...obj,
});
export const createMainPage = () => {
const acceptedTypes = {};
for (let typeDef of MESSAGE_TYPES) {
acceptedTypes[typeDef.type] = true;
}
return createPage({
name: 'Main',
acceptedTypes,
});
};
export const createMessage = payload => ({
createdAt: Date.now(),
...payload,
});
export const serializeMessage = message => ({
type: message.type,
text: message.text,
html: message.html,
times: message.times,
createdAt: message.createdAt,
});
export const isSameMessage = (a, b) => (
typeof a.text === 'string' && a.text === b.text
|| typeof a.html === 'string' && a.html === b.html
);
-172
View File
@@ -1,172 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { addChatPage, changeChatPage, loadChat, removeChatPage, toggleAcceptedType, updateChatPage, updateMessageCount, changeScrollTracking } from './actions';
import { canPageAcceptType, createMainPage } from './model';
const mainPage = createMainPage();
export const initialState = {
version: 5,
currentPageId: mainPage.id,
scrollTracking: true,
pages: [
mainPage.id,
],
pageById: {
[mainPage.id]: mainPage,
},
};
export const chatReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === loadChat.type) {
// Validate version and/or migrate state
if (payload?.version !== state.version) {
return state;
}
// Reset page message counts
// NOTE: We are mutably changing the payload on the assumption
// that it is a copy that comes straight from the web storage.
for (let id of Object.keys(payload.pageById)) {
const page = payload.pageById[id];
page.unreadCount = 0;
}
return {
...state,
...payload,
};
}
if (type === changeScrollTracking.type) {
const scrollTracking = payload;
const nextState = {
...state,
scrollTracking,
};
if (scrollTracking) {
const pageId = state.currentPageId;
const page = {
...state.pageById[pageId],
unreadCount: 0,
};
nextState.pageById = {
...state.pageById,
[pageId]: page,
};
}
return nextState;
}
if (type === updateMessageCount.type) {
const countByType = payload;
const pages = state.pages.map(id => state.pageById[id]);
const currentPage = state.pageById[state.currentPageId];
const nextPageById = { ...state.pageById };
for (let page of pages) {
let unreadCount = 0;
for (let type of Object.keys(countByType)) {
// Message does not belong here
if (!canPageAcceptType(page, type)) {
continue;
}
// Current page is scroll tracked
if (page === currentPage && state.scrollTracking) {
continue;
}
// This page received the same message which we can read
// on the current page.
if (page !== currentPage && canPageAcceptType(currentPage, type)) {
continue;
}
unreadCount += countByType[type];
}
if (unreadCount > 0) {
nextPageById[page.id] = {
...page,
unreadCount: page.unreadCount + unreadCount,
};
}
}
return {
...state,
pageById: nextPageById,
};
}
if (type === addChatPage.type) {
return {
...state,
currentPageId: payload.id,
pages: [...state.pages, payload.id],
pageById: {
...state.pageById,
[payload.id]: payload,
},
};
}
if (type === changeChatPage.type) {
const { pageId } = payload;
const page = {
...state.pageById[pageId],
unreadCount: 0,
};
return {
...state,
currentPageId: pageId,
pageById: {
...state.pageById,
[pageId]: page,
},
};
}
if (type === updateChatPage.type) {
const { pageId, ...update } = payload;
const page = {
...state.pageById[pageId],
...update,
};
return {
...state,
pageById: {
...state.pageById,
[pageId]: page,
},
};
}
if (type === toggleAcceptedType.type) {
const { pageId, type } = payload;
const page = { ...state.pageById[pageId] };
page.acceptedTypes = { ...page.acceptedTypes };
page.acceptedTypes[type] = !page.acceptedTypes[type];
return {
...state,
pageById: {
...state.pageById,
[pageId]: page,
},
};
}
if (type === removeChatPage.type) {
const { pageId } = payload;
const nextState = {
...state,
pages: [...state.pages],
pageById: {
...state.pageById,
},
};
delete nextState.pageById[pageId];
nextState.pages = nextState.pages.filter(id => id !== pageId);
if (nextState.pages.length === 0) {
nextState.pages.push(mainPage.id);
nextState.pageById[mainPage.id] = mainPage;
nextState.currentPageId = mainPage.id;
}
if (!nextState.currentPageId || nextState.currentPageId === pageId) {
nextState.currentPageId = nextState.pages[0];
}
return nextState;
}
return state;
};
-488
View File
@@ -1,488 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { EventEmitter } from 'common/events';
import { classes } from 'common/react';
import { createLogger } from 'tgui/logging';
import { COMBINE_MAX_MESSAGES, COMBINE_MAX_TIME_WINDOW, IMAGE_RETRY_DELAY, IMAGE_RETRY_LIMIT, IMAGE_RETRY_MESSAGE_AGE, MAX_PERSISTED_MESSAGES, MAX_VISIBLE_MESSAGES, MESSAGE_PRUNE_INTERVAL, MESSAGE_TYPES, MESSAGE_TYPE_INTERNAL, MESSAGE_TYPE_UNKNOWN } from './constants';
import { canPageAcceptType, createMessage, isSameMessage } from './model';
import { highlightNode, linkifyNode } from './replaceInTextNode';
const logger = createLogger('chatRenderer');
// We consider this as the smallest possible scroll offset
// that is still trackable.
const SCROLL_TRACKING_TOLERANCE = 24;
const findNearestScrollableParent = startingNode => {
const body = document.body;
let node = startingNode;
while (node && node !== body) {
// This definitely has a vertical scrollbar, because it reduces
// scrollWidth of the element. Might not work if element uses
// overflow: hidden.
if (node.scrollWidth < node.offsetWidth) {
return node;
}
node = node.parentNode;
}
return window;
};
const createHighlightNode = (text, color) => {
const node = document.createElement('span');
node.className = 'Chat__highlight';
node.setAttribute('style', 'background-color:' + color);
node.textContent = text;
return node;
};
const createMessageNode = () => {
const node = document.createElement('div');
node.className = 'ChatMessage';
return node;
};
const createReconnectedNode = () => {
const node = document.createElement('div');
node.className = 'Chat__reconnected';
return node;
};
const handleImageError = e => {
setTimeout(() => {
/** @type {HTMLImageElement} */
const node = e.target;
const attempts = parseInt(node.getAttribute('data-reload-n'), 10) || 0;
if (attempts >= IMAGE_RETRY_LIMIT) {
logger.error(`failed to load an image after ${attempts} attempts`);
return;
}
const src = node.src;
node.src = null;
node.src = src + '#' + attempts;
node.setAttribute('data-reload-n', attempts + 1);
}, IMAGE_RETRY_DELAY);
};
/**
* Assigns a "times-repeated" badge to the message.
*/
const updateMessageBadge = message => {
const { node, times } = message;
if (!node || !times) {
// Nothing to update
return;
}
const foundBadge = node.querySelector('.Chat__badge');
const badge = foundBadge || document.createElement('div');
badge.textContent = times;
badge.className = classes([
'Chat__badge',
'Chat__badge--animate',
]);
requestAnimationFrame(() => {
badge.className = 'Chat__badge';
});
if (!foundBadge) {
node.appendChild(badge);
}
};
class ChatRenderer {
constructor() {
/** @type {HTMLElement} */
this.loaded = false;
/** @type {HTMLElement} */
this.rootNode = null;
this.queue = [];
this.messages = [];
this.visibleMessages = [];
this.page = null;
this.events = new EventEmitter();
// Scroll handler
/** @type {HTMLElement} */
this.scrollNode = null;
this.scrollTracking = true;
this.handleScroll = type => {
const node = this.scrollNode;
const height = node.scrollHeight;
const bottom = node.scrollTop + node.offsetHeight;
const scrollTracking = (
Math.abs(height - bottom) < SCROLL_TRACKING_TOLERANCE
);
if (scrollTracking !== this.scrollTracking) {
this.scrollTracking = scrollTracking;
this.events.emit('scrollTrackingChanged', scrollTracking);
logger.debug('tracking', this.scrollTracking);
}
};
this.ensureScrollTracking = () => {
if (this.scrollTracking) {
this.scrollToBottom();
}
};
// Periodic message pruning
setInterval(() => this.pruneMessages(), MESSAGE_PRUNE_INTERVAL);
}
isReady() {
return this.loaded && this.rootNode && this.page;
}
mount(node) {
// Mount existing root node on top of the new node
if (this.rootNode) {
node.appendChild(this.rootNode);
}
// Initialize the root node
else {
this.rootNode = node;
}
// Find scrollable parent
this.scrollNode = findNearestScrollableParent(this.rootNode);
this.scrollNode.addEventListener('scroll', this.handleScroll);
setImmediate(() => {
this.scrollToBottom();
});
// Flush the queue
this.tryFlushQueue();
}
onStateLoaded() {
this.loaded = true;
this.tryFlushQueue();
}
tryFlushQueue() {
if (this.isReady() && this.queue.length > 0) {
this.processBatch(this.queue);
this.queue = [];
}
}
assignStyle(style = {}) {
for (let key of Object.keys(style)) {
this.rootNode.style.setProperty(key, style[key]);
}
}
setHighlight(text, color) {
if (!text || !color) {
this.highlightRegex = null;
this.highlightColor = null;
return;
}
const allowedRegex = /^[a-z0-9_\-\s]+$/ig;
const lines = String(text)
.split(',')
.map(str => str.trim())
.filter(str => (
// Must be longer than one character
str && str.length > 1
// Must be alphanumeric (with some punctuation)
&& allowedRegex.test(str)
));
// Nothing to match, reset highlighting
if (lines.length === 0) {
this.highlightRegex = null;
this.highlightColor = null;
return;
}
this.highlightRegex = new RegExp('(' + lines.join('|') + ')', 'gi');
this.highlightColor = color;
}
scrollToBottom() {
// scrollHeight is always bigger than scrollTop and is
// automatically clamped to the valid range.
this.scrollNode.scrollTop = this.scrollNode.scrollHeight;
}
changePage(page) {
if (!this.isReady()) {
this.page = page;
this.tryFlushQueue();
return;
}
this.page = page;
// Fast clear of the root node
this.rootNode.textContent = '';
this.visibleMessages = [];
// Re-add message nodes
const fragment = document.createDocumentFragment();
let node;
for (let message of this.messages) {
if (canPageAcceptType(page, message.type)) {
node = message.node;
fragment.appendChild(node);
this.visibleMessages.push(message);
}
}
if (node) {
this.rootNode.appendChild(fragment);
node.scrollIntoView();
}
}
getCombinableMessage(predicate) {
const now = Date.now();
const len = this.visibleMessages.length;
const from = len - 1;
const to = Math.max(0, len - COMBINE_MAX_MESSAGES);
for (let i = from; i >= to; i--) {
const message = this.visibleMessages[i];
const matches = (
// Is not an internal message
!message.type.startsWith(MESSAGE_TYPE_INTERNAL)
// Text payload must fully match
&& isSameMessage(message, predicate)
// Must land within the specified time window
&& now < message.createdAt + COMBINE_MAX_TIME_WINDOW
);
if (matches) {
return message;
}
}
return null;
}
processBatch(batch, options = {}) {
const {
prepend,
notifyListeners = true,
} = options;
const now = Date.now();
// Queue up messages until chat is ready
if (!this.isReady()) {
if (prepend) {
this.queue = [...batch, ...this.queue];
}
else {
this.queue = [...this.queue, ...batch];
}
return;
}
// Insert messages
const fragment = document.createDocumentFragment();
const countByType = {};
let node;
for (let payload of batch) {
const message = createMessage(payload);
// Combine messages
const combinable = this.getCombinableMessage(message);
if (combinable) {
combinable.times = (combinable.times || 1) + 1;
updateMessageBadge(combinable);
continue;
}
// Reuse message node
if (message.node) {
node = message.node;
}
// Reconnected
else if (message.type === 'internal/reconnected') {
node = createReconnectedNode();
}
// Create message node
else {
node = createMessageNode();
// Payload is plain text
if (message.text) {
node.textContent = message.text;
}
// Payload is HTML
else if (message.html) {
node.innerHTML = message.html;
}
else {
logger.error('Error: message is missing text payload', message);
}
// Highlight text
if (!message.avoidHighlighting && this.highlightRegex) {
const highlighted = highlightNode(node,
this.highlightRegex,
text => (
createHighlightNode(text, this.highlightColor)
));
if (highlighted) {
node.className += ' ChatMessage--highlighted';
}
}
// Linkify text
const linkifyNodes = node.querySelectorAll('.linkify');
for (let i = 0; i < linkifyNodes.length; ++i) {
linkifyNode(linkifyNodes[i]);
}
// Assign an image error handler
if (now < message.createdAt + IMAGE_RETRY_MESSAGE_AGE) {
const imgNodes = node.querySelectorAll('img');
for (let i = 0; i < imgNodes.length; i++) {
const imgNode = imgNodes[i];
imgNode.addEventListener('error', handleImageError);
}
}
}
// Store the node in the message
message.node = node;
// Query all possible selectors to find out the message type
if (!message.type) {
// IE8: Does not support querySelector on elements that
// are not yet in the document.
const typeDef = !Byond.IS_LTE_IE8 && MESSAGE_TYPES
.find(typeDef => (
typeDef.selector && node.querySelector(typeDef.selector)
));
message.type = typeDef?.type || MESSAGE_TYPE_UNKNOWN;
}
updateMessageBadge(message);
if (!countByType[message.type]) {
countByType[message.type] = 0;
}
countByType[message.type] += 1;
// TODO: Detect duplicates
this.messages.push(message);
if (canPageAcceptType(this.page, message.type)) {
fragment.appendChild(node);
this.visibleMessages.push(message);
}
}
if (node) {
const firstChild = this.rootNode.childNodes[0];
if (prepend && firstChild) {
this.rootNode.insertBefore(fragment, firstChild);
}
else {
this.rootNode.appendChild(fragment);
}
if (this.scrollTracking) {
setImmediate(() => this.scrollToBottom());
}
}
// Notify listeners that we have processed the batch
if (notifyListeners) {
this.events.emit('batchProcessed', countByType);
}
}
pruneMessages() {
if (!this.isReady()) {
return;
}
// Delay pruning because user is currently interacting
// with chat history
if (!this.scrollTracking) {
logger.debug('pruning delayed');
return;
}
// Visible messages
{
const messages = this.visibleMessages;
const fromIndex = Math.max(0,
messages.length - MAX_VISIBLE_MESSAGES);
if (fromIndex > 0) {
this.visibleMessages = messages.slice(fromIndex);
for (let i = 0; i < fromIndex; i++) {
const message = messages[i];
this.rootNode.removeChild(message.node);
// Mark this message as pruned
message.node = 'pruned';
}
// Remove pruned messages from the message array
this.messages = this.messages.filter(message => (
message.node !== 'pruned'
));
logger.log(`pruned ${fromIndex} visible messages`);
}
}
// All messages
{
const fromIndex = Math.max(0,
this.messages.length - MAX_PERSISTED_MESSAGES);
if (fromIndex > 0) {
this.messages = this.messages.slice(fromIndex);
logger.log(`pruned ${fromIndex} stored messages`);
}
}
}
rebuildChat() {
if (!this.isReady()) {
return;
}
// Make a copy of messages
const fromIndex = Math.max(0,
this.messages.length - MAX_PERSISTED_MESSAGES);
const messages = this.messages.slice(fromIndex);
// Remove existing nodes
for (let message of messages) {
message.node = undefined;
}
// Fast clear of the root node
this.rootNode.textContent = '';
this.messages = [];
this.visibleMessages = [];
// Repopulate the chat log
this.processBatch(messages, {
notifyListeners: false,
});
}
saveToDisk() {
// Allow only on IE11
if (Byond.IS_LTE_IE10) {
return;
}
// Compile currently loaded stylesheets as CSS text
let cssText = '';
const styleSheets = document.styleSheets;
for (let i = 0; i < styleSheets.length; i++) {
const cssRules = styleSheets[i].cssRules;
for (let i = 0; i < cssRules.length; i++) {
const rule = cssRules[i];
cssText += rule.cssText + '\n';
}
}
cssText += 'body, html { background-color: #141414 }\n';
// Compile chat log as HTML text
let messagesHtml = '';
for (let message of this.visibleMessages) {
if (message.node) {
messagesHtml += message.node.outerHTML + '\n';
}
}
// Create a page
const pageHtml = '<!doctype html>\n'
+ '<html>\n'
+ '<head>\n'
+ '<title>SS13 Chat Log</title>\n'
+ '<style>\n' + cssText + '</style>\n'
+ '</head>\n'
+ '<body>\n'
+ '<div class="Chat">\n'
+ messagesHtml
+ '</div>\n'
+ '</body>\n'
+ '</html>\n';
// Create and send a nice blob
const blob = new Blob([pageHtml]);
const timestamp = new Date()
.toISOString()
.substring(0, 19)
.replace(/[-:]/g, '')
.replace('T', '-');
window.navigator.msSaveBlob(blob, `ss13-chatlog-${timestamp}.html`);
}
}
// Make chat renderer global so that we can continue using the same
// instance after hot code replacement.
if (!window.__chatRenderer__) {
window.__chatRenderer__ = new ChatRenderer();
}
/** @type {ChatRenderer} */
export const chatRenderer = window.__chatRenderer__;
@@ -1,128 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
/**
* Replaces text matching a regular expression with a custom node.
*/
export const replaceInTextNode = (regex, createNode) => node => {
const text = node.textContent;
const textLength = text.length;
let match;
let lastIndex = 0;
let fragment;
let n = 0;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(text)) {
n += 1;
// Lazy init fragment
if (!fragment) {
fragment = document.createDocumentFragment();
}
const matchText = match[0];
const matchLength = matchText.length;
const matchIndex = match.index;
// Insert previous unmatched chunk
if (lastIndex < matchIndex) {
fragment.appendChild(document.createTextNode(
text.substring(lastIndex, matchIndex)));
}
lastIndex = matchIndex + matchLength;
// Create a wrapper node
fragment.appendChild(createNode(matchText));
}
if (fragment) {
// Insert the remaining unmatched chunk
if (lastIndex < textLength) {
fragment.appendChild(document.createTextNode(
text.substring(lastIndex, textLength)));
}
// Commit the fragment
node.parentNode.replaceChild(fragment, node);
}
return n;
};
// Highlight
// --------------------------------------------------------
/**
* Default highlight node.
*/
const createHighlightNode = text => {
const node = document.createElement('span');
node.setAttribute('style',
'background-color:#fd4;color:#000');
node.textContent = text;
return node;
};
/**
* Highlights the text in the node based on the provided regular expression.
*
* @param {Node} node Node which you want to process
* @param {RegExp} regex Regular expression to highlight
* @param {(text: string) => Node} createNode Highlight node creator
* @returns {number} Number of matches
*/
export const highlightNode = (
node,
regex,
createNode = createHighlightNode,
) => {
if (!createNode) {
createNode = createHighlightNode;
}
let n = 0;
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i];
// Is a text node
if (node.nodeType === 3) {
n += replaceInTextNode(regex, createNode)(node);
}
else {
n += highlightNode(node, regex, createNode);
}
}
return n;
};
// Linkify
// --------------------------------------------------------
const URL_REGEX = /(?:(?:https?:\/\/)|(?:www\.))(?:[^ ]*?\.[^ ]*?)+[-A-Za-z0-9+&@#/%?=~_|$!:,.;()]+/ig;
/**
* Highlights the text in the node based on the provided regular expression.
*
* @param {Node} node Node which you want to process
* @returns {number} Number of matches
*/
export const linkifyNode = node => {
let n = 0;
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i];
const tag = String(node.nodeName).toLowerCase();
// Is a text node
if (node.nodeType === 3) {
n += linkifyTextNode(node);
}
else if (tag !== 'a') {
n += linkifyNode(node);
}
}
return n;
};
const linkifyTextNode = replaceInTextNode(URL_REGEX, text => {
const node = document.createElement('a');
node.href = text;
node.textContent = text;
return node;
});
@@ -1,21 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { map } from 'common/collections';
export const selectChat = state => state.chat;
export const selectChatPages = state => (
map(id => state.chat.pageById[id])(state.chat.pages)
);
export const selectCurrentChatPage = state => (
state.chat.pageById[state.chat.currentPageId]
);
export const selectChatPageById = id => state => (
state.chat.pageById[id]
);
-11
View File
@@ -1,11 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createAction } from 'common/redux';
export const roundRestarted = createAction('roundrestart');
export const connectionLost = createAction('game/connectionLost');
export const connectionRestored = createAction('game/connectionRestored');
@@ -1,7 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const CONNECTION_LOST_AFTER = 15000;
-12
View File
@@ -1,12 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useSelector } from 'common/redux';
import { selectGame } from './selectors';
export const useGame = context => {
return useSelector(context, selectGame);
};
-9
View File
@@ -1,9 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { useGame } from './hooks';
export { gameMiddleware } from './middleware';
export { gameReducer } from './reducer';
@@ -1,48 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { pingSuccess } from '../ping/actions';
import { connectionLost, connectionRestored, roundRestarted } from './actions';
import { selectGame } from './selectors';
import { CONNECTION_LOST_AFTER } from './constants';
const withTimestamp = action => ({
...action,
meta: {
...action.meta,
now: Date.now(),
},
});
export const gameMiddleware = store => {
let lastPingedAt;
setInterval(() => {
const state = store.getState();
if (!state) {
return;
}
const game = selectGame(state);
const pingsAreFailing = lastPingedAt
&& Date.now() >= lastPingedAt + CONNECTION_LOST_AFTER;
if (!game.connectionLostAt && pingsAreFailing) {
store.dispatch(withTimestamp(connectionLost()));
}
if (game.connectionLostAt && !pingsAreFailing) {
store.dispatch(withTimestamp(connectionRestored()));
}
}, 1000);
return next => action => {
const { type, payload, meta } = action;
if (type === pingSuccess.type) {
lastPingedAt = meta.now;
return next(action);
}
if (type === roundRestarted.type) {
return next(withTimestamp(action));
}
return next(action);
};
};
-39
View File
@@ -1,39 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { connectionLost } from './actions';
import { connectionRestored } from './actions';
const initialState = {
// TODO: This is where round info should be.
roundId: null,
roundTime: null,
roundRestartedAt: null,
connectionLostAt: null,
};
export const gameReducer = (state = initialState, action) => {
const { type, payload, meta } = action;
if (type === 'roundrestart') {
return {
...state,
roundRestartedAt: meta.now,
};
}
if (type === connectionLost.type) {
return {
...state,
connectionLostAt: meta.now,
};
}
if (type === connectionRestored.type) {
return {
...state,
connectionLostAt: null,
};
}
return state;
};
@@ -1,7 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const selectGame = state => state.game;
-113
View File
@@ -1,113 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
// Themes
import './styles/main.scss';
import './styles/themes/light.scss';
import { perf } from 'common/perf';
import { combineReducers } from 'common/redux';
import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
import { setupGlobalEvents } from 'tgui/events';
import { captureExternalLinks } from 'tgui/links';
import { createRenderer } from 'tgui/renderer';
import { configureStore, StoreProvider } from 'tgui/store';
import { audioMiddleware, audioReducer } from './audio';
import { chatMiddleware, chatReducer } from './chat';
import { gameMiddleware, gameReducer } from './game';
import { setupPanelFocusHacks } from './panelFocus';
import { pingMiddleware, pingReducer } from './ping';
import { settingsMiddleware, settingsReducer } from './settings';
import { telemetryMiddleware } from './telemetry';
perf.mark('inception', window.performance?.timing?.navigationStart);
perf.mark('init');
const store = configureStore({
reducer: combineReducers({
audio: audioReducer,
chat: chatReducer,
game: gameReducer,
ping: pingReducer,
settings: settingsReducer,
}),
middleware: {
pre: [
chatMiddleware,
pingMiddleware,
telemetryMiddleware,
settingsMiddleware,
audioMiddleware,
gameMiddleware,
],
},
});
const renderApp = createRenderer(() => {
const { Panel } = require('./Panel');
return (
<StoreProvider store={store}>
<Panel />
</StoreProvider>
);
});
const setupApp = () => {
// Delay setup
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupApp);
return;
}
setupGlobalEvents({
ignoreWindowFocus: true,
});
setupPanelFocusHacks();
captureExternalLinks();
// Re-render UI on store updates
store.subscribe(renderApp);
// Dispatch incoming messages as store actions
Byond.subscribe((type, payload) => store.dispatch({ type, payload }));
// Unhide the panel
Byond.winset('output', {
'is-visible': false,
});
Byond.winset('browseroutput', {
'is-visible': true,
'is-disabled': false,
'pos': '0x0',
'size': '0x0',
});
// Resize the panel to match the non-browser output
Byond.winget('output').then(output => {
Byond.winset('browseroutput', {
'size': output.size,
});
});
// Enable hot module reloading
if (module.hot) {
setupHotReloading();
module.hot.accept([
'./audio',
'./chat',
'./game',
'./Notifications',
'./Panel',
'./ping',
'./settings',
'./telemetry',
], () => {
renderApp();
});
}
};
setupApp();
-13
View File
@@ -1,13 +0,0 @@
{
"private": true,
"name": "tgui-panel",
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.3.8",
"inferno": "^7.4.11",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}
}
-47
View File
@@ -1,47 +0,0 @@
/**
* Basically, hacks from goonchat which try to keep the map focused at all
* times, except for when some meaningful action happens o
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { vecLength, vecSubtract } from 'common/vector';
import { canStealFocus, globalEvents } from 'tgui/events';
import { focusMap } from 'tgui/focus';
// Empyrically determined number for the smallest possible
// text you can select with the mouse.
const MIN_SELECTION_DISTANCE = 10;
const deferredFocusMap = () => setImmediate(() => focusMap());
export const setupPanelFocusHacks = () => {
let focusStolen = false;
let clickStartPos = null;
window.addEventListener('focusin', e => {
focusStolen = canStealFocus(e.target);
});
window.addEventListener('mousedown', e => {
clickStartPos = [e.screenX, e.screenY];
});
window.addEventListener('mouseup', e => {
if (clickStartPos) {
const clickEndPos = [e.screenX, e.screenY];
const dist = vecLength(vecSubtract(clickEndPos, clickStartPos));
if (dist >= MIN_SELECTION_DISTANCE) {
focusStolen = true;
}
}
if (!focusStolen) {
deferredFocusMap();
}
});
globalEvents.on('keydown', key => {
if (key.isModifierKey()) {
return;
}
deferredFocusMap();
});
};
@@ -1,31 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Color } from 'common/color';
import { toFixed } from 'common/math';
import { useSelector } from 'common/redux';
import { Box } from 'tgui/components';
import { selectPing } from './selectors';
export const PingIndicator = (props, context) => {
const ping = useSelector(context, selectPing);
const color = Color.lookup(ping.networkQuality, [
new Color(220, 40, 40),
new Color(220, 200, 40),
new Color(60, 220, 40),
]);
const roundtrip = ping.roundtrip
? toFixed(ping.roundtrip)
: '--';
return (
<div className="Ping">
<Box
className="Ping__indicator"
backgroundColor={color} />
{roundtrip}
</div>
);
};
-25
View File
@@ -1,25 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createAction } from 'common/redux';
export const pingSuccess = createAction(
'ping/success',
ping => {
const now = Date.now();
const roundtrip = (now - ping.sentAt) * 0.5;
return {
payload: {
lastId: ping.id,
roundtrip,
},
meta: { now },
};
}
);
export const pingFail = createAction('ping/fail');
export const pingReply = createAction('ping/reply');
@@ -1,12 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const PING_INTERVAL = 2500;
export const PING_TIMEOUT = 2000;
export const PING_MAX_FAILS = 3;
export const PING_QUEUE_SIZE = 8;
export const PING_ROUNDTRIP_BEST = 50;
export const PING_ROUNDTRIP_WORST = 200;
-9
View File
@@ -1,9 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { pingMiddleware } from './middleware';
export { PingIndicator } from './PingIndicator';
export { pingReducer } from './reducer';
@@ -1,53 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { pingFail, pingSuccess } from './actions';
import { PING_INTERVAL, PING_QUEUE_SIZE, PING_TIMEOUT } from './constants';
export const pingMiddleware = store => {
let initialized = false;
let index = 0;
let interval;
const pings = [];
const sendPing = () => {
for (let i = 0; i < PING_QUEUE_SIZE; i++) {
const ping = pings[i];
if (ping && Date.now() - ping.sentAt > PING_TIMEOUT) {
pings[i] = null;
store.dispatch(pingFail());
}
}
const ping = { index, sentAt: Date.now() };
pings[index] = ping;
Byond.sendMessage('ping', { index });
index = (index + 1) % PING_QUEUE_SIZE;
};
return next => action => {
const { type, payload } = action;
if (!initialized) {
initialized = true;
interval = setInterval(sendPing, PING_INTERVAL);
sendPing();
}
if (type === 'roundrestart') {
// Stop pinging because dreamseeker is currently reconnecting.
// Topic calls in the middle of reconnect will crash the connection.
clearInterval(interval);
return next(action);
}
if (type === 'pingReply') {
const { index } = payload;
const ping = pings[index];
// Received a timed out ping
if (!ping) {
return;
}
pings[index] = null;
return next(pingSuccess(ping));
}
return next(action);
};
};
-42
View File
@@ -1,42 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { clamp01, scale } from 'common/math';
import { pingFail, pingSuccess } from './actions';
import { PING_MAX_FAILS, PING_ROUNDTRIP_BEST, PING_ROUNDTRIP_WORST } from './constants';
export const pingReducer = (state = {}, action) => {
const { type, payload } = action;
if (type === pingSuccess.type) {
const { roundtrip } = payload;
const prevRoundtrip = state.roundtripAvg || roundtrip;
const roundtripAvg = Math.round(prevRoundtrip * 0.4 + roundtrip * 0.6);
const networkQuality = 1 - scale(roundtripAvg,
PING_ROUNDTRIP_BEST, PING_ROUNDTRIP_WORST);
return {
roundtrip,
roundtripAvg,
failCount: 0,
networkQuality,
};
}
if (type === pingFail.type) {
const { failCount = 0 } = state;
const networkQuality = clamp01(state.networkQuality
- failCount / PING_MAX_FAILS);
const nextState = {
...state,
failCount: failCount + 1,
networkQuality,
};
if (failCount > PING_MAX_FAILS) {
nextState.roundtrip = undefined;
nextState.roundtripAvg = undefined;
}
return nextState;
}
return state;
};
@@ -1,7 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const selectPing = state => state.ping;
@@ -1,177 +0,0 @@
/**
* @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 } from '../chat/actions';
import { THEMES } from '../themes';
import { changeSettingsTab, updateSettings } from './actions';
import { FONTS, SETTINGS_TABS } from './constants';
import { selectActiveTab, selectSettings } from './selectors';
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 />
)}
</Stack.Item>
</Stack>
);
};
export const SettingsGeneral = (props, context) => {
const {
theme,
fontFamily,
fontSize,
lineHeight,
highlightText,
highlightColor,
} = useSelector(context, selectSettings);
const dispatch = useDispatch(context);
const [freeFont, setFreeFont] = useLocalState(context, "freeFont", false);
return (
<Section>
<LabeledList>
<LabeledList.Item label="Theme">
<Dropdown
selected={theme}
options={THEMES}
onSelected={value => dispatch(updateSettings({
theme: value,
}))} />
</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>
<Divider />
<Box>
<Flex mb={1} color="label" align="baseline">
<Flex.Item grow={1}>
Highlight words (comma separated):
</Flex.Item>
<Flex.Item shrink={0}>
<ColorBox mr={1} color={highlightColor} />
<Input
width="5em"
monospace
placeholder="#ffffff"
value={highlightColor}
onInput={(e, value) => dispatch(updateSettings({
highlightColor: value,
}))} />
</Flex.Item>
</Flex>
<TextArea
height="3em"
value={highlightText}
onChange={(e, value) => dispatch(updateSettings({
highlightText: value,
}))} />
</Box>
<Divider />
<Box>
<Button
icon="check"
onClick={() => dispatch(rebuildChat())}>
Apply now
</Button>
<Box inline fontSize="0.9em" ml={1} color="label">
Can freeze the chat for a while.
</Box>
</Box>
<Divider />
<Button
icon="save"
onClick={() => dispatch(saveChatToDisk())}>
Save chat log
</Button>
</Section>
);
};
@@ -1,13 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createAction } from 'common/redux';
export const updateSettings = createAction('settings/update');
export const loadSettings = createAction('settings/load');
export const changeSettingsTab = createAction('settings/changeTab');
export const toggleSettings = createAction('settings/toggle');
export const openChatSettings = createAction('settings/openChatTab');
@@ -1,33 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const SETTINGS_TABS = [
{
id: 'general',
name: 'General',
},
{
id: 'chatPage',
name: 'Chat Tabs',
},
];
export const FONTS_DISABLED = "Default";
export const FONTS = [
FONTS_DISABLED,
'Verdana',
'Arial',
'Arial Black',
'Comic Sans MS',
'Impact',
'Lucida Sans Unicode',
'Tahoma',
'Trebuchet MS',
'Courier New',
'Lucida Console',
];
@@ -1,20 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useDispatch, useSelector } from 'common/redux';
import { updateSettings, toggleSettings } from './actions';
import { selectSettings } from './selectors';
export const useSettings = context => {
const settings = useSelector(context, selectSettings);
const dispatch = useDispatch(context);
return {
...settings,
visible: settings.view.visible,
toggle: () => dispatch(toggleSettings()),
update: obj => dispatch(updateSettings(obj)),
};
};
@@ -1,10 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { useSettings } from './hooks';
export { settingsMiddleware } from './middleware';
export { settingsReducer } from './reducer';
export { SettingsPanel } from './SettingsPanel';
@@ -1,57 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { storage } from 'common/storage';
import { setClientTheme } from '../themes';
import { loadSettings, updateSettings } from './actions';
import { selectSettings } from './selectors';
import { FONTS_DISABLED } from './constants';
const setGlobalFontSize = fontSize => {
document.documentElement.style
.setProperty('font-size', fontSize + 'px');
document.body.style
.setProperty('font-size', fontSize + 'px');
};
const setGlobalFontFamily = fontFamily => {
if (fontFamily === FONTS_DISABLED) fontFamily = null;
document.documentElement.style
.setProperty('font-family', fontFamily);
document.body.style
.setProperty('font-family', fontFamily);
};
export const settingsMiddleware = store => {
let initialized = false;
return next => action => {
const { type, payload } = action;
if (!initialized) {
initialized = true;
storage.get('panel-settings').then(settings => {
store.dispatch(loadSettings(settings));
});
}
if (type === updateSettings.type || type === loadSettings.type) {
// Set client theme
const theme = payload?.theme;
if (theme) {
setClientTheme(theme);
}
// Pass action to get an updated state
next(action);
const settings = selectSettings(store.getState());
// Update global UI font size
setGlobalFontSize(settings.fontSize);
setGlobalFontFamily(settings.fontFamily);
// Save settings to the web storage
storage.set('panel-settings', settings);
return;
}
return next(action);
};
};
@@ -1,74 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { changeSettingsTab, loadSettings, openChatSettings, toggleSettings, updateSettings } from './actions';
import { FONTS, SETTINGS_TABS } from './constants';
const initialState = {
version: 1,
fontSize: 13,
fontFamily: FONTS[0],
lineHeight: 1.2,
theme: 'light',
adminMusicVolume: 0.5,
highlightText: '',
highlightColor: '#ffdd44',
view: {
visible: false,
activeTab: SETTINGS_TABS[0].id,
},
};
export const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === updateSettings.type) {
return {
...state,
...payload,
};
}
if (type === loadSettings.type) {
// Validate version and/or migrate state
if (!payload?.version) {
return state;
}
delete payload.view;
return {
...state,
...payload,
};
}
if (type === toggleSettings.type) {
return {
...state,
view: {
...state.view,
visible: !state.view.visible,
},
};
}
if (type === openChatSettings.type) {
return {
...state,
view: {
...state.view,
visible: true,
activeTab: 'chatPage',
},
};
}
if (type === changeSettingsTab.type) {
const { tabId } = payload;
return {
...state,
view: {
...state.view,
activeTab: tabId,
},
};
}
return state;
};
@@ -1,8 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const selectSettings = state => state.settings;
export const selectActiveTab = state => state.settings.view.activeTab;
@@ -1,100 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use 'sass:color';
@use "sass:math";
@use '~tgui/styles/base.scss';
@use '~tgui/styles/colors.scss';
$text-color: #abc6ec !default;
$color-bg-section: base.$color-bg-section !default;
.Chat {
color: $text-color;
}
.Chat__badge {
display: inline-block;
min-width: 0.5em;
font-size: 0.7em;
padding: 0.2em 0.3em;
line-height: 1;
color: white;
text-align: center;
white-space: nowrap;
vertical-align: middle;
background-color: crimson;
border-radius: 10px;
transition: font-size 200ms ease-out;
&:before {
content: 'x';
}
}
.Chat__badge--animate {
font-size: 0.9em;
transition: font-size 0ms;
}
.Chat__scrollButton {
position: fixed;
right: 2em;
bottom: 1em;
}
.Chat__reconnected {
font-size: 0.85em;
text-align: center;
margin: 1em 0 2em;
&:before {
content: 'Reconnected';
display: inline-block;
border-radius: 1em;
padding: 0 0.7em;
color: colors.$red;
background-color: $color-bg-section;
}
&:after {
content: '';
display: block;
margin-top: -0.75em;
border-bottom: (math.div(1em, 6)) solid colors.$red;
}
}
.Chat__highlight {
color: #000;
}
.Chat__highlight--restricted {
color: #fff;
background-color: #a00;
font-weight: bold;
}
.ChatMessage {
word-wrap: break-word;
}
.ChatMessage--highlighted {
position: relative;
border-left: (math.div(1em, 6)) solid rgba(255, 221, 68);
padding-left: 0.5em;
&:after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 221, 68, 0.1);
// Make this click-through since this is an overlay
pointer-events: none;
}
}
@@ -1,26 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
.Notifications {
position: absolute;
bottom: 1em;
left: 1em;
right: 2em;
}
.Notification {
color: #fff;
background-color: crimson;
padding: 0.5em;
margin: 1em 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
@@ -1,28 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use "sass:math";
$border-color: rgba(140, 140, 140, 0.5) !default;
.Ping {
position: relative;
padding: 0.125em 0.25em;
border: (math.div(1em, 12)) solid $border-color;
border-radius: 0.25em;
width: 3.75em;
text-align: right;
}
.Ping__indicator {
content: '';
position: absolute;
top: 0.5em;
left: 0.5em;
width: 0.5em;
height: 0.5em;
background-color: #888;
border-radius: 0.25em;
}
@@ -1,883 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
em {
font-style: normal;
font-weight: bold;
}
img {
margin: 0;
padding: 0;
line-height: 1;
-ms-interpolation-mode: nearest-neighbor;
image-rendering: pixelated;
}
img.icon {
height: 1em;
min-height: 16px;
width: auto;
vertical-align: bottom;
}
a {
color: #397ea5;
}
a.visited {
color: #7c00e6;
}
a:visited {
color: #7c00e6;
}
a.popt {
text-decoration: none;
}
/* POPUPS */
.popup {
position: fixed;
top: 50%;
left: 50%;
background: #ddd;
}
.popup .close {
position: absolute;
background: #aaa;
top: 0;
right: 0;
color: #333;
text-decoration: none;
z-index: 2;
padding: 0 10px;
height: 30px;
line-height: 30px;
}
.popup .close:hover {
background: #999;
}
.popup .head {
background: #999;
color: #ddd;
padding: 0 10px;
height: 30px;
line-height: 30px;
text-transform: uppercase;
font-size: 0.9em;
font-weight: bold;
border-bottom: 2px solid green;
}
.popup input {
border: 1px solid #999;
background: #fff;
margin: 0;
padding: 5px;
outline: none;
color: #333;
}
.popup input[type=text]:hover,
.popup input[type=text]:active,
.popup input[type=text]:focus {
border-color: green;
}
.popup input[type=submit] {
padding: 5px 10px;
background: #999;
color: #ddd;
text-transform: uppercase;
font-size: 0.9em;
font-weight: bold;
}
.popup input[type=submit]:hover,
.popup input[type=submit]:focus,
.popup input[type=submit]:active {
background: #aaa;
cursor: pointer;
}
.changeFont {
padding: 10px;
}
.changeFont a {
display: block;
text-decoration: none;
padding: 3px;
color: #333;
}
.changeFont a:hover {
background: #ccc;
}
.highlightPopup {
padding: 10px;
text-align: center;
}
.highlightPopup input[type=text] {
display: block;
width: 215px;
text-align: left;
margin-top: 5px;
}
.highlightPopup input.highlightColor {
background-color: #FFFF00;
}
.highlightPopup input.highlightTermSubmit {
margin-top: 5px;
}
/* ADMIN CONTEXT MENU */
.contextMenu {
background-color: #ddd;
position: fixed;
margin: 2px;
width: 150px;
}
.contextMenu a {
display: block;
padding: 2px 5px;
text-decoration: none;
color: #333;
}
.contextMenu a:hover {
background-color: #ccc;
}
/* ADMIN FILTER MESSAGES MENU */
.filterMessages {
padding: 5px;
}
.filterMessages div {
padding: 2px 0;
}
.filterMessages input {
}
.filterMessages label {
}
.icon-stack {
height: 1em;
line-height: 1em;
width: 1em;
vertical-align: middle;
margin-top: -2px;
}
/*****************************************
*
* OUTPUT ACTUALLY RELATED TO MESSAGES
*
******************************************/
/* MOTD */
.motd {
color: #a4bad6;
font-family: Verdana, sans-serif;
white-space: normal;
}
.motd h1,
.motd h2,
.motd h3,
.motd h4,
.motd h5,
.motd h6 {
color: #a4bad6;
text-decoration: underline;
}
.motd a,
.motd a:link,
.motd a:visited,
.motd a:active,
.motd a:hover {
color: #a4bad6;
}
/* ADD HERE FOR BOLD */
.bold,
.name,
.prefix,
.ooc,
.looc,
.adminooc,
.admin,
.medal,
.yell {
font-weight: bold;
}
/* ADD HERE FOR ITALIC */
.italic, .italics {
font-style: italic;
}
/* OUTPUT COLORS */
.highlight {
background: yellow;
}
h1, h2, h3, h4, h5, h6 {
color: #a4bad6;
font-family: Georgia, Verdana, sans-serif;
}
h1.alert, h2.alert {
color: #a4bad6;
}
em {
font-style: normal;
font-weight: bold;
}
.ooc {
color: #cca300;
font-weight: bold;
}
.adminobserverooc {
color: #0099cc;
font-weight: bold;
}
.adminooc {
color: #3d5bc3;
font-weight: bold;
}
.adminsay {
color: #ff4500;
font-weight: bold;
}
.admin {
color: #5975da;
font-weight: bold;
}
.name {
font-weight: bold;
}
.say, .emote, .infoplain, .oocplain, .warningplain {
}
.deadsay {
color: #e2c1ff;
}
.binarysay {
color: #1e90ff;
}
.binarysay a {
color: #00ff00;
}
.binarysay a:active, .binarysay a:visited {
color: #88ff88;
}
/* RADIO COLORS */
/* IF YOU CHANGE THIS KEEP IT IN SYNC WITH TGUI CONSTANTS */
.radio {
color: #1ecc43;
}
.sciradio {
color: #c68cfa;
}
.comradio {
color: #fcdf03;
}
.secradio {
color: #dd3535;
}
.medradio {
color: #57b8f0;
}
.engradio {
color: #f37746;
}
.suppradio {
color: #b88646;
}
.servradio {
color: #6ca729;
}
.syndradio {
color: #8f4a4b;
}
.centcomradio {
color: #2681a5;
}
.aiprivradio {
color: #d65d95;
}
.redteamradio {
color: #ff4444;
}
.blueteamradio {
color: #3434fd;
}
.greenteamradio {
color: #34fd34;
}
.yellowteamradio {
color: #fdfd34;
}
.yell {
font-weight: bold;
}
.alert {
color: #d82020;
}
.userdanger {
color: #c51e1e;
font-weight: bold;
font-size: 185%;
}
.danger {
color: #c51e1e;
}
.warning {
color: #c51e1e;
font-style: italic;
}
.alertwarning {
color: #FF0000;
font-weight: bold;
}
.boldwarning {
color: #c51e1e;
font-style: italic;
font-weight: bold;
}
.announce {
color: #c51e1e;
font-weight: bold;
}
.boldannounce {
color: #c51e1e;
font-weight: bold;
}
.minorannounce {
font-weight: bold;
font-size: 185%;
}
.greenannounce {
color: #059223;
font-weight: bold;
}
.rose {
color: #ff5050;
}
.info {
color: #9ab0ff;
}
.notice {
color: #6685f5;
}
.tinynotice {
color: #6685f5;
font-size: 85%;
}
.tinynoticeital {
color: #6685f5;
font-style: italic;
font-size: 85%;
}
.smallnotice {
color: #6685f5;
font-size: 90%;
}
.smallnoticeital {
color: #6685f5;
font-style: italic;
font-size: 90%;
}
.boldnotice {
color: #6685f5;
font-weight: bold;
}
.hear {
color: #6685f5;
font-style: italic;
}
.adminnotice {
color: #6685f5;
}
.adminhelp {
color: #ff0000;
font-weight: bold;
}
.unconscious {
color: #a4bad6;
font-weight: bold;
}
.suicide {
color: #ff5050;
font-style: italic;
}
.green {
color: #059223;
}
.red {
color: #FF0000;
}
.blue {
color: #215cff;
}
.nicegreen {
color: #059223;
}
.cult {
color: #973e3b;
}
.cultitalic {
color: #973e3b;
font-style: italic;
}
.cultbold {
color: #973e3b;
font-style: italic;
font-weight: bold;
}
.cultboldtalic {
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.cultlarge {
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.narsie {
color: #973e3b;
font-weight: bold;
font-size: 925%;
}
.narsiesmall {
color: #973e3b;
font-weight: bold;
font-size: 370%;
}
.colossus {
color: #7F282A;
font-size: 310%;
}
.hierophant {
color: #b441ee;
font-weight: bold;
font-style: italic;
}
.hierophant_warning {
color: #c56bf1;
font-style: italic;
}
.purple {
color: #9956d3;
}
.holoparasite {
color: #88809c;
}
.revennotice {
color: #c099e2;
}
.revenboldnotice {
color: #c099e2;
font-weight: bold;
}
.revenbignotice {
color: #c099e2;
font-weight: bold;
font-size: 185%;
}
.revenminor {
color: #823abb;
}
.revenwarning {
color: #760fbb;
font-style: italic;
}
.revendanger {
color: #760fbb;
font-weight: bold;
font-size: 185%;
}
.deconversion_message {
color: #a947ff;
font-size: 185%;
font-style: italic;
}
.ghostalert {
color: #6600ff;
font-style: italic;
font-weight: bold;
}
.alien {
color: #855d85;
}
.noticealien {
color: #059223;
}
.alertalien {
color: #059223;
font-weight: bold;
}
.changeling {
color: #059223;
font-style: italic;
}
.alertsyndie {
color: #FF0000;
font-size: 185%;
font-weight: bold;
}
.spider {
color: #8800ff;
font-weight: bold;
font-size: 185%;
}
.interface {
color: #750e75;
}
.sans {
font-family: "Comic Sans MS", cursive, sans-serif;
}
.papyrus {
font-family: "Papyrus", cursive, sans-serif;
}
.robot {
font-family: "Courier New", cursive, sans-serif;
}
.tape_recorder {
color: #FF0000;
font-family: "Courier New", cursive, sans-serif;
}
.command_headset {
font-weight: bold;
font-size: 160%;
}
.small {
font-size: 60%;
}
.big {
font-size: 185%;
}
.reallybig {
font-size: 245%;
}
.extremelybig {
font-size: 310%;
}
.greentext {
color: #059223;
font-size: 185%;
}
.redtext {
color: #c51e1e;
font-size: 185%;
}
.clown {
color: #ff70c1;
font-size: 160%;
font-family: "Comic Sans MS", cursive, sans-serif;
font-weight: bold;
}
.singing {
font-family: "Trebuchet MS", cursive, sans-serif;
font-style: italic;
}
.his_grace {
color: #15D512;
font-family: "Courier New", cursive, sans-serif;
font-style: italic;
}
.hypnophrase {
color: #202020;
font-weight: bold;
animation: hypnocolor 1500ms infinite;
animation-direction: alternate;
}
@keyframes hypnocolor {
0% {
color: #202020;
}
25% {
color: #4b02ac;
}
50% {
color: #9f41f1;
}
75% {
color: #541c9c;
}
100% {
color: #7adbf3;
}
}
.phobia {
color: #dd0000;
font-weight: bold;
animation: phobia 750ms infinite;
}
@keyframes phobia {
0% {
color: #f75a5a;
}
50% {
color: #dd0000;
}
100% {
color: #f75a5a;
}
}
.icon {
height: 1em;
width: auto;
}
.bigicon {
font-size: 2.5em;
}
.memo {
color: #638500;
text-align: center;
}
.memoedit {
text-align: center;
font-size: 125%;
}
.abductor {
color: #c204c2;
font-style: italic;
}
.mind_control {
color: #df3da9;
font-size: 100%;
font-weight: bold;
font-style: italic;
}
.slime {
color: #00CED1;
}
.drone {
color: #848482;
}
.monkey {
color: #975032;
}
.swarmer {
color: #2C75FF;
}
.resonate {
color: #298F85;
}
.monkeyhive {
color: #a56408;
}
.monkeylead {
color: #af6805;
font-size: 80%;
}
.connectionClosed, .fatalError {
background: red;
color: white;
padding: 5px;
}
.connectionClosed.restored {
background: green;
}
.internal.boldnshit {
color: #3d5bc3;
font-weight: bold;
}
/* HELPER CLASSES */
.text-normal {
font-weight: normal;
font-style: normal;
}
.hidden {
display: none;
visibility: hidden;
}
.ml-1 {
margin-left: 1em;
}
.ml-2 {
margin-left: 2em;
}
.ml-3 {
margin-left: 3em;
}
@@ -1,921 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
html, body {
padding: 0;
margin: 0;
height: 100%;
color: #000000;
}
body {
background: #fff;
font-family: Verdana, sans-serif;
font-size: 13px;
line-height: 1.2;
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
}
em {
font-style: normal;
font-weight: bold;
}
img {
margin: 0;
padding: 0;
line-height: 1;
-ms-interpolation-mode: nearest-neighbor;
image-rendering: pixelated;
}
img.icon {
height: 1em;
min-height: 16px;
width: auto;
vertical-align: bottom;
}
a {
color: #0000ff;
}
a.visited {
color: #ff00ff;
}
a:visited {
color: #ff00ff;
}
a.popt {
text-decoration: none;
}
/* POPUPS */
.popup {
position: fixed;
top: 50%;
left: 50%;
background: #ddd;
}
.popup .close {
position: absolute;
background: #aaa;
top: 0;
right: 0;
color: #333;
text-decoration: none;
z-index: 2;
padding: 0 10px;
height: 30px;
line-height: 30px;
}
.popup .close:hover {
background: #999;
}
.popup .head {
background: #999;
color: #ddd;
padding: 0 10px;
height: 30px;
line-height: 30px;
text-transform: uppercase;
font-size: 0.9em;
font-weight: bold;
border-bottom: 2px solid green;
}
.popup input {
border: 1px solid #999;
background: #fff;
margin: 0;
padding: 5px;
outline: none;
color: #333;
}
.popup input[type=text]:hover,
.popup input[type=text]:active,
.popup input[type=text]:focus {
border-color: green;
}
.popup input[type=submit] {
padding: 5px 10px;
background: #999;
color: #ddd;
text-transform: uppercase;
font-size: 0.9em;
font-weight: bold;
}
.popup input[type=submit]:hover,
.popup input[type=submit]:focus,
.popup input[type=submit]:active {
background: #aaa;
cursor: pointer;
}
.changeFont {
padding: 10px;
}
.changeFont a {
display: block;
text-decoration: none;
padding: 3px;
color: #333;
}
.changeFont a:hover {
background: #ccc;
}
.highlightPopup {
padding: 10px;
text-align: center;
}
.highlightPopup input[type=text] {
display: block;
width: 215px;
text-align: left;
margin-top: 5px;
}
.highlightPopup input.highlightColor {
background-color: #FFFF00;
}
.highlightPopup input.highlightTermSubmit {
margin-top: 5px;
}
/* ADMIN CONTEXT MENU */
.contextMenu {
background-color: #ddd;
position: fixed;
margin: 2px;
width: 150px;
}
.contextMenu a {
display: block;
padding: 2px 5px;
text-decoration: none;
color: #333;
}
.contextMenu a:hover {
background-color: #ccc;
}
/* ADMIN FILTER MESSAGES MENU */
.filterMessages {
padding: 5px;
}
.filterMessages div {
padding: 2px 0;
}
.filterMessages input {
}
.filterMessages label {
}
.icon-stack {
height: 1em;
line-height: 1em;
width: 1em;
vertical-align: middle;
margin-top: -2px;
}
/*****************************************
*
* OUTPUT ACTUALLY RELATED TO MESSAGES
*
******************************************/
/* MOTD */
.motd {
color: #638500;
font-family: Verdana, sans-serif;
white-space: normal;
}
.motd h1,
.motd h2,
.motd h3,
.motd h4,
.motd h5,
.motd h6 {
color: #638500;
text-decoration: underline;
}
.motd a,
.motd a:link,
.motd a:visited,
.motd a:active,
.motd a:hover {
color: #638500;
}
/* ADD HERE FOR BOLD */
.bold,
.name,
.prefix,
.ooc,
.looc,
.adminooc,
.admin,
.medal,
.yell {
font-weight: bold;
}
/* ADD HERE FOR ITALIC */
.italic,
.italics {
font-style: italic;
}
/* OUTPUT COLORS */
.highlight {
background: yellow;
}
h1, h2, h3, h4, h5, h6 {
color: #0000ff;
font-family: Georgia, Verdana, sans-serif;
}
h1.alert, h2.alert {
color: #000000;
}
em {
font-style: normal;
font-weight: bold;
}
.ooc {
color: #002eb8;
font-weight: bold;
}
.adminobserverooc {
color: #0099cc;
font-weight: bold;
}
.adminooc {
color: #700038;
font-weight: bold;
}
.adminsay {
color: #ff4500;
font-weight: bold;
}
.admin {
color: #4473ff;
font-weight: bold;
}
.name {
font-weight: bold;
}
.say, .emote, .infoplain, .oocplain, .warningplain {
}
.deadsay {
color: #5c00e6;
}
.binarysay {
color: #20c20e;
background-color: #000000;
display: block;
}
.binarysay a {
color: #00ff00;
}
.binarysay a:active,
.binarysay a:visited {
color: #88ff88;
}
.radio {
color: #008000;
}
.sciradio {
color: #993399;
}
.comradio {
color: #948f02;
}
.secradio {
color: #a30000;
}
.medradio {
color: #337296;
}
.engradio {
color: #fb5613;
}
.suppradio {
color: #a8732b;
}
.servradio {
color: #6eaa2c;
}
.syndradio {
color: #6d3f40;
}
.centcomradio {
color: #686868;
}
.aiprivradio {
color: #ff00ff;
}
.redteamradio {
color: #ff0000;
}
.blueteamradio {
color: #0000ff;
}
.greenteamradio {
color: #00ff00;
}
.yellowteamradio {
color: #d1ba22;
}
.yell {
font-weight: bold;
}
.alert {
color: #ff0000;
}
h1.alert, h2.alert {
color: #000000;
}
.userdanger {
color: #ff0000;
font-weight: bold;
font-size: 185%;
}
.bolddanger {
color: #ff0000;
font-weight: bold;
}
.danger {
color: #ff0000;
}
.tinydanger {
color: #ff0000;
font-size: 85%;
}
.smalldanger {
color: #ff0000;
font-size: 90%;
}
.warning {
color: #ff0000;
font-style: italic;
}
.alertwarning {
color: #FF0000;
font-weight: bold;
}
.boldwarning {
color: #ff0000;
font-style: italic;
font-weight: bold;
}
.announce {
color: #228b22;
font-weight: bold;
}
.boldannounce {
color: #ff0000;
font-weight: bold;
}
.minorannounce {
font-weight: bold;
font-size: 185%;
}
.greenannounce {
color: #00ff00;
font-weight: bold;
}
.rose {
color: #ff5050;
}
.info {
color: #0000CC;
}
.notice {
color: #000099;
}
.tinynotice {
color: #000099;
font-size: 85%;
}
.tinynoticeital {
color: #000099;
font-style: italic;
font-size: 85%;
}
.smallnotice {
color: #000099;
font-size: 90%;
}
.smallnoticeital {
color: #000099;
font-style: italic;
font-size: 90%;
}
.boldnotice {
color: #000099;
font-weight: bold;
}
.hear {
color: #000099;
font-style: italic;
}
.adminnotice {
color: #0000ff;
}
.adminhelp {
color: #ff0000;
font-weight: bold;
}
.unconscious {
color: #0000ff;
font-weight: bold;
}
.suicide {
color: #ff5050;
font-style: italic;
}
.green {
color: #03ff39;
}
.red {
color: #FF0000;
}
.blue {
color: #0000FF;
}
.nicegreen {
color: #14a833;
}
.cult {
color: #973e3b;
}
.cultitalic {
color: #973e3b;
font-style: italic;
}
.cultbold {
color: #973e3b;
font-style: italic;
font-weight: bold;
}
.cultboldtalic {
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.cultlarge {
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.narsie {
color: #973e3b;
font-weight: bold;
font-size: 925%;
}
.narsiesmall {
color: #973e3b;
font-weight: bold;
font-size: 370%;
}
.colossus {
color: #7F282A;
font-size: 310%;
}
.hierophant {
color: #660099;
font-weight: bold;
font-style: italic;
}
.hierophant_warning {
color: #660099;
font-style: italic;
}
.purple {
color: #5e2d79;
}
.holoparasite {
color: #35333a;
}
.revennotice {
color: #1d2953;
}
.revenboldnotice {
color: #1d2953;
font-weight: bold;
}
.revenbignotice {
color: #1d2953;
font-weight: bold;
font-size: 185%;
}
.revenminor {
color: #823abb;
}
.revenwarning {
color: #760fbb;
font-style: italic;
}
.revendanger {
color: #760fbb;
font-weight: bold;
font-size: 185%;
}
.deconversion_message {
color: #5000A0;
font-size: 185%;
font-style: italic;
}
.ghostalert {
color: #5c00e6;
font-style: italic;
font-weight: bold;
}
.alien {
color: #543354;
}
.noticealien {
color: #00c000;
}
.alertalien {
color: #00c000;
font-weight: bold;
}
.changeling {
color: #800080;
font-style: italic;
}
.alertsyndie {
color: #FF0000;
font-size: 185%;
font-weight: bold;
}
.spider {
color: #4d004d;
font-weight: bold;
font-size: 185%;
}
.interface {
color: #330033;
}
.sans {
font-family: "Comic Sans MS", cursive, sans-serif;
}
.papyrus {
font-family: "Papyrus", cursive, sans-serif;
}
.robot {
font-family: "Courier New", cursive, sans-serif;
}
.tape_recorder {
color: #800000;
font-family: "Courier New", cursive, sans-serif;
}
.command_headset {
font-weight: bold;
font-size: 160%;
}
.small {
font-size: 60%;
}
.big {
font-size: 185%;
}
.reallybig {
font-size: 245%;
}
.extremelybig {
font-size: 310%;
}
.greentext {
color: #00FF00;
font-size: 185%;
}
.redtext {
color: #FF0000;
font-size: 185%;
}
.clown {
color: #FF69Bf;
font-size: 160%;
font-family: "Comic Sans MS", cursive, sans-serif;
font-weight: bold;
}
.singing {
font-family: "Trebuchet MS", cursive, sans-serif;
font-style: italic;
}
.his_grace {
color: #15D512;
font-family: "Courier New", cursive, sans-serif;
font-style: italic;
}
.hypnophrase {
color: #0d0d0d;
font-weight: bold;
animation: hypnocolor 1500ms infinite;
animation-direction: alternate;
}
@keyframes hypnocolor {
0% {
color: #0d0d0d;
}
25% {
color: #410194;
}
50% {
color: #7f17d8;
}
75% {
color: #410194;
}
100% {
color: #3bb5d3;
}
}
.phobia {
color: #dd0000;
font-weight: bold;
animation: phobia 750ms infinite;
}
@keyframes phobia {
0% {
color: #0d0d0d;
}
50% {
color: #dd0000;
}
100% {
color: #0d0d0d;
}
}
.icon {
height: 1em;
width: auto;
}
.bigicon {
font-size: 2.5em;
}
.memo {
color: #638500;
text-align: center;
}
.memoedit {
text-align: center;
font-size: 125%;
}
.abductor {
color: #800080;
font-style: italic;
}
.mind_control {
color: #A00D6F;
font-size: 100%;
font-weight: bold;
font-style: italic;
}
.slime {
color: #00CED1;
}
.drone {
color: #848482;
}
.monkey {
color: #975032;
}
.swarmer {
color: #2C75FF;
}
.resonate {
color: #298F85;
}
.monkeyhive {
color: #774704;
}
.monkeylead {
color: #774704;
font-size: 80%;
}
.connectionClosed,
.fatalError {
background: red;
color: white;
padding: 5px;
}
.connectionClosed.restored {
background: green;
}
.internal.boldnshit {
color: blue;
font-weight: bold;
}
/* HELPER CLASSES */
.text-normal {
font-weight: normal;
font-style: normal;
}
.hidden {
display: none;
visibility: hidden;
}
.ml-1 {
margin-left: 1em;
}
.ml-2 {
margin-left: 2em;
}
.ml-3 {
margin-left: 3em;
}
-62
View File
@@ -1,62 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use 'sass:meta';
@use "sass:math";
@use 'sass:color';
@use '~tgui/styles/colors.scss';
@use '~tgui/styles/base.scss' with (
$color-bg: #202020,
$color-bg-section: color.adjust(#202020, $lightness: -5%),
$color-bg-grad-spread: 0%,
);
// Core styles
@include meta.load-css('~tgui/styles/reset.scss');
// Atomic classes
@include meta.load-css('~tgui/styles/atomic/candystripe.scss');
@include meta.load-css('~tgui/styles/atomic/color.scss');
@include meta.load-css('~tgui/styles/atomic/debug-layout.scss');
@include meta.load-css('~tgui/styles/atomic/outline.scss');
@include meta.load-css('~tgui/styles/atomic/text.scss');
// Components
@include meta.load-css('~tgui/styles/components/BlockQuote.scss');
@include meta.load-css('~tgui/styles/components/Button.scss');
@include meta.load-css('~tgui/styles/components/ColorBox.scss');
@include meta.load-css('~tgui/styles/components/Dimmer.scss');
@include meta.load-css('~tgui/styles/components/Divider.scss');
@include meta.load-css('~tgui/styles/components/Dropdown.scss');
@include meta.load-css('~tgui/styles/components/Flex.scss');
@include meta.load-css('~tgui/styles/components/Input.scss');
@include meta.load-css('~tgui/styles/components/Knob.scss');
@include meta.load-css('~tgui/styles/components/LabeledList.scss');
@include meta.load-css('~tgui/styles/components/Modal.scss');
@include meta.load-css('~tgui/styles/components/NanoMap.scss');
@include meta.load-css('~tgui/styles/components/NoticeBox.scss');
@include meta.load-css('~tgui/styles/components/NumberInput.scss');
@include meta.load-css('~tgui/styles/components/ProgressBar.scss');
@include meta.load-css('~tgui/styles/components/Section.scss');
@include meta.load-css('~tgui/styles/components/Slider.scss');
@include meta.load-css('~tgui/styles/components/Stack.scss');
@include meta.load-css('~tgui/styles/components/Table.scss');
@include meta.load-css('~tgui/styles/components/Tabs.scss');
@include meta.load-css('~tgui/styles/components/TextArea.scss');
@include meta.load-css('~tgui/styles/components/Tooltip.scss');
// Components specific to tgui-panel
@include meta.load-css('./components/Chat.scss');
@include meta.load-css('./components/Ping.scss');
@include meta.load-css('./components/Notifications.scss');
// Layouts
@include meta.load-css('~tgui/styles/layouts/Layout.scss');
// @include meta.load-css('~tgui/styles/layouts/TitleBar.scss');
@include meta.load-css('~tgui/styles/layouts/Window.scss');
// Goonchat styles
@include meta.load-css('./goon/chat-dark.scss');
@@ -1,76 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use 'sass:color';
@use 'sass:meta';
@use '~tgui/styles/colors.scss' with (
$primary: #ffffff,
$bg-lightness: -25%,
$fg-lightness: -10%,
$label: #3b3b3b,
// Makes button look actually grey due to weird maths.
$grey: #ffffff,
// Commenting out color maps will adjust all colors based on the lightness
// settings above, but will add extra 10KB to the theme.
// $fg-map-keys: (),
// $bg-map-keys: (),
);
@use '~tgui/styles/base.scss' with (
$color-fg: #000000,
$color-bg: #eeeeee,
$color-bg-section: #ffffff,
$color-bg-grad-spread: 0%,
);
// A fat warning to anyone who wants to use this: this only half works.
// It was made almost purely for the nuke ui, and requires a good amount of manual hacks to get it working as intended.
.theme-light {
// Atomic classes
@include meta.load-css('~tgui/styles/atomic/color.scss');
// Components
@include meta.load-css('~tgui/styles/components/Tabs.scss', $with: (
'text-color': rgba(0, 0, 0, 0.5),
'color-default': rgba(0, 0, 0, 1),
));
@include meta.load-css('~tgui/styles/components/Section.scss');
@include meta.load-css('~tgui/styles/components/Button.scss', $with: (
'color-default': #bbbbbb,
'color-disabled': #363636,
'color-selected': #0668b8,
'color-caution': #be6209,
'color-danger': #9a9d00,
'color-transparent-text': rgba(0, 0, 0, 0.5),
));
@include meta.load-css('~tgui/styles/components/Input.scss', $with: (
'border-color': colors.fg(colors.$label),
'background-color': #ffffff,
));
@include meta.load-css('~tgui/styles/components/NumberInput.scss');
@include meta.load-css('~tgui/styles/components/TextArea.scss');
@include meta.load-css('~tgui/styles/components/Knob.scss');
@include meta.load-css('~tgui/styles/components/Slider.scss');
@include meta.load-css('~tgui/styles/components/ProgressBar.scss');
// Components specific to tgui-panel
@include meta.load-css('../components/Chat.scss', $with: (
'text-color': #000000,
));
// Layouts
@include meta.load-css('~tgui/styles/layouts/Layout.scss', $with: (
'scrollbar-color-multiplier': -1,
));
@include meta.load-css('~tgui/styles/layouts/Window.scss');
@include meta.load-css('~tgui/styles/layouts/TitleBar.scss', $with: (
'text-color': rgba(0, 0, 0, 0.75),
'background-color': base.$color-bg,
'shadow-color-core': rgba(0, 0, 0, 0.25),
));
// Goonchat styles
@include meta.load-css('../goon/chat-light.scss');
}
-88
View File
@@ -1,88 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { storage } from 'common/storage';
import { createLogger } from 'tgui/logging';
const logger = createLogger('telemetry');
const MAX_CONNECTIONS_STORED = 10;
const connectionsMatch = (a, b) => (
a.ckey === b.ckey
&& a.address === b.address
&& a.computer_id === b.computer_id
);
export const telemetryMiddleware = store => {
let telemetry;
let wasRequestedWithPayload;
return next => action => {
const { type, payload } = action;
// Handle telemetry requests
if (type === 'telemetry/request') {
// Defer telemetry request until we have the actual telemetry
if (!telemetry) {
logger.debug('deferred');
wasRequestedWithPayload = payload;
return;
}
logger.debug('sending');
const limits = payload?.limits || {};
// Trim connections according to the server limit
const connections = telemetry.connections.slice(0, limits.connections);
Byond.sendMessage('telemetry', { connections });
return;
}
// Keep telemetry up to date
if (type === 'backend/update') {
next(action);
(async () => {
// Extract client data
const client = payload?.config?.client;
if (!client) {
logger.error('backend/update payload is missing client data!');
return;
}
// Load telemetry
if (!telemetry) {
telemetry = await storage.get('telemetry') || {};
if (!telemetry.connections) {
telemetry.connections = [];
}
logger.debug('retrieved telemetry from storage', telemetry);
}
// Append a connection record
let telemetryMutated = false;
const duplicateConnection = telemetry.connections
.find(conn => connectionsMatch(conn, client));
if (!duplicateConnection) {
telemetryMutated = true;
telemetry.connections.unshift(client);
if (telemetry.connections.length > MAX_CONNECTIONS_STORED) {
telemetry.connections.pop();
}
}
// Save telemetry
if (telemetryMutated) {
logger.debug('saving telemetry to storage', telemetry);
storage.set('telemetry', telemetry);
}
// Continue deferred telemetry requests
if (wasRequestedWithPayload) {
const payload = wasRequestedWithPayload;
wasRequestedWithPayload = null;
store.dispatch({
type: 'telemetry/request',
payload,
});
}
})();
return;
}
return next(action);
};
};
-134
View File
@@ -1,134 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const THEMES = ['light', 'dark'];
const COLOR_DARK_BG = '#202020';
const COLOR_DARK_BG_DARKER = '#171717';
const COLOR_DARK_TEXT = '#a4bad6';
let setClientThemeTimer = null;
/**
* Darkmode preference, originally by Kmc2000.
*
* This lets you switch client themes by using winset.
*
* If you change ANYTHING in interface/skin.dmf you need to change it here.
*
* There's no way round it. We're essentially changing the skin by hand.
* It's painful but it works, and is the way Lummox suggested.
*/
export const setClientTheme = name => {
// Transmit once for fast updates and again in a little while in case we won
// the race against statbrowser init.
clearInterval(setClientThemeTimer);
Byond.command(`.output statbrowser:set_theme ${name}`);
setClientThemeTimer = setTimeout(() => {
Byond.command(`.output statbrowser:set_theme ${name}`);
}, 1500);
if (name === 'light') {
return Byond.winset({
// Main windows
'infowindow.background-color': 'none',
'infowindow.text-color': '#000000',
'info.background-color': 'none',
'info.text-color': '#000000',
'browseroutput.background-color': 'none',
'browseroutput.text-color': '#000000',
'outputwindow.background-color': 'none',
'outputwindow.text-color': '#000000',
'mainwindow.background-color': 'none',
'split.background-color': 'none',
// Buttons
'changelog.background-color': 'none',
'changelog.text-color': '#000000',
'rules.background-color': 'none',
'rules.text-color': '#000000',
'wiki.background-color': 'none',
'wiki.text-color': '#000000',
'forum.background-color': 'none',
'forum.text-color': '#000000',
'github.background-color': 'none',
'github.text-color': '#000000',
'report-issue.background-color': 'none',
'report-issue.text-color': '#000000',
// Status and verb tabs
'output.background-color': 'none',
'output.text-color': '#000000',
'statwindow.background-color': 'none',
'statwindow.text-color': '#000000',
'stat.background-color': '#FFFFFF',
'stat.tab-background-color': 'none',
'stat.text-color': '#000000',
'stat.tab-text-color': '#000000',
'stat.prefix-color': '#000000',
'stat.suffix-color': '#000000',
// Say, OOC, me Buttons etc.
'saybutton.background-color': 'none',
'saybutton.text-color': '#000000',
'oocbutton.background-color': 'none',
'oocbutton.text-color': '#000000',
'mebutton.background-color': 'none',
'mebutton.text-color': '#000000',
'asset_cache_browser.background-color': 'none',
'asset_cache_browser.text-color': '#000000',
'tooltip.background-color': 'none',
'tooltip.text-color': '#000000',
});
}
if (name === 'dark') {
Byond.winset({
// Main windows
'infowindow.background-color': COLOR_DARK_BG,
'infowindow.text-color': COLOR_DARK_TEXT,
'info.background-color': COLOR_DARK_BG,
'info.text-color': COLOR_DARK_TEXT,
'browseroutput.background-color': COLOR_DARK_BG,
'browseroutput.text-color': COLOR_DARK_TEXT,
'outputwindow.background-color': COLOR_DARK_BG,
'outputwindow.text-color': COLOR_DARK_TEXT,
'mainwindow.background-color': COLOR_DARK_BG,
'split.background-color': COLOR_DARK_BG,
// Buttons
'changelog.background-color': '#494949',
'changelog.text-color': COLOR_DARK_TEXT,
'rules.background-color': '#494949',
'rules.text-color': COLOR_DARK_TEXT,
'wiki.background-color': '#494949',
'wiki.text-color': COLOR_DARK_TEXT,
'forum.background-color': '#494949',
'forum.text-color': COLOR_DARK_TEXT,
'github.background-color': '#3a3a3a',
'github.text-color': COLOR_DARK_TEXT,
'report-issue.background-color': '#492020',
'report-issue.text-color': COLOR_DARK_TEXT,
// Status and verb tabs
'output.background-color': COLOR_DARK_BG_DARKER,
'output.text-color': COLOR_DARK_TEXT,
'statwindow.background-color': COLOR_DARK_BG_DARKER,
'statwindow.text-color': COLOR_DARK_TEXT,
'stat.background-color': COLOR_DARK_BG_DARKER,
'stat.tab-background-color': COLOR_DARK_BG,
'stat.text-color': COLOR_DARK_TEXT,
'stat.tab-text-color': COLOR_DARK_TEXT,
'stat.prefix-color': COLOR_DARK_TEXT,
'stat.suffix-color': COLOR_DARK_TEXT,
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
'oocbutton.background-color': COLOR_DARK_BG,
'oocbutton.text-color': COLOR_DARK_TEXT,
'mebutton.background-color': COLOR_DARK_BG,
'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,
'tooltip.text-color': COLOR_DARK_TEXT,
});
}
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-4
View File
@@ -36,10 +36,6 @@ module.exports = (env = {}, argv) => {
'./packages/tgui-polyfill',
'./packages/tgui',
],
'tgui-panel': [
'./packages/tgui-polyfill',
'./packages/tgui-panel',
],
},
output: {
path: argv.useTmpFolder
-13
View File
@@ -9168,19 +9168,6 @@ resolve@^2.0.0-next.3:
languageName: unknown
linkType: soft
"tgui-panel@workspace:packages/tgui-panel":
version: 0.0.0-use.local
resolution: "tgui-panel@workspace:packages/tgui-panel"
dependencies:
common: "workspace:*"
dompurify: ^2.3.8
inferno: ^7.4.11
tgui: "workspace:*"
tgui-dev-server: "workspace:*"
tgui-polyfill: "workspace:*"
languageName: unknown
linkType: soft
"tgui-polyfill@workspace:*, tgui-polyfill@workspace:packages/tgui-polyfill":
version: 0.0.0-use.local
resolution: "tgui-polyfill@workspace:packages/tgui-polyfill"