File transfer

This commit is contained in:
2026-06-11 20:33:44 -05:00
parent 2cb1d58264
commit b362357bcb
58 changed files with 5096 additions and 4 deletions

View File

@@ -0,0 +1,158 @@
/**
* Console Command Input with Autocomplete
* Ball Studios <https://git.balls.studio>
*/
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import http from '@/api/http';
import AutocompleteDropdown from './AutocompleteDropdown';
import SyntaxHelper from './SyntaxHelper';
import DangerCommandWarning from './DangerCommandWarning';
interface Suggestion {
command: string;
description: string;
syntax: string;
dangerous: boolean;
danger_reason?: string;
plugin: string;
examples: string[];
}
interface Props {
onSend: (command: string) => void;
disabled?: boolean;
}
const DANGEROUS = ['stop','restart','ban','ban-ip','op','deop','whitelist off','save-off','kill @e','kill @a'];
export default function CommandInput({ onSend, disabled = false }: Props) {
const { id: serverUuid } = useParams<{ id: string }>();
const [value, setValue] = useState('');
const [suggestions, setSugs]= useState<Suggestion[]>([]);
const [selected, setSelected]= useState(-1);
const [showDanger, setDanger]= useState<Suggestion | null>(null);
const [activeSug, setActiveSug] = useState<Suggestion | null>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const inputRef = useRef<HTMLInputElement>(null);
const isDanger = (cmd: string) =>
DANGEROUS.some(d => cmd.trim().toLowerCase().startsWith(d));
const fetchSuggestions = useCallback(async (q: string) => {
if (!q.trim() || !serverUuid) { setSugs([]); return; }
try {
const { data } = await http.get(
`/api/client/servers/${serverUuid}/addons/console/autocomplete`,
{ params: { q } }
);
setSugs(data.data ?? []);
setSelected(-1);
} catch {
setSugs([]);
}
}, [serverUuid]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setValue(v);
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => fetchSuggestions(v), 150);
// Update active suggestion for syntax helper
const match = suggestions.find(s => s.command === v.split(' ')[0]);
setActiveSug(match ?? null);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') { e.preventDefault(); setSelected(s => Math.min(s + 1, suggestions.length - 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setSelected(s => Math.max(s - 1, -1)); }
if (e.key === 'Tab' && suggestions.length > 0) {
e.preventDefault();
const idx = selected >= 0 ? selected : 0;
applySelection(suggestions[idx]);
}
if (e.key === 'Escape') { setSugs([]); setSelected(-1); }
if (e.key === 'Enter') { e.preventDefault(); submit(); }
};
const applySelection = (sug: Suggestion) => {
setValue(sug.command + ' ');
setSugs([]);
setSelected(-1);
setActiveSug(sug);
inputRef.current?.focus();
};
const submit = () => {
const cmd = value.trim();
if (!cmd) return;
if (isDanger(cmd)) {
const sug = suggestions.find(s => cmd.startsWith(s.command)) ?? {
command: cmd, description: '', syntax: '', dangerous: true, danger_reason: 'Potentially destructive command.', plugin: '', examples: [],
};
setDanger(sug);
return;
}
send(cmd);
};
const send = (cmd: string) => {
onSend(cmd);
setValue('');
setSugs([]);
setActiveSug(null);
setDanger(null);
};
useEffect(() => () => clearTimeout(debounceRef.current), []);
return (
<div className='addon-console-input-wrap'>
<div className='addon-console-input-row'>
<input
ref={inputRef}
type='text'
className='addon-console-input'
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder='Enter command…'
disabled={disabled}
autoComplete='off'
spellCheck={false}
aria-label='Console command input'
aria-autocomplete='list'
/>
<button
className='addon-console-send'
onClick={submit}
disabled={disabled || !value.trim()}
aria-label='Send command'
></button>
</div>
{/* Syntax helper */}
{activeSug && <SyntaxHelper suggestion={activeSug} />}
{/* Dropdown */}
{suggestions.length > 0 && (
<AutocompleteDropdown
suggestions={suggestions}
selectedIndex={selected}
onSelect={applySelection}
/>
)}
{/* Danger modal */}
{showDanger && (
<DangerCommandWarning
suggestion={showDanger}
onConfirm={() => send(value.trim())}
onCancel={() => { setDanger(null); inputRef.current?.focus(); }}
/>
)}
</div>
);
}