XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
import { BellRing, CalendarDays, LogIn, MessageSquareText, Plus, Save, ScrollText, ShieldCheck, Trash2 } from 'lucide-react';
import { useCallback, useEffect, useState, type ReactNode } from 'react';
import { RichTextEditor } from '../components/rich-text-editor';
import { variableAppliesToEvent } from '../components/rich-text-variables';
import { Badge, Button, Field, Input, PageHeader, Panel, ResourceState, Toast } from '../components/ui';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import type { ServerSettings } from '../types';

type MessageSection = 'join' | 'firstJoin' | 'daily' | 'rules' | 'maintenance';

export function MessagesPage() {
  const { t } = useI18n();
  const { api, session } = useServer();
  const resource = useApiResource(() => api.settings(), [], ['settings']);
  const catalogResource = useApiResource(() => api.triggerCatalog(), []);
  const variableCatalog = catalogResource.data?.variables?.filter((variable) =>
    variable.templateAllowed !== false && variable.sensitive !== true
    && variableAppliesToEvent(variable, 'player.join'));
  const owner = session?.actor?.roles.includes('owner') === true;
  const [value, setValue] = useState<ServerSettings>();
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState<string>();
  const [error, setError] = useState<string>();
  const [section, setSection] = useState<MessageSection>('join');
  useEffect(() => setValue(resource.data), [resource.data]);

  const toggle = (key: keyof ServerSettings, checked: boolean) => {
    setValue((current) => current ? { ...current, [key]: checked } : current);
  };
  const text = (key: keyof ServerSettings, next: string) => {
    setValue((current) => current ? { ...current, [key]: next } : current);
  };
  const save = useCallback(async () => {
    if (!value || !owner) return;
    setBusy(true); setError(undefined); setMessage(undefined);
    try {
      const operation = await api.updateMessageSettings({
        joinWelcomeEnabled: value.joinWelcomeEnabled,
        joinWelcomeMessage: value.joinWelcomeMessage,
        firstJoinMessageEnabled: value.firstJoinMessageEnabled,
        firstJoinMessage: value.firstJoinMessage,
        dailyAnnouncementsEnabled: value.dailyAnnouncementsEnabled,
        dailyAnnouncements: value.dailyAnnouncements,
        announcementTimeZone: value.announcementTimeZone,
        rulesReminderEnabled: value.rulesReminderEnabled,
        rulesMessage: value.rulesMessage,
        maintenanceJoinReminderEnabled: value.maintenanceJoinReminderEnabled,
      });
      setMessage(operation.message ?? t('messages.saved'));
      resource.reload();
    } catch (cause) {
      setError(cause instanceof Error ? cause.message : String(cause));
    } finally { setBusy(false); }
  }, [api, owner, resource, t, value]);

  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return;
      event.preventDefault();
      if (!busy) void save();
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [busy, save]);

  const sectionEnabled = (id: MessageSection) => value ? ({
    join: value.joinWelcomeEnabled,
    firstJoin: value.firstJoinMessageEnabled,
    daily: value.dailyAnnouncementsEnabled,
    rules: value.rulesReminderEnabled,
    maintenance: value.maintenanceJoinReminderEnabled,
  })[id] : false;
  const sections: Array<{ id: MessageSection; label: string; icon: ReactNode }> = [
    { id: 'join', label: t('messages.joinWelcome'), icon: <LogIn size={17} /> },
    { id: 'firstJoin', label: t('messages.firstJoin'), icon: <ShieldCheck size={17} /> },
    { id: 'daily', label: t('messages.daily'), icon: <CalendarDays size={17} /> },
    { id: 'rules', label: t('messages.rules'), icon: <ScrollText size={17} /> },
    { id: 'maintenance', label: t('messages.maintenanceReminder'), icon: <BellRing size={17} /> },
  ];

  return <div className="page">
    <PageHeader title={t('messages.title')} description={t('messages.description')} actions={value ? <Badge tone={value.features.joinExperienceEnabled ? 'good' : 'warn'}>{value.features.joinExperienceEnabled ? t('common.enabled') : t('common.disabled')}</Badge> : undefined} />
    <ResourceState loading={resource.loading || !value} error={resource.error} onRetry={resource.reload}>
      {value && <form className="form-stack" onSubmit={(event) => { event.preventDefault(); void save(); }}>
        <div className="confirm-box"><MessageSquareText /><div><strong>{t('messages.richHint')}</strong><p>{t('messages.variablesHint')}</p></div></div>
        <div className="message-workspace">
          <nav className="message-section-nav" aria-label={t('messages.sections')}>
            {sections.map((item) => <button type="button" key={item.id} className={section === item.id ? 'is-active' : ''} aria-selected={section === item.id} onClick={() => setSection(item.id)}>{item.icon}<span>{item.label}</span><i className={sectionEnabled(item.id) ? 'is-enabled' : ''} aria-label={sectionEnabled(item.id) ? t('common.enabled') : t('common.disabled')} /></button>)}
          </nav>
          <div className="message-section-content" role="tabpanel">
          {section === 'join' && <MessagePanel icon={<LogIn size={18} />} title={t('messages.joinWelcome')} enabled={value.joinWelcomeEnabled} onEnabled={(checked) => toggle('joinWelcomeEnabled', checked)}>
            <RichTextEditor value={value.joinWelcomeMessage} defaultSenderName={value.defaultMessageSender}
              variableCatalog={variableCatalog} onChange={(next) => text('joinWelcomeMessage', next)} />
          </MessagePanel>}
          {section === 'firstJoin' && <MessagePanel icon={<ShieldCheck size={18} />} title={t('messages.firstJoin')} enabled={value.firstJoinMessageEnabled} onEnabled={(checked) => toggle('firstJoinMessageEnabled', checked)}>
            <RichTextEditor value={value.firstJoinMessage} defaultSenderName={value.defaultMessageSender}
              variableCatalog={variableCatalog} onChange={(next) => text('firstJoinMessage', next)} />
          </MessagePanel>}
          {section === 'rules' && <MessagePanel icon={<ScrollText size={18} />} title={t('messages.rules')} enabled={value.rulesReminderEnabled} onEnabled={(checked) => toggle('rulesReminderEnabled', checked)}>
            <RichTextEditor value={value.rulesMessage} defaultSenderName={value.defaultMessageSender}
              variableCatalog={variableCatalog} onChange={(next) => text('rulesMessage', next)} />
          </MessagePanel>}
          {section === 'daily' && <Panel title={<><CalendarDays size={18} />{t('messages.daily')}</>}>
            <label className="toggle-row"><input type="checkbox" checked={value.dailyAnnouncementsEnabled} onChange={(event) => toggle('dailyAnnouncementsEnabled', event.target.checked)} /><span><strong>{value.dailyAnnouncementsEnabled ? t('common.enabled') : t('common.disabled')}</strong><small>{t('messages.onceDaily')}</small></span></label>
            <Field label={t('messages.timeZone')}><Input value={value.announcementTimeZone} onChange={(event) => text('announcementTimeZone', event.target.value)} placeholder="Asia/Shanghai" required /></Field>
            <div className="daily-announcement-list">
              <div className="daily-announcement-list__heading"><strong>{t('messages.dailyList')}</strong><Button type="button" variant="ghost" disabled={value.dailyAnnouncements.length >= 20} onClick={() => setValue((current) => current ? { ...current, dailyAnnouncements: [...current.dailyAnnouncements, ''] } : current)}><Plus size={14} />{t('messages.add')}</Button></div>
              {value.dailyAnnouncements.map((announcement, index) => <section className="daily-announcement" key={index}>
                <div className="daily-announcement__heading"><span>{t('messages.item')} {index + 1}</span><Button type="button" variant="ghost" onClick={() => setValue((current) => current ? { ...current, dailyAnnouncements: current.dailyAnnouncements.filter((_, itemIndex) => itemIndex !== index) } : current)}><Trash2 size={14} />{t('common.delete')}</Button></div>
                <RichTextEditor compact value={announcement} defaultSenderName={value.defaultMessageSender}
                  variableCatalog={variableCatalog} onChange={(next) => setValue((current) => current ? { ...current, dailyAnnouncements: current.dailyAnnouncements.map((item, itemIndex) => itemIndex === index ? next : item) } : current)} />
              </section>)}
            </div>
          </Panel>}
          {section === 'maintenance' && <Panel title={<><BellRing size={18} />{t('messages.behavior')}</>}>
            <label className="toggle-row"><input type="checkbox" checked={value.maintenanceJoinReminderEnabled} onChange={(event) => toggle('maintenanceJoinReminderEnabled', event.target.checked)} /><span><strong>{t('messages.maintenanceReminder')}</strong><small>{t('messages.maintenanceHint')}</small></span></label>
          </Panel>}
          </div>
        </div>
        {!owner && <div className="form-error">{t('messages.ownerOnly')}</div>}
        {error && <Toast message={error} tone="danger" onClose={() => setError(undefined)} />}
        {message && <Toast message={message} tone="good" onClose={() => setMessage(undefined)} />}
        <div className="button-row"><Button type="submit" disabled={!owner || busy}><Save size={15} />{t('messages.save')}</Button><span className="keyboard-hint"><kbd>Ctrl</kbd>+<kbd>S</kbd></span>{message && <Badge tone="good">{t('messages.live')}</Badge>}</div>
      </form>}
    </ResourceState>
  </div>;
}

function MessagePanel({ icon, title, enabled, onEnabled, children }: {
  icon: ReactNode;
  title: string;
  enabled: boolean;
  onEnabled: (checked: boolean) => void;
  children: ReactNode;
}) {
  const { t } = useI18n();
  return <Panel title={<>{icon}{title}</>}>
    <label className="toggle-row"><input type="checkbox" checked={enabled} onChange={(event) => onEnabled(event.target.checked)} /><span><strong>{enabled ? t('common.enabled') : t('common.disabled')}</strong></span></label>
    {children}
  </Panel>;
}