2024-08-12 01:13:42 +00:00
|
|
|
import { TricksterCharacter } from "../lib/trickster"
|
|
|
|
|
import { useSessionContext } from "../context/SessionContext"
|
|
|
|
|
|
|
|
|
|
import 'handsontable/dist/handsontable.full.min.css';
|
|
|
|
|
|
|
|
|
|
import { registerAllModules } from 'handsontable/registry';
|
|
|
|
|
import { HotTable, HotTableClass } from '@handsontable/react';
|
2025-05-13 21:02:59 +00:00
|
|
|
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState} from "react";
|
2024-08-12 01:13:42 +00:00
|
|
|
import { InventoryTable } from "../lib/table";
|
|
|
|
|
import { DotLoader } from "react-spinners";
|
2025-05-13 21:02:59 +00:00
|
|
|
import { useResizeObserver } from "@mantine/hooks";
|
2024-08-12 01:13:42 +00:00
|
|
|
import { Columns } from "../lib/columns";
|
|
|
|
|
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
|
|
|
|
|
import log from "loglevel";
|
2025-05-13 21:02:59 +00:00
|
|
|
import { charactersAtom, currentCharacterInventoryAtom, currentCharacterItemsAtom, LTOApi, selectedTargetInventoryAtom } from "../state/atoms";
|
|
|
|
|
import Select from 'react-select';
|
|
|
|
|
import { useAtom, useAtomValue } from "jotai";
|
|
|
|
|
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, size, useDismiss, useFloating, useInteractions, useListNavigation, useRole } from "@floating-ui/react";
|
|
|
|
|
import Fuse from "fuse.js";
|
2024-08-12 01:13:42 +00:00
|
|
|
|
|
|
|
|
registerAllModules();
|
|
|
|
|
type Size = {
|
|
|
|
|
width?: number
|
|
|
|
|
height?: number
|
|
|
|
|
}
|
2025-05-13 21:02:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface InventoryItemProps {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
active: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InventoryItem = forwardRef<
|
|
|
|
|
HTMLDivElement,
|
|
|
|
|
InventoryItemProps & React.HTMLProps<HTMLDivElement>
|
|
|
|
|
>(({ children, active, ...rest }, ref) => {
|
|
|
|
|
const id = useId();
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
role="option"
|
|
|
|
|
id={id}
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
{...rest}
|
|
|
|
|
style={{
|
|
|
|
|
background: active ? "lightblue" : "none",
|
|
|
|
|
padding: 4,
|
|
|
|
|
cursor: "default",
|
|
|
|
|
...rest.style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const InventoryTargetSelector = () => {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const [inputValue, setInputValue] = useState("");
|
|
|
|
|
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const listRef = useRef<Array<HTMLElement | null>>([]);
|
|
|
|
|
|
|
|
|
|
const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({
|
|
|
|
|
whileElementsMounted: autoUpdate,
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange: setOpen,
|
|
|
|
|
middleware: [
|
|
|
|
|
flip({ padding: 10 }),
|
|
|
|
|
size({
|
|
|
|
|
apply({ rects, availableHeight, elements }) {
|
|
|
|
|
Object.assign(elements.floating.style, {
|
|
|
|
|
width: `${rects.reference.width}px`,
|
|
|
|
|
maxHeight: `${availableHeight}px`,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
padding: 10,
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const role = useRole(context, { role: "listbox" });
|
|
|
|
|
const dismiss = useDismiss(context);
|
|
|
|
|
const listNav = useListNavigation(context, {
|
|
|
|
|
listRef,
|
|
|
|
|
activeIndex,
|
|
|
|
|
onNavigate: setActiveIndex,
|
|
|
|
|
virtual: true,
|
|
|
|
|
loop: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
|
|
|
|
[role, dismiss, listNav]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
|
|
|
const value = event.target.value;
|
|
|
|
|
setInputValue(value);
|
|
|
|
|
setSelectedTargetInventory(undefined)
|
|
|
|
|
|
|
|
|
|
if (value) {
|
|
|
|
|
setOpen(true);
|
|
|
|
|
setActiveIndex(0);
|
|
|
|
|
} else {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const { data: subaccounts } = useAtomValue(charactersAtom)
|
|
|
|
|
|
|
|
|
|
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
|
|
|
|
|
|
|
|
|
|
const searcher = useMemo(()=>{
|
|
|
|
|
return new Fuse(subaccounts?.flatMap(x=>[
|
|
|
|
|
x.bank,
|
|
|
|
|
x.character,
|
|
|
|
|
])||[], {
|
|
|
|
|
keys:["path","name"],
|
|
|
|
|
findAllMatches: true,
|
|
|
|
|
threshold: 0.8,
|
|
|
|
|
useExtendedSearch: true,
|
|
|
|
|
})
|
|
|
|
|
}, [subaccounts])
|
|
|
|
|
|
|
|
|
|
const items = searcher.search(inputValue || "!-", {limit: 10}).map(x=>x.item)
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<input
|
|
|
|
|
className="border border-black-1 bg-gray-100 placeholder-gray-600"
|
|
|
|
|
{...getReferenceProps({
|
|
|
|
|
ref: refs.setReference,
|
|
|
|
|
onChange,
|
|
|
|
|
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
|
|
|
|
|
placeholder: "Target Inventory",
|
|
|
|
|
"aria-autocomplete": "list",
|
|
|
|
|
onKeyDown(event) {
|
|
|
|
|
if (
|
|
|
|
|
event.key === "Enter" &&
|
|
|
|
|
activeIndex != null &&
|
|
|
|
|
items[activeIndex]
|
|
|
|
|
) {
|
|
|
|
|
setSelectedTargetInventory(items[activeIndex])
|
|
|
|
|
setInputValue(items[activeIndex].name);
|
|
|
|
|
setActiveIndex(null);
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
{open && (
|
|
|
|
|
<FloatingPortal>
|
|
|
|
|
<FloatingFocusManager
|
|
|
|
|
context={context}
|
|
|
|
|
initialFocus={-1}
|
|
|
|
|
visuallyHiddenDismiss
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
{...getFloatingProps({
|
|
|
|
|
ref: refs.setFloating,
|
|
|
|
|
style: {
|
|
|
|
|
...floatingStyles,
|
|
|
|
|
background: "#eee",
|
|
|
|
|
color: "black",
|
|
|
|
|
overflowY: "auto",
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
{items.map((item, index) => (
|
|
|
|
|
<InventoryItem
|
|
|
|
|
{...getItemProps({
|
|
|
|
|
key: item.path,
|
|
|
|
|
ref(node) {
|
|
|
|
|
listRef.current[index] = node;
|
|
|
|
|
},
|
|
|
|
|
onClick() {
|
|
|
|
|
setInputValue(item.name);
|
|
|
|
|
setSelectedTargetInventory(item);
|
|
|
|
|
setOpen(false);
|
|
|
|
|
refs.domReference.current?.focus();
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
active={activeIndex === index}
|
|
|
|
|
>
|
|
|
|
|
{item.name}
|
|
|
|
|
</InventoryItem>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</FloatingFocusManager>
|
|
|
|
|
</FloatingPortal>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-08-12 01:13:42 +00:00
|
|
|
export const Inventory = () => {
|
|
|
|
|
const {activeTable, columns, tags, orders} = useSessionContext()
|
|
|
|
|
|
2025-05-13 21:02:59 +00:00
|
|
|
const [ref, {height}] = useResizeObserver({})
|
2024-08-12 01:13:42 +00:00
|
|
|
|
2025-05-13 21:02:59 +00:00
|
|
|
const {data:character, isLoading, isFetching } = useAtomValue(currentCharacterInventoryAtom)
|
|
|
|
|
//const sendOrders = useCallback(()=>{
|
|
|
|
|
// if(!hotTableComponent.current?.hotInstance){
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
// const hott = hotTableComponent.current?.hotInstance
|
|
|
|
|
// const headers = hott.getColHeader()
|
|
|
|
|
// const dat = hott.getData()
|
|
|
|
|
// const idxNumber = headers.indexOf(Columns.MoveCount.displayName)
|
|
|
|
|
// const idxTarget = headers.indexOf(Columns.Move.displayName)
|
|
|
|
|
// const origin = activeTable
|
|
|
|
|
// const pending:OrderDetails[] = [];
|
|
|
|
|
// for(const row of dat) {
|
|
|
|
|
// try{
|
|
|
|
|
// const nm = Number(row[idxNumber].replace("x",""))
|
|
|
|
|
// const target = (row[idxTarget] as string).replaceAll("-","").trim()
|
|
|
|
|
// if(!isNaN(nm) && nm > 0 && target.length > 0){
|
|
|
|
|
// const info:OrderDetails = {
|
|
|
|
|
// item_uid: row[0].toString(),
|
|
|
|
|
// count: nm,
|
|
|
|
|
// origin_path: activeTable,
|
|
|
|
|
// target_path: target,
|
|
|
|
|
// }
|
|
|
|
|
// pending.push(info)
|
|
|
|
|
// }
|
|
|
|
|
// }catch(e){
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// log.debug("OrderDetails", pending)
|
|
|
|
|
// const chars = new Map<string,TricksterCharacter>()
|
|
|
|
|
// const manager = new OrderSender(orders, chars)
|
|
|
|
|
// for(const d of pending){
|
|
|
|
|
// const order = manager.send(d)
|
|
|
|
|
// //order.tick(api)
|
|
|
|
|
// }
|
|
|
|
|
//}, [orders])
|
2024-08-12 01:13:42 +00:00
|
|
|
|
|
|
|
|
const Loading = ()=>{
|
|
|
|
|
return <div role="status" className="flex align-center justify-center">
|
|
|
|
|
<div className="justify-center py-4">
|
|
|
|
|
<DotLoader color="#dddddd"/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2025-05-13 21:02:59 +00:00
|
|
|
|
|
|
|
|
const items = useAtomValue(currentCharacterItemsAtom)
|
|
|
|
|
|
2024-08-12 01:13:42 +00:00
|
|
|
return <div ref={ref} className={``}>
|
2025-05-13 21:02:59 +00:00
|
|
|
<div className="flex flex-row py-2 justify-end">
|
|
|
|
|
<InventoryTargetSelector/>
|
2024-08-12 01:13:42 +00:00
|
|
|
<div
|
|
|
|
|
onClick={(e)=>{
|
2025-05-13 21:02:59 +00:00
|
|
|
// sendOrders()
|
2024-08-12 01:13:42 +00:00
|
|
|
}}
|
|
|
|
|
className="
|
|
|
|
|
hover:cursor-pointer
|
|
|
|
|
border border-black-1
|
|
|
|
|
bg-green-200
|
|
|
|
|
px-2 py-1
|
2025-05-13 21:02:59 +00:00
|
|
|
">Move Selected</div>
|
2024-08-12 01:13:42 +00:00
|
|
|
</div>
|
2025-05-13 21:02:59 +00:00
|
|
|
{(isLoading || isFetching) ? <Loading/> : <>
|
|
|
|
|
<div>
|
|
|
|
|
total: {items.size}
|
|
|
|
|
</div>
|
|
|
|
|
</> }
|
2024-08-12 01:13:42 +00:00
|
|
|
</div>
|
|
|
|
|
}
|