Skip to content

Commit

Permalink
feat(panel): global hotkey to playerlist filter
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Jan 12, 2024
1 parent 86dcf94 commit 49fa854
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
- Monitoring:
- Auto Restart FXServer on crash or hang
- Server’s CPU/RAM consumption
- Live Console (with log file and command history)
- Live Console (with log file, command history and search)
- Server tick time performance chart with player count ([example](https://i.imgur.com/VG8hpzr.gif))
- Server Activity Log (connections/disconnections, kills, chat, explosions and [custom commands](docs/custom_serverlog.md))
- Player Manager:
Expand All @@ -63,7 +63,7 @@
- Scheduled restarts with warning announcements and custom events ([more info](docs/events.md))
- Translation Support ([more info](docs/translation.md))
- FiveM's Server CFG editor & validator
- Responsive(ish) web interface with Dark Mode 😎
- Responsive web interface with Dark Mode 😎

Also, check our [Feature Graveyard](docs/feature_graveyard.md) for the features that are no longer among us (RIP).

Expand Down
5 changes: 3 additions & 2 deletions docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
- [x] fix z-order, cant click postpone/support on the warning bar
- [x] Update packages for all workspaces + root
- [x] FIXME: deployer "show db advanced options" has broken css and causes tx inception
- [ ] Check layout on 4k and ultrawide screens
- [ ] global hotkey to go to the player filter
- [x] Check layout on 4k and ultrawide screens
- [x] global hotkey to go to the player filter
- careful to also handle child iframe and terminal canvas
- add util function that gets a KeyboardEvent and returns the name of the hotkey pressed, then apply it to child iframe, global keydown handler, and xterm
- [ ] FIXME: check if we need or can do something to prevent NUI CSRF
Expand Down Expand Up @@ -52,6 +52,7 @@
- [ ] fix(nui/PlayerModel): require OneSync for bring and goto (PR #851)
- [ ] fix issue where the forced password change on save reloads the page instead of moving to the identifiers tab
- [ ] Remove old live console menu links
- [ ] Add clear copyright/license notice at the bottom of the server sidebar?

- [ ] onesync should be legacy by default
- [ ] check all discord invites (use utm params maybe?)
Expand Down
10 changes: 8 additions & 2 deletions panel/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { InjectedTxConsts } from '@shared/otherTypes';
import type { InjectedTxConsts } from '@shared/otherTypes';
import type { GlobalHotkeyAction } from './lib/hotkeyEventListener';

type LogoutNoticeMessage = { type: 'logoutNotice' };
type OpenAccountModalMessage = { type: 'openAccountModal' };
type OpenPlayerModalMessage = { type: 'openPlayerModal', ref: PlayerModalRefType };
type navigateToPageMessage = { type: 'navigateToPage', href: string };
type liveConsoleSearchHotkeyMessage = { type: 'liveConsoleSearchHotkey', action: string };
type globalHotkeyMessage = {
type: 'globalHotkey';
action: GlobalHotkeyAction;
};

export declare global {
interface Window {
Expand All @@ -16,5 +21,6 @@ export declare global {
| MessageEvent<OpenAccountModalMessage>
| MessageEvent<OpenPlayerModalMessage>
| MessageEvent<navigateToPageMessage>
| MessageEvent<liveConsoleSearchHotkeyMessage>;
| MessageEvent<liveConsoleSearchHotkeyMessage>
| MessageEvent<globalHotkeyMessage>;
}
2 changes: 1 addition & 1 deletion panel/src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const applyNewTheme = (oldTheme: string, newTheme: string) => {
}

//Changing iframe theme
const iframeBody = (document.getElementById('legacyPageiframe') as HTMLObjectElement)?.contentDocument?.body;
const iframeBody = (document.getElementById('legacyPageIframe') as HTMLObjectElement)?.contentDocument?.body;
if (iframeBody) {
if (iframeTheme === 'dark') {
iframeBody.classList.add('theme--dark');
Expand Down
21 changes: 6 additions & 15 deletions panel/src/layout/MainShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { navigate as setLocation } from 'wouter/use-location';
import MainSocket from './MainSocket';
import { TooltipProvider } from '@/components/ui/tooltip';
import { useToggleTheme } from '@/hooks/useTheme';
import { hotkeyEventListener } from '@/lib/hotkeyEventListener';
import BreakpointDebugger from '@/components/BreakpointDebugger';


Expand All @@ -26,7 +27,7 @@ export default function MainShell() {
const openPlayerModal = useOpenPlayerModal();
const toggleTheme = useToggleTheme();

//Listener for messages from child iframes (legacy routes)
//Listener for messages from child iframes (legacy routes) or other sources
useEventListener('message', (e: TxMessageEvent) => {
if (e.data.type === 'logoutNotice') {
expireSession('child iframe', 'got logoutNotice');
Expand All @@ -36,24 +37,14 @@ export default function MainShell() {
openPlayerModal(e.data.ref);
} else if (e.data.type === 'navigateToPage') {
setLocation(e.data.href);
} else if (e.data.type === 'globalHotkey' && e.data.action === 'toggleLightMode') {
toggleTheme();
}
});

//Listens to hotkeys - DEBUG only for now
//Listens to hotkeys
//NOTE: WILL NOT WORK IF THE FOCUS IS ON THE IFRAME
useEventListener('keydown', (e: KeyboardEvent) => {
if (!window.txConsts.showAdvanced) return;
if (e.ctrlKey && e.key === 'k') {
const el = document.getElementById('playerlistFilter');
if (el) {
el.focus();
e.preventDefault();
}
} else if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'l') {
toggleTheme();
e.preventDefault();
}
});
useEventListener('keydown', hotkeyEventListener);

return <>
<TooltipProvider delayDuration={300} disableHoverableContent={true}>
Expand Down
37 changes: 26 additions & 11 deletions panel/src/layout/playerlistSidebar/Playerlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
DropdownMenuSeparator, DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { useOpenPlayerModal } from "@/hooks/playerModal";
import InlineCode from "@/components/InlineCode";
import { useEventListener } from "usehooks-ts";


//NOTE: Move the styles (except color) to global.css since this component is rendered often
Expand All @@ -40,11 +42,18 @@ type PlayerlistFilterProps = {
setFilterString: (s: string) => void;
};
function PlayerlistFilter({ filterString, setFilterString }: PlayerlistFilterProps) {
const inputRef = useRef<HTMLInputElement>(null);
useEventListener('message', (e: TxMessageEvent) => {
if (e.data.type === 'globalHotkey' && e.data.action === 'focusPlayerlistFilter') {
inputRef.current?.focus();
}
});

return (
<div className="pt-2 px-2 flex gap-2">
<div className="relative w-full">
<Input
id="playerlistFilter"
ref={inputRef}
className="h-8"
placeholder="Filter by Name or ID"
value={filterString}
Expand All @@ -55,12 +64,18 @@ function PlayerlistFilter({ filterString, setFilterString }: PlayerlistFilterPro
}
}}
/>
{filterString && <button
className="absolute right-2 top-0 bottom-0 text-zinc-500 dark:text-zinc-400 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg"
onClick={() => setFilterString('')}
>
<XIcon />
</button>}
{filterString ? (
<button
className="absolute right-2 inset-y-0 text-zinc-500 dark:text-zinc-400 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg"
onClick={() => setFilterString('')}
>
<XIcon />
</button>
) : (
<div className="absolute right-2 inset-y-0 flex items-center text-zinc-500 dark:text-zinc-400">
<InlineCode className="text-xs tracking-wide select-none">ctrl+k</InlineCode>
</div>
)}
</div>
{/* <DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -133,8 +148,8 @@ function PlayerlistFilter({ filterString, setFilterString }: PlayerlistFilterPro
const PlayerlistFilterMemo = memo(PlayerlistFilter);


type PlayerlistPlayerProps = {
virtualItem: VirtualItem,
type PlayerlistPlayerProps = {
virtualItem: VirtualItem,
player: PlayerlistPlayerType,
modalOpener: (netid: number) => void,
}
Expand Down Expand Up @@ -196,8 +211,8 @@ export default function Playerlist() {
const virtualItems = rowVirtualizer.getVirtualItems();

const modalOpener = (netid: number) => {
if(!serverMutex) return;
openPlayerModal({mutex: serverMutex, netid});
if (!serverMutex) return;
openPlayerModal({ mutex: serverMutex, netid });
}

return (
Expand Down
37 changes: 37 additions & 0 deletions panel/src/lib/hotkeyEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { throttle } from "throttle-debounce";

export type GlobalHotkeyAction = 'focusPlayerlistFilter' | 'toggleLightMode';

const keyDebounceTime = 150; //ms
const sendHotkeyEvent = throttle(keyDebounceTime, (action: GlobalHotkeyAction) => {
console.log('sending hotkey event', action);
window.postMessage({
type: 'globalHotkey',
action,
});
}, { noTrailing: true });


/**
* Handles events and returns true if the event was handled.
*/
export function handleHotkeyEvent(e: KeyboardEvent) {
if (e.code === 'KeyK' && e.ctrlKey) {
sendHotkeyEvent('focusPlayerlistFilter');
return true;
} else if (e.code === 'KeyL' && e.ctrlKey && e.shiftKey && window.txConsts.showAdvanced) {
sendHotkeyEvent('toggleLightMode');
return true;
}
return false;
}


/**
* Event listener for hotkeys with preventDefault.
*/
export function hotkeyEventListener(e: KeyboardEvent) {
if (handleHotkeyEvent(e)) {
e.preventDefault();
}
}
16 changes: 15 additions & 1 deletion panel/src/pages/Iframe.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { useEffect, useRef } from "react";
import { hotkeyEventListener } from "@/lib/hotkeyEventListener";

type Props = {
legacyUrl: string;
};

export default function Iframe({ legacyUrl }: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);

//NOTE: if you open adminManager with autofill, the autofill will continue in the searchParams
//This is an annoying issue to fix, so #wontfix
const searchParams = location.search ?? '';
const hashParams = location.hash ?? '';

//Listens to hotkeys in the iframe
useEffect(() => {
if (!iframeRef.current) return;
iframeRef.current.contentWindow?.addEventListener('keydown', hotkeyEventListener);
}, []);

return (
<iframe id="legacyPageiframe"
<iframe
ref={iframeRef}
id="legacyPageIframe" //required for the theme switcher
src={`./legacy/${legacyUrl}${searchParams}${hashParams}`}
className="w-full"
></iframe>
Expand Down
3 changes: 3 additions & 0 deletions panel/src/pages/LiveConsole/LiveConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import terminalOptions from "./xtermOptions";
import './xtermOverrides.css';
import '@xterm/xterm/css/xterm.css';
import { getSocket, openExternalLink } from '@/lib/utils';
import { handleHotkeyEvent } from '@/lib/hotkeyEventListener';


const keyDebounceTime = 150; //ms
Expand Down Expand Up @@ -128,6 +129,8 @@ export default function LiveConsole() {
} else if (e.code === 'End') {
scrollBottom();
return false;
} else if (handleHotkeyEvent(e)) {
return false;
}
return true;
});
Expand Down
1 change: 0 additions & 1 deletion panel/src/pages/LiveConsole/LiveConsoleSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export default function LiveConsoleSearchBar({ show, setShow, searchAddon }: Liv
<div className="relative">
<Input
ref={inputRef}
id="playerlistFilter"
className="h-8"
placeholder="Search string"
onKeyDown={handleInputKeyDown}
Expand Down

0 comments on commit 49fa854

Please sign in to comment.