mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Add FurniEditor tool with Next.js API integration
- FurniEditor component with Search/Edit tabs (NitroCard UI) - useFurniEditor hook connecting to Next.js API routes via Vite proxy - Edit Furni button in room infostand (godMode) with sprite ID lookup - Toolbar: 3-column flex layout (icons | chat | friends) - Heroicons SVG for ID/Sprite display in infostand and edit view - Vite config: proxy /api to Next.js, aliases for renderer3 packages
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<base href="/client/" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CampaignView } from './campaign/CampaignView';
|
||||
import { CatalogView } from './catalog/CatalogView';
|
||||
import { ChatHistoryView } from './chat-history/ChatHistoryView';
|
||||
import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView';
|
||||
import { FurniEditorView } from './furni-editor/FurniEditorView';
|
||||
import { FriendsView } from './friends/FriendsView';
|
||||
import { GameCenterView } from './game-center/GameCenterView';
|
||||
import { GroupsView } from './groups/GroupsView';
|
||||
@@ -115,6 +116,7 @@ export const MainView: FC<{}> = props =>
|
||||
<CampaignView />
|
||||
<GameCenterView />
|
||||
<FloorplanEditorView />
|
||||
<FurniEditorView />
|
||||
<YoutubeTvView />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useFurniEditor } from '../../hooks/furni-editor';
|
||||
import { FurniEditorEditView } from './views/FurniEditorEditView';
|
||||
import { FurniEditorSearchView } from './views/FurniEditorSearchView';
|
||||
|
||||
const TAB_SEARCH = 0;
|
||||
const TAB_EDIT = 1;
|
||||
|
||||
export const FurniEditorView: FC<{}> = () =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ activeTab, setActiveTab ] = useState(TAB_SEARCH);
|
||||
|
||||
const {
|
||||
items, total, page, loading, error, clearError,
|
||||
selectedItem, catalogItems, furniDataEntry,
|
||||
interactions,
|
||||
searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions
|
||||
} = useFurniEditor();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(prev => !prev);
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'furni-editor/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isVisible) loadInteractions();
|
||||
}, [ isVisible ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const handler = async (e: CustomEvent<{ spriteId: number }>) =>
|
||||
{
|
||||
const { spriteId } = e.detail;
|
||||
|
||||
const ok = await loadBySpriteId(spriteId);
|
||||
|
||||
if(ok) setActiveTab(TAB_EDIT);
|
||||
};
|
||||
|
||||
window.addEventListener('furni-editor:open', handler as EventListener);
|
||||
|
||||
return () => window.removeEventListener('furni-editor:open', handler as EventListener);
|
||||
}, [ loadBySpriteId ]);
|
||||
|
||||
const handleSelect = useCallback(async (id: number) =>
|
||||
{
|
||||
const ok = await loadDetail(id);
|
||||
|
||||
if(ok) setActiveTab(TAB_EDIT);
|
||||
}, [ loadDetail ]);
|
||||
|
||||
const handleBack = useCallback(() =>
|
||||
{
|
||||
setActiveTab(TAB_SEARCH);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() =>
|
||||
{
|
||||
setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView uniqueKey="furni-editor" className="w-[620px] h-[520px]">
|
||||
<NitroCardHeaderView headerText="Furni Editor" onCloseClick={ handleClose } />
|
||||
<NitroCardTabsView>
|
||||
<NitroCardTabsItemView isActive={ activeTab === TAB_SEARCH } onClick={ () => setActiveTab(TAB_SEARCH) }>
|
||||
Search
|
||||
</NitroCardTabsItemView>
|
||||
<NitroCardTabsItemView isActive={ activeTab === TAB_EDIT } onClick={ () => selectedItem && setActiveTab(TAB_EDIT) }>
|
||||
Edit
|
||||
</NitroCardTabsItemView>
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
{ error &&
|
||||
<div className="bg-[#f8d7da] border border-[#f5c6cb] rounded p-2 text-[#721c24] text-xs mb-1 flex justify-between items-center">
|
||||
<span>{ error }</span>
|
||||
<span className="cursor-pointer font-bold" onClick={ clearError }>x</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ activeTab === TAB_SEARCH &&
|
||||
<FurniEditorSearchView
|
||||
items={ items }
|
||||
total={ total }
|
||||
page={ page }
|
||||
loading={ loading }
|
||||
onSearch={ searchItems }
|
||||
onSelect={ handleSelect }
|
||||
/>
|
||||
}
|
||||
|
||||
{ activeTab === TAB_EDIT && selectedItem &&
|
||||
<FurniEditorEditView
|
||||
item={ selectedItem }
|
||||
catalogItems={ catalogItems }
|
||||
furniDataEntry={ furniDataEntry }
|
||||
interactions={ interactions }
|
||||
loading={ loading }
|
||||
onUpdate={ updateItem }
|
||||
onDelete={ deleteItem }
|
||||
onBack={ handleBack }
|
||||
onRefresh={ loadDetail }
|
||||
/>
|
||||
}
|
||||
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import { Button, Column, Flex, Text } from '../../../common';
|
||||
|
||||
interface FurniEditorCreateViewProps
|
||||
{
|
||||
interactions: string[];
|
||||
loading: boolean;
|
||||
onCreate: (fields: Record<string, unknown>) => Promise<number | null>;
|
||||
onCreated: (id: number) => void;
|
||||
}
|
||||
|
||||
export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = props =>
|
||||
{
|
||||
const { interactions, loading, onCreate, onCreated } = props;
|
||||
const [ success, setSuccess ] = useState<number | null>(null);
|
||||
|
||||
const [ form, setForm ] = useState({
|
||||
itemName: '',
|
||||
publicName: '',
|
||||
spriteId: 0,
|
||||
type: 's' as 's' | 'i',
|
||||
width: 1,
|
||||
length: 1,
|
||||
stackHeight: 0,
|
||||
allowStack: true,
|
||||
allowSit: false,
|
||||
allowLay: false,
|
||||
allowWalk: false,
|
||||
allowGift: true,
|
||||
allowTrade: true,
|
||||
allowRecycle: true,
|
||||
allowMarketplaceSell: true,
|
||||
allowInventoryStack: true,
|
||||
interactionType: '',
|
||||
interactionModesCount: 1,
|
||||
customparams: '',
|
||||
});
|
||||
|
||||
const setField = useCallback((key: string, value: unknown) =>
|
||||
{
|
||||
setForm(prev => ({ ...prev, [key]: value }));
|
||||
setSuccess(null);
|
||||
}, []);
|
||||
|
||||
const handleCreate = useCallback(async () =>
|
||||
{
|
||||
if(!form.itemName || !form.publicName) return;
|
||||
|
||||
const id = await onCreate(form);
|
||||
|
||||
if(id)
|
||||
{
|
||||
setSuccess(id);
|
||||
setTimeout(() => onCreated(id), 1000);
|
||||
}
|
||||
}, [ form, onCreate, onCreated ]);
|
||||
|
||||
const inputClass = 'form-control form-control-sm';
|
||||
const labelClass = 'text-[11px] font-bold text-[#333] mb-0';
|
||||
|
||||
return (
|
||||
<Column gap={ 1 } className="h-full overflow-auto">
|
||||
{ success &&
|
||||
<div className="bg-[#d4edda] border border-[#c3e6cb] rounded p-2 text-[#155724] text-xs">
|
||||
Item created with ID #{ success }!
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Basic Info</Text>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className={ labelClass }>Item Name *</label>
|
||||
<input className={ inputClass } value={ form.itemName } onChange={ e => setField('itemName', e.target.value) } placeholder="my_custom_furni" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Public Name *</label>
|
||||
<input className={ inputClass } value={ form.publicName } onChange={ e => setField('publicName', e.target.value) } placeholder="My Custom Furni" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Sprite ID</label>
|
||||
<input type="number" className={ inputClass } value={ form.spriteId } onChange={ e => setField('spriteId', Number(e.target.value)) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Type</label>
|
||||
<select className="form-select form-select-sm" value={ form.type } onChange={ e => setField('type', e.target.value) }>
|
||||
<option value="s">Floor (s)</option>
|
||||
<option value="i">Wall (i)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Dimensions</Text>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className={ labelClass }>Width</label>
|
||||
<input type="number" className={ inputClass } value={ form.width } onChange={ e => setField('width', Number(e.target.value)) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Length</label>
|
||||
<input type="number" className={ inputClass } value={ form.length } onChange={ e => setField('length', Number(e.target.value)) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Stack Height</label>
|
||||
<input type="number" step="0.01" className={ inputClass } value={ form.stackHeight } onChange={ e => setField('stackHeight', Number(e.target.value)) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Permissions</Text>
|
||||
<div className="grid grid-cols-3 gap-x-3 gap-y-1">
|
||||
{ [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => (
|
||||
<label key={ key } className="flex items-center gap-1 text-[11px] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ (form as any)[key] }
|
||||
onChange={ e => setField(key, e.target.checked) }
|
||||
/>
|
||||
{ key.replace('allow', '') }
|
||||
</label>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Interaction</Text>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="col-span-2">
|
||||
<label className={ labelClass }>Type</label>
|
||||
<select className="form-select form-select-sm" value={ form.interactionType } onChange={ e => setField('interactionType', e.target.value) }>
|
||||
<option value="">none</option>
|
||||
{ interactions.map(i => (
|
||||
<option key={ i } value={ i }>{ i }</option>
|
||||
)) }
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Modes</label>
|
||||
<input type="number" className={ inputClass } value={ form.interactionModesCount } onChange={ e => setField('interactionModesCount', Number(e.target.value)) } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<label className={ labelClass }>Custom Params</label>
|
||||
<input className={ inputClass } value={ form.customparams } onChange={ e => setField('customparams', e.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Flex className="mt-1">
|
||||
<Button variant="success" disabled={ loading || !form.itemName || !form.publicName } onClick={ handleCreate }>
|
||||
{ loading ? 'Creating...' : 'Create Item' }
|
||||
</Button>
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Column, Flex, Text } from '../../../common';
|
||||
import { CatalogRef, FurniDetail } from '../../../hooks/furni-editor';
|
||||
|
||||
interface FurniEditorEditViewProps
|
||||
{
|
||||
item: FurniDetail;
|
||||
catalogItems: CatalogRef[];
|
||||
furniDataEntry: Record<string, unknown> | null;
|
||||
interactions: string[];
|
||||
loading: boolean;
|
||||
onUpdate: (id: number, fields: Record<string, unknown>) => Promise<boolean>;
|
||||
onDelete: (id: number) => Promise<boolean>;
|
||||
onBack: () => void;
|
||||
onRefresh: (id: number) => void;
|
||||
}
|
||||
|
||||
export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
|
||||
{
|
||||
const { item, catalogItems, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onRefresh } = props;
|
||||
|
||||
const [ form, setForm ] = useState({
|
||||
itemName: '',
|
||||
publicName: '',
|
||||
spriteId: 0,
|
||||
type: 's',
|
||||
width: 1,
|
||||
length: 1,
|
||||
stackHeight: 0,
|
||||
allowStack: true,
|
||||
allowWalk: false,
|
||||
allowSit: false,
|
||||
allowLay: false,
|
||||
allowGift: true,
|
||||
allowTrade: true,
|
||||
allowRecycle: true,
|
||||
allowMarketplaceSell: true,
|
||||
allowInventoryStack: true,
|
||||
interactionType: '',
|
||||
interactionModesCount: 0,
|
||||
customparams: '',
|
||||
});
|
||||
|
||||
const [ confirmDelete, setConfirmDelete ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!item) return;
|
||||
|
||||
setForm({
|
||||
itemName: item.itemName || '',
|
||||
publicName: item.publicName || '',
|
||||
spriteId: item.spriteId || 0,
|
||||
type: item.type || 's',
|
||||
width: item.width || 1,
|
||||
length: item.length || 1,
|
||||
stackHeight: item.stackHeight || 0,
|
||||
allowStack: !!item.allowStack,
|
||||
allowWalk: !!item.allowWalk,
|
||||
allowSit: !!item.allowSit,
|
||||
allowLay: !!item.allowLay,
|
||||
allowGift: !!item.allowGift,
|
||||
allowTrade: !!item.allowTrade,
|
||||
allowRecycle: !!item.allowRecycle,
|
||||
allowMarketplaceSell: !!item.allowMarketplaceSell,
|
||||
allowInventoryStack: !!item.allowInventoryStack,
|
||||
interactionType: item.interactionType || '',
|
||||
interactionModesCount: item.interactionModesCount || 0,
|
||||
customparams: item.customparams || '',
|
||||
});
|
||||
|
||||
setConfirmDelete(false);
|
||||
}, [ item ]);
|
||||
|
||||
const setField = useCallback((key: string, value: unknown) =>
|
||||
{
|
||||
setForm(prev => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(async () =>
|
||||
{
|
||||
const ok = await onUpdate(item.id, form);
|
||||
|
||||
if(ok) onRefresh(item.id);
|
||||
}, [ item, form, onUpdate, onRefresh ]);
|
||||
|
||||
const handleDelete = useCallback(async () =>
|
||||
{
|
||||
if(!confirmDelete) return setConfirmDelete(true);
|
||||
|
||||
const ok = await onDelete(item.id);
|
||||
|
||||
if(ok) onBack();
|
||||
}, [ confirmDelete, item, onDelete, onBack ]);
|
||||
|
||||
const inputClass = 'form-control form-control-sm';
|
||||
const labelClass = 'text-[11px] font-bold text-[#333] mb-0';
|
||||
|
||||
return (
|
||||
<Column gap={ 1 } className="h-full overflow-auto">
|
||||
<Flex gap={ 1 } alignItems="center" className="mb-1">
|
||||
<Button variant="secondary" onClick={ onBack }>Back</Button>
|
||||
<Flex alignItems="center" gap={ 1 } className="bg-[#e9ecef] px-2 py-0.5 rounded">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#1e7295]">
|
||||
<path fillRule="evenodd" d="M4.93 1.31a41.401 41.401 0 0 1 10.14 0C16.194 1.45 17 2.414 17 3.517V18.25a.75.75 0 0 1-1.075.676l-2.8-1.344-2.8 1.344a.75.75 0 0 1-.65 0l-2.8-1.344-2.8 1.344A.75.75 0 0 1 3 18.25V3.517c0-1.103.806-2.068 1.93-2.207Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<Text bold className="text-[12px]">{ item.id }</Text>
|
||||
<span className="text-[#999] mx-0.5">|</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#1e7295]">
|
||||
<path d="M12.586 2.586a2 2 0 1 1 2.828 2.828l-3 3a2 2 0 0 1-2.828 0 1 1 0 0 0-1.414 1.414 4 4 0 0 0 5.656 0l3-3a4 4 0 0 0-5.656-5.656l-1.5 1.5a1 1 0 1 0 1.414 1.414l1.5-1.5ZM7.414 17.414a2 2 0 1 1-2.828-2.828l3-3a2 2 0 0 1 2.828 0 1 1 0 0 0 1.414-1.414 4 4 0 0 0-5.656 0l-3 3a4 4 0 0 0 5.656 5.656l1.5-1.5a1 1 0 1 0-1.414-1.414l-1.5 1.5Z" />
|
||||
</svg>
|
||||
<Text bold className="text-[12px]">{ item.spriteId }</Text>
|
||||
</Flex>
|
||||
<Text small variant="gray">({ item.usageCount } in use)</Text>
|
||||
</Flex>
|
||||
|
||||
{ /* Basic Info */ }
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Basic Info</Text>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className={ labelClass }>Item Name</label>
|
||||
<input className={ inputClass } value={ form.itemName } onChange={ e => setField('itemName', e.target.value) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Public Name</label>
|
||||
<input className={ inputClass } value={ form.publicName } onChange={ e => setField('publicName', e.target.value) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Sprite ID</label>
|
||||
<input type="number" className={ inputClass } value={ form.spriteId } onChange={ e => setField('spriteId', Number(e.target.value)) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Type</label>
|
||||
<select className="form-select form-select-sm" value={ form.type } onChange={ e => setField('type', e.target.value) }>
|
||||
<option value="s">Floor (s)</option>
|
||||
<option value="i">Wall (i)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Dimensions */ }
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Dimensions</Text>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className={ labelClass }>Width</label>
|
||||
<input type="number" className={ inputClass } value={ form.width } onChange={ e => setField('width', Number(e.target.value)) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Length</label>
|
||||
<input type="number" className={ inputClass } value={ form.length } onChange={ e => setField('length', Number(e.target.value)) } />
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Stack Height</label>
|
||||
<input type="number" step="0.01" className={ inputClass } value={ form.stackHeight } onChange={ e => setField('stackHeight', Number(e.target.value)) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Permissions */ }
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Permissions</Text>
|
||||
<div className="grid grid-cols-3 gap-x-3 gap-y-1">
|
||||
{ [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => (
|
||||
<label key={ key } className="flex items-center gap-1 text-[11px] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={ (form as any)[key] }
|
||||
onChange={ e => setField(key, e.target.checked) }
|
||||
/>
|
||||
{ key.replace('allow', '') }
|
||||
</label>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Interaction */ }
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Interaction</Text>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="col-span-2">
|
||||
<label className={ labelClass }>Type</label>
|
||||
<select className="form-select form-select-sm" value={ form.interactionType } onChange={ e => setField('interactionType', e.target.value) }>
|
||||
<option value="">none</option>
|
||||
{ interactions.map(i => (
|
||||
<option key={ i } value={ i }>{ i }</option>
|
||||
)) }
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={ labelClass }>Modes</label>
|
||||
<input type="number" className={ inputClass } value={ form.interactionModesCount } onChange={ e => setField('interactionModesCount', Number(e.target.value)) } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<label className={ labelClass }>Custom Params</label>
|
||||
<input className={ inputClass } value={ form.customparams } onChange={ e => setField('customparams', e.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Catalog References */ }
|
||||
{ catalogItems.length > 0 &&
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Catalog ({ catalogItems.length })</Text>
|
||||
<div className="text-[10px] space-y-0.5">
|
||||
{ catalogItems.map(ci => (
|
||||
<div key={ ci.id } className="flex justify-between bg-[#f5f5f5] px-2 py-0.5 rounded">
|
||||
<span>{ ci.catalogName } (page: { ci.pageName })</span>
|
||||
<span>{ ci.costCredits }c + { ci.costPoints }p</span>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ /* FurniData.json Entry */ }
|
||||
{ furniDataEntry &&
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">FurniData.json</Text>
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[10px]">
|
||||
{ Object.entries(furniDataEntry).map(([ key, value ]) => (
|
||||
<div key={ key } className="flex justify-between bg-[#f5f5f5] px-2 py-0.5 rounded">
|
||||
<span className="font-bold text-[#555]">{ key }</span>
|
||||
<span className="text-[#333] truncate ml-1 max-w-[120px] text-right">{ String(value ?? '') }</span>
|
||||
</div>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ /* Actions */ }
|
||||
<Flex gap={ 1 } justifyContent="between" className="mt-1">
|
||||
<Button variant="success" disabled={ loading } onClick={ handleSave }>
|
||||
{ loading ? 'Saving...' : 'Save' }
|
||||
</Button>
|
||||
<Button
|
||||
variant={ confirmDelete ? 'danger' : 'warning' }
|
||||
disabled={ loading || item.usageCount > 0 }
|
||||
onClick={ handleDelete }
|
||||
>
|
||||
{ confirmDelete ? 'Confirm Delete' : 'Delete' }
|
||||
</Button>
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Column, Flex, Text } from '../../../common';
|
||||
import { FurniItem } from '../../../hooks/furni-editor';
|
||||
|
||||
interface FurniEditorSearchViewProps
|
||||
{
|
||||
items: FurniItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
onSearch: (query: string, type: string, page: number) => void;
|
||||
onSelect: (id: number) => void;
|
||||
}
|
||||
|
||||
export const FurniEditorSearchView: FC<FurniEditorSearchViewProps> = props =>
|
||||
{
|
||||
const { items, total, page, loading, onSearch, onSelect } = props;
|
||||
const [ query, setQuery ] = useState('');
|
||||
const [ typeFilter, setTypeFilter ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
onSearch('', '', 1);
|
||||
}, []);
|
||||
|
||||
const handleSearch = useCallback(() =>
|
||||
{
|
||||
onSearch(query, typeFilter, 1);
|
||||
}, [ query, typeFilter, onSearch ]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) =>
|
||||
{
|
||||
if(e.key === 'Enter') handleSearch();
|
||||
}, [ handleSearch ]);
|
||||
|
||||
const totalPages = Math.ceil(total / 20);
|
||||
|
||||
return (
|
||||
<Column gap={ 1 } className="h-full">
|
||||
<Flex gap={ 1 } alignItems="end">
|
||||
<Column gap={ 0 } className="flex-1">
|
||||
<Text small bold>Search</Text>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="ID, name or sprite ID..."
|
||||
value={ query }
|
||||
onChange={ e => setQuery(e.target.value) }
|
||||
onKeyDown={ handleKeyDown }
|
||||
/>
|
||||
</Column>
|
||||
<Column gap={ 0 } className="w-[80px]">
|
||||
<Text small bold>Type</Text>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={ typeFilter }
|
||||
onChange={ e => setTypeFilter(e.target.value) }
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="s">Floor (s)</option>
|
||||
<option value="i">Wall (i)</option>
|
||||
</select>
|
||||
</Column>
|
||||
<Button variant="primary" disabled={ loading } onClick={ handleSearch }>
|
||||
{ loading ? '...' : 'Search' }
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Column gap={ 0 } className="flex-1 overflow-auto border border-[#ccc] rounded bg-white">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="bg-[#e8e8e8] sticky top-0">
|
||||
<th className="px-2 py-1 text-left">ID</th>
|
||||
<th className="px-2 py-1 text-left">Sprite</th>
|
||||
<th className="px-2 py-1 text-left">Name</th>
|
||||
<th className="px-2 py-1 text-left">Public Name</th>
|
||||
<th className="px-2 py-1 text-center">Type</th>
|
||||
<th className="px-2 py-1 text-left">Interaction</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ items.map(item => (
|
||||
<tr
|
||||
key={ item.id }
|
||||
className="cursor-pointer hover:bg-[#d4edfa] border-b border-[#eee] transition-colors"
|
||||
onClick={ () => onSelect(item.id) }
|
||||
>
|
||||
<td className="px-2 py-1 font-mono">{ item.id }</td>
|
||||
<td className="px-2 py-1 font-mono">{ item.spriteId }</td>
|
||||
<td className="px-2 py-1 truncate max-w-[120px]">{ item.itemName }</td>
|
||||
<td className="px-2 py-1 truncate max-w-[120px]">{ item.publicName }</td>
|
||||
<td className="px-2 py-1 text-center">
|
||||
<span className={ `px-1 rounded text-white text-[10px] ${ item.type === 's' ? 'bg-[#1e7295]' : 'bg-[#6b7280]' }` }>
|
||||
{ item.type === 's' ? 'Floor' : 'Wall' }
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-1 text-[10px]">{ item.interactionType || '-' }</td>
|
||||
</tr>
|
||||
)) }
|
||||
{ items.length === 0 && !loading &&
|
||||
<tr>
|
||||
<td colSpan={ 6 } className="px-2 py-4 text-center text-[#999]">No items found</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</Column>
|
||||
|
||||
{ totalPages > 1 &&
|
||||
<Flex gap={ 1 } justifyContent="between" alignItems="center">
|
||||
<Text small variant="gray">
|
||||
{ total } items - Page { page }/{ totalPages }
|
||||
</Text>
|
||||
<Flex gap={ 1 }>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={ page <= 1 }
|
||||
onClick={ () => onSearch(query, typeFilter, page - 1) }
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={ page >= totalPages }
|
||||
onClick={ () => onSearch(query, typeFilter, page + 1) }
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -552,7 +552,21 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
{ godMode &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
{ canSeeFurniId && <Text small wrap variant="white">ID: { avatarInfo.id }</Text> }
|
||||
{ canSeeFurniId &&
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
|
||||
<path fillRule="evenodd" d="M4.93 1.31a41.401 41.401 0 0 1 10.14 0C16.194 1.45 17 2.414 17 3.517V18.25a.75.75 0 0 1-1.075.676l-2.8-1.344-2.8 1.344a.75.75 0 0 1-.65 0l-2.8-1.344-2.8 1.344A.75.75 0 0 1 3 18.25V3.517c0-1.103.806-2.068 1.93-2.207Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<Text small wrap variant="white">ID: { avatarInfo.id }</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
|
||||
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
|
||||
</svg>
|
||||
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text>
|
||||
</div>
|
||||
</div> }
|
||||
{ (!avatarInfo.isWallItem && canMove) &&
|
||||
<>
|
||||
<button
|
||||
@@ -560,6 +574,19 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
onClick={ () => setDropdownOpen(!dropdownOpen) }>
|
||||
{ dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` }
|
||||
</button>
|
||||
<button
|
||||
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
|
||||
onClick={ () =>
|
||||
{
|
||||
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
|
||||
const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
|
||||
|
||||
CreateLinkEvent('furni-editor/show');
|
||||
|
||||
if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } }));
|
||||
} }>
|
||||
Edit Furni
|
||||
</button>
|
||||
{ dropdownOpen &&
|
||||
<div className="flex gap-[4px] w-full">
|
||||
{ /* Left panel: position + rotation */ }
|
||||
|
||||
@@ -69,9 +69,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
||||
</motion.div> )}
|
||||
</AnimatePresence>
|
||||
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 } justifyContent="between">
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 }>
|
||||
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
|
||||
<Flex center pointer className={ 'relative w-[50px] h-[45px] overflow-hidden ' + (isMeExpanded ? 'active ' : '') } onClick={ event =>
|
||||
{
|
||||
setMeExpanded(!isMeExpanded);
|
||||
@@ -97,10 +96,11 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<ToolbarItemView icon="camera" onClick={ event => CreateLinkEvent('camera/toggle') } /> }
|
||||
{ isMod &&
|
||||
<ToolbarItemView icon="modtools" onClick={ event => CreateLinkEvent('mod-tools/toggle') } /> }
|
||||
{ isMod &&
|
||||
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('furni-editor/toggle') } /> }
|
||||
</Flex>
|
||||
<Flex alignItems="center" id="toolbar-chat-input-container" />
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Flex alignItems="center" justifyContent="center" className="flex-1 min-w-0 max-w-[600px] mx-auto" id="toolbar-chat-input-container" />
|
||||
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
|
||||
<Flex gap={ 2 }>
|
||||
<ToolbarItemView icon="friendall" onClick={ event => CreateLinkEvent('friends/toggle') }>
|
||||
{ (requests.length > 0) &&
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useFurniEditor';
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export interface FurniItem
|
||||
{
|
||||
id: number;
|
||||
spriteId: number;
|
||||
itemName: string;
|
||||
publicName: string;
|
||||
type: string;
|
||||
width: number;
|
||||
length: number;
|
||||
stackHeight: number;
|
||||
allowStack: boolean;
|
||||
allowWalk: boolean;
|
||||
allowSit: boolean;
|
||||
allowLay: boolean;
|
||||
interactionType: string;
|
||||
interactionModesCount: number;
|
||||
}
|
||||
|
||||
export interface FurniDetail extends FurniItem
|
||||
{
|
||||
allowGift: boolean;
|
||||
allowTrade: boolean;
|
||||
allowRecycle: boolean;
|
||||
allowMarketplaceSell: boolean;
|
||||
allowInventoryStack: boolean;
|
||||
vendingIds: string;
|
||||
customparams: string;
|
||||
effectIdMale: number;
|
||||
effectIdFemale: number;
|
||||
clothingOnWalk: string;
|
||||
multiheight: string;
|
||||
description: string;
|
||||
usageCount: number;
|
||||
}
|
||||
|
||||
export interface CatalogRef
|
||||
{
|
||||
id: number;
|
||||
catalogName: string;
|
||||
costCredits: number;
|
||||
costPoints: number;
|
||||
pointsType: number;
|
||||
pageId: number;
|
||||
pageName: string;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/admin/furni-editor';
|
||||
|
||||
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T>
|
||||
{
|
||||
const res = await fetch(url, { credentials: 'include', ...options });
|
||||
const data = await res.json();
|
||||
|
||||
if(!res.ok || data.error) throw new Error(data.error || 'API error');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useFurniEditor = () =>
|
||||
{
|
||||
const [ items, setItems ] = useState<FurniItem[]>([]);
|
||||
const [ total, setTotal ] = useState(0);
|
||||
const [ page, setPage ] = useState(1);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ error, setError ] = useState<string | null>(null);
|
||||
const [ selectedItem, setSelectedItem ] = useState<FurniDetail | null>(null);
|
||||
const [ catalogItems, setCatalogItems ] = useState<CatalogRef[]>([]);
|
||||
const [ interactions, setInteractions ] = useState<string[]>([]);
|
||||
const [ furniDataEntry, setFurniDataEntry ] = useState<Record<string, unknown> | null>(null);
|
||||
|
||||
const clearError = useCallback(() => setError(null), []);
|
||||
|
||||
const searchItems = useCallback(async (query: string, type: string, pg: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) });
|
||||
|
||||
if(type) params.set('type', type);
|
||||
|
||||
const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`);
|
||||
|
||||
setItems(data.items);
|
||||
setTotal(data.total);
|
||||
setPage(data.page);
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadDetail = useCallback(async (id: number): Promise<boolean> =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record<string, unknown> | null }>(`${ API_BASE }/detail?id=${ id }`);
|
||||
|
||||
setSelectedItem(data.item);
|
||||
setCatalogItems(data.catalogItems);
|
||||
setFurniDataEntry(data.furniDataEntry);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateItem = useCallback(async (id: number, fields: Record<string, unknown>) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
await apiFetch(`${ API_BASE }/update?id=${ id }`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fields)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createItem = useCallback(async (fields: Record<string, unknown>) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ id: number }>(`${ API_BASE }`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fields)
|
||||
});
|
||||
|
||||
return data.id;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteItem = useCallback(async (id: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' });
|
||||
|
||||
return true;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadInteractions = useCallback(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ interactions: Array<string | { name: string }> }>(`${ API_BASE }/interactions`);
|
||||
|
||||
setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name));
|
||||
}
|
||||
catch {}
|
||||
}, []);
|
||||
|
||||
const loadBySpriteId = useCallback(async (spriteId: number): Promise<boolean> =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`);
|
||||
|
||||
return await loadDetail(data.id);
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
}, [ loadDetail ]);
|
||||
|
||||
return {
|
||||
items, total, page, loading, error, clearError,
|
||||
selectedItem, setSelectedItem, catalogItems, furniDataEntry,
|
||||
interactions,
|
||||
searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions
|
||||
};
|
||||
};
|
||||
+36
-2
@@ -3,12 +3,46 @@ import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
const renderer3 = resolve(__dirname, '..', 'renderer3');
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [ react(), tsconfigPaths() ],
|
||||
server: {
|
||||
fs: {
|
||||
allow: [
|
||||
resolve(__dirname), // nitro3 itself
|
||||
renderer3, // renderer3 source + packages
|
||||
]
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'~': resolve(__dirname, 'node_modules')
|
||||
'~': resolve(__dirname, 'node_modules'),
|
||||
// Renderer3 workspace packages → point to their src/index.ts
|
||||
'@nitrots/api': resolve(renderer3, 'packages/api/src/index.ts'),
|
||||
'@nitrots/assets': resolve(renderer3, 'packages/assets/src/index.ts'),
|
||||
'@nitrots/avatar': resolve(renderer3, 'packages/avatar/src/index.ts'),
|
||||
'@nitrots/camera': resolve(renderer3, 'packages/camera/src/index.ts'),
|
||||
'@nitrots/communication': resolve(renderer3, 'packages/communication/src/index.ts'),
|
||||
'@nitrots/configuration': resolve(renderer3, 'packages/configuration/src/index.ts'),
|
||||
'@nitrots/events': resolve(renderer3, 'packages/events/src/index.ts'),
|
||||
'@nitrots/localization': resolve(renderer3, 'packages/localization/src/index.ts'),
|
||||
'@nitrots/room': resolve(renderer3, 'packages/room/src/index.ts'),
|
||||
'@nitrots/session': resolve(renderer3, 'packages/session/src/index.ts'),
|
||||
'@nitrots/sound': resolve(renderer3, 'packages/sound/src/index.ts'),
|
||||
'@nitrots/utils/src': resolve(renderer3, 'packages/utils/src'),
|
||||
'@nitrots/utils': resolve(renderer3, 'packages/utils/src/index.ts'),
|
||||
// Resolve pixi.js and pixi-filters from renderer3's node_modules
|
||||
'pixi.js': resolve(renderer3, 'node_modules/pixi.js'),
|
||||
'pixi-filters': resolve(renderer3, 'node_modules/pixi-filters'),
|
||||
'howler': resolve(renderer3, 'node_modules/howler'),
|
||||
}
|
||||
},
|
||||
build: {
|
||||
@@ -21,7 +55,7 @@ export default defineConfig({
|
||||
{
|
||||
if(id.includes('node_modules'))
|
||||
{
|
||||
if(id.includes('@nitrots/nitro-renderer')) return 'nitro-renderer';
|
||||
if(id.includes('@nitrots/nitro-renderer') || id.includes('renderer3')) return 'nitro-renderer';
|
||||
|
||||
return 'vendor';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user