5180 fichiers 38.5 Mo 0 téléchargements 242 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / components / wired-tools / WiredShareTabView.tsx
WiredShareTabView.tsx 15 Ko Télécharger le fichier
import {
    GetSessionDataManager,
    IWiredSetupImportReport,
    WiredSetupExportComposer,
    WiredSetupExportResultEvent,
    WiredSetupImportComposer,
    WiredSetupImportResultEvent
} from '@octane/renderer';
import { ChangeEvent, FC, useRef, useState } from 'react';
import {
    CopyToClipboard,
    isWiredShareCode,
    LocalizeText,
    localizeWithFallback,
    normalizeWiredShareCode,
    SendMessageComposer,
    WIRED_SHARE_ANCHOR_AVATAR,
    WIRED_SHARE_ANCHOR_ORIGINAL,
    WIRED_SHARE_MAX_CODE_LENGTH,
    WIRED_SHARE_STATUS_OK,
    wiredImportEntryText,
    wiredImportReasonText,
    wiredShareErrorText,
    wiredShareFileName
} from '../../api';
import { Button, Text } from '../../common';
import { useMessageEvent, useNotification, useRoom, useWiredTools } from '../../hooks';
import { Checkbox } from '../../common/Checkbox';
import { Radio } from '../../common/Radio';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';

const furniName = (type: string): string => GetSessionDataManager()?.getFloorItemDataByName?.(type)?.name || type;

/**
 * Wired setup share and import. Share asks the server for a code of every wired box in the room
 * (copy it or save it as a file); import pastes or loads a code, previews where every box lands and
 * applies it after a confirm. The server validates the whole code and only configures boxes the
 * importer placed, or places from their own inventory; nothing here is trusted.
 */
export const WiredShareTabView: FC<{}> = () => {
    const { roomSession = null } = useRoom();
    const { showConfirm = null } = useNotification();
    const { roomSettings } = useWiredTools();
    const canModify = roomSettings.isLoaded && roomSettings.canModify;

    const [exportCode, setExportCode] = useState('');
    const [exportCount, setExportCount] = useState(0);
    const [exportError, setExportError] = useState(0);
    const [exportBusy, setExportBusy] = useState(false);
    const [copied, setCopied] = useState(false);

    const [importCode, setImportCode] = useState('');
    const [anchorMode, setAnchorMode] = useState(WIRED_SHARE_ANCHOR_AVATAR);
    const [placeFromInventory, setPlaceFromInventory] = useState(false);
    const [importBusy, setImportBusy] = useState(false);
    const [report, setReport] = useState<IWiredSetupImportReport | null>(null);
    const fileInput = useRef<HTMLInputElement>(null);

    useMessageEvent<WiredSetupExportResultEvent>(WiredSetupExportResultEvent, (event) => {
        const parser = event.getParser();

        if (!parser) return;

        setExportBusy(false);
        setCopied(false);
        setExportError(parser.status);
        setExportCount(parser.boxCount);
        setExportCode(parser.status === WIRED_SHARE_STATUS_OK ? parser.code : '');
    });

    useMessageEvent<WiredSetupImportResultEvent>(WiredSetupImportResultEvent, (event) => {
        const parsed = event.getParser()?.report ?? null;

        setImportBusy(false);
        setReport(parsed);
    });

    const requestExport = () => {
        if (!canModify || exportBusy) return;

        setExportBusy(true);
        SendMessageComposer(new WiredSetupExportComposer(1));
    };

    const copyCode = () => {
        if (!exportCode) return;

        void CopyToClipboard(exportCode).then((ok) => setCopied(ok));
    };

    const downloadCode = () => {
        if (!exportCode || typeof URL === 'undefined' || !URL.createObjectURL) return;

        const url = URL.createObjectURL(new Blob([ exportCode ], { type: 'text/plain' }));
        const link = document.createElement('a');

        link.href = url;
        link.download = wiredShareFileName(roomSession?.roomId ?? 0);
        link.click();
        window.setTimeout(() => URL.revokeObjectURL(url), 1000);
    };

    const loadFile = (event: ChangeEvent<HTMLInputElement>) => {
        const file = event.target.files?.[0];

        event.target.value = '';
        if (!file || file.size > WIRED_SHARE_MAX_CODE_LENGTH * 2) return;

        void file.text().then((text) => {
            setImportCode(normalizeWiredShareCode(text));
            setReport(null);
        });
    };

    const sendImport = (confirm: boolean) => {
        const code = normalizeWiredShareCode(importCode);

        if (!canModify || importBusy || !isWiredShareCode(code)) return;

        setImportBusy(true);
        SendMessageComposer(new WiredSetupImportComposer(code, anchorMode, placeFromInventory, confirm));
    };

    const confirmImport = () => {
        if (!showConfirm) {
            sendImport(true);
            return;
        }

        showConfirm(
            localizeWithFallback(
                'wiredmenu.share.import.confirm.warning',
                'Import this wired? The settings of the boxes it finds are overwritten.'
            ),
            () => sendImport(true),
            null,
            LocalizeText('generic.ok'),
            LocalizeText('generic.cancel'),
            LocalizeText('generic.alert.title')
        );
    };

    const counts = report ? [ String(report.configured), String(report.placed), String(report.missing), String(report.refused) ] : [];
    const countKeys = [ 'configured', 'placed', 'missing', 'refused' ];
    const codeIsValid = isWiredShareCode(importCode);

    return (
        <NativeTextTree>
            <div className="p-3 min-h-[360px] flex flex-col gap-3">
                <div className="rounded bg-[#dfddd7] p-3 flex flex-col gap-2">
                    <Text bold small>
                        {localizeWithFallback('wiredmenu.share.export.title', 'Share wired:')}
                    </Text>
                    <Text small>
                        {localizeWithFallback(
                            'wiredmenu.share.export.info',
                            'Make a code of all wired in this room. Someone else can import it in their own room. No furni go with it, only the settings.'
                        )}
                    </Text>
                    <div className="flex gap-2">
                        <Button disabled={!canModify || exportBusy} variant="secondary" onClick={requestExport}>
                            {localizeWithFallback('wiredmenu.share.export.create', 'Make code')}
                        </Button>
                        <Button disabled={!exportCode} variant="secondary" onClick={copyCode}>
                            {copied ? localizeWithFallback('wiredmenu.share.export.copied', 'Code copied') : localizeWithFallback('wiredmenu.share.export.copy', 'Copy')}
                        </Button>
                        <Button disabled={!exportCode} variant="secondary" onClick={downloadCode}>
                            {localizeWithFallback('wiredmenu.share.export.download', 'Save as file')}
                        </Button>
                    </div>
                    {exportError !== WIRED_SHARE_STATUS_OK && <Text small className="text-[#a40000]">{wiredShareErrorText(exportError)}</Text>}
                    {exportCode && (
                        <>
                            <Text small>{localizeWithFallback('wiredmenu.share.export.count', '%count% wired boxes in this code.', [ 'count' ], [ String(exportCount) ])}</Text>
                            <textarea
                                readOnly
                                aria-label="Wired code"
                                className="w-full h-[60px] rounded border border-[#9d998e] bg-[#f4f0e8] p-1 text-[10px] font-mono break-all"
                                value={exportCode}
                                onFocus={(event) => event.target.select()}
                            />
                        </>
                    )}
                </div>
                <div className="rounded bg-[#dfddd7] p-3 flex flex-col gap-2">
                    <Text bold small>
                        {localizeWithFallback('wiredmenu.share.import.title', 'Import wired:')}
                    </Text>
                    <Text small>
                        {localizeWithFallback(
                            'wiredmenu.share.import.info',
                            'Paste a code or open a file. Only wired boxes you placed in this room, or have placed from your own inventory, get the settings.'
                        )}
                    </Text>
                    <textarea
                        aria-label="Wired code to import"
                        className="w-full h-[60px] rounded border border-[#9d998e] bg-[#f4f0e8] p-1 text-[10px] font-mono break-all"
                        maxLength={WIRED_SHARE_MAX_CODE_LENGTH * 2}
                        placeholder={localizeWithFallback('wiredmenu.share.import.paste', 'Paste the wired code here')}
                        value={importCode}
                        onChange={(event) => {
                            setImportCode(event.target.value);
                            setReport(null);
                        }}
                    />
                    <div className="flex items-center gap-2">
                        <Button variant="secondary" onClick={() => fileInput.current?.click()}>
                            {localizeWithFallback('wiredmenu.share.import.open_file', 'Open file')}
                        </Button>
                        <input ref={fileInput} accept=".txt,text/plain" className="hidden" type="file" onChange={loadFile} />
                    </div>
                    <div className="flex flex-col gap-1 text-[12px] text-[#222]">
                        <span>{localizeWithFallback('wiredmenu.share.import.anchor', 'Position:')}</span>
                        <label className="flex items-center gap-2">
                            <Radio checked={anchorMode === WIRED_SHARE_ANCHOR_AVATAR} name="wired-share-anchor" onCheckedChange={ () => {
                                    setAnchorMode(WIRED_SHARE_ANCHOR_AVATAR);
                                    setReport(null);
                                } } />
                            <span>{localizeWithFallback('wiredmenu.share.import.anchor.avatar', 'From the tile I am standing on')}</span>
                        </label>
                        <label className="flex items-center gap-2">
                            <Radio checked={anchorMode === WIRED_SHARE_ANCHOR_ORIGINAL} name="wired-share-anchor" onCheckedChange={ () => {
                                    setAnchorMode(WIRED_SHARE_ANCHOR_ORIGINAL);
                                    setReport(null);
                                } } />
                            <span>{localizeWithFallback('wiredmenu.share.import.anchor.original', 'On the same tiles as in the original room')}</span>
                        </label>
                        <label className="flex items-center gap-2">
                            <Checkbox checked={placeFromInventory} onCheckedChange={ nextChecked => {
                                    setPlaceFromInventory(nextChecked);
                                    setReport(null);
                                } } />
                            <span>{localizeWithFallback('wiredmenu.share.import.place_from_inventory', 'Place missing boxes from my inventory')}</span>
                        </label>
                    </div>
                    <div className="flex gap-2">
                        <Button disabled={!canModify || importBusy || !codeIsValid} variant="secondary" onClick={() => sendImport(false)}>
                            {localizeWithFallback('wiredmenu.share.import.preview', 'Preview')}
                        </Button>
                        <Button
                            disabled={!canModify || importBusy || !codeIsValid || !report || report.confirmed || report.status !== WIRED_SHARE_STATUS_OK}
                            variant="success"
                            onClick={confirmImport}
                        >
                            {localizeWithFallback('wiredmenu.share.import.confirm', 'Import')}
                        </Button>
                    </div>
                    {report && report.status !== WIRED_SHARE_STATUS_OK && <Text small className="text-[#a40000]">{wiredShareErrorText(report.status)}</Text>}
                    {report && report.status === WIRED_SHARE_STATUS_OK && (
                        <>
                            <Text small bold>
                                {report.confirmed
                                    ? localizeWithFallback(
                                          'wiredmenu.share.import.done',
                                          'Import done: %configured% configured, %placed% placed, %missing% missing, %refused% failed.',
                                          countKeys,
                                          counts
                                      )
                                    : localizeWithFallback(
                                          'wiredmenu.share.import.summary',
                                          '%configured% to configure, %placed% to place, %missing% missing, %refused% not possible.',
                                          countKeys,
                                          counts
                                      )}
                            </Text>
                            <div className="max-h-[160px] overflow-y-auto rounded border border-[#9d998e] bg-[#f4f0e8]">
                                <table className="w-full text-[12px]">
                                    <thead>
                                        <tr className="text-left">
                                            <th className="px-1">{localizeWithFallback('wiredmenu.share.col.box', 'Box')}</th>
                                            <th className="px-1">{localizeWithFallback('wiredmenu.share.col.tile', 'Tile')}</th>
                                            <th className="px-1">{localizeWithFallback('wiredmenu.share.col.status', 'Status')}</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        {report.entries.map((entry) => (
                                            <tr key={entry.index} className="border-t border-[#d6d1c4] align-top">
                                                <td className="px-1">{furniName(entry.type)}</td>
                                                <td className="px-1 whitespace-nowrap">
                                                    {entry.x},{entry.y}
                                                </td>
                                                <td className="px-1">
                                                    {wiredImportEntryText(entry.status)}
                                                    {entry.reason && <span className="text-[#7a2f00]"> — {wiredImportReasonText(entry.reason)}</span>}
                                                </td>
                                            </tr>
                                        ))}
                                    </tbody>
                                </table>
                            </div>
                        </>
                    )}
                </div>
            </div>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser