This commit is contained in:
a 2025-06-20 00:41:10 -05:00
parent 8c9437c0be
commit bd20e23b15
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
42 changed files with 10412 additions and 6466 deletions

7
.biomeignore Normal file
View File

@ -0,0 +1,7 @@
dist
**/vendor/**
**/locales/**
generated.*
node_modules
*.min.js
*.min.css

59
biome.json Normal file
View File

@ -0,0 +1,59 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"files": {
"ignoreUnknown": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noUselessElse": "error",
"useConst": "warn",
"useImportType": "off",
"useNodejsImportProtocol": "off"
},
"suspicious": {
"noConsole": "error",
"noRedeclare": "off",
"noDoubleEquals": "warn",
"noExplicitAny": "off"
},
"correctness": {
"noUndeclaredVariables": "off",
"useExhaustiveDependencies": "off",
"noUnusedImports": "warn"
},
"complexity": {
"noExtraBooleanCast": "warn",
"noBannedTypes": "off"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
},
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"semicolons": "asNeeded",
"trailingCommas": "all",
"arrowParentheses": "asNeeded"
}
},
"json": {
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
}
}
}

View File

@ -1,54 +0,0 @@
module.exports = {
settings: {
react: {
version: 'detect',
},
},
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:react/jsx-runtime',
],
ignorePatterns: ['dist', '.eslintrc.cjs', '**/vendor/**', '**/locales/**', 'generated.*'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
},
},
plugins: ['react-refresh', 'react'],
rules: {
'no-extra-semi': 'off', // this one needs to stay off
'@react/no-children-prop': 'off', // tanstack form uses this as a pattern
'react/no-children-prop': 'off', // tanstack form uses this as a pattern
'no-duplicate-imports': 'warn',
'@typescript-eslint/no-extra-semi': 'off',
'sort-imports': 'off',
'react-hooks/exhaustive-deps': 'off',
'react-refresh/only-export-components': 'off',
'no-case-declarations': 'off',
'no-redeclare': 'off',
'no-undef': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/strict-boolean-expressions': ['error', {
"allowString": true,
"allowNumber": true,
"allowNullableObject": true,
"allowNullableBoolean": true,
"allowNullableString": true,
"allowNullableNumber": false,
"allowNullableEnum": true,
"allowAny": true
}],
'react/prop-types': 'off',
'no-lonely-if': 2,
'no-console': 2,
},
}

View File

@ -2,31 +2,29 @@
"name": "lifeto-shop", "name": "lifeto-shop",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write ."
}, },
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.27.8", "@floating-ui/react": "^0.27.8",
"@handsontable/react": "^15.3.0", "@handsontable/react": "^15.3.0",
"@mantine/hooks": "^8.0.0", "@mantine/hooks": "^8.0.0",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.76.0", "@tanstack/react-query": "^5.76.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@types/qs": "^6.9.18", "@types/qs": "^6.9.18",
"@types/react": "^19.1.4", "@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
"arktype": "^2.1.20", "arktype": "^2.1.20",
"axios": "^1.9.0", "axios": "^1.9.0",
"eslint": "^9.26.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"handsontable": "^15.3.0", "handsontable": "^15.3.0",
"jotai": "^2.12.4", "jotai": "^2.12.4",
@ -49,6 +47,7 @@
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.0.0",
"@tailwindcss/postcss": "^4.1.6", "@tailwindcss/postcss": "^4.1.6",
"@types/node": "^22.15.18", "@types/node": "^22.15.18",
"postcss": "^8.5.3", "postcss": "^8.5.3",

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,23 +1,23 @@
import { FC } from "react"; import { FC } from 'react'
import { LoginWidget } from "./components/login"; import { CharacterRoulette } from './components/characters'
import { CharacterRoulette } from "./components/characters"; import { Inventory } from './components/inventory/index'
import { Inventory } from "./components/inventory/index"; import { LoginWidget } from './components/login'
export const App: FC = () => { export const App: FC = () => {
return ( return (
<> <>
<div className="flex flex-row mx-auto p-4 gap-8 w-full h-full"> <div className="flex flex-row mx-auto p-4 gap-8 w-full h-full">
<div className="flex flex-col"> <div className="flex flex-col">
<LoginWidget/> <LoginWidget />
<CharacterRoulette/> <CharacterRoulette />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Inventory/> <Inventory />
</div> </div>
</div> </div>
</> </>
); )
}; }
/* /*
<div className="flex flex-col p-4 h-full"> <div className="flex flex-col p-4 h-full">

View File

@ -1,123 +1,122 @@
import { TricksterCharacter } from "../lib/trickster"
import Fuse from 'fuse.js'
import { useAtom } from "jotai"
import { charactersAtom, selectedCharacterAtom } from "../state/atoms"
import { useMemo, useState } from "react";
import { import {
useFloating,
autoUpdate, autoUpdate,
offset, FloatingPortal,
flip, flip,
offset,
shift, shift,
useHover,
useFocus,
useDismiss, useDismiss,
useRole, useFloating,
useFocus,
useHover,
useInteractions, useInteractions,
FloatingPortal useRole,
} from "@floating-ui/react"; } from '@floating-ui/react'
import Fuse from 'fuse.js'
import { useAtom } from 'jotai'
import { useMemo, useState } from 'react'
import { TricksterCharacter } from '../lib/trickster'
import { charactersAtom, selectedCharacterAtom } from '../state/atoms'
export const CharacterCard = ({character}:{ export const CharacterCard = ({ character }: { character: TricksterCharacter }) => {
character: TricksterCharacter, const [isOpen, setIsOpen] = useState(false)
})=>{
const [isOpen, setIsOpen] = useState(false);
const { refs, floatingStyles, context } = useFloating({ const { refs, floatingStyles, context } = useFloating({
open: isOpen, open: isOpen,
onOpenChange: setIsOpen, onOpenChange: setIsOpen,
placement: "top", placement: 'top',
// Make sure the tooltip stays on the screen // Make sure the tooltip stays on the screen
whileElementsMounted: autoUpdate, whileElementsMounted: autoUpdate,
middleware: [ middleware: [
offset(5), offset(5),
flip({ flip({
fallbackAxisSideDirection: "start" fallbackAxisSideDirection: 'start',
}), }),
shift() shift(),
] ],
}); })
// Event listeners to change the open state // Event listeners to change the open state
const hover = useHover(context, { move: false }); const hover = useHover(context, { move: false })
const focus = useFocus(context); const focus = useFocus(context)
const dismiss = useDismiss(context); const dismiss = useDismiss(context)
// Role props for screen readers // Role props for screen readers
const role = useRole(context, { role: "tooltip" }); const role = useRole(context, { role: 'tooltip' })
// Merge all the interactions into prop getters // Merge all the interactions into prop getters
const { getReferenceProps, getFloatingProps } = useInteractions([ const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role])
hover,
focus,
dismiss,
role
]);
const [selectedCharacter, setSelectedCharacter] = useAtom(selectedCharacterAtom) const [selectedCharacter, setSelectedCharacter] = useAtom(selectedCharacterAtom)
return (
<>
return <> <div
<div onClick={()=>{ onClick={() => {
setSelectedCharacter(character) setSelectedCharacter(character)
}} }}
ref={refs.setReference}
ref={refs.setReference} {...getReferenceProps()} {...getReferenceProps()}
className={` className={`
flex flex-col border border-black flex flex-col border border-black
hover:cursor-pointer hover:cursor-pointer
hover:bg-blue-100 hover:bg-blue-100
p-2 ${character.path === selectedCharacter?.path? `bg-blue-200 hover:bg-blue-100` : ""}`}> p-2 ${character.path === selectedCharacter?.path ? `bg-blue-200 hover:bg-blue-100` : ''}`}
<div className="flex flex-col justify-between h-full"> >
<div className="flex flex-col gap-2"> <div className="flex flex-col justify-between h-full">
<div className="flex flex-row justify-center" <div className="flex flex-col gap-2">
> <div className="flex flex-row justify-center">
{character.base_job === -8 ? {character.base_job === -8 ? (
<img <img className="h-8" src="https://beta.lifeto.co/item_img/gel.nri.003.000.png" />
className="h-8" ) : (
src="https://beta.lifeto.co/item_img/gel.nri.003.000.png" <img
/> className="h-16"
: src={`https://knowledge.lifeto.co/animations/character/chr${(
<img character.current_job -
className="h-16" character.base_job -
src={`https://knowledge.lifeto.co/animations/character/chr${ 1
(character.current_job - character.base_job - 1).toString().padStart(3,"0") )
}_13.png`}/> .toString()
} .padStart(3, '0')}_13.png`}
</div> />
<FloatingPortal> )}
{isOpen && ( </div>
<div <FloatingPortal>
className="Tooltip" {isOpen && (
ref={refs.setFloating} <div
style={floatingStyles} className="Tooltip"
{...getFloatingProps()} ref={refs.setFloating}
> style={floatingStyles}
<div className="flex flex-col gap-1 bg-white"> {...getFloatingProps()}
{character.base_job === -8 ? "bank" : character.name} >
<div className="flex flex-col gap-1 bg-white">
{character.base_job === -8 ? 'bank' : character.name}
</div>
</div> </div>
</div> )}
)} </FloatingPortal>
</FloatingPortal> </div>
</div> </div>
</div> </div>
</div> </>
</> )
} }
const PleaseLogin = () => { const PleaseLogin = () => {
return <><div className="align-center">no characters (not logged in?)</div></> return (
<>
<div className="align-center">no characters (not logged in?)</div>
</>
)
} }
export const CharacterRoulette = ()=>{ export const CharacterRoulette = () => {
const [{data: rawCharacters}] = useAtom(charactersAtom) const [{ data: rawCharacters }] = useAtom(charactersAtom)
const [search, setSearch] = useState("") const [search, setSearch] = useState('')
const { characters, fuse } = useMemo(()=>{ const { characters, fuse } = useMemo(() => {
if(!rawCharacters) { if (!rawCharacters) {
return { return {
characters: [], characters: [],
fuse: new Fuse([], {}) fuse: new Fuse([], {}),
} }
} }
// transform characters into pairs between the bank and not bank // transform characters into pairs between the bank and not bank
@ -127,37 +126,40 @@ export const CharacterRoulette = ()=>{
findAllMatches: true, findAllMatches: true,
threshold: 0.8, threshold: 0.8,
useExtendedSearch: true, useExtendedSearch: true,
keys: ["character.name"], keys: ['character.name'],
}), }),
} }
}, [rawCharacters]) }, [rawCharacters])
if(!characters || characters.length == 0) { if (!characters || characters.length === 0) {
return <PleaseLogin/> return <PleaseLogin />
} }
const searchResults = fuse.search(search || "!-----", { const searchResults = fuse
limit: 20, .search(search || '!-----', {
}).map((x)=>{ limit: 20,
return <div className="flex flex-col" key={`${x.item.character.account_id}`}> })
<CharacterCard key={x.item.bank.id} character={x.item.bank} /> .map(x => {
<CharacterCard key={x.item.character.id} character={x.item.character} /> return (
</div> <div className="flex flex-col" key={`${x.item.character.account_id}`}>
}) <CharacterCard key={x.item.bank.id} character={x.item.bank} />
return <> <CharacterCard key={x.item.character.id} character={x.item.character} />
<div className="flex flex-col gap-1"> </div>
<input )
className="border border-black-1 bg-gray-100 placeholder-gray-600 p-1 max-w-[180px]" })
placeholder="search character..." return (
value={search} <>
onChange={(e)=>{ <div className="flex flex-col gap-1">
setSearch(e.target.value) <input
}} className="border border-black-1 bg-gray-100 placeholder-gray-600 p-1 max-w-[180px]"
></input> placeholder="search character..."
<div className="flex flex-row flex-wrap overflow-x-scroll gap-1 h-full min-h-36 max-w-48"> value={search}
{searchResults ? searchResults : <> onChange={e => {
</>} setSearch(e.target.value)
}}
></input>
<div className="flex flex-row flex-wrap overflow-x-scroll gap-1 h-full min-h-36 max-w-48">
{searchResults ? searchResults : <></>}
</div>
</div> </div>
</div> </>
</> )
} }

View File

@ -1,11 +1,19 @@
import { clearItemSelectionActionAtom, currentCharacterItemsAtom, filteredCharacterItemsAtom, inventoryFilterAtom, inventoryItemsCurrentPageAtom, inventoryPageRangeAtom, itemSelectionSelectAllFilterActionAtom, itemSelectionSelectAllPageActionAtom, paginateInventoryActionAtom, preferenceInventorySearch, selectedCharacterAtom, setInventoryFilterTabActionAtom} from "@/state/atoms"; import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import {useAtom, useAtomValue, useSetAtom } from "jotai"; import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { InventoryTargetSelector } from './movetarget'; import {
import { InventoryTable } from './table'; clearItemSelectionActionAtom,
import { FaArrowLeft, FaArrowRight } from "react-icons/fa"; filteredCharacterItemsAtom,
inventoryFilterAtom,
inventoryPageRangeAtom,
itemSelectionSelectAllFilterActionAtom,
itemSelectionSelectAllPageActionAtom,
paginateInventoryActionAtom,
preferenceInventorySearch,
selectedCharacterAtom,
setInventoryFilterTabActionAtom,
} from '@/state/atoms'
import { InventoryTargetSelector } from './movetarget'
import { InventoryTable } from './table'
const sections = [ const sections = [
{ name: 'all', value: '' }, { name: 'all', value: '' },
@ -14,7 +22,6 @@ const sections = [
{ name: 'drill', value: '3' }, { name: 'drill', value: '3' },
{ name: 'pet', value: '4' }, { name: 'pet', value: '4' },
{ name: 'etc', value: '5' }, { name: 'etc', value: '5' },
] ]
const cardSections = [ const cardSections = [
@ -26,50 +33,59 @@ const cardSections = [
{ name: 'arcana', value: '15' }, { name: 'arcana', value: '15' },
] ]
const InventoryTabs = ()=> { const InventoryTabs = () => {
const inventoryFilter = useAtomValue(inventoryFilterAtom)
const inventoryFilter= useAtomValue(inventoryFilterAtom)
const setInventoryFilterTab = useSetAtom(setInventoryFilterTabActionAtom) const setInventoryFilterTab = useSetAtom(setInventoryFilterTabActionAtom)
const inventoryRange = useAtomValue(inventoryPageRangeAtom) const inventoryRange = useAtomValue(inventoryPageRangeAtom)
const items = useAtomValue(filteredCharacterItemsAtom) const items = useAtomValue(filteredCharacterItemsAtom)
console.log("items", items) const sharedStyle = 'hover:cursor-pointer hover:bg-gray-200 px-2 pr-4 border border-gray-200'
const sharedStyle = "hover:cursor-pointer hover:bg-gray-200 px-2 pr-4 border border-gray-200" const selectedStyle = 'bg-gray-200 border-b-2 border-black-1'
const selectedStyle = "bg-gray-200 border-b-2 border-black-1" return (
return <div className="flex flex-row gap-1 justify-between"> <div className="flex flex-row gap-1 justify-between">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
{sections.map(x=>{ {sections.map(x => {
return <div return (
onClick={()=>{ <div
setInventoryFilterTab(x.value) onClick={() => {
}} setInventoryFilterTab(x.value)
key={x.name} }}
className={`${sharedStyle} key={x.name}
${inventoryFilter.tab === x.value ? selectedStyle : ""}`} className={`${sharedStyle}
>{x.name}</div> ${inventoryFilter.tab === x.value ? selectedStyle : ''}`}
})} >
{x.name}
</div>
)
})}
</div>
<div className="flex flex-row gap-1">
{cardSections.map(x => {
return (
<div
onClick={() => {
setInventoryFilterTab(x.value)
}}
key={x.name}
className={`${sharedStyle}
${inventoryFilter.tab === x.value ? selectedStyle : ''}`}
>
{x.name}
</div>
)
})}
</div>
</div> </div>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1 items-center px-1 bg-yellow-100">
{cardSections.map(x=>{ <div className="whitespace-pre select-none">
return <div {inventoryRange.start}..{inventoryRange.end}/{items.length}{' '}
onClick={()=>{ </div>
setInventoryFilterTab(x.value)
}}
key={x.name}
className={`${sharedStyle}
${inventoryFilter.tab === x.value ? selectedStyle : ""}`}
>{x.name}</div>
})}
</div> </div>
</div> </div>
<div className="flex flex-row gap-1 items-center px-1 bg-yellow-100"> )
<div className="whitespace-pre select-none">{inventoryRange.start}..{inventoryRange.end}/{items.length} </div>
</div>
</div>
} }
export const Inventory = () => { export const Inventory = () => {
const selectedCharacter = useAtomValue(selectedCharacterAtom) const selectedCharacter = useAtomValue(selectedCharacterAtom)
const clearItemSelection = useSetAtom(clearItemSelectionActionAtom) const clearItemSelection = useSetAtom(clearItemSelectionActionAtom)
@ -77,73 +93,88 @@ export const Inventory = () => {
const addFilterItemSelection = useSetAtom(itemSelectionSelectAllFilterActionAtom) const addFilterItemSelection = useSetAtom(itemSelectionSelectAllFilterActionAtom)
const [search, setSearch] = useAtom(preferenceInventorySearch) const [search, setSearch] = useAtom(preferenceInventorySearch)
const paginateInventory = useSetAtom(paginateInventoryActionAtom) const paginateInventory = useSetAtom(paginateInventoryActionAtom)
if(!selectedCharacter){ if (!selectedCharacter) {
return <div> return <div>select a character</div>
select a character
</div>
} }
return <div className={`flex flex-col h-full w-full`}> return (
<div className="flex flex-col py-2 flex-0 justify-between h-full"> <div className={`flex flex-col h-full w-full`}>
<div className="flex flex-row justify-between"> <div className="flex flex-col py-2 flex-0 justify-between h-full">
<div className="flex flex-row gap-2"> <div className="flex flex-row justify-between">
<div className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300" <div className="flex flex-row gap-2">
onClick={()=>{ <div
addPageItemSelection() className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
}} onClick={() => {
>select filtered</div> addPageItemSelection()
<div className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300" }}
onClick={()=>{ >
addFilterItemSelection() select filtered
}} </div>
>select page</div> <div
<div className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300" className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
onClick={()=>{ onClick={() => {
clearItemSelection() addFilterItemSelection()
}} }}
>clear </div> >
select page
</div>
<div
className="whitespace-pre bg-blue-200 px-2 py-1 rounded-xl hover:cursor-pointer hover:bg-blue-300"
onClick={() => {
clearItemSelection()
}}
>
clear{' '}
</div>
</div>
<div className="flex flex-row">
<InventoryTargetSelector />
<div
onClick={_e => {
// sendOrders()
}}
className="hover:cursor-pointer whitespace-preborder border-black-1 bg-orange-200 hover:bg-orange-300 px-2 py-1"
>
Move Selected
</div>
</div>
</div> </div>
<div className="flex flex-row">
<InventoryTargetSelector/>
<div
onClick={(e)=>{
// sendOrders()
}}
className="hover:cursor-pointer whitespace-preborder border-black-1 bg-orange-200 hover:bg-orange-300 px-2 py-1">Move Selected</div>
</div>
</div>
<div className="flex flex-row gap-2 justify-between"> <div className="flex flex-row gap-2 justify-between">
<div className="flex flex-row gap-0 items-center"> <div className="flex flex-row gap-0 items-center">
<input <input
type="text" type="text"
value={search} value={search}
className="border border-black-1 px-2 py-1" className="border border-black-1 px-2 py-1"
placeholder="search..." placeholder="search..."
onChange={(e)=>{ onChange={e => {
setSearch(e.target.value) setSearch(e.target.value)
}} }}
/> />
<div <div
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center" className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center"
onClick={()=>{ onClick={() => {
paginateInventory(-1) paginateInventory(-1)
}} }}
><FaArrowLeft/></div> >
<div <FaArrowLeft />
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center" </div>
onClick={()=>{ <div
paginateInventory(1) className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 py-1 h-full flex items-center"
}} onClick={() => {
><FaArrowRight/></div> paginateInventory(1)
}}
>
<FaArrowRight />
</div>
</div>
</div> </div>
</div> </div>
<InventoryTabs />
<div className="flex flex-col flex-1 h-full border border-black-2">
<InventoryTable />
</div>
</div> </div>
<InventoryTabs /> )
<div className="flex flex-col flex-1 h-full border border-black-2">
<InventoryTable />
</div>
</div>
} }

View File

@ -1,22 +1,30 @@
import {
import { forwardRef, useId, useMemo, useRef, useState} from "react"; autoUpdate,
import { useAtom, useAtomValue } from "jotai"; FloatingFocusManager,
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, size, useDismiss, useFloating, useInteractions, useListNavigation, useRole } from "@floating-ui/react"; FloatingPortal,
import Fuse from "fuse.js"; flip,
import { charactersAtom, selectedTargetInventoryAtom } from "@/state/atoms"; size,
useDismiss,
useFloating,
useInteractions,
useListNavigation,
useRole,
} from '@floating-ui/react'
import Fuse from 'fuse.js'
import { useAtom, useAtomValue } from 'jotai'
import { forwardRef, useId, useMemo, useRef, useState } from 'react'
import { charactersAtom, selectedTargetInventoryAtom } from '@/state/atoms'
interface AccountInventorySelectorItemProps { interface AccountInventorySelectorItemProps {
children: React.ReactNode; children: React.ReactNode
active: boolean; active: boolean
} }
const AccountInventorySelectorItem = forwardRef< const AccountInventorySelectorItem = forwardRef<
HTMLDivElement, HTMLDivElement,
AccountInventorySelectorItemProps & React.HTMLProps<HTMLDivElement> AccountInventorySelectorItemProps & React.HTMLProps<HTMLDivElement>
>(({ children, active, ...rest }, ref) => { >(({ children, active, ...rest }, ref) => {
const id = useId(); const id = useId()
return ( return (
<div <div
ref={ref} ref={ref}
@ -25,22 +33,22 @@ const AccountInventorySelectorItem = forwardRef<
aria-selected={active} aria-selected={active}
{...rest} {...rest}
style={{ style={{
background: active ? "lightblue" : "none", background: active ? 'lightblue' : 'none',
padding: 4, padding: 4,
cursor: "default", cursor: 'default',
...rest.style, ...rest.style,
}} }}
> >
{children} {children}
</div> </div>
); )
}); })
export const InventoryTargetSelector = () => { export const InventoryTargetSelector = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState('')
const [activeIndex, setActiveIndex] = useState<number | null>(null); const [activeIndex, setActiveIndex] = useState<number | null>(null)
const listRef = useRef<Array<HTMLElement | null>>([]); const listRef = useRef<Array<HTMLElement | null>>([])
const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({ const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({
whileElementsMounted: autoUpdate, whileElementsMounted: autoUpdate,
@ -53,56 +61,55 @@ export const InventoryTargetSelector = () => {
Object.assign(elements.floating.style, { Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`, width: `${rects.reference.width}px`,
maxHeight: `${availableHeight}px`, maxHeight: `${availableHeight}px`,
}); })
}, },
padding: 10, padding: 10,
}), }),
], ],
}); })
const role = useRole(context, { role: "listbox" }); const role = useRole(context, { role: 'listbox' })
const dismiss = useDismiss(context); const dismiss = useDismiss(context)
const listNav = useListNavigation(context, { const listNav = useListNavigation(context, {
listRef, listRef,
activeIndex, activeIndex,
onNavigate: setActiveIndex, onNavigate: setActiveIndex,
virtual: true, virtual: true,
loop: true, loop: true,
}); })
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions( const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
[role, dismiss, listNav] role,
); dismiss,
listNav,
])
function onChange(event: React.ChangeEvent<HTMLInputElement>) { function onChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value; const value = event.target.value
setInputValue(value); setInputValue(value)
setSelectedTargetInventory(undefined) setSelectedTargetInventory(undefined)
if (value) { if (value) {
setOpen(true); setOpen(true)
setActiveIndex(0); setActiveIndex(0)
} else { } else {
setOpen(false); setOpen(false)
} }
} }
const { data: subaccounts } = useAtomValue(charactersAtom) const { data: subaccounts } = useAtomValue(charactersAtom)
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom) const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
const searcher = useMemo(()=>{ const searcher = useMemo(() => {
return new Fuse(subaccounts?.flatMap(x=>[ return new Fuse(subaccounts?.flatMap(x => [x.bank, x.character]) || [], {
x.bank, keys: ['path', 'name'],
x.character, findAllMatches: true,
])||[], { threshold: 0.8,
keys:["path","name"], useExtendedSearch: true,
findAllMatches: true, })
threshold: 0.8,
useExtendedSearch: true,
})
}, [subaccounts]) }, [subaccounts])
const items = searcher.search(inputValue || "!-", {limit: 10}).map(x=>x.item) const items = searcher.search(inputValue || '!-', { limit: 10 }).map(x => x.item)
return ( return (
<> <>
<input <input
@ -111,40 +118,32 @@ export const InventoryTargetSelector = () => {
ref: refs.setReference, ref: refs.setReference,
onChange, onChange,
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue, value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
placeholder: "Target Inventory", placeholder: 'Target Inventory',
"aria-autocomplete": "list", 'aria-autocomplete': 'list',
onFocus() { onFocus() {
setOpen(true); setOpen(true)
}, },
onKeyDown(event) { onKeyDown(event) {
if ( if (event.key === 'Enter' && activeIndex != null && items[activeIndex]) {
event.key === "Enter" &&
activeIndex != null &&
items[activeIndex]
) {
setSelectedTargetInventory(items[activeIndex]) setSelectedTargetInventory(items[activeIndex])
setInputValue(items[activeIndex].name); setInputValue(items[activeIndex].name)
setActiveIndex(null); setActiveIndex(null)
setOpen(false); setOpen(false)
} }
}, },
})} })}
/> />
{open && ( {open && (
<FloatingPortal> <FloatingPortal>
<FloatingFocusManager <FloatingFocusManager context={context} initialFocus={-1} visuallyHiddenDismiss>
context={context}
initialFocus={-1}
visuallyHiddenDismiss
>
<div <div
{...getFloatingProps({ {...getFloatingProps({
ref: refs.setFloating, ref: refs.setFloating,
style: { style: {
...floatingStyles, ...floatingStyles,
background: "#eee", background: '#eee',
color: "black", color: 'black',
overflowY: "auto", overflowY: 'auto',
}, },
})} })}
> >
@ -153,13 +152,13 @@ export const InventoryTargetSelector = () => {
{...getItemProps({ {...getItemProps({
key: item.path, key: item.path,
ref(node) { ref(node) {
listRef.current[index] = node; listRef.current[index] = node
}, },
onClick() { onClick() {
setInputValue(item.name); setInputValue(item.name)
setSelectedTargetInventory(item); setSelectedTargetInventory(item)
setOpen(false); setOpen(false)
refs.domReference.current?.focus(); refs.domReference.current?.focus()
}, },
})} })}
active={activeIndex === index} active={activeIndex === index}
@ -172,5 +171,5 @@ export const InventoryTargetSelector = () => {
</FloatingPortal> </FloatingPortal>
)} )}
</> </>
); )
} }

View File

@ -1,38 +1,29 @@
import { StatsColumns } from "@/lib/columns" import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { ItemWithSelection } from "@/lib/table/defs" import { atom, useAtomValue } from 'jotai'
import { InventoryColumns } from "@/lib/table/tanstack" import { useMemo } from 'react'
import { inventoryItemsCurrentPageAtom, preferenceInventoryTab } from "@/state/atoms" import { StatsColumns } from '@/lib/columns'
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" import { ItemWithSelection } from '@/lib/table/defs'
import { atom, useAtom, useAtomValue } from "jotai" import { InventoryColumns } from '@/lib/table/tanstack'
import { useMemo } from "react" import { inventoryItemsCurrentPageAtom, preferenceInventoryTab } from '@/state/atoms'
const columnVisibilityAtom = atom(get => {
const columnVisibilityAtom = atom((get)=>{
const itemTab = get(preferenceInventoryTab) const itemTab = get(preferenceInventoryTab)
if(!["2","4"].includes(itemTab)) { if (!['2', '4'].includes(itemTab)) {
return Object.fromEntries([ return Object.fromEntries([...StatsColumns.map(x => [`stats.${x}`, false]), ['slots', false]])
...StatsColumns.map(x=>["stats."+x,false]),
["slots",false]
])
}
return {
} }
return {}
}) })
export const InventoryTable = () => { export const InventoryTable = () => {
const items = useAtomValue(inventoryItemsCurrentPageAtom) const items = useAtomValue(inventoryItemsCurrentPageAtom)
const columns = useMemo(()=>{ const columns = useMemo(() => {
return [ return [...Object.values(InventoryColumns)]
...Object.values(InventoryColumns)
]
}, []) }, [])
const columnVisibility = useAtomValue(columnVisibilityAtom) const columnVisibility = useAtomValue(columnVisibilityAtom)
console.log(columnVisibility)
const table = useReactTable<ItemWithSelection>({ const table = useReactTable<ItemWithSelection>({
getRowId: row =>row.item.unique_id.toString(), getRowId: row => row.item.unique_id.toString(),
data: items, data: items,
state: { state: {
columnVisibility, columnVisibility,
@ -44,27 +35,20 @@ export const InventoryTable = () => {
return ( return (
<div className="overflow-y-auto h-full mb-32"> <div className="overflow-y-auto h-full mb-32">
<table <table
onContextMenu={(e)=>{ onContextMenu={e => {
e.preventDefault() e.preventDefault()
return return
}} }}
className="border-spacing-x-2 border-separate"> className="border-spacing-x-2 border-separate"
>
<thead className="sticky top-0 z-10 select-none bg-white"> <thead className="sticky top-0 z-10 select-none bg-white">
{table.getHeaderGroups().map(headerGroup => ( {table.getHeaderGroups().map(headerGroup => (
<tr <tr className="" key={headerGroup.id}>
className=""
key={headerGroup.id}>
{headerGroup.headers.map(header => ( {headerGroup.headers.map(header => (
<th <th key={header.id} className="text-left">
key={header.id}
className="text-left"
>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(header.column.columnDef.header, header.getContext())}
header.column.columnDef.header,
header.getContext()
)}
</th> </th>
))} ))}
</tr> </tr>
@ -72,14 +56,9 @@ export const InventoryTable = () => {
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
{table.getRowModel().rows.map(row => ( {table.getRowModel().rows.map(row => (
<tr <tr key={row.id} className={''}>
key={row.id}
className={""}
>
{row.getVisibleCells().map(cell => ( {row.getVisibleCells().map(cell => (
<td key={cell.id}> <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))} ))}
</tr> </tr>
))} ))}

View File

@ -1,80 +1,87 @@
import { useState } from "react" import { useAtom } from 'jotai'
import useLocalStorage from "use-local-storage" import { useState } from 'react'
import { useAtom } from "jotai" import useLocalStorage from 'use-local-storage'
import { loginStatusAtom } from "../state/atoms" import { LoginHelper } from '../lib/session'
import { LoginHelper } from "../lib/session" import { loginStatusAtom } from '../state/atoms'
export const LoginWidget = () => { export const LoginWidget = () => {
const [username, setUsername] = useLocalStorage("input_username","", {syncData: false}) const [username, setUsername] = useLocalStorage('input_username', '', { syncData: false })
const [password, setPassword] = useState("") const [password, setPassword] = useState('')
const [{data:loginState, refetch: refetchLoginState}] = useAtom(loginStatusAtom) const [{ data: loginState, refetch: refetchLoginState }] = useAtom(loginStatusAtom)
const [loginError, setLoginError] = useState("") const [loginError, setLoginError] = useState('')
if(loginState?.logged_in){ if (loginState?.logged_in) {
return <> return (
<div className="flex flex-row justify-between px-2"> <>
<div> <div className="flex flex-row justify-between px-2">
{loginState.community_name} <div>{loginState.community_name}</div>
<div className="flex flex-row gap-2">
<button
onClick={() => {
LoginHelper.logout().finally(() => {
refetchLoginState()
})
return
}}
className="text-blue-400 text-xs hover:cursor-pointer hover:text-blue-600"
>
logout
</button>
</div>
</div> </div>
<div className="flex flex-row gap-2"> </>
<button )
onClick={()=>{
LoginHelper.logout().finally(()=>{
refetchLoginState()
})
return
}}
className="text-blue-400 text-xs hover:cursor-pointer hover:text-blue-600">
logout
</button>
</div>
</div>
</>
} }
return <> return (
<div className="flex flex-col"> <>
<form action={ <div className="flex flex-col">
()=>{ <form
LoginHelper.login(username,password).catch((e)=>{ action={() => {
setLoginError(e.message) LoginHelper.login(username, password)
}).finally(()=>{ .catch(e => {
refetchLoginState() setLoginError(e.message)
refetchLoginState() })
}) .finally(() => {
}} refetchLoginState()
className="flex flex-col gap-1 p-2 justify-left"> refetchLoginState()
{ loginError ? (<div className="text-red-500 text-xs"> })
{loginError} }}
</div>) : null} className="flex flex-col gap-1 p-2 justify-left"
<div> >
<input {loginError ? <div className="text-red-500 text-xs">{loginError}</div> : null}
onChange={(e)=>{ <div>
setUsername(e.target.value) <input
}} onChange={e => {
value={username} setUsername(e.target.value)
id="username" }}
placeholder="username" className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"/> value={username}
</div> id="username"
<div> placeholder="username"
<input className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"
onChange={(e)=>{ />
setPassword(e.target.value) </div>
}} <div>
value={password} <input
type="password" placeholder="password" className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"/> onChange={e => {
</div> setPassword(e.target.value)
<button }}
type="submit" value={password}
className="border-b border-gray-600 px-2 py-1 hover:text-gray-600 hover:cursor-pointer"> type="password"
login placeholder="password"
</button> className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"
</form> />
</div> </div>
</> <button
type="submit"
className="border-b border-gray-600 px-2 py-1 hover:text-gray-600 hover:cursor-pointer"
>
login
</button>
</form>
</div>
</>
)
} }

View File

@ -1,19 +1,14 @@
import { SessionContextProvider } from "./SessionContext"; import { SessionContextProvider } from './SessionContext'
interface IContext { interface IContext {
children: React.ReactNode; children: React.ReactNode
} }
function AppContext(props: IContext): any { function AppContext(props: IContext): any {
const { children } = props; const { children } = props
const providers = [ const providers = [SessionContextProvider]
SessionContextProvider, const res = providers.reduceRight((acc, CurrVal) => <CurrVal>{acc as any}</CurrVal>, children)
]; return res as any
const res = providers.reduceRight(
(acc, CurrVal) => <CurrVal>{acc as any}</CurrVal>,
children,
);
return res as any;
} }
export default AppContext; export default AppContext

View File

@ -1,21 +1,22 @@
import { createContext, Dispatch, SetStateAction, useContext, useState } from "react"; import { createContext, useContext, useState } from 'react'
type Setter<T> = React.Dispatch<React.SetStateAction<T | undefined>>; type Setter<T> = React.Dispatch<React.SetStateAction<T | undefined>>
type MustSetter<T> = React.Dispatch<React.SetStateAction<T>>; type MustSetter<T> = React.Dispatch<React.SetStateAction<T>>
import useLocalStorage from "use-local-storage";
import { OrderTracker } from "../lib/lifeto/order_manager"; import useLocalStorage from 'use-local-storage'
import { ColumnSet } from "../lib/table"; import { BasicColumns, ColumnInfo, ColumnName, DetailsColumns, MoveColumns } from '../lib/columns'
import { StoreAccounts, StoreChars, StoreColSet, StoreStr } from "../lib/storage"; import { OrderTracker } from '../lib/lifeto/order_manager'
import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns' import { StoreColSet } from '../lib/storage'
import { ColumnSet } from '../lib/table'
interface SessionContextProps { interface SessionContextProps {
orders: OrderTracker; orders: OrderTracker
activeTable: string; activeTable: string
screen: string; screen: string
columns: ColumnSet; columns: ColumnSet
tags: ColumnSet; tags: ColumnSet
dirty: number; dirty: number
currentSearch: string; currentSearch: string
setActiveTable: Setter<string> setActiveTable: Setter<string>
setScreen: Setter<string> setScreen: Setter<string>
@ -23,49 +24,57 @@ interface SessionContextProps {
setCurrentSearch: MustSetter<string> setCurrentSearch: MustSetter<string>
} }
const _defaultColumn: (ColumnInfo | ColumnName)[] = [
const _defaultColumn:(ColumnInfo| ColumnName)[] = [
...BasicColumns, ...BasicColumns,
...MoveColumns, ...MoveColumns,
...DetailsColumns, ...DetailsColumns,
] ]
const SessionContext = createContext({} as SessionContextProps)
const SessionContext = createContext({} as SessionContextProps); const dotry = (x: any, d: any) => {
try {
const dotry = (x:any, d: any)=>{
try{
return x() return x()
}catch{ } catch {
return d return d
} }
} }
export const SessionContextProvider = ({ children }: { children: any }) => { export const SessionContextProvider = ({ children }: { children: any }) => {
const [activeTable, setActiveTable] = useLocalStorage<string>("activeTable","") const [activeTable, setActiveTable] = useLocalStorage<string>('activeTable', '')
const [screen, setScreen] = useLocalStorage<string>("screen","") const [screen, setScreen] = useLocalStorage<string>('screen', '')
const [columns ] = useState<ColumnSet>(new ColumnSet(_defaultColumn)); const [columns] = useState<ColumnSet>(new ColumnSet(_defaultColumn))
const [tags ] = useState<ColumnSet>(dotry(()=>StoreColSet.Revive("tags"), new ColumnSet())); const [tags] = useState<ColumnSet>(dotry(() => StoreColSet.Revive('tags'), new ColumnSet()))
const [orders ] = useState<OrderTracker>(new OrderTracker()); const [orders] = useState<OrderTracker>(new OrderTracker())
const [dirty, setDirty] = useState<number>(0); const [dirty, setDirty] = useState<number>(0)
const [currentSearch, setCurrentSearch] = useState<string>(""); const [currentSearch, setCurrentSearch] = useState<string>('')
return ( return (
<SessionContext.Provider value={{ <SessionContext.Provider
orders, activeTable, screen, columns, tags, dirty, currentSearch, value={{
setActiveTable, setScreen, setDirty, setCurrentSearch, orders,
}}>{children}</SessionContext.Provider> activeTable,
); screen,
}; columns,
tags,
dirty,
currentSearch,
setActiveTable,
setScreen,
setDirty,
setCurrentSearch,
}}
>
{children}
</SessionContext.Provider>
)
}
export const useSessionContext = (): SessionContextProps => { export const useSessionContext = (): SessionContextProps => {
const context = useContext<SessionContextProps>(SessionContext); const context = useContext<SessionContextProps>(SessionContext)
if (context === null) { if (context === null) {
throw new Error( throw new Error('"useSessionContext" should be used inside a "SessionContextProvider"')
'"useSessionContext" should be used inside a "SessionContextProvider"',
);
} }
return context; return context
}; }

View File

@ -1,13 +1,13 @@
@import 'tailwindcss'; @import "tailwindcss";
html { html {
cursor: url(/public/cursor.png), auto !important; cursor: url(/assets/cursor.png), auto !important;
} }
@theme { @theme {
--cursor-default: url(/public/cursor.png), auto !important; --cursor-default: url(/assets/cursor.png), auto !important;
--cursor-pointer: url(/public/cursor.png), pointer !important; --cursor-pointer: url(/assets/cursor.png), pointer !important;
--cursor-text: url(/public/cursor.png), pointer !important; --cursor-text: url(/assets/cursor.png), pointer !important;
} }
/* /*
The default border color has changed to `currentcolor` in Tailwind CSS v4, The default border color has changed to `currentcolor` in Tailwind CSS v4,

View File

@ -1,16 +1,16 @@
import React from "react"; import React from 'react'
import ReactDOM from "react-dom/client"; import ReactDOM from 'react-dom/client'
import { App } from "./App"; import { App } from './App'
import AppContext from "./context/AppContext"; import AppContext from './context/AppContext'
import './lib/superjson'
import './index.css'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'jotai'
import "./lib/superjson";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Provider } from "jotai";
const queryClient = new QueryClient() const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render( ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<Provider> <Provider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@ -20,4 +20,4 @@ ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
</QueryClientProvider> </QueryClientProvider>
</Provider> </Provider>
</React.StrictMode>, </React.StrictMode>,
); )

View File

@ -1,34 +1,33 @@
import { TricksterItem } from "../trickster" import { TricksterItem } from '../trickster'
export const BasicColumns = [ export const BasicColumns = ['uid', 'Image', 'Name', 'Count'] as const
"uid","Image","Name","Count",
] as const
export const DetailsColumns = [ export const DetailsColumns = ['Desc', 'Use'] as const
"Desc","Use",
] as const
export const MoveColumns = [ export const MoveColumns = ['MoveCount', 'Move'] as const
"MoveCount","Move",
] as const
export const TagColumns = [ export const TagColumns = ['All', 'Equip', 'Drill', 'Card', 'Quest', 'Consume', 'Compound'] as const
"All","Equip","Drill","Card","Quest","Consume", "Compound"
] as const
export const EquipmentColumns = [ export const EquipmentColumns = ['MinLvl', 'Slots', 'RefineNumber', 'RefineState'] as const
"MinLvl","Slots","RefineNumber","RefineState",
] as const
export const StatsColumns = [ export const StatsColumns = [
"HV","AC","LK","WT","HP","MA","DP","DX","MP","AP","MD","DA","GunAP" 'HV',
'AC',
'LK',
'WT',
'HP',
'MA',
'DP',
'DX',
'MP',
'AP',
'MD',
'DA',
'GunAP',
] as const ] as const
export const DebugColumns = []
export const DebugColumns = [ export const HackColumns = [] as const
]
export const HackColumns = [
] as const
export const ColumnNames = [ export const ColumnNames = [
...BasicColumns, ...BasicColumns,
@ -40,35 +39,34 @@ export const ColumnNames = [
...HackColumns, ...HackColumns,
] as const ] as const
export type ColumnName = typeof ColumnNames[number] export type ColumnName = (typeof ColumnNames)[number]
const c = (a:ColumnName | ColumnInfo):ColumnName => { const c = (a: ColumnName | ColumnInfo): ColumnName => {
switch(typeof a) { switch (typeof a) {
case "string": case 'string':
return a return a
case "object": case 'object':
return a.name return a.name
} }
} }
export const LazyColumn = c; export const LazyColumn = c
export const ColumnSorter = (a:ColumnName | ColumnInfo, b: ColumnName | ColumnInfo):number => { export const ColumnSorter = (a: ColumnName | ColumnInfo, b: ColumnName | ColumnInfo): number => {
let n1 = ColumnNames.indexOf(c(a)) const n1 = ColumnNames.indexOf(c(a))
let n2 = ColumnNames.indexOf(c(b)) const n2 = ColumnNames.indexOf(c(b))
if(n1 == n2) { if (n1 === n2) {
return 0 return 0
} }
return n1 > n2 ? 1 : -1 return n1 > n2 ? 1 : -1
} }
export interface ColumnInfo { export interface ColumnInfo {
name: ColumnName name: ColumnName
displayName:string displayName: string
options?:(s:string[])=>string[] options?: (s: string[]) => string[]
renderer?:any renderer?: any
filtering?:boolean filtering?: boolean
writable?:boolean writable?: boolean
getter(item:TricksterItem):(string | number) getter(item: TricksterItem): string | number
} }

View File

@ -1,464 +1,510 @@
import Handsontable from "handsontable" import Handsontable from 'handsontable'
import Core from "handsontable/core" import Core from 'handsontable/core'
import { textRenderer } from "handsontable/renderers" import { textRenderer } from 'handsontable/renderers'
import numbro from "numbro" import numbro from 'numbro'
import { TricksterItem } from "../trickster" import { TricksterItem } from '../trickster'
import {ColumnName, ColumnInfo} from "./column" import { ColumnInfo, ColumnName } from './column'
export const ColumnByNames = (...n:ColumnName[]) => { export const ColumnByNames = (...n: ColumnName[]) => {
return n.map(ColumnByName) return n.map(ColumnByName)
} }
export const ColumnByName = (n:ColumnName) => { export const ColumnByName = (n: ColumnName) => {
return Columns[n] return Columns[n]
} }
class Image implements ColumnInfo { class Image implements ColumnInfo {
name:ColumnName = 'Image' name: ColumnName = 'Image'
displayName = " " displayName = ' '
renderer = coverRenderer renderer = coverRenderer
getter(item:TricksterItem):(string|number) { getter(item: TricksterItem): string | number {
return item.item_image ? item.item_image : "" return item.item_image ? item.item_image : ''
} }
} }
function coverRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function coverRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
td: any,
_row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
if (stringifiedValue.startsWith('http')) { if (stringifiedValue.startsWith('http')) {
const img:any = document.createElement('IMG'); const img: any = document.createElement('IMG')
img.src = value; img.src = value
Handsontable.dom.addEvent(img, 'mousedown', event =>{ Handsontable.dom.addEvent(img, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(img); td.appendChild(img)
} }
} }
class Name implements ColumnInfo { class Name implements ColumnInfo {
name:ColumnName = "Name" name: ColumnName = 'Name'
displayName = "Name" displayName = 'Name'
filtering = true filtering = true
renderer = nameRenderer renderer = nameRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_name return item.item_name
} }
} }
function nameRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function nameRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
let showText = stringifiedValue; td: any,
const div= document.createElement('div'); _row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
const showText = stringifiedValue
const div = document.createElement('div')
div.innerHTML = showText div.innerHTML = showText
div.title = showText div.title = showText
div.style.maxWidth = "20ch" div.style.maxWidth = '20ch'
div.style.textOverflow = "ellipsis" div.style.textOverflow = 'ellipsis'
div.style.overflow= "hidden" div.style.overflow = 'hidden'
div.style.whiteSpace= "nowrap" div.style.whiteSpace = 'nowrap'
Handsontable.dom.addEvent(div, 'mousedown', event =>{ Handsontable.dom.addEvent(div, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(div); td.appendChild(div)
td.classList.add("htLeft") td.classList.add('htLeft')
} }
class Count implements ColumnInfo { class Count implements ColumnInfo {
name:ColumnName = "Count" name: ColumnName = 'Count'
displayName = "Count" displayName = 'Count'
renderer = "numeric" renderer = 'numeric'
filtering = true filtering = true
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_count return item.item_count
} }
} }
const spacer = "-----------" const spacer = '-----------'
class Move implements ColumnInfo { class Move implements ColumnInfo {
name:ColumnName = "Move" name: ColumnName = 'Move'
displayName = "Target" displayName = 'Target'
writable = true writable = true
options = getMoveTargets options = getMoveTargets
getter(item:TricksterItem):(string|number){ getter(_item: TricksterItem): string | number {
return spacer return spacer
} }
} }
const getMoveTargets = (invs: string[]):string[] => { const getMoveTargets = (invs: string[]): string[] => {
let out:string[] = []; const out: string[] = []
out.push(spacer) out.push(spacer)
for(const k of invs){ for (const k of invs) {
out.push(k) out.push(k)
} }
out.push("") out.push('')
out.push("") out.push('')
out.push("TRASH") out.push('TRASH')
return out return out
} }
class MoveCount implements ColumnInfo { class MoveCount implements ColumnInfo {
name:ColumnName = "MoveCount" name: ColumnName = 'MoveCount'
displayName = "Move #" displayName = 'Move #'
renderer = moveCountRenderer renderer = moveCountRenderer
writable = true writable = true
getter(item:TricksterItem):(string|number){ getter(_item: TricksterItem): string | number {
return "" return ''
} }
} }
function moveCountRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) { function moveCountRenderer(
let newValue = value; instance: Core,
td: any,
row: number,
col: number,
prop: any,
value: any,
cellProperties: any,
) {
let newValue = value
if (Handsontable.helper.isNumeric(newValue)) { if (Handsontable.helper.isNumeric(newValue)) {
const numericFormat = cellProperties.numericFormat; const numericFormat = cellProperties.numericFormat
const cellCulture = numericFormat && numericFormat.culture || '-'; const cellCulture = numericFormat?.culture || '-'
const cellFormatPattern = numericFormat && numericFormat.pattern; const cellFormatPattern = numericFormat?.pattern
const className = cellProperties.className || ''; const className = cellProperties.className || ''
const classArr = className.length ? className.split(' ') : []; const classArr = className.length ? className.split(' ') : []
if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) { if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) {
const shortTag:any = cellCulture.replace('-', ''); const shortTag: any = cellCulture.replace('-', '')
const langData = (numbro as any)[shortTag]; const langData = (numbro as any)[shortTag]
if (langData) { if (langData) {
numbro.registerLanguage(langData); numbro.registerLanguage(langData)
} }
} }
const totalCount = Number(instance.getCell(row,col-1)?.innerHTML) const totalCount = Number(instance.getCell(row, col - 1)?.innerHTML)
numbro.setLanguage(cellCulture); numbro.setLanguage(cellCulture)
const num = numbro(newValue) const num = numbro(newValue)
if(totalCount < num.value()) { if (totalCount < num.value()) {
const newNum = numbro(totalCount) const newNum = numbro(totalCount)
newValue = newNum.format(cellFormatPattern || '0'); newValue = newNum.format(cellFormatPattern || '0')
}else { } else {
newValue = num.format(cellFormatPattern || '0'); newValue = num.format(cellFormatPattern || '0')
} }
if (classArr.indexOf('htLeft') < 0 && classArr.indexOf('htCenter') < 0 && if (
classArr.indexOf('htRight') < 0 && classArr.indexOf('htJustify') < 0) { classArr.indexOf('htLeft') < 0 &&
classArr.push('htRight'); classArr.indexOf('htCenter') < 0 &&
classArr.indexOf('htRight') < 0 &&
classArr.indexOf('htJustify') < 0
) {
classArr.push('htRight')
} }
if (classArr.indexOf('htNumeric') < 0) { if (classArr.indexOf('htNumeric') < 0) {
classArr.push('htNumeric'); classArr.push('htNumeric')
} }
cellProperties.className = classArr.join(' '); cellProperties.className = classArr.join(' ')
td.dir = 'ltr'; td.dir = 'ltr'
newValue = newValue + "x" newValue = `${newValue}x`
}else { } else {
newValue = "" newValue = ''
} }
textRenderer(instance, td, row, col, prop, newValue, cellProperties); textRenderer(instance, td, row, col, prop, newValue, cellProperties)
} }
class Equip implements ColumnInfo { class Equip implements ColumnInfo {
name:ColumnName = "Equip" name: ColumnName = 'Equip'
displayName = "equip" displayName = 'equip'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.is_equip ? 1 : 0 return item.is_equip ? 1 : 0
} }
} }
class Drill implements ColumnInfo { class Drill implements ColumnInfo {
name:ColumnName = "Drill" name: ColumnName = 'Drill'
displayName = "drill" displayName = 'drill'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.is_drill ? 1 : 0 return item.is_drill ? 1 : 0
} }
} }
class All implements ColumnInfo { class All implements ColumnInfo {
name:ColumnName = "All" name: ColumnName = 'All'
displayName = "swap" displayName = 'swap'
getter(_:TricksterItem):(string|number){ getter(_: TricksterItem): string | number {
return -10000 return -10000
} }
} }
class uid implements ColumnInfo { class uid implements ColumnInfo {
name:ColumnName = "uid" name: ColumnName = 'uid'
displayName = "id" displayName = 'id'
renderer = invisibleRenderer renderer = invisibleRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.unique_id return item.unique_id
} }
} }
function invisibleRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) { function invisibleRenderer(
Handsontable.dom.empty(td); _instance: Core,
td: any,
_row: number,
_col: number,
_prop: any,
_value: any,
_cellProperties: any,
) {
Handsontable.dom.empty(td)
} }
class Card implements ColumnInfo { class Card implements ColumnInfo {
name:ColumnName = "Card" name: ColumnName = 'Card'
displayName = "card" displayName = 'card'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return cardFilter(item) ? 1 : 0 return cardFilter(item) ? 1 : 0
} }
} }
const cardFilter= (item:TricksterItem): boolean => { const cardFilter = (item: TricksterItem): boolean => {
return (item.item_name.endsWith(" Card") || item.item_name.startsWith("Star Card")) return item.item_name.endsWith(' Card') || item.item_name.startsWith('Star Card')
} }
class Compound implements ColumnInfo { class Compound implements ColumnInfo {
name:ColumnName = "Compound" name: ColumnName = 'Compound'
displayName = "comp" displayName = 'comp'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return compFilter(item) ? 1 : 0 return compFilter(item) ? 1 : 0
} }
} }
const compFilter= (item:TricksterItem): boolean => { const compFilter = (item: TricksterItem): boolean => {
return (item.item_comment.toLowerCase().includes("compound item")) return item.item_comment.toLowerCase().includes('compound item')
} }
class Quest implements ColumnInfo { class Quest implements ColumnInfo {
name:ColumnName = "Quest" name: ColumnName = 'Quest'
displayName = "quest" displayName = 'quest'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return questFilter(item) ? 1 : 0 return questFilter(item) ? 1 : 0
} }
} }
const questFilter= (item:TricksterItem): boolean => { const questFilter = (_item: TricksterItem): boolean => {
return false return false
} }
class Consume implements ColumnInfo { class Consume implements ColumnInfo {
name:ColumnName = "Consume" name: ColumnName = 'Consume'
displayName = "eat" displayName = 'eat'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return consumeFilter(item) ? 1 : 0 return consumeFilter(item) ? 1 : 0
} }
} }
const consumeFilter= (item:TricksterItem): boolean => { const consumeFilter = (item: TricksterItem): boolean => {
const tl = item.item_use.toLowerCase() const tl = item.item_use.toLowerCase()
return tl.includes("recover") || tl.includes("restores") return tl.includes('recover') || tl.includes('restores')
} }
class AP implements ColumnInfo { class AP implements ColumnInfo {
name:ColumnName = "AP" name: ColumnName = 'AP'
displayName = "AP" displayName = 'AP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["AP"] : "" return item.stats ? item.stats.AP : ''
} }
} }
class GunAP implements ColumnInfo { class GunAP implements ColumnInfo {
name:ColumnName = "GunAP" name: ColumnName = 'GunAP'
displayName = "Gun AP" displayName = 'Gun AP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["Gun AP"] : "" return item.stats ? item.stats['Gun AP'] : ''
} }
} }
class AC implements ColumnInfo { class AC implements ColumnInfo {
name:ColumnName = "AC" name: ColumnName = 'AC'
displayName = "AC" displayName = 'AC'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["AC"] : "" return item.stats ? item.stats.AC : ''
} }
} }
class DX implements ColumnInfo { class DX implements ColumnInfo {
name:ColumnName = "DX" name: ColumnName = 'DX'
displayName = "DX" displayName = 'DX'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["DX"] : "" return item.stats ? item.stats.DX : ''
} }
} }
class MP implements ColumnInfo { class MP implements ColumnInfo {
name:ColumnName = "MP" name: ColumnName = 'MP'
displayName = "MP" displayName = 'MP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["MP"] : "" return item.stats ? item.stats.MP : ''
} }
} }
class MA implements ColumnInfo { class MA implements ColumnInfo {
name:ColumnName = "MA" name: ColumnName = 'MA'
displayName = "MA" displayName = 'MA'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["MA"] : "" return item.stats ? item.stats.MA : ''
} }
} }
class MD implements ColumnInfo { class MD implements ColumnInfo {
name:ColumnName = "MD" name: ColumnName = 'MD'
displayName = "MD" displayName = 'MD'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["MD"] : "" return item.stats ? item.stats.MD : ''
} }
} }
class WT implements ColumnInfo { class WT implements ColumnInfo {
name:ColumnName = "WT" name: ColumnName = 'WT'
displayName = "WT" displayName = 'WT'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["WT"] : "" return item.stats ? item.stats.WT : ''
} }
} }
class DA implements ColumnInfo { class DA implements ColumnInfo {
name:ColumnName = "DA" name: ColumnName = 'DA'
displayName = "DA" displayName = 'DA'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["DA"] : "" return item.stats ? item.stats.DA : ''
} }
} }
class LK implements ColumnInfo { class LK implements ColumnInfo {
name:ColumnName = "LK" name: ColumnName = 'LK'
displayName = "LK" displayName = 'LK'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["LK"] : "" return item.stats ? item.stats.LK : ''
} }
} }
class HP implements ColumnInfo { class HP implements ColumnInfo {
name:ColumnName = "HP" name: ColumnName = 'HP'
displayName = "HP" displayName = 'HP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["HP"] : "" return item.stats ? item.stats.HP : ''
} }
} }
class DP implements ColumnInfo { class DP implements ColumnInfo {
name:ColumnName = "DP" name: ColumnName = 'DP'
displayName = "DP" displayName = 'DP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["DP"] : "" return item.stats ? item.stats.DP : ''
} }
} }
class HV implements ColumnInfo { class HV implements ColumnInfo {
name:ColumnName = "HV" name: ColumnName = 'HV'
displayName = "HV" displayName = 'HV'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["HV"] : "" return item.stats ? item.stats.HV : ''
} }
} }
class MinLvl implements ColumnInfo { class MinLvl implements ColumnInfo {
name:ColumnName = "MinLvl" name: ColumnName = 'MinLvl'
displayName = "lvl" displayName = 'lvl'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
//TODO: //TODO:
return item.item_min_level? item.item_min_level:"" return item.item_min_level ? item.item_min_level : ''
} }
} }
class Slots implements ColumnInfo { class Slots implements ColumnInfo {
name:ColumnName = "Slots" name: ColumnName = 'Slots'
displayName = "slots" displayName = 'slots'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
//TODO: //TODO:
return item.item_slots ? item.item_slots : "" return item.item_slots ? item.item_slots : ''
} }
} }
class RefineNumber implements ColumnInfo { class RefineNumber implements ColumnInfo {
name:ColumnName = "RefineNumber" name: ColumnName = 'RefineNumber'
displayName = "refine" displayName = 'refine'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.refine_level ? item.refine_level : 0 return item.refine_level ? item.refine_level : 0
} }
} }
class RefineState implements ColumnInfo { class RefineState implements ColumnInfo {
name:ColumnName = "RefineState" name: ColumnName = 'RefineState'
displayName = "bork" displayName = 'bork'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.refine_state ? item.refine_state : 0 return item.refine_state ? item.refine_state : 0
} }
} }
class Desc implements ColumnInfo { class Desc implements ColumnInfo {
name:ColumnName = "Desc" name: ColumnName = 'Desc'
displayName = "desc" displayName = 'desc'
renderer = descRenderer renderer = descRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_comment return item.item_comment
} }
} }
function descRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function descRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
let showText = stringifiedValue; td: any,
const div= document.createElement('div'); _row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
const showText = stringifiedValue
const div = document.createElement('div')
div.innerHTML = showText div.innerHTML = showText
div.title = showText div.title = showText
div.style.maxWidth = "30ch" div.style.maxWidth = '30ch'
div.style.textOverflow = "ellipsis" div.style.textOverflow = 'ellipsis'
div.style.overflow= "hidden" div.style.overflow = 'hidden'
div.style.whiteSpace= "nowrap" div.style.whiteSpace = 'nowrap'
Handsontable.dom.addEvent(div, 'mousedown', event =>{ Handsontable.dom.addEvent(div, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(div); td.appendChild(div)
td.classList.add("htLeft") td.classList.add('htLeft')
} }
class Use implements ColumnInfo { class Use implements ColumnInfo {
name:ColumnName = "Use" name: ColumnName = 'Use'
displayName = "use" displayName = 'use'
renderer= useRenderer; renderer = useRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_use return item.item_use
} }
} }
function useRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function useRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
let showText = stringifiedValue; td: any,
const div= document.createElement('div'); _row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
const showText = stringifiedValue
const div = document.createElement('div')
div.title = showText div.title = showText
div.innerHTML = showText div.innerHTML = showText
div.style.maxWidth = "30ch" div.style.maxWidth = '30ch'
div.style.textOverflow = "ellipsis" div.style.textOverflow = 'ellipsis'
div.style.overflow= "hidden" div.style.overflow = 'hidden'
div.style.whiteSpace= "nowrap" div.style.whiteSpace = 'nowrap'
Handsontable.dom.addEvent(div, 'mousedown', event =>{ Handsontable.dom.addEvent(div, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(div); td.appendChild(div)
td.classList.add("htLeft") td.classList.add('htLeft')
} }
export const Columns:{[Property in ColumnName]:ColumnInfo}= { export const Columns: { [Property in ColumnName]: ColumnInfo } = {
Use: new Use(), Use: new Use(),
Desc: new Desc(), Desc: new Desc(),
Image: new Image(), Image: new Image(),
Name: new Name(), Name: new Name(),
Count: new Count(), Count: new Count(),
Move: new Move(), Move: new Move(),
MoveCount: new MoveCount(), MoveCount: new MoveCount(),
Equip: new Equip(), Equip: new Equip(),
Drill: new Drill(), Drill: new Drill(),
Card: new Card(), Card: new Card(),
Quest: new Quest(), Quest: new Quest(),
Consume: new Consume(), Consume: new Consume(),
AP: new AP(), AP: new AP(),
GunAP: new GunAP(), GunAP: new GunAP(),
AC: new AC(), AC: new AC(),
DX: new DX(), DX: new DX(),
MP: new MP(), MP: new MP(),
MA: new MA(), MA: new MA(),
MD: new MD(), MD: new MD(),
WT: new WT(), WT: new WT(),
DA: new DA(), DA: new DA(),
LK: new LK(), LK: new LK(),
HP: new HP(), HP: new HP(),
DP: new DP(), DP: new DP(),
HV: new HV(), HV: new HV(),
MinLvl: new MinLvl(), MinLvl: new MinLvl(),
Slots: new Slots(), Slots: new Slots(),
RefineNumber: new RefineNumber(), RefineNumber: new RefineNumber(),
RefineState: new RefineState(), RefineState: new RefineState(),
All: new All(), All: new All(),
Compound: new Compound(), Compound: new Compound(),
uid: new uid(), uid: new uid(),
} }

View File

@ -1,3 +1,2 @@
export * from "./column" export * from './column'
export * from "./column_impl" export * from './column_impl'

View File

@ -1,12 +1,18 @@
import { TricksterAccount, TricksterInventory } from "../trickster" import { TricksterAccount, TricksterInventory } from '../trickster'
export const BankEndpoints = ["internal-xfer-item", "bank-item", "sell-item","buy-from-order","cancel-order"] as const export const BankEndpoints = [
export type BankEndpoint = typeof BankEndpoints[number] 'internal-xfer-item',
'bank-item',
'sell-item',
'buy-from-order',
'cancel-order',
] as const
export type BankEndpoint = (typeof BankEndpoints)[number]
export interface LTOApi { export interface LTOApi {
GetInventory:(path:string)=>Promise<TricksterInventory> GetInventory: (path: string) => Promise<TricksterInventory>
GetAccounts:() =>Promise<Array<TricksterAccount>> GetAccounts: () => Promise<Array<TricksterAccount>>
GetLoggedin:() =>Promise<boolean> GetLoggedin: () => Promise<boolean>
BankAction:<T, D>(e:BankEndpoint, t:T) => Promise<D> BankAction: <T, D>(e: BankEndpoint, t: T) => Promise<D>
} }

View File

@ -1,3 +1,3 @@
export * from "./lifeto" export * from './api'
export * from "./api" export * from './lifeto'
export * from "./stateful" export * from './stateful'

View File

@ -1,115 +1,131 @@
import { Axios, AxiosResponse, Method } from "axios" import { AxiosResponse, Method } from 'axios'
import log from "loglevel" import log from 'loglevel'
import { bank_endpoint, EndpointCreator, market_endpoint, Session } from "../session" import { bank_endpoint, EndpointCreator, market_endpoint, Session } from '../session'
import { TricksterAccount, TricksterAccountInfo, TricksterInventory, TricksterItem} from "../trickster" import { TricksterAccount, TricksterInventory, TricksterItem } from '../trickster'
import { BankEndpoint, LTOApi } from "./api" import { BankEndpoint, LTOApi } from './api'
export const pathIsBank = (path:string):boolean => { export const pathIsBank = (path: string): boolean => {
if(path.includes("/")) { if (path.includes('/')) {
return false return false
} }
return true return true
} }
export const splitPath = (path:string):[string,string]=>{ export const splitPath = (path: string): [string, string] => {
const spl = path.split("/") const spl = path.split('/')
switch(spl.length) { switch (spl.length) {
case 1: case 1:
return [spl[0], ""] return [spl[0], '']
case 2: case 2:
return [spl[0],spl[1]] return [spl[0], spl[1]]
} }
return ["",""] return ['', '']
} }
export class LTOApiv0 implements LTOApi { export class LTOApiv0 implements LTOApi {
s: Session s: Session
constructor(s:Session) { constructor(s: Session) {
this.s = s this.s = s
} }
BankAction = async <T,D>(e: BankEndpoint, t: T):Promise<D> => { BankAction = async <T, D>(e: BankEndpoint, t: T): Promise<D> => {
let VERB:Method | "POSTFORM" = "POST" let VERB: Method | 'POSTFORM' = 'POST'
let endpoint:EndpointCreator = bank_endpoint let endpoint: EndpointCreator = bank_endpoint
switch(e){ switch (e) {
case "buy-from-order": case 'buy-from-order':
case "cancel-order": case 'cancel-order':
endpoint = market_endpoint endpoint = market_endpoint
case "sell-item": case 'sell-item':
VERB = "POSTFORM" VERB = 'POSTFORM'
default: default:
} }
return this.s.request(VERB as any,e,t,endpoint).then((x)=>{ return this.s.request(VERB as any, e, t, endpoint).then(x => {
return x.data return x.data
})
}
GetInventory = async (char_path: string):Promise<TricksterInventory> =>{
if(char_path.startsWith(":")) {
char_path = char_path.replace(":","")
}
let type = char_path.includes("/") ? "char" : "account"
return this.s.request("GET", `v3/item-manager/items/${type}/${char_path}`,undefined).then((ans:AxiosResponse)=>{
const o = ans.data
log.debug("GetInventory", o)
let name = "bank"
let id = 0
let galders = 0
if(pathIsBank(char_path)){
let [char, val] = Object.entries(o.characters)[0] as [string,any]
name = val.name
id = Number(char)
galders = 0
}else {
let [char, val] = Object.entries(o.characters)[0] as [string,any]
name = val.name
id = Number(char)
galders = val.galders
}
let out:TricksterInventory = {
account_name: o.account.account_gid,
account_id: o.account.account_code,
name,
id,
path: char_path,
galders,
items: new Map((Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):[string, TricksterItem]=>{
v.unique_id = Number(k)
v.id = k
return [k, v]
})),
}
return out
}) })
} }
GetAccounts = async ():Promise<TricksterAccount[]> => { GetInventory = async (char_path: string): Promise<TricksterInventory> => {
return this.s.request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{ if (char_path.startsWith(':')) {
log.debug("GetAccounts", ans.data) char_path = char_path.replace(':', '')
return ans.data.map((x:any):TricksterAccount=>{ }
const type = char_path.includes('/') ? 'char' : 'account'
return this.s
.request('GET', `v3/item-manager/items/${type}/${char_path}`, undefined)
.then((ans: AxiosResponse) => {
const o = ans.data
log.debug('GetInventory', o)
let name = 'bank'
let id = 0
let galders = 0
if (pathIsBank(char_path)) {
const [char, val] = Object.entries(o.characters)[0] as [string, any]
name = val.name
id = Number(char)
galders = 0
} else {
const [char, val] = Object.entries(o.characters)[0] as [string, any]
name = val.name
id = Number(char)
galders = val.galders
}
const out: TricksterInventory = {
account_name: o.account.account_gid,
account_id: o.account.account_code,
name,
id,
path: char_path,
galders,
items: new Map(
(Object.entries(o.items) as any).map(
([k, v]: [string, TricksterItem]): [string, TricksterItem] => {
v.unique_id = Number(k)
v.id = k
return [k, v]
},
),
),
}
return out
})
}
GetAccounts = async (): Promise<TricksterAccount[]> => {
return this.s.request('GET', 'characters/list', undefined).then((ans: AxiosResponse) => {
log.debug('GetAccounts', ans.data)
return ans.data.map((x: any): TricksterAccount => {
return { return {
name: x.name, name: x.name,
characters: [ characters: [
{account_name:x.name, id: x.id,account_id:x.id, path:x.name, name: x.name+'/bank', class:-8, base_job: -8, current_job: -8}, {
...Object.values(x.characters).map((z:any)=>{ account_name: x.name,
id: x.id,
account_id: x.id,
path: x.name,
name: `${x.name}/bank`,
class: -8,
base_job: -8,
current_job: -8,
},
...Object.values(x.characters).map((z: any) => {
return { return {
account_name:x.name, account_name: x.name,
account_id: x.id, account_id: x.id,
id: z.id, id: z.id,
name: z.name, name: z.name,
path: x.name+"/"+z.name, path: `${x.name}/${z.name}`,
class: z.class, class: z.class,
base_job: z.base_job, base_job: z.base_job,
current_job: z.current_job, current_job: z.current_job,
} }
})], }),
],
} }
}) })
}) })
} }
GetLoggedin = async ():Promise<boolean> => { GetLoggedin = async (): Promise<boolean> => {
return this.s.request("POST", "accounts/list",undefined).then((ans:AxiosResponse)=>{ return this.s.request('POST', 'accounts/list', undefined).then((ans: AxiosResponse) => {
if(ans.status == 401) { if (ans.status === 401) {
return false return false
} }
if(ans.status == 200) { if (ans.status === 200) {
return true return true
} }
return false return false

View File

@ -1,56 +1,55 @@
import { LTOApi } from "./api" import { debug } from 'loglevel'
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { RefStore } from "../../state/state"; import { RefStore } from '../../state/state'
import { debug } from "loglevel"; import { LTOApi } from './api'
export const TxnStates = ["PENDING","INFLIGHT","WORKING","ERROR","SUCCESS"] as const export const TxnStates = ['PENDING', 'INFLIGHT', 'WORKING', 'ERROR', 'SUCCESS'] as const
export type TxnState = typeof TxnStates[number] export type TxnState = (typeof TxnStates)[number]
export interface TxnDetails { export interface TxnDetails {
item_uid: string | "galders" item_uid: string | 'galders'
count:number count: number
origin:string origin: string
target:string target: string
origin_path:string origin_path: string
target_path:string target_path: string
origin_account:string origin_account: string
target_account:string target_account: string
} }
export interface Envelope<REQ,RESP> { export interface Envelope<REQ, RESP> {
req: REQ req: REQ
resp: RESP resp: RESP
state: TxnState state: TxnState
} }
export abstract class Order { export abstract class Order {
action_id: string action_id: string
details?:TxnDetails details?: TxnDetails
created:Date created: Date
state: TxnState state: TxnState
constructor(details?:TxnDetails) { constructor(details?: TxnDetails) {
this.state = "PENDING" this.state = 'PENDING'
this.details = details this.details = details
this.created = new Date() this.created = new Date()
this.action_id = uuidv4(); this.action_id = uuidv4()
} }
mark(t:TxnState) { mark(t: TxnState) {
this.state = t this.state = t
} }
abstract tick(r:RefStore, api:LTOApi):Promise<any> abstract tick(r: RefStore, api: LTOApi): Promise<any>
abstract status():string abstract status(): string
abstract progress():[number, number] abstract progress(): [number, number]
abstract error():string abstract error(): string
abstract order_type:OrderType abstract order_type: OrderType
parse(i:any):Order { parse(i: any): Order {
this.action_id = i.action_id this.action_id = i.action_id
this.details = i.details this.details = i.details
this.created = new Date(i.created) this.created = new Date(i.created)
@ -63,20 +62,20 @@ export abstract class BasicOrder extends Order {
stage: number stage: number
err?: string err?: string
constructor(details:TxnDetails) { constructor(details: TxnDetails) {
super(details) super(details)
this.stage = 0 this.stage = 0
} }
progress():[number,number]{ progress(): [number, number] {
return [this.stage, 1] return [this.stage, 1]
} }
status():string { status(): string {
return this.state return this.state
} }
error():string { error(): string {
return this.err ? this.err : "" return this.err ? this.err : ''
} }
parse(i:any):BasicOrder { parse(i: any): BasicOrder {
this.stage = i.stage this.stage = i.stage
this.err = i.err this.err = i.err
super.parse(i) super.parse(i)
@ -85,31 +84,38 @@ export abstract class BasicOrder extends Order {
} }
/// start user defined /// start user defined
export const OrderTypes = ["InvalidOrder","BankItem","InternalXfer", "PrivateMarket","MarketMove", "MarketMoveToChar"] export const OrderTypes = [
export type OrderType = typeof OrderTypes[number] 'InvalidOrder',
'BankItem',
'InternalXfer',
'PrivateMarket',
'MarketMove',
'MarketMoveToChar',
]
export type OrderType = (typeof OrderTypes)[number]
export class InvalidOrder extends Order{ export class InvalidOrder extends Order {
order_type = "InvalidOrder" order_type = 'InvalidOrder'
msg:string msg: string
constructor(msg: string){ constructor(msg: string) {
super(undefined) super(undefined)
this.msg = msg this.msg = msg
this.mark("ERROR") this.mark('ERROR')
} }
status():string { status(): string {
return "ERROR" return 'ERROR'
} }
progress():[number, number] { progress(): [number, number] {
return [0,0] return [0, 0]
} }
error(): string { error(): string {
return this.msg return this.msg
} }
async tick(r:RefStore, api:LTOApi):Promise<void> { async tick(_r: RefStore, _api: LTOApi): Promise<void> {
return return
} }
parse(i:any):InvalidOrder { parse(i: any): InvalidOrder {
super.parse(i) super.parse(i)
this.msg = i.msg this.msg = i.msg
return this return this
@ -122,55 +128,60 @@ export interface BasicResponse {
message?: string message?: string
} }
export interface InternalXferRequest { export interface InternalXferRequest {
item_uid:string item_uid: string
qty:string qty: string
account:string account: string
new_char:string new_char: string
} }
export interface InternalXferResponse extends BasicResponse {} export interface InternalXferResponse extends BasicResponse {}
export class InternalXfer extends BasicOrder{ export class InternalXfer extends BasicOrder {
order_type = "InternalXfer" order_type = 'InternalXfer'
originalRequest:InternalXferRequest originalRequest: InternalXferRequest
originalResponse?:InternalXferResponse originalResponse?: InternalXferResponse
constructor(details:TxnDetails) { constructor(details: TxnDetails) {
super(details) super(details)
this.originalRequest = { this.originalRequest = {
item_uid: details.item_uid, item_uid: details.item_uid,
qty: details.count.toString(), qty: details.count.toString(),
new_char: details.target, new_char: details.target,
account: details.origin, account: details.origin,
} }
} }
async tick(r:RefStore, api:LTOApi):Promise<void> { async tick(r: RefStore, api: LTOApi): Promise<void> {
if(this.state !== "PENDING") { if (this.state !== 'PENDING') {
return return
} }
this.mark("WORKING") this.mark('WORKING')
return api.BankAction<InternalXferRequest, InternalXferResponse>("internal-xfer-item",this.originalRequest) return api
.then((x:InternalXferResponse)=>{ .BankAction<InternalXferRequest, InternalXferResponse>(
if(x.status === 'success'){ 'internal-xfer-item',
this.originalResponse = x this.originalRequest,
)
.then((x: InternalXferResponse) => {
if (x.status === 'success') {
this.originalResponse = x
this.stage = 1
this.mark('SUCCESS')
const origin_item = r.invs.value.get(this.details?.origin_path!)?.items[
this.details?.item_uid!
]!
origin_item.item_count = origin_item.item_count - this.details?.count!
} else {
throw x.message
}
})
.catch(e => {
debug('InternalXfer', e)
this.stage = 1 this.stage = 1
this.mark("SUCCESS") this.err = e
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]! this.mark('ERROR')
origin_item.item_count = origin_item.item_count - this.details?.count! })
}else{
throw x.message
}
})
.catch((e)=>{
debug("InternalXfer",e)
this.stage = 1
this.err = e
this.mark("ERROR")
})
} }
parse(i:any):InternalXfer { parse(i: any): InternalXfer {
super.parse(i) super.parse(i)
this.originalRequest = i.originalRequest this.originalRequest = i.originalRequest
this.originalResponse = i.originalResponse this.originalResponse = i.originalResponse
@ -179,52 +190,55 @@ export class InternalXfer extends BasicOrder{
} }
export interface BankItemRequest { export interface BankItemRequest {
item_uid:string item_uid: string
qty:string qty: string
account:string account: string
} }
export interface BankItemResponse extends BasicResponse {} export interface BankItemResponse extends BasicResponse {}
export class BankItem extends BasicOrder{ export class BankItem extends BasicOrder {
order_type = "BankItem"; order_type = 'BankItem'
originalRequest:BankItemRequest originalRequest: BankItemRequest
originalResponse?:BankItemResponse originalResponse?: BankItemResponse
constructor(details:TxnDetails) { constructor(details: TxnDetails) {
super(details) super(details)
this.originalRequest = { this.originalRequest = {
item_uid: details.item_uid, item_uid: details.item_uid,
qty: details.count.toString(), qty: details.count.toString(),
account: details.target, account: details.target,
} }
} }
async tick(r:RefStore, api:LTOApi):Promise<void> { async tick(r: RefStore, api: LTOApi): Promise<void> {
if(this.state !== "PENDING" ){ if (this.state !== 'PENDING') {
return return
} }
this.mark("WORKING") this.mark('WORKING')
return api.BankAction<BankItemRequest, BankItemResponse>("bank-item",this.originalRequest) return api
.then((x)=>{ .BankAction<BankItemRequest, BankItemResponse>('bank-item', this.originalRequest)
debug("BankItem",x) .then(x => {
if(x.status === 'success'){ debug('BankItem', x)
this.stage = 1 if (x.status === 'success') {
this.originalResponse = x this.stage = 1
this.mark("SUCCESS") this.originalResponse = x
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]! this.mark('SUCCESS')
origin_item.item_count = origin_item.item_count - this.details?.count! const origin_item = r.invs.value.get(this.details?.origin_path!)?.items[
}else { this.details?.item_uid!
throw x.message ? x.message : "unknown error" ]!
origin_item.item_count = origin_item.item_count - this.details?.count!
} else {
throw x.message ? x.message : 'unknown error'
} }
}) })
.catch((e)=>{ .catch(e => {
this.stage = 1 this.stage = 1
this.err = e this.err = e
this.mark("ERROR") this.mark('ERROR')
}) })
} }
parse(i:any):BankItem { parse(i: any): BankItem {
super.parse(i) super.parse(i)
this.originalRequest = i.originalRequest this.originalRequest = i.originalRequest
this.originalResponse = i.originalResponse this.originalResponse = i.originalResponse
@ -232,69 +246,70 @@ export class BankItem extends BasicOrder{
} }
} }
export interface PrivateMarketRequest { export interface PrivateMarketRequest {
item_uid:string item_uid: string
qty:string qty: string
account:string account: string
currency:string currency: string
price:number price: number
private:number private: number
} }
export interface PrivateMarketResponse extends BasicResponse {} export interface PrivateMarketResponse extends BasicResponse {}
export class PrivateMarket extends BasicOrder{ export class PrivateMarket extends BasicOrder {
order_type = "PrivateMarket"; order_type = 'PrivateMarket'
originalRequest:PrivateMarketRequest originalRequest: PrivateMarketRequest
originalResponse?:PrivateMarketResponse originalResponse?: PrivateMarketResponse
listingId?: string listingId?: string
listingHash?: string listingHash?: string
constructor(details:TxnDetails) { constructor(details: TxnDetails) {
super(details) super(details)
this.originalRequest = { this.originalRequest = {
item_uid: details.item_uid, item_uid: details.item_uid,
qty: details.count.toString(), qty: details.count.toString(),
account: details.origin_account, account: details.origin_account,
private: 1, private: 1,
currency: "0", currency: '0',
price: 1, price: 1,
} }
} }
async tick(r:RefStore, api:LTOApi):Promise<void> { async tick(r: RefStore, api: LTOApi): Promise<void> {
if(this.state !== "PENDING" ){ if (this.state !== 'PENDING') {
return return
} }
this.mark("WORKING") this.mark('WORKING')
return api.BankAction<PrivateMarketRequest, PrivateMarketResponse>("sell-item",this.originalRequest) return api
.then((x)=>{ .BankAction<PrivateMarketRequest, PrivateMarketResponse>('sell-item', this.originalRequest)
debug("PrivateMarket",x) .then(x => {
if(x.status === 'success'){ debug('PrivateMarket', x)
if (x.status === 'success') {
this.stage = 1
this.originalResponse = x
this.mark('SUCCESS')
this.listingId = x.data.listing_id
this.listingHash = x.data.hash
try {
const origin_item = r.invs.value.get(this.details?.origin_path!)?.items[
this.details?.item_uid!
]!
origin_item.item_count = origin_item.item_count - this.details?.count!
} catch (_e) {}
} else {
throw x.message ? x.message : 'unknown error'
}
})
.catch(e => {
this.stage = 1 this.stage = 1
this.originalResponse = x this.err = e
this.mark("SUCCESS") this.mark('ERROR')
this.listingId = x.data["listing_id"] })
this.listingHash = x.data["hash"]
try{
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
origin_item.item_count = origin_item.item_count - this.details?.count!
}catch(e){
}
}else {
throw x.message ? x.message : "unknown error"
}
})
.catch((e)=>{
this.stage = 1
this.err = e
this.mark("ERROR")
})
} }
parse(i:any):PrivateMarket { parse(i: any): PrivateMarket {
super.parse(i) super.parse(i)
this.originalRequest = i.originalRequest this.originalRequest = i.originalRequest
this.originalResponse = i.originalResponse this.originalResponse = i.originalResponse
@ -304,11 +319,10 @@ export class PrivateMarket extends BasicOrder{
} }
} }
export interface MarketMoveRequest { export interface MarketMoveRequest {
listing_id?: string listing_id?: string
qty:string qty: string
account:string account: string
character: string character: string
} }
@ -316,77 +330,77 @@ export interface MarketMoveResponse extends BasicResponse {
item_uid: string item_uid: string
} }
export class MarketMove extends PrivateMarket { export class MarketMove extends PrivateMarket {
order_type = "MarketMove"; order_type = 'MarketMove'
moveRequest:MarketMoveRequest moveRequest: MarketMoveRequest
moveResponse?:MarketMoveResponse moveResponse?: MarketMoveResponse
moveStage:number moveStage: number
moveState: TxnState moveState: TxnState
newUid: string newUid: string
constructor(details:TxnDetails) { constructor(details: TxnDetails) {
super(details) super(details)
this.moveStage = 0 this.moveStage = 0
this.moveState = "PENDING" this.moveState = 'PENDING'
this.newUid = "" this.newUid = ''
this.moveRequest = { this.moveRequest = {
qty: details.count.toString(), qty: details.count.toString(),
account: details.target_account, account: details.target_account,
character: (details.target_path.includes("/")) ? details.target : "0" , character: details.target_path.includes('/') ? details.target : '0',
listing_id: "", // not initially populated listing_id: '', // not initially populated
} }
} }
async tick(r:RefStore, api:LTOApi):Promise<void> { async tick(r: RefStore, api: LTOApi): Promise<void> {
try { try {
await super.tick(r, api) await super.tick(r, api)
}catch(e){ } catch (_e) {
return return
} }
switch(super.status()) { switch (super.status()) {
case "SUCCESS": case 'SUCCESS':
break; break
case "ERROR": case 'ERROR':
this.moveState = "ERROR" this.moveState = 'ERROR'
return return
default: default:
return return
} }
if(this.moveState !== "PENDING" ){ if (this.moveState !== 'PENDING') {
return return
} }
this.moveRequest.listing_id = `${this.listingId}-${this.listingHash}` this.moveRequest.listing_id = `${this.listingId}-${this.listingHash}`
this.moveState = "WORKING" this.moveState = 'WORKING'
return api.BankAction<MarketMoveRequest, MarketMoveResponse>("buy-from-order",this.moveRequest) return api
.then((x)=>{ .BankAction<MarketMoveRequest, MarketMoveResponse>('buy-from-order', this.moveRequest)
debug("MarketMove",x) .then(x => {
this.moveResponse = x debug('MarketMove', x)
if(x.status === 'success'){ this.moveResponse = x
this.moveStage = 1 if (x.status === 'success') {
this.moveState = "SUCCESS" this.moveStage = 1
this.newUid = x.item_uid this.moveState = 'SUCCESS'
}else { this.newUid = x.item_uid
throw x ? x : "unknown error" } else {
throw x ? x : 'unknown error'
} }
}) })
.catch((e)=>{ .catch(e => {
this.moveStage = 1 this.moveStage = 1
this.err = e this.err = e
this.moveState = "ERROR" this.moveState = 'ERROR'
}) })
} }
progress():[number,number]{ progress(): [number, number] {
return [this.stage + this.moveStage, 2] return [this.stage + this.moveStage, 2]
} }
status():string { status(): string {
return this.moveState return this.moveState
} }
parse(i:any):MarketMove { parse(i: any): MarketMove {
super.parse(i) super.parse(i)
this.moveRequest = i.moveRequest this.moveRequest = i.moveRequest
this.moveResponse = i.moveResponse this.moveResponse = i.moveResponse
@ -397,71 +411,72 @@ export class MarketMove extends PrivateMarket {
} }
export class MarketMoveToChar extends MarketMove { export class MarketMoveToChar extends MarketMove {
order_type = "MarketMoveToChar"; order_type = 'MarketMoveToChar'
charRequest:InternalXferRequest charRequest: InternalXferRequest
charResponse?:InternalXferResponse charResponse?: InternalXferResponse
charStage:number charStage: number
charState: TxnState charState: TxnState
constructor(details:TxnDetails) { constructor(details: TxnDetails) {
super(details) super(details)
this.charStage = 0 this.charStage = 0
this.charState = "PENDING" this.charState = 'PENDING'
this.charRequest = { this.charRequest = {
item_uid: "", item_uid: '',
qty: details.count.toString(), qty: details.count.toString(),
new_char: details.target, new_char: details.target,
account: details.target_account, account: details.target_account,
} }
} }
async tick(r:RefStore, api:LTOApi):Promise<void> { async tick(r: RefStore, api: LTOApi): Promise<void> {
try { try {
await super.tick(r, api) await super.tick(r, api)
}catch(e){ } catch (_e) {
return return
} }
switch(super.status()) { switch (super.status()) {
case "SUCCESS": case 'SUCCESS':
break; break
case "ERROR": case 'ERROR':
this.charState = "ERROR" this.charState = 'ERROR'
return return
default: default:
return return
} }
if(this.charState !== "PENDING" ){ if (this.charState !== 'PENDING') {
return return
} }
this.charState = "WORKING" this.charState = 'WORKING'
this.charRequest.item_uid = this.newUid this.charRequest.item_uid = this.newUid
return api.BankAction<InternalXferRequest, InternalXferResponse>("internal-xfer-item",this.charRequest) return api
.then((x)=>{ .BankAction<InternalXferRequest, InternalXferResponse>('internal-xfer-item', this.charRequest)
debug("MarketMoveToChar",x) .then(x => {
this.charResponse = x debug('MarketMoveToChar', x)
if(x.status === 'success'){ this.charResponse = x
this.charStage = 1 if (x.status === 'success') {
this.charState = "SUCCESS" this.charStage = 1
}else { this.charState = 'SUCCESS'
throw x ? x : "unknown error" } else {
throw x ? x : 'unknown error'
} }
}) })
.catch((e)=>{ .catch(e => {
this.charStage = 1 this.charStage = 1
this.err = e this.err = e
this.charState = "ERROR" this.charState = 'ERROR'
}) })
} }
progress():[number,number]{ progress(): [number, number] {
return [this.stage +this.moveStage+ this.charStage, 3] return [this.stage + this.moveStage + this.charStage, 3]
} }
status():string { status(): string {
return this.charState return this.charState
} }
parse(i:any):MarketMoveToChar { parse(i: any): MarketMoveToChar {
super.parse(i) super.parse(i)
this.charRequest = i.charRequest this.charRequest = i.charRequest
this.charResponse = i.charResponse this.charResponse = i.charResponse

View File

@ -1,73 +1,78 @@
import { RefStore } from "../../state/state"; import { RefStore } from '../../state/state'
import { Serializable } from "../storage"; import { Serializable } from '../storage'
import { TricksterCharacter } from "../trickster"; import { TricksterCharacter } from '../trickster'
import { LTOApi } from "./api"; import { LTOApi } from './api'
import { pathIsBank, splitPath } from "./lifeto"; import { pathIsBank, splitPath } from './lifeto'
import { BankItem, InternalXfer, InvalidOrder, MarketMove, Order,MarketMoveToChar, TxnDetails } from "./order"; import {
BankItem,
InternalXfer,
InvalidOrder,
MarketMove,
MarketMoveToChar,
Order,
TxnDetails,
} from './order'
export interface OrderDetails { export interface OrderDetails {
item_uid: string | "galders" item_uid: string | 'galders'
count:number count: number
origin_path:string origin_path: string
target_path:string target_path: string
} }
const notSupported = new InvalidOrder("not supported yet") const notSupported = new InvalidOrder('not supported yet')
const notFound = new InvalidOrder("character not found") const notFound = new InvalidOrder('character not found')
export class OrderTracker implements Serializable<OrderTracker> { export class OrderTracker implements Serializable<OrderTracker> {
orders: {[key:string]:Order} = {} orders: { [key: string]: Order } = {}
async tick(r:RefStore, api:LTOApi):Promise<any> { async tick(r: RefStore, api: LTOApi): Promise<any> {
let hasDirty = false let hasDirty = false
console.log("ticking") for (const [id, order] of Object.entries(this.orders)) {
for(const [id, order] of Object.entries(this.orders)) { if (order.status() === 'SUCCESS' || order.status() === 'ERROR') {
if(order.status() == "SUCCESS" || order.status() == "ERROR") {
console.log("finished order", order)
hasDirty = true hasDirty = true
delete this.orders[id] delete this.orders[id]
} }
order.tick(r,api) order.tick(r, api)
} }
if(hasDirty){ if (hasDirty) {
r.dirty.value++ r.dirty.value++
} }
return return
} }
parse(s: any): OrderTracker { parse(s: any): OrderTracker {
if(s == undefined) { if (s === undefined) {
return new OrderTracker() return new OrderTracker()
} }
if(s.orders == undefined) { if (s.orders === undefined) {
return new OrderTracker() return new OrderTracker()
} }
this.orders = {} this.orders = {}
const raw: Order[] = Object.values(s.orders) const raw: Order[] = Object.values(s.orders)
for(const o of raw) { for (const o of raw) {
let newOrder:Order | undefined = undefined let newOrder: Order | undefined
console.log("loading", o) if (o.details) {
if(o.details){ if (o.status() === 'SUCCESS' || o.status() === 'ERROR') {
if(o.status() == "SUCCESS" || o.status() == "ERROR") {
continue continue
} }
switch(o.order_type) { switch (o.order_type) {
case "InternalXfer": case 'InternalXfer':
newOrder = new InternalXfer(o.details).parse(o) newOrder = new InternalXfer(o.details).parse(o)
break; break
case "BankItem": case 'BankItem':
newOrder = new BankItem(o.details).parse(o) newOrder = new BankItem(o.details).parse(o)
break; break
case "MarketMove": case 'MarketMove':
newOrder = new MarketMove(o.details).parse(o) newOrder = new MarketMove(o.details).parse(o)
case "MarketMoveToChar": case 'MarketMoveToChar':
newOrder = new MarketMoveToChar(o.details).parse(o) newOrder = new MarketMoveToChar(o.details).parse(o)
break; break
case "InvalidOrder": case 'InvalidOrder':
newOrder = new InvalidOrder("").parse(o) newOrder = new InvalidOrder('').parse(o)
break; break
} }
if(newOrder) { if (newOrder) {
this.orders[newOrder.action_id] = newOrder this.orders[newOrder.action_id] = newOrder
} }
} }
@ -79,79 +84,78 @@ export class OrderTracker implements Serializable<OrderTracker> {
export class OrderSender { export class OrderSender {
constructor( constructor(
private orders: OrderTracker, private orders: OrderTracker,
private chars: Map<string,TricksterCharacter>, private chars: Map<string, TricksterCharacter>,
) { ) {}
}
send(o:OrderDetails):Order { send(o: OrderDetails): Order {
const formed = this.form(o) const formed = this.form(o)
this.orders.orders[formed.action_id] = formed this.orders.orders[formed.action_id] = formed
return formed return formed
} }
form(o:OrderDetails):Order { form(o: OrderDetails): Order {
// bank to bank // bank to bank
if(pathIsBank(o.origin_path) && pathIsBank(o.target_path)) { if (pathIsBank(o.origin_path) && pathIsBank(o.target_path)) {
return this.bank_to_bank(o) return this.bank_to_bank(o)
} }
// bank to user // bank to user
if(pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) { if (pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) {
return this.bank_to_user(o) return this.bank_to_user(o)
} }
// user to bank // user to bank
if(!pathIsBank(o.origin_path) && pathIsBank(o.target_path)) { if (!pathIsBank(o.origin_path) && pathIsBank(o.target_path)) {
return this.user_to_bank(o) return this.user_to_bank(o)
} }
// user to user // user to user
if(!pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) { if (!pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) {
return this.user_to_user(o) return this.user_to_user(o)
} }
return notSupported return notSupported
} }
bank_to_bank(o:OrderDetails): Order{ bank_to_bank(o: OrderDetails): Order {
const origin = this.chars.get(o.origin_path) const origin = this.chars.get(o.origin_path)
const target = this.chars.get(o.target_path) const target = this.chars.get(o.target_path)
if(!(origin && target)) { if (!(origin && target)) {
return notFound return notFound
} }
return new MarketMove(this.transformInternalOrder(o)) return new MarketMove(this.transformInternalOrder(o))
} }
bank_to_user(o:OrderDetails): Order{ bank_to_user(o: OrderDetails): Order {
// get the uid of the bank // get the uid of the bank
const origin = this.chars.get(o.origin_path) const origin = this.chars.get(o.origin_path)
const target = this.chars.get(o.target_path) const target = this.chars.get(o.target_path)
if(!(origin && target)) { if (!(origin && target)) {
return notFound return notFound
} }
const [account, name] = splitPath(target.path) const [_account, _name] = splitPath(target.path)
/*if(account != origin.path) { /*if(account != origin.path) {
return new MarketMoveToChar(this.transformInternalOrder(o)) return new MarketMoveToChar(this.transformInternalOrder(o))
}*/ }*/
return new InternalXfer(this.transformInternalOrder(o)) return new InternalXfer(this.transformInternalOrder(o))
} }
user_to_bank(o:OrderDetails): Order{ user_to_bank(o: OrderDetails): Order {
const origin = this.chars.get(o.origin_path) const origin = this.chars.get(o.origin_path)
const target = this.chars.get(o.target_path) const target = this.chars.get(o.target_path)
if(!(origin && target)) { if (!(origin && target)) {
return notFound return notFound
} }
const [account, name] = splitPath(origin.path) const [_account, _name] = splitPath(origin.path)
/*if(account != target.path) { /*if(account != target.path) {
return new MarketMove(this.transformInternalOrder(o)) return new MarketMove(this.transformInternalOrder(o))
}*/ }*/
return new BankItem(this.transformInternalOrder(o)) return new BankItem(this.transformInternalOrder(o))
} }
user_to_user(o:OrderDetails): Order{ user_to_user(o: OrderDetails): Order {
const origin = this.chars.get(o.origin_path) const origin = this.chars.get(o.origin_path)
const target = this.chars.get(o.target_path) const target = this.chars.get(o.target_path)
if(!(origin && target)) { if (!(origin && target)) {
return notFound return notFound
} }
// return new MarketMoveToChar(this.transformInternalOrder(o)) // return new MarketMoveToChar(this.transformInternalOrder(o))
return new InternalXfer(this.transformInternalOrder(o)) return new InternalXfer(this.transformInternalOrder(o))
} }
private transformInternalOrder(o:OrderDetails):TxnDetails { private transformInternalOrder(o: OrderDetails): TxnDetails {
const origin = this.chars.get(o.origin_path)! const origin = this.chars.get(o.origin_path)!
const target = this.chars.get(o.target_path)! const target = this.chars.get(o.target_path)!
return { return {
@ -166,4 +170,3 @@ export class OrderSender {
} }
} }
} }

View File

@ -1,49 +1,52 @@
import { RefStore } from "../../state/state"; import { RefStore } from '../../state/state'
import { bank_endpoint, Session } from "../session"; import { Session } from '../session'
import { TricksterAccount, TricksterInventory } from "../trickster"; import { TricksterAccount, TricksterInventory } from '../trickster'
import { BankEndpoint, LTOApi } from "./api"; import { BankEndpoint, LTOApi } from './api'
export interface SessionBinding { export interface SessionBinding {
new(s:Session):LTOApi new (s: Session): LTOApi
} }
export const getLTOState = <A extends LTOApi>(c: new (s:Session) => A,s:Session, r:RefStore): LTOApi => { export const getLTOState = <A extends LTOApi>(
return new StatefulLTOApi(new c(s),r); c: new (s: Session) => A,
s: Session,
r: RefStore,
): LTOApi => {
return new StatefulLTOApi(new c(s), r)
} }
export class StatefulLTOApi implements LTOApi { export class StatefulLTOApi implements LTOApi {
u: LTOApi u: LTOApi
r: RefStore r: RefStore
constructor(s:LTOApi, r:RefStore){ constructor(s: LTOApi, r: RefStore) {
this.u = s this.u = s
this.r=r this.r = r
} }
BankAction = <T,D>(e: BankEndpoint, t: T):Promise<D> => { BankAction = <T, D>(e: BankEndpoint, t: T): Promise<D> => {
return this.u.BankAction(e,t) return this.u.BankAction(e, t)
} }
GetInventory = async (path:string):Promise<TricksterInventory>=>{ GetInventory = async (path: string): Promise<TricksterInventory> => {
const inv = await this.u.GetInventory(path) const inv = await this.u.GetInventory(path)
if(this.r.invs.value.get(inv.path)){ if (this.r.invs.value.get(inv.path)) {
this.r.invs.value.get(inv.path)!.items = inv.items this.r.invs.value.get(inv.path)!.items = inv.items
}else{ } else {
this.r.invs.value.set(inv.path,inv) this.r.invs.value.set(inv.path, inv)
} }
if(inv.galders) { if (inv.galders) {
this.r.invs.value.get(inv.path)!.galders = inv.galders this.r.invs.value.get(inv.path)!.galders = inv.galders
} }
this.r.dirty.value = this.r.dirty.value + 1 this.r.dirty.value = this.r.dirty.value + 1
return inv return inv
} }
GetAccounts = async ():Promise<TricksterAccount[]> => { GetAccounts = async (): Promise<TricksterAccount[]> => {
const xs = await this.u.GetAccounts() const xs = await this.u.GetAccounts()
xs.forEach((x)=>{ xs.forEach(x => {
x.characters.forEach((ch)=>{ x.characters.forEach(ch => {
this.r.chars.value.set(ch.path,ch) this.r.chars.value.set(ch.path, ch)
}) })
}) })
return xs return xs
} }
GetLoggedin= async ():Promise<boolean>=>{ GetLoggedin = async (): Promise<boolean> => {
return this.u.GetLoggedin() return this.u.GetLoggedin()
} }
} }

View File

@ -1,37 +1,36 @@
import Handsontable from "handsontable" import Handsontable from 'handsontable'
import numbro from 'numbro'; import Core from 'handsontable/core'
import { textRenderer } from "handsontable/renderers" import { textRenderer } from 'handsontable/renderers'
import { TricksterInventory, TricksterItem } from "./trickster" import numbro from 'numbro'
import Core from "handsontable/core"; import { TricksterItem } from './trickster'
import { RefStore } from "../state/state";
export const BasicColumns = ['Image', 'Name', 'Count'] as const
export const BasicColumns = [ export const DetailsColumns = ['Desc', 'Use'] as const
"Image","Name","Count",
] as const
export const DetailsColumns = [ export const MoveColumns = ['MoveCount', 'Move'] as const
"Desc","Use",
] as const
export const MoveColumns = [ export const TagColumns = ['All', 'Equip', 'Drill', 'Card', 'Quest', 'Consume', 'Compound'] as const
"MoveCount","Move",
] as const
export const TagColumns = [ export const EquipmentColumns = ['MinLvl', 'Slots', 'RefineNumber', 'RefineState'] as const
"All","Equip","Drill","Card","Quest","Consume", "Compound"
] as const
export const EquipmentColumns = [
"MinLvl","Slots","RefineNumber","RefineState",
] as const
export const StatsColumns = [ export const StatsColumns = [
"AP","GunAP","AC","DX","MP","MA","MD","WT","DA","LK","HP","DP","HV", 'AP',
'GunAP',
'AC',
'DX',
'MP',
'MA',
'MD',
'WT',
'DA',
'LK',
'HP',
'DP',
'HV',
] as const ] as const
export const HackColumns = [ export const HackColumns = [] as const
] as const
export const ColumnNames = [ export const ColumnNames = [
...BasicColumns, ...BasicColumns,
@ -43,478 +42,520 @@ export const ColumnNames = [
...HackColumns, ...HackColumns,
] as const ] as const
export type ColumnName = typeof ColumnNames[number] export type ColumnName = (typeof ColumnNames)[number]
const c = (a:ColumnName | ColumnInfo):ColumnName => { const c = (a: ColumnName | ColumnInfo): ColumnName => {
switch(typeof a) { switch (typeof a) {
case "string": case 'string':
return a return a
case "object": case 'object':
return a.name return a.name
} }
} }
export const LazyColumn = c; export const LazyColumn = c
export const ColumnSorter = (a:ColumnName | ColumnInfo, b: ColumnName | ColumnInfo):number => { export const ColumnSorter = (a: ColumnName | ColumnInfo, b: ColumnName | ColumnInfo): number => {
let n1 = ColumnNames.indexOf(c(a)) const n1 = ColumnNames.indexOf(c(a))
let n2 = ColumnNames.indexOf(c(b)) const n2 = ColumnNames.indexOf(c(b))
if(n1 == n2) { if (n1 === n2) {
return 0 return 0
} }
return n1 > n2 ? 1 : -1 return n1 > n2 ? 1 : -1
} }
export interface ColumnInfo { export interface ColumnInfo {
name: ColumnName name: ColumnName
displayName:string displayName: string
options?:(s:string[])=>string[] options?: (s: string[]) => string[]
renderer?:any renderer?: any
filtering?:boolean filtering?: boolean
writable?:boolean writable?: boolean
getter(item:TricksterItem):(string | number) getter(item: TricksterItem): string | number
} }
class Image implements ColumnInfo { class Image implements ColumnInfo {
name:ColumnName = 'Image' name: ColumnName = 'Image'
displayName = " " displayName = ' '
renderer = coverRenderer renderer = coverRenderer
getter(item:TricksterItem):(string|number) { getter(item: TricksterItem): string | number {
return item.item_image ? item.item_image : "" return item.item_image ? item.item_image : ''
} }
} }
function coverRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function coverRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
td: any,
_row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
if (stringifiedValue.startsWith('http')) { if (stringifiedValue.startsWith('http')) {
const img:any = document.createElement('IMG'); const img: any = document.createElement('IMG')
img.src = value; img.src = value
Handsontable.dom.addEvent(img, 'mousedown', event =>{ Handsontable.dom.addEvent(img, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(img); td.appendChild(img)
} else { } else {
} }
} }
class Name implements ColumnInfo { class Name implements ColumnInfo {
name:ColumnName = "Name" name: ColumnName = 'Name'
displayName = "Name" displayName = 'Name'
filtering = true filtering = true
renderer = nameRenderer renderer = nameRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_name return item.item_name
} }
} }
function nameRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function nameRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
let showText = stringifiedValue; td: any,
const div= document.createElement('div'); _row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
const showText = stringifiedValue
const div = document.createElement('div')
div.innerHTML = showText div.innerHTML = showText
div.title = showText div.title = showText
div.style.maxWidth = "20ch" div.style.maxWidth = '20ch'
div.style.textOverflow = "ellipsis" div.style.textOverflow = 'ellipsis'
div.style.overflow= "hidden" div.style.overflow = 'hidden'
div.style.whiteSpace= "nowrap" div.style.whiteSpace = 'nowrap'
Handsontable.dom.addEvent(div, 'mousedown', event =>{ Handsontable.dom.addEvent(div, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(div); td.appendChild(div)
td.classList.add("htLeft") td.classList.add('htLeft')
} }
class Count implements ColumnInfo { class Count implements ColumnInfo {
name:ColumnName = "Count" name: ColumnName = 'Count'
displayName = "Count" displayName = 'Count'
renderer = "numeric" renderer = 'numeric'
filtering = true filtering = true
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_count return item.item_count
} }
} }
class Move implements ColumnInfo { class Move implements ColumnInfo {
name:ColumnName = "Move" name: ColumnName = 'Move'
displayName = "Target" displayName = 'Target'
writable = true writable = true
options = getMoveTargets options = getMoveTargets
getter(item:TricksterItem):(string|number){ getter(_item: TricksterItem): string | number {
return "---------------------------------------------" return '---------------------------------------------'
} }
} }
const getMoveTargets = (invs: string[]):string[] => { const getMoveTargets = (invs: string[]): string[] => {
let out:string[] = []; const out: string[] = []
for(const k of invs){ for (const k of invs) {
out.push(k) out.push(k)
} }
out.push("") out.push('')
out.push("") out.push('')
out.push("!TRASH") out.push('!TRASH')
return out return out
} }
class MoveCount implements ColumnInfo { class MoveCount implements ColumnInfo {
name:ColumnName = "MoveCount" name: ColumnName = 'MoveCount'
displayName = "Move #" displayName = 'Move #'
renderer = moveCountRenderer renderer = moveCountRenderer
writable = true writable = true
getter(item:TricksterItem):(string|number){ getter(_item: TricksterItem): string | number {
return "" return ''
} }
} }
function moveCountRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) { function moveCountRenderer(
let newValue = value; instance: Core,
td: any,
row: number,
col: number,
prop: any,
value: any,
cellProperties: any,
) {
let newValue = value
if (Handsontable.helper.isNumeric(newValue)) { if (Handsontable.helper.isNumeric(newValue)) {
const numericFormat = cellProperties.numericFormat; const numericFormat = cellProperties.numericFormat
const cellCulture = numericFormat && numericFormat.culture || '-'; const cellCulture = numericFormat?.culture || '-'
const cellFormatPattern = numericFormat && numericFormat.pattern; const cellFormatPattern = numericFormat?.pattern
const className = cellProperties.className || ''; const className = cellProperties.className || ''
const classArr = className.length ? className.split(' ') : []; const classArr = className.length ? className.split(' ') : []
if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) { if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) {
const shortTag:any = cellCulture.replace('-', ''); const shortTag: any = cellCulture.replace('-', '')
const langData = (numbro as any)[shortTag]; const langData = (numbro as any)[shortTag]
if (langData) { if (langData) {
numbro.registerLanguage(langData); numbro.registerLanguage(langData)
} }
} }
const totalCount = Number(instance.getCell(row,col-1)?.innerHTML) const totalCount = Number(instance.getCell(row, col - 1)?.innerHTML)
numbro.setLanguage(cellCulture); numbro.setLanguage(cellCulture)
const num = numbro(newValue) const num = numbro(newValue)
if(totalCount < num.value()) { if (totalCount < num.value()) {
const newNum = numbro(totalCount) const newNum = numbro(totalCount)
newValue = newNum.format(cellFormatPattern || '0'); newValue = newNum.format(cellFormatPattern || '0')
}else { } else {
newValue = num.format(cellFormatPattern || '0'); newValue = num.format(cellFormatPattern || '0')
} }
if (classArr.indexOf('htLeft') < 0 && classArr.indexOf('htCenter') < 0 && if (
classArr.indexOf('htRight') < 0 && classArr.indexOf('htJustify') < 0) { classArr.indexOf('htLeft') < 0 &&
classArr.push('htRight'); classArr.indexOf('htCenter') < 0 &&
classArr.indexOf('htRight') < 0 &&
classArr.indexOf('htJustify') < 0
) {
classArr.push('htRight')
} }
if (classArr.indexOf('htNumeric') < 0) { if (classArr.indexOf('htNumeric') < 0) {
classArr.push('htNumeric'); classArr.push('htNumeric')
} }
cellProperties.className = classArr.join(' '); cellProperties.className = classArr.join(' ')
td.dir = 'ltr'; td.dir = 'ltr'
newValue = newValue + "x" newValue = `${newValue}x`
}else { } else {
newValue = "" newValue = ''
} }
textRenderer(instance, td, row, col, prop, newValue, cellProperties); textRenderer(instance, td, row, col, prop, newValue, cellProperties)
} }
class Equip implements ColumnInfo { class Equip implements ColumnInfo {
name:ColumnName = "Equip" name: ColumnName = 'Equip'
displayName = "equip" displayName = 'equip'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.is_equip ? 1 : 0 return item.is_equip ? 1 : 0
} }
} }
class Drill implements ColumnInfo { class Drill implements ColumnInfo {
name:ColumnName = "Drill" name: ColumnName = 'Drill'
displayName = "drill" displayName = 'drill'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.is_drill ? 1 : 0 return item.is_drill ? 1 : 0
} }
} }
class All implements ColumnInfo { class All implements ColumnInfo {
name:ColumnName = "All" name: ColumnName = 'All'
displayName = "swap" displayName = 'swap'
getter(_:TricksterItem):(string|number){ getter(_: TricksterItem): string | number {
return -10000 return -10000
} }
} }
class Card implements ColumnInfo { class Card implements ColumnInfo {
name:ColumnName = "Card" name: ColumnName = 'Card'
displayName = "card" displayName = 'card'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return cardFilter(item) ? 1 : 0 return cardFilter(item) ? 1 : 0
} }
} }
const cardFilter= (item:TricksterItem): boolean => { const cardFilter = (item: TricksterItem): boolean => {
return (item.item_name.endsWith(" Card") || item.item_name.startsWith("Star Card")) return item.item_name.endsWith(' Card') || item.item_name.startsWith('Star Card')
} }
class Compound implements ColumnInfo { class Compound implements ColumnInfo {
name:ColumnName = "Compound" name: ColumnName = 'Compound'
displayName = "comp" displayName = 'comp'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return compFilter(item) ? 1 : 0 return compFilter(item) ? 1 : 0
} }
} }
const compFilter= (item:TricksterItem): boolean => { const compFilter = (item: TricksterItem): boolean => {
return (item.item_comment.toLowerCase().includes("compound item")) return item.item_comment.toLowerCase().includes('compound item')
} }
class Quest implements ColumnInfo { class Quest implements ColumnInfo {
name:ColumnName = "Quest" name: ColumnName = 'Quest'
displayName = "quest" displayName = 'quest'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return questFilter(item) ? 1 : 0 return questFilter(item) ? 1 : 0
} }
} }
const questFilter= (item:TricksterItem): boolean => { const questFilter = (_item: TricksterItem): boolean => {
return false return false
} }
class Consume implements ColumnInfo { class Consume implements ColumnInfo {
name:ColumnName = "Consume" name: ColumnName = 'Consume'
displayName = "eat" displayName = 'eat'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return consumeFilter(item) ? 1 : 0 return consumeFilter(item) ? 1 : 0
} }
} }
const consumeFilter= (item:TricksterItem): boolean => { const consumeFilter = (item: TricksterItem): boolean => {
const tl = item.item_use.toLowerCase() const tl = item.item_use.toLowerCase()
return tl.includes("recover") || tl.includes("restores") return tl.includes('recover') || tl.includes('restores')
} }
class AP implements ColumnInfo { class AP implements ColumnInfo {
name:ColumnName = "AP" name: ColumnName = 'AP'
displayName = "AP" displayName = 'AP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["AP"] : "" return item.stats ? item.stats.AP : ''
} }
} }
class GunAP implements ColumnInfo { class GunAP implements ColumnInfo {
name:ColumnName = "GunAP" name: ColumnName = 'GunAP'
displayName = "Gun AP" displayName = 'Gun AP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["Gun AP"] : "" return item.stats ? item.stats['Gun AP'] : ''
} }
} }
class AC implements ColumnInfo { class AC implements ColumnInfo {
name:ColumnName = "AC" name: ColumnName = 'AC'
displayName = "AC" displayName = 'AC'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["AC"] : "" return item.stats ? item.stats.AC : ''
} }
} }
class DX implements ColumnInfo { class DX implements ColumnInfo {
name:ColumnName = "DX" name: ColumnName = 'DX'
displayName = "DX" displayName = 'DX'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["DX"] : "" return item.stats ? item.stats.DX : ''
} }
} }
class MP implements ColumnInfo { class MP implements ColumnInfo {
name:ColumnName = "MP" name: ColumnName = 'MP'
displayName = "MP" displayName = 'MP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["MP"] : "" return item.stats ? item.stats.MP : ''
} }
} }
class MA implements ColumnInfo { class MA implements ColumnInfo {
name:ColumnName = "MA" name: ColumnName = 'MA'
displayName = "MA" displayName = 'MA'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["MA"] : "" return item.stats ? item.stats.MA : ''
} }
} }
class MD implements ColumnInfo { class MD implements ColumnInfo {
name:ColumnName = "MD" name: ColumnName = 'MD'
displayName = "MD" displayName = 'MD'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["MD"] : "" return item.stats ? item.stats.MD : ''
} }
} }
class WT implements ColumnInfo { class WT implements ColumnInfo {
name:ColumnName = "WT" name: ColumnName = 'WT'
displayName = "WT" displayName = 'WT'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["WT"] : "" return item.stats ? item.stats.WT : ''
} }
} }
class DA implements ColumnInfo { class DA implements ColumnInfo {
name:ColumnName = "DA" name: ColumnName = 'DA'
displayName = "DA" displayName = 'DA'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["DA"] : "" return item.stats ? item.stats.DA : ''
} }
} }
class LK implements ColumnInfo { class LK implements ColumnInfo {
name:ColumnName = "LK" name: ColumnName = 'LK'
displayName = "LK" displayName = 'LK'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["LK"] : "" return item.stats ? item.stats.LK : ''
} }
} }
class HP implements ColumnInfo { class HP implements ColumnInfo {
name:ColumnName = "HP" name: ColumnName = 'HP'
displayName = "HP" displayName = 'HP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["HP"] : "" return item.stats ? item.stats.HP : ''
} }
} }
class DP implements ColumnInfo { class DP implements ColumnInfo {
name:ColumnName = "DP" name: ColumnName = 'DP'
displayName = "DP" displayName = 'DP'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["DP"] : "" return item.stats ? item.stats.DP : ''
} }
} }
class HV implements ColumnInfo { class HV implements ColumnInfo {
name:ColumnName = "HV" name: ColumnName = 'HV'
displayName = "HV" displayName = 'HV'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.stats ? item.stats["HV"] : "" return item.stats ? item.stats.HV : ''
} }
} }
class MinLvl implements ColumnInfo { class MinLvl implements ColumnInfo {
name:ColumnName = "MinLvl" name: ColumnName = 'MinLvl'
displayName = "lvl" displayName = 'lvl'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
//TODO: //TODO:
return item.item_min_level? item.item_min_level:"" return item.item_min_level ? item.item_min_level : ''
} }
} }
class Slots implements ColumnInfo { class Slots implements ColumnInfo {
name:ColumnName = "Slots" name: ColumnName = 'Slots'
displayName = "slots" displayName = 'slots'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
//TODO: //TODO:
return item.item_slots ? item.item_slots : "" return item.item_slots ? item.item_slots : ''
} }
} }
class RefineNumber implements ColumnInfo { class RefineNumber implements ColumnInfo {
name:ColumnName = "RefineNumber" name: ColumnName = 'RefineNumber'
displayName = "refine" displayName = 'refine'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.refine_level ? item.refine_level : 0 return item.refine_level ? item.refine_level : 0
} }
} }
class RefineState implements ColumnInfo { class RefineState implements ColumnInfo {
name:ColumnName = "RefineState" name: ColumnName = 'RefineState'
displayName = "bork" displayName = 'bork'
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.refine_state ? item.refine_state : 0 return item.refine_state ? item.refine_state : 0
} }
} }
class Desc implements ColumnInfo { class Desc implements ColumnInfo {
name:ColumnName = "Desc" name: ColumnName = 'Desc'
displayName = "desc" displayName = 'desc'
renderer = descRenderer renderer = descRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_comment return item.item_comment
} }
} }
function descRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function descRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
let showText = stringifiedValue; td: any,
const div= document.createElement('div'); _row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
const showText = stringifiedValue
const div = document.createElement('div')
div.innerHTML = showText div.innerHTML = showText
div.title = showText div.title = showText
div.style.maxWidth = "30ch" div.style.maxWidth = '30ch'
div.style.textOverflow = "ellipsis" div.style.textOverflow = 'ellipsis'
div.style.overflow= "hidden" div.style.overflow = 'hidden'
div.style.whiteSpace= "nowrap" div.style.whiteSpace = 'nowrap'
Handsontable.dom.addEvent(div, 'mousedown', event =>{ Handsontable.dom.addEvent(div, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(div); td.appendChild(div)
td.classList.add("htLeft") td.classList.add('htLeft')
} }
class Use implements ColumnInfo { class Use implements ColumnInfo {
name:ColumnName = "Use" name: ColumnName = 'Use'
displayName = "use" displayName = 'use'
renderer= useRenderer; renderer = useRenderer
getter(item:TricksterItem):(string|number){ getter(item: TricksterItem): string | number {
return item.item_use return item.item_use
} }
} }
function useRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) { function useRenderer(
const stringifiedValue = Handsontable.helper.stringify(value); _instance: any,
let showText = stringifiedValue; td: any,
const div= document.createElement('div'); _row: any,
_col: any,
_prop: any,
value: any,
_cellProperties: any,
) {
const stringifiedValue = Handsontable.helper.stringify(value)
const showText = stringifiedValue
const div = document.createElement('div')
div.title = showText div.title = showText
div.innerHTML = showText div.innerHTML = showText
div.style.maxWidth = "30ch" div.style.maxWidth = '30ch'
div.style.textOverflow = "ellipsis" div.style.textOverflow = 'ellipsis'
div.style.overflow= "hidden" div.style.overflow = 'hidden'
div.style.whiteSpace= "nowrap" div.style.whiteSpace = 'nowrap'
Handsontable.dom.addEvent(div, 'mousedown', event =>{ Handsontable.dom.addEvent(div, 'mousedown', event => {
event!.preventDefault(); event?.preventDefault()
}); })
Handsontable.dom.empty(td); Handsontable.dom.empty(td)
td.appendChild(div); td.appendChild(div)
td.classList.add("htLeft") td.classList.add('htLeft')
} }
export const ColumnByNames = (...n:ColumnName[]) => { export const ColumnByNames = (...n: ColumnName[]) => {
return n.map(ColumnByName) return n.map(ColumnByName)
} }
export const ColumnByName = (n:ColumnName) => { export const ColumnByName = (n: ColumnName) => {
return Columns[n] return Columns[n]
} }
export const test = <T extends ColumnInfo>(n:(new ()=>T)):[string,T] => { export const test = <T extends ColumnInfo>(n: new () => T): [string, T] => {
let nn = new n() const nn = new n()
return [nn.name, nn] return [nn.name, nn]
} }
export const Columns:{[Property in ColumnName]:ColumnInfo}= { export const Columns: { [Property in ColumnName]: ColumnInfo } = {
Use: new Use(), Use: new Use(),
Desc: new Desc(), Desc: new Desc(),
Image: new Image(), Image: new Image(),
Name: new Name(), Name: new Name(),
Count: new Count(), Count: new Count(),
Move: new Move(), Move: new Move(),
MoveCount: new MoveCount(), MoveCount: new MoveCount(),
Equip: new Equip(), Equip: new Equip(),
Drill: new Drill(), Drill: new Drill(),
Card: new Card(), Card: new Card(),
Quest: new Quest(), Quest: new Quest(),
Consume: new Consume(), Consume: new Consume(),
AP: new AP(), AP: new AP(),
GunAP: new GunAP(), GunAP: new GunAP(),
AC: new AC(), AC: new AC(),
DX: new DX(), DX: new DX(),
MP: new MP(), MP: new MP(),
MA: new MA(), MA: new MA(),
MD: new MD(), MD: new MD(),
WT: new WT(), WT: new WT(),
DA: new DA(), DA: new DA(),
LK: new LK(), LK: new LK(),
HP: new HP(), HP: new HP(),
DP: new DP(), DP: new DP(),
HV: new HV(), HV: new HV(),
MinLvl: new MinLvl(), MinLvl: new MinLvl(),
Slots: new Slots(), Slots: new Slots(),
RefineNumber: new RefineNumber(), RefineNumber: new RefineNumber(),
RefineState: new RefineState(), RefineState: new RefineState(),
All: new All(), All: new All(),
Compound: new Compound(), Compound: new Compound(),
} }

File diff suppressed because one or more lines are too long

View File

@ -1,117 +1,121 @@
import axios, { AxiosError, AxiosResponse, Method } from "axios"; import axios, { AxiosError, AxiosResponse, Method } from 'axios'
import qs from "qs"; import qs from 'qs'
import { getCookie, removeCookie } from "typescript-cookie"; import { TricksterAccountInfo } from './trickster'
import { TricksterAccountInfo } from "./trickster";
export const SITE_ROOT = '/lifeto/'
export const SITE_ROOT = "/lifeto/" export const API_ROOT = 'api/lifeto/'
export const BANK_ROOT = 'v2/item-manager/'
export const MARKET_ROOT = 'marketplace-api/'
export const API_ROOT = "api/lifeto/" const raw_endpoint = (name: string): string => {
export const BANK_ROOT = "v2/item-manager/" return SITE_ROOT + name
export const MARKET_ROOT = "marketplace-api/"
const raw_endpoint = (name:string):string =>{
return SITE_ROOT+name
} }
const login_endpoint = (name:string)=>{ const login_endpoint = (name: string) => {
return SITE_ROOT + name + "?canonical=1" return `${SITE_ROOT + name}?canonical=1`
} }
export const api_endpoint = (name:string):string =>{ export const api_endpoint = (name: string): string => {
return SITE_ROOT+API_ROOT + name return SITE_ROOT + API_ROOT + name
} }
export const bank_endpoint = (name:string):string =>{ export const bank_endpoint = (name: string): string => {
return SITE_ROOT+BANK_ROOT + name return SITE_ROOT + BANK_ROOT + name
} }
export const market_endpoint = (name:string):string =>{ export const market_endpoint = (name: string): string => {
return SITE_ROOT+MARKET_ROOT+ name return SITE_ROOT + MARKET_ROOT + name
} }
export const EndpointCreators = [ export const EndpointCreators = [api_endpoint, bank_endpoint, market_endpoint]
api_endpoint,
bank_endpoint,
market_endpoint,
]
export type EndpointCreator = typeof EndpointCreators[number] export type EndpointCreator = (typeof EndpointCreators)[number]
export interface Session { export interface Session {
request:(verb:Method,url:string,data:any,c?:EndpointCreator)=>Promise<any> request: (verb: Method, url: string, data: any, c?: EndpointCreator) => Promise<any>
} }
export class LoginHelper { export class LoginHelper {
constructor(){ static login = async (user: string, pass: string): Promise<TokenSession> => {
} return axios
static login = async (user:string, pass: string):Promise<TokenSession> =>{ .get(login_endpoint('login'), {
return axios.get(login_endpoint("login"),{ withCredentials: false,
withCredentials:false, maxRedirects: 0,
maxRedirects: 0, xsrfCookieName: 'XSRF-TOKEN',
xsrfCookieName: "XSRF-TOKEN", })
}) .then(async () => {
.then(async ()=>{ return axios.post(
return axios.post(login_endpoint("login"),{ login_endpoint('login'),
login:user, {
password:pass, login: user,
redirectTo:"lifeto" password: pass,
},{ redirectTo: 'lifeto',
withCredentials:false, },
{
withCredentials: false,
maxRedirects: 0, maxRedirects: 0,
xsrfCookieName: "XSRF-TOKEN", xsrfCookieName: 'XSRF-TOKEN',
}) },
}).then(async ()=>{ )
})
.then(async () => {
return new TokenSession() return new TokenSession()
}).catch((e)=>{ })
if(e instanceof AxiosError) { .catch(e => {
if(e.code == "ERR_BAD_REQUEST") { if (e instanceof AxiosError) {
throw "invalid username/password" if (e.code === 'ERR_BAD_REQUEST') {
throw 'invalid username/password'
} }
throw e.message throw e.message
} }
throw e throw e
}) })
} }
static info = async ():Promise<TricksterAccountInfo> =>{ static info = async (): Promise<TricksterAccountInfo> => {
return axios.get(raw_endpoint("settings/info"),{withCredentials:false}).then((ans:AxiosResponse)=>{ return axios
return ans.data .get(raw_endpoint('settings/info'), { withCredentials: false })
}) .then((ans: AxiosResponse) => {
return ans.data
})
} }
static logout = async ():Promise<void> =>{ static logout = async (): Promise<void> => {
return axios.get(login_endpoint("logout"),{withCredentials:false}).catch(()=>{}).then(()=>{}) return axios
.get(login_endpoint('logout'), { withCredentials: false })
.catch(() => {})
.then(() => {})
} }
} }
export class TokenSession implements Session { export class TokenSession implements Session {
constructor(){ request = async (
} verb: string,
url: string,
request = async (verb:string,url:string,data:any, c:EndpointCreator = api_endpoint):Promise<AxiosResponse> => { data: any,
c: EndpointCreator = api_endpoint,
): Promise<AxiosResponse> => {
let promise let promise
switch (verb.toLowerCase()){ switch (verb.toLowerCase()) {
case "post": case 'post':
promise = axios.post(c(url),data,this.genHeaders()) promise = axios.post(c(url), data, this.genHeaders())
break; break
case "postform": case 'postform':
promise = axios.postForm(c(url),data) promise = axios.postForm(c(url), data)
break; break
case "postraw": case 'postraw': {
const querystring = qs.stringify(data) const querystring = qs.stringify(data)
promise = axios.post(c(url),querystring,this.genHeaders()) promise = axios.post(c(url), querystring, this.genHeaders())
break; break
case "get": }
default: default:
promise = axios.get(c(url),this.genHeaders()) promise = axios.get(c(url), this.genHeaders())
} }
return promise return promise
} }
genHeaders = ()=>{ genHeaders = () => {
const out = { const out = {
headers:{ headers: {
Accept: "application/json", Accept: 'application/json',
"Update-Insecure-Requests": 1, 'Update-Insecure-Requests': 1,
}, },
withCredentials:true withCredentials: true,
} }
return out return out
} }

View File

@ -1,76 +0,0 @@
//class helper {
// Revive<T>(t:string, _type:string):string {
// return t
// }
// Revive<T>(t:string, _type:string[]):string[]{
// return t.split(",")
// }
// Revive<T>(t:string, _type:number):number {
// return Number(t)
// }
// Revive<T>(t:string, _type:number[]):number[]{
// return t.split(",").map(Number)
// }
//}
import { ColumnSet } from "./table"
import { TricksterAccount, TricksterCharacter, TricksterInventory } from "./trickster"
export const ARRAY_SEPERATOR = ","
let as = ARRAY_SEPERATOR
export interface Reviver<T> {
Murder(t:T):string
Revive(s:string):T
}
export const StoreStr= {
Murder: (s:string):string=>s,
Revive: (s:string):string=>s
}
export const StoreNum = {
Murder: (s:number):string=>s.toString(),
Revive: (s:string):number=>Number(s)
}
export const StoreStrSet = {
Murder: (s:Set<string>):string=>Array.from(s).join(as),
Revive: (s:string):Set<string>=>new Set(s.split(as))
}
export const StoreColSet = {
Murder: (s:ColumnSet):string=>Array.from(s.s.values()).join(as),
Revive: (s:string):ColumnSet=>new ColumnSet(s.split(as) as any)
}
export const StoreChars = {
Murder: (s:Map<string,TricksterCharacter>):string=>{
let o = JSON.stringify(Array.from(s.entries()))
return o
},
Revive: (s:string):Map<string,TricksterCharacter>=>new Map(JSON.parse(s)),
}
export const StoreAccounts = {
Murder: (s:Map<string,TricksterAccount>):string=>{
let o = JSON.stringify(Array.from(s.entries()))
return o
},
Revive: (s:string):Map<string,TricksterAccount>=>new Map(JSON.parse(s)),
}
export const StoreJsonable = {
Murder: <T extends object>(s:T):string=>JSON.stringify(Object.entries(s)),
Revive: <T>(s:string):T=>JSON.parse(s),
}
export interface Serializable<T> {
parse(s:any):T
}
export const StoreSerializable = <T extends Serializable<T>>(n:(new ()=>T))=>{
return {
Murder: (s:T):string=>JSON.stringify(s),
Revive: (s:string):T=>new n().parse(JSON.parse(s))
}
}

View File

@ -1 +0,0 @@
import SuperJSON from "superjson";

View File

@ -1,8 +1,6 @@
import { TricksterInventory } from "./trickster" import { HotTableProps } from '@handsontable/react'
import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./columns" import { ColumnInfo, ColumnName, ColumnSorter, Columns, LazyColumn } from './columns'
import { HotTableProps } from "@handsontable/react" import { TricksterInventory } from './trickster'
export interface InventoryTableOptions { export interface InventoryTableOptions {
columns: ColumnSet columns: ColumnSet
@ -12,16 +10,16 @@ export interface InventoryTableOptions {
} }
export interface Mappable<T> { export interface Mappable<T> {
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]
} }
export class ColumnSet implements Mappable<ColumnInfo>{ export class ColumnSet implements Mappable<ColumnInfo> {
s: Set<ColumnName> = new Set() s: Set<ColumnName> = new Set()
size: number; size: number
dirty = 0 dirty = 0
constructor(i?:Iterable<ColumnInfo | ColumnName>){ constructor(i?: Iterable<ColumnInfo | ColumnName>) {
if(i){ if (i) {
for (const a of i) { for (const a of i) {
if(Columns[LazyColumn(a)]){ if (Columns[LazyColumn(a)]) {
this.s.add(LazyColumn(a)) this.s.add(LazyColumn(a))
} }
} }
@ -29,33 +27,46 @@ export class ColumnSet implements Mappable<ColumnInfo>{
this.size = 0 this.size = 0
this.mark() this.mark()
} }
map<U>(callbackfn: (value: ColumnInfo, index: number, array: ColumnInfo[]) => U, thisArg?: any): U[] { map<U>(
callbackfn: (value: ColumnInfo, index: number, array: ColumnInfo[]) => U,
thisArg?: any,
): U[] {
return Array.from(this.values()).map(callbackfn, thisArg) return Array.from(this.values()).map(callbackfn, thisArg)
} }
[Symbol.iterator](): IterableIterator<ColumnInfo>{ [Symbol.iterator](): IterableIterator<ColumnInfo> {
return this.values() return this.values()
} }
[Symbol.toStringTag] = "ColumnSet"; [Symbol.toStringTag] = 'ColumnSet'
entries(): IterableIterator<[ColumnInfo, ColumnInfo]>{ entries(): IterableIterator<[ColumnInfo, ColumnInfo]> {
return Array.from(this.values()).map((x):[ColumnInfo,ColumnInfo]=>{return [x,x]}).values() return Array.from(this.values())
.map((x): [ColumnInfo, ColumnInfo] => {
return [x, x]
})
.values()
} }
keys(): IterableIterator<ColumnInfo>{ keys(): IterableIterator<ColumnInfo> {
return this.values() return this.values()
} }
forEach(callbackfn: (value: ColumnInfo, value2: ColumnInfo, set: Set<ColumnInfo>) => void, thisArg?: any): void{ forEach(
Array.from(this.values()).forEach((v)=>{ callbackfn: (value: ColumnInfo, value2: ColumnInfo, set: Set<ColumnInfo>) => void,
if(this.has(v)) { thisArg?: any,
): void {
Array.from(this.values()).forEach(v => {
if (this.has(v)) {
callbackfn(v, v, new Set(this.values())) callbackfn(v, v, new Set(this.values()))
} }
}, thisArg) }, thisArg)
} }
values(): IterableIterator<ColumnInfo>{ values(): IterableIterator<ColumnInfo> {
return Array.from(this.s.values()).sort(ColumnSorter).map((a, b)=>{ return Array.from(this.s.values())
return Columns[a] .sort(ColumnSorter)
}).values() .map((a, _b) => {
return Columns[a]
})
.values()
} }
mark() { mark() {
this.dirty= this.dirty+ 1 this.dirty = this.dirty + 1
this.size = this.s.size this.size = this.s.size
} }
add(value: ColumnInfo | ColumnName): this { add(value: ColumnInfo | ColumnName): this {
@ -67,72 +78,70 @@ export class ColumnSet implements Mappable<ColumnInfo>{
this.mark() this.mark()
this.s.clear() this.s.clear()
} }
delete(value: ColumnInfo | ColumnName): boolean{ delete(value: ColumnInfo | ColumnName): boolean {
this.mark() this.mark()
return this.s.delete(LazyColumn(value)) return this.s.delete(LazyColumn(value))
} }
has(value: ColumnInfo | ColumnName): boolean{ has(value: ColumnInfo | ColumnName): boolean {
return this.s.has(LazyColumn(value)) return this.s.has(LazyColumn(value))
} }
} }
export class InventoryTable { export class InventoryTable {
inv!:TricksterInventory inv!: TricksterInventory
o!: InventoryTableOptions o!: InventoryTableOptions
constructor(inv:TricksterInventory, o:InventoryTableOptions) { constructor(inv: TricksterInventory, o: InventoryTableOptions) {
this.setInv(inv) this.setInv(inv)
this.setOptions(o) this.setOptions(o)
} }
setOptions(o:InventoryTableOptions) { setOptions(o: InventoryTableOptions) {
this.o = o this.o = o
} }
setInv(inv:TricksterInventory) { setInv(inv: TricksterInventory) {
this.inv = inv this.inv = inv
} }
getTableColumnNames(): string[] { getTableColumnNames(): string[] {
return this.o.columns.map(x=>x.displayName) return this.o.columns.map(x => x.displayName)
} }
getTableColumnSettings(){ getTableColumnSettings() {}
getTableRows(): any[][] {
}
getTableRows():any[][] {
return Object.values(this.inv.items) return Object.values(this.inv.items)
.filter((item):boolean=>{ .filter((item): boolean => {
if(item.item_count <= 0) { if (item.item_count <= 0) {
return false return false
} }
let found = true let found = true
let hasAll = this.o.tags.has("All") const hasAll = this.o.tags.has('All')
if(this.o.tags.s.size > 0) { if (this.o.tags.s.size > 0) {
found = hasAll found = hasAll
for(const tag of this.o.tags.values()) { for (const tag of this.o.tags.values()) {
if(tag.name =="All") { if (tag.name === 'All') {
continue continue
} }
if(tag.getter(item) === 1) { if (tag.getter(item) === 1) {
return !hasAll return !hasAll
}
} }
} }
} return found
return found })
}) .map(item => {
.map((item)=>{ return this.o.columns.map(x => {
return this.o.columns.map(x=>{ return x.getter(item)
return x.getter(item) })
}) })
})
} }
BuildTable():TableRecipe { BuildTable(): TableRecipe {
const s = DefaultSettings() const s = DefaultSettings()
const dat = this.getTableRows() const dat = this.getTableRows()
return { return {
data: dat, data: dat,
settings: { settings: {
data: dat, data: dat,
colHeaders:this.getTableColumnNames(), colHeaders: this.getTableColumnNames(),
columns:this.getTableColumnSettings(), columns: this.getTableColumnSettings(),
...s ...s,
}, },
} }
} }
@ -142,4 +151,3 @@ export interface TableRecipe {
data: any[][] data: any[][]
settings: HotTableProps settings: HotTableProps
} }

View File

@ -1,11 +1,11 @@
import { TricksterItem } from "../trickster"; import { TricksterItem } from '../trickster'
export interface ItemSelectionStatus { export interface ItemSelectionStatus {
selected: boolean; selected: boolean
amount?: number; amount?: number
} }
export interface ItemWithSelection { export interface ItemWithSelection {
item: TricksterItem item: TricksterItem
status?: ItemSelectionStatus; status?: ItemSelectionStatus
} }

View File

@ -1,138 +1,145 @@
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table'
import { ItemWithSelection } from './defs'; import { useAtomValue, useSetAtom } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'; import { useMemo } from 'react'
import { currentItemSelectionAtom, itemSelectionSetActionAtom } from '@/state/atoms'; import { currentItemSelectionAtom, itemSelectionSetActionAtom } from '@/state/atoms'
import { useMemo } from 'react'; import { StatsColumns } from '../columns'
import { StatsColumns } from '../columns'; import { ItemWithSelection } from './defs'
const ch = createColumnHelper<ItemWithSelection>(); const ch = createColumnHelper<ItemWithSelection>()
const columns = { const columns = {
icon: ch.display({ icon: ch.display({
id: 'icon', id: 'icon',
header: function Component(col) { header: function Component(_col) {
return <div className="flex flex-row justify-center"></div> return <div className="flex flex-row justify-center"></div>
}, },
cell: function Component({ row }){ cell: function Component({ row }) {
const setItemSelection= useSetAtom(itemSelectionSetActionAtom); const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
const c = useAtomValue(currentItemSelectionAtom); const c = useAtomValue(currentItemSelectionAtom)
const selected = useMemo(()=> { const selected = useMemo(() => {
return c[0].has(row.original.item.id); return c[0].has(row.original.item.id)
}, [c]) }, [c])
return <div return (
className={`no-select flex flex-row ${ row.original.status?.selected ? "animate-pulse" : ""}`} <div
onClick={(e)=>{ className={`no-select flex flex-row ${row.original.status?.selected ? 'animate-pulse' : ''}`}
setItemSelection({ onClick={_e => {
[row.original.item.id]: selected ? undefined : row.original.item.item_count, setItemSelection({
}) [row.original.item.id]: selected ? undefined : row.original.item.item_count,
}} })
> }}
<div className="flex flex-row w-6 h-6 justify-center"> >
<img src={row.original.item.item_image || ""} alt="icon" className="select-none object-contain select-none"/> <div className="flex flex-row w-6 h-6 justify-center">
<img
src={row.original.item.item_image || ''}
alt="icon"
className="select-none object-contain select-none"
/>
</div>
</div> </div>
</div> )
}, },
}), }),
count: ch.display({ count: ch.display({
id: 'count', id: 'count',
header: function Component(col){ header: function Component(_col) {
return <div className="flex flex-row justify-center">#</div> return <div className="flex flex-row justify-center">#</div>
}, },
cell: function Component({ row }){ cell: function Component({ row }) {
const c = useAtomValue(currentItemSelectionAtom); const c = useAtomValue(currentItemSelectionAtom)
const setItemSelection= useSetAtom(itemSelectionSetActionAtom); const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
const currentValue = useMemo(()=> { const currentValue = useMemo(() => {
const got = c[0].get(row.original.item.id); const got = c[0].get(row.original.item.id)
if(got !== undefined) { if (got !== undefined) {
return got.toString(); return got.toString()
} }
return "" return ''
}, [c]) }, [c])
const itemCount = row.original.item.item_count const itemCount = row.original.item.item_count
return <div return (
className={`flex flex-row select-none ${ row.original.status?.selected ? "bg-gray-200" : ""}`} <div
> className={`flex flex-row select-none ${row.original.status?.selected ? 'bg-gray-200' : ''}`}
<input >
className="w-10 text-center " <input
value={currentValue} className="w-10 text-center "
onChange={(e)=>{ value={currentValue}
if(e.target.value === ""){ onChange={e => {
setItemSelection({[row.original.item.id]: undefined}); if (e.target.value === '') {
return setItemSelection({ [row.original.item.id]: undefined })
} return
if(e.target.value === "-"){ }
if (e.target.value === '-') {
setItemSelection({
[row.original.item.id]: itemCount,
})
}
let parsedInt = parseInt(e.target.value)
if (Number.isNaN(parsedInt)) {
return
}
if (parsedInt > itemCount) {
parsedInt = itemCount
}
setItemSelection({ setItemSelection({
[row.original.item.id]: itemCount, [row.original.item.id]: parsedInt,
}) })
} }}
let parsedInt = parseInt(e.target.value); placeholder={itemCount.toString()}
if (isNaN(parsedInt)) { />
return; </div>
} )
if(parsedInt > itemCount){
parsedInt = itemCount;
}
setItemSelection({
[row.original.item.id]: parsedInt,
})
}}
placeholder={itemCount.toString()} />
</div>
}, },
}), }),
name: ch.display({ name: ch.display({
id: 'name', id: 'name',
header: (col)=> { header: _col => {
return <div return <div className="flex flex-row text-sm">name</div>
className="flex flex-row text-sm"
>name</div>
}, },
cell: function Component({ row }){ cell: function Component({ row }) {
return <div className="flex flex-row whitespace-pre"> return (
<span>{row.original.item.item_name}</span> <div className="flex flex-row whitespace-pre">
</div> <span>{row.original.item.item_name}</span>
</div>
)
}, },
}), }),
slots: ch.display({ slots: ch.display({
id: 'slots', id: 'slots',
header: (col)=>{ header: _col => {
return <div return <div className="flex flex-row text-sm">slots</div>
className="flex flex-row text-sm"
>slots</div>
}, },
cell: function Component({ row }){ cell: function Component({ row }) {
return <div className="flex flex-row justify-center"> return (
<span>{row.original.item.item_slots}</span> <div className="flex flex-row justify-center">
</div> <span>{row.original.item.item_slots}</span>
</div>
)
}, },
}), }),
stats: ch.group({ stats: ch.group({
id: 'stats', id: 'stats',
header: (col)=>{ header: _col => {
return <div return <div className="flex flex-row text-sm">stats</div>
className="flex flex-row text-sm"
>stats</div>
}, },
columns: [ columns: [
...StatsColumns.map((c)=>{ ...StatsColumns.map(c => {
return ch.display({ return ch.display({
id: 'stats.'+c, id: `stats.${c}`,
header: (col)=>{ header: _col => {
return <div return <div className="flex flex-row text-sm justify-center">{c}</div>
className="flex flex-row text-sm justify-center"
>{c}</div>
}, },
cell: function Component({ row }){ cell: function Component({ row }) {
const stats = row.original.item.stats const stats = row.original.item.stats
const stat = stats ? stats[c] : "" const stat = stats ? stats[c] : ''
return <div className={`flex flex-row justify-start ${stat ? "border" : ""}`}> return (
<span>{stat}</span> <div className={`flex flex-row justify-start ${stat ? 'border' : ''}`}>
</div> <span>{stat}</span>
</div>
)
}, },
}) })
}) }),
] ],
}), }),
} as const; } as const
export const InventoryColumns = columns; export const InventoryColumns = columns

View File

@ -1,22 +1,22 @@
export interface TricksterItem { export interface TricksterItem {
id: string; id: string
unique_id: number; unique_id: number
item_name: string; item_name: string
item_count: number; item_count: number
item_comment: string; item_comment: string
item_use: string; item_use: string
item_slots?: number; item_slots?: number
item_tab: number item_tab: number
item_type: number, item_type: number
item_min_level?: number; item_min_level?: number
is_equip?: boolean; is_equip?: boolean
is_drill?: boolean; is_drill?: boolean
item_expire_time?: string; item_expire_time?: string
refine_level?: number; refine_level?: number
refine_type?: number; refine_type?: number
refine_state?: number; refine_state?: number
item_image?: string; item_image?: string
stats?: {[key: string]:any} stats?: { [key: string]: any }
} }
export interface TricksterAccountInfo { export interface TricksterAccountInfo {
@ -25,7 +25,7 @@ export interface TricksterAccountInfo {
} }
export interface TricksterAccount { export interface TricksterAccount {
name:string name: string
characters: TricksterCharacter[] characters: TricksterCharacter[]
} }
@ -43,56 +43,55 @@ export interface TricksterCharacter extends Identifier {
current_job: number current_job: number
} }
export interface TricksterInventory extends Identifier{ export interface TricksterInventory extends Identifier {
galders?:number galders?: number
items: Map<string, TricksterItem> items: Map<string, TricksterItem>
} }
const jobMap: { [key: number]: string } = {
const jobMap:{[key:number]:string} = {
//---- job 1, fm //---- job 1, fm
1: "schoolgirl", 1: 'schoolgirl',
2: "fighter", 2: 'fighter',
3: "librarian", 3: 'librarian',
4: "shaman", 4: 'shaman',
5: "archeologist", 5: 'archeologist',
6: "engineer", 6: 'engineer',
7: "model", 7: 'model',
8: "teacher", 8: 'teacher',
//---- job 2 fm //---- job 2 fm
9: "boxer", 9: 'boxer',
10: "warrior", 10: 'warrior',
11: "bard", 11: 'bard',
12: "magician", 12: 'magician',
13: "explorer", 13: 'explorer',
14: "inventor", 14: 'inventor',
15: "entertainer", 15: 'entertainer',
16: "card master", 16: 'card master',
//---- //----
17: "champion", 17: 'champion',
18: "duelist", 18: 'duelist',
19: "mercinary", 19: 'mercinary',
20: "gladiator", 20: 'gladiator',
21: "soul master", 21: 'soul master',
22: "witch", 22: 'witch',
23: "wizard", 23: 'wizard',
24: "dark lord", 24: 'dark lord',
25: "priest", 25: 'priest',
26: "thief master", 26: 'thief master',
27: "hunter lord", 27: 'hunter lord',
28: "cyber hunter", 28: 'cyber hunter',
29: "scientist", 29: 'scientist',
30: "primadonna", 30: 'primadonna',
31: "diva", 31: 'diva',
32: "duke", 32: 'duke',
33: "gambler", 33: 'gambler',
} }
export const JobNumberToString = (n:number):string=> { export const JobNumberToString = (n: number): string => {
if(n == -8) { if (n === -8) {
return "bank" return 'bank'
} }
if(jobMap[n] != undefined) { if (jobMap[n] !== undefined) {
return jobMap[n] return jobMap[n]
} }
return n.toString() return n.toString()

View File

@ -1,24 +1,19 @@
import { Session, TokenSession } from './lib/session' import { Session, TokenSession } from './lib/session'
export const LIFETO_COOKIE_PREFIX = 'LIFETO_PANEL_'
export const nameCookie = (...s: string[]): string => {
export const LIFETO_COOKIE_PREFIX="LIFETO_PANEL_" return LIFETO_COOKIE_PREFIX + s.join('_').toUpperCase()
export const nameCookie = (...s:string[]):string=>{
return LIFETO_COOKIE_PREFIX+s.join("_").toUpperCase()
} }
export class Storage { export class Storage {
GetSession():Session { GetSession(): Session {
return new TokenSession() return new TokenSession()
} }
RemoveSession() { RemoveSession() {}
} AddSession(_s: Session) {
AddSession(s:Session) { // setCookie(nameCookie("xsrf"),s.xsrf)
// setCookie(nameCookie("xsrf"),s.xsrf)
} }
} }
export const storage = new Storage() export const storage = new Storage()

View File

@ -1,37 +1,38 @@
import { AxiosError } from 'axios'; import { AxiosError } from 'axios'
import Fuse from 'fuse.js'
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { focusAtom } from 'jotai-optics'
import { atomWithQuery } from 'jotai-tanstack-query'
import { ItemWithSelection } from '@/lib/table/defs'
import { LTOApiv0 } from '../lib/lifeto' import { LTOApiv0 } from '../lib/lifeto'
import { LoginHelper, TokenSession } from '../lib/session' import { LoginHelper, TokenSession } from '../lib/session'
import { atomWithQuery } from 'jotai-tanstack-query' import { TricksterCharacter, TricksterItem } from '../lib/trickster'
import {atomWithStorage} from "jotai/utils"; import { createSuperjsonStorage } from './storage'
import { atom } from 'jotai';
import { TricksterCharacter, TricksterInventory, TricksterItem } from '../lib/trickster';
import {focusAtom} from "jotai-optics";
import { createSuperjsonStorage, superJsonStorage } from './storage';
import { ItemWithSelection } from '@/lib/table/defs';
import Fuse from 'fuse.js';
export const LTOApi = new LTOApiv0(new TokenSession()) export const LTOApi = new LTOApiv0(new TokenSession())
export const loginStatusAtom = atomWithQuery(_get => {
export const loginStatusAtom = atomWithQuery((get) => {
return { return {
queryKey: ['login_status'], queryKey: ['login_status'],
enabled: true, enabled: true,
placeholderData: { placeholderData: {
logged_in: false, logged_in: false,
community_name: "...", community_name: '...',
}, },
queryFn: async () => { queryFn: async () => {
return LoginHelper.info().then(info => { return LoginHelper.info()
return { .then(info => {
logged_in: true, return {
community_name: info.community_name, logged_in: true,
} community_name: info.community_name,
}).catch(e => { }
if(e instanceof AxiosError) { })
.catch(e => {
if (e instanceof AxiosError) {
return { return {
logged_in: false, logged_in: false,
community_name: "...", community_name: '...',
} }
} }
throw e throw e
@ -40,40 +41,45 @@ export const loginStatusAtom = atomWithQuery((get) => {
} }
}) })
export const charactersAtom = atomWithQuery(get => {
const { data: loginStatus } = get(loginStatusAtom)
export const charactersAtom = atomWithQuery((get) => {
const {data: loginStatus} = get(loginStatusAtom)
console.log("charactersAtom", loginStatus)
return { return {
queryKey: ['characters', loginStatus?.community_name || "..."], queryKey: ['characters', loginStatus?.community_name || '...'],
enabled: !!loginStatus?.logged_in, enabled: !!loginStatus?.logged_in,
refetchOnMount: true, refetchOnMount: true,
queryFn: async ()=> { queryFn: async () => {
return LTOApi.GetAccounts().then(x=>{ return LTOApi.GetAccounts().then(x => {
if(!x) { if (!x) {
return undefined return undefined
} }
const rawCharacters = x.flatMap(x=>{return x?.characters}) const rawCharacters = x.flatMap(x => {
const characterPairs: Record<string, {bank?: TricksterCharacter, character?: TricksterCharacter}> = {} return x?.characters
rawCharacters.forEach(x=>{ })
let item = characterPairs[x.account_name] const characterPairs: Record<
if(!item) { string,
item = {} { bank?: TricksterCharacter; character?: TricksterCharacter }
} > = {}
if(x.class === -8) { rawCharacters.forEach(
item.bank = x x => {
} else { let item = characterPairs[x.account_name]
item.character = x if (!item) {
} item = {}
characterPairs[x.account_name] = item }
}, [rawCharacters]) if (x.class === -8) {
const cleanCharacterPairs = Object.values(characterPairs).filter(x=>{ item.bank = x
if(!(!!x.bank && !!x.character)) { } else {
item.character = x
}
characterPairs[x.account_name] = item
},
[rawCharacters],
)
const cleanCharacterPairs = Object.values(characterPairs).filter(x => {
if (!(!!x.bank && !!x.character)) {
return false return false
} }
return true return true
}) as Array<{bank: TricksterCharacter, character: TricksterCharacter}> }) as Array<{ bank: TricksterCharacter; character: TricksterCharacter }>
return cleanCharacterPairs return cleanCharacterPairs
}) })
@ -81,46 +87,49 @@ export const charactersAtom = atomWithQuery((get) => {
} }
}) })
export const selectedCharacterAtom = atomWithStorage<TricksterCharacter | undefined>("lto_state.selected_character", undefined) export const selectedCharacterAtom = atomWithStorage<TricksterCharacter | undefined>(
'lto_state.selected_character',
undefined,
)
export const selectedTargetInventoryAtom = atom<TricksterCharacter | undefined>(undefined) export const selectedTargetInventoryAtom = atom<TricksterCharacter | undefined>(undefined)
export const currentFilter = atom<undefined>(undefined) export const currentFilter = atom<undefined>(undefined)
export const currentCharacterInventoryAtom = atomWithQuery(get => {
export const currentCharacterInventoryAtom = atomWithQuery((get) => {
const currentCharacter = get(selectedCharacterAtom) const currentCharacter = get(selectedCharacterAtom)
return { return {
queryKey:["inventory", currentCharacter?.path || "-"], queryKey: ['inventory', currentCharacter?.path || '-'],
queryFn: async ()=> { queryFn: async () => {
return LTOApi.GetInventory(currentCharacter?.path|| "-") return LTOApi.GetInventory(currentCharacter?.path || '-')
}, },
enabled: !!currentCharacter, enabled: !!currentCharacter,
// placeholderData: keepPreviousData, // placeholderData: keepPreviousData,
} }
}) })
const inventoryDisplaySettings= atomWithStorage<{ const inventoryDisplaySettings = atomWithStorage<{
page_size: number page_size: number
}>("preference.inventory_display_settings", { }>(
page_size: 25, 'preference.inventory_display_settings',
}, createSuperjsonStorage()) {
page_size: 25,
},
createSuperjsonStorage(),
)
export const inventoryDisplaySettingsAtoms = { export const inventoryDisplaySettingsAtoms = {
pageSize: focusAtom(inventoryDisplaySettings, x=>x.prop('page_size')), pageSize: focusAtom(inventoryDisplaySettings, x => x.prop('page_size')),
} }
export const currentCharacterItemsAtom = atom(get => {
export const currentCharacterItemsAtom = atom((get)=>{ const { data: inventory } = get(currentCharacterInventoryAtom)
const {data: inventory} = get(currentCharacterInventoryAtom)
const items = inventory?.items || new Map<string, TricksterItem>() const items = inventory?.items || new Map<string, TricksterItem>()
return { return {
items, items,
searcher: new Fuse(Array.from(items.values()), { searcher: new Fuse(Array.from(items.values()), {
keys: [ keys: ['item_name'],
'item_name',
],
useExtendedSearch: true, useExtendedSearch: true,
}) }),
} }
}) })
@ -131,23 +140,26 @@ export interface InventoryFilter {
sort_reverse: boolean sort_reverse: boolean
} }
export const inventoryFilterAtom = atomWithStorage<InventoryFilter>("preference.inventory_filter", { export const inventoryFilterAtom = atomWithStorage<InventoryFilter>(
search: "", 'preference.inventory_filter',
tab: "", {
sort: "", search: '',
sort_reverse:false, tab: '',
}, createSuperjsonStorage()) sort: '',
sort_reverse: false,
export const preferenceInventorySearch = focusAtom(inventoryFilterAtom, x=>x.prop('search')) },
export const preferenceInventoryTab = focusAtom(inventoryFilterAtom, x=>x.prop('tab')) createSuperjsonStorage(),
export const preferenceInventorySort = focusAtom(inventoryFilterAtom, x=>x.prop('sort')) )
export const preferenceInventorySortReverse = focusAtom(inventoryFilterAtom, x=>x.prop('sort_reverse'))
export const preferenceInventorySearch = focusAtom(inventoryFilterAtom, x => x.prop('search'))
export const preferenceInventoryTab = focusAtom(inventoryFilterAtom, x => x.prop('tab'))
export const preferenceInventorySort = focusAtom(inventoryFilterAtom, x => x.prop('sort'))
export const preferenceInventorySortReverse = focusAtom(inventoryFilterAtom, x =>
x.prop('sort_reverse'),
)
export const setInventoryFilterTabActionAtom = atom(null, (_get, set, tab: string) => { export const setInventoryFilterTabActionAtom = atom(null, (_get, set, tab: string) => {
set(inventoryFilterAtom, x=>{ set(inventoryFilterAtom, x => {
return { return {
...x, ...x,
tab, tab,
@ -161,98 +173,106 @@ export const inventoryPageRangeAtom = atom({
}) })
export const nextInventoryPageActionAtom = atom(null, (get, set) => { export const nextInventoryPageActionAtom = atom(null, (get, set) => {
const {start, end} = get(inventoryPageRangeAtom) const { start, end } = get(inventoryPageRangeAtom)
set(inventoryPageRangeAtom, { set(inventoryPageRangeAtom, {
start: start + end, start: start + end,
end: end + end, end: end + end,
}) })
}) })
export const currentItemSelectionAtom = atom<[Map<string, number>, number]>([new Map<string, number>(), 0]) export const currentItemSelectionAtom = atom<[Map<string, number>, number]>([
export const currentInventorySearchQueryAtom = atom("") new Map<string, number>(),
0,
])
export const currentInventorySearchQueryAtom = atom('')
export const filteredCharacterItemsAtom = atom((get)=>{ export const filteredCharacterItemsAtom = atom(get => {
const { items, searcher } = get(currentCharacterItemsAtom) const { items, searcher } = get(currentCharacterItemsAtom)
const [selection] = get(currentItemSelectionAtom) const [selection] = get(currentItemSelectionAtom)
const filter = get(inventoryFilterAtom) const filter = get(inventoryFilterAtom)
const out: ItemWithSelection[] = [] const out: ItemWithSelection[] = []
for (const [_, value] of items.entries()) { for (const [_, value] of items.entries()) {
if(filter.search !== "") { if (filter.search !== '') {
if(!value.item_name.toLowerCase().includes(filter.search)) { if (!value.item_name.toLowerCase().includes(filter.search)) {
continue continue
} }
} }
if(filter.tab !== "") { if (filter.tab !== '') {
if(value.item_tab !== parseInt(filter.tab)) { if (value.item_tab !== parseInt(filter.tab)) {
continue continue
} }
} }
let status = undefined let status
if(selection.has(value.id)) { if (selection.has(value.id)) {
status = { status = {
selected: true, selected: true,
} }
} }
out.push({item: value, status}) out.push({ item: value, status })
} }
switch(filter.sort) { switch (filter.sort) {
case "count": case 'count':
out.sort((a, b)=>{ out.sort((a, b) => {
return b.item.item_count - a.item.item_count return b.item.item_count - a.item.item_count
}) })
break; break
case "type": case 'type':
out.sort((a, b)=>{ out.sort((a, b) => {
return a.item.item_tab - b.item.item_tab return a.item.item_tab - b.item.item_tab
}) })
break; break
case "name": case 'name':
out.sort((a, b)=>{ out.sort((a, b) => {
return a.item.item_name.localeCompare(b.item.item_name) return a.item.item_name.localeCompare(b.item.item_name)
}) })
break; break
} }
if(filter.sort && filter.sort_reverse) { if (filter.sort && filter.sort_reverse) {
out.reverse() out.reverse()
} }
return out return out
}) })
export const inventoryItemsCurrentPageAtom = atom((get)=>{ export const inventoryItemsCurrentPageAtom = atom(get => {
const items = get(filteredCharacterItemsAtom) const items = get(filteredCharacterItemsAtom)
const {start, end} = get(inventoryPageRangeAtom) const { start, end } = get(inventoryPageRangeAtom)
return items.slice(start, end).map((item): ItemWithSelection =>{ return items.slice(start, end).map((item): ItemWithSelection => {
return item return item
}) })
}) })
export const rowSelectionLastActionAtom = atom<{ export const rowSelectionLastActionAtom = atom<
index: number | {
action: "add" | "remove" index: number
}| undefined>(undefined) action: 'add' | 'remove'
}
| undefined
>(undefined)
export const clearItemSelectionActionAtom = atom(null, (_get, set) => { export const clearItemSelectionActionAtom = atom(null, (_get, set) => {
set(currentItemSelectionAtom, [new Map<string,number>(), 0]) set(currentItemSelectionAtom, [new Map<string, number>(), 0])
}) })
export const itemSelectionSetActionAtom = atom(null, (get, set, arg: Record<string,number | undefined> ) => { export const itemSelectionSetActionAtom = atom(
const cur = get(currentItemSelectionAtom) null,
for(const [key, value] of Object.entries(arg)) { (get, set, arg: Record<string, number | undefined>) => {
if(value === undefined) { const cur = get(currentItemSelectionAtom)
cur[0].delete(key) for (const [key, value] of Object.entries(arg)) {
} else { if (value === undefined) {
cur[0].set(key, value) cur[0].delete(key)
} else {
cur[0].set(key, value)
}
} }
} set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
set(currentItemSelectionAtom, [cur[0], cur[1] + 1]) },
}) )
export const itemSelectionSelectAllFilterActionAtom = atom(null, (get, set) => { export const itemSelectionSelectAllFilterActionAtom = atom(null, (get, set) => {
const cur = get(currentItemSelectionAtom) const cur = get(currentItemSelectionAtom)
const items = get(filteredCharacterItemsAtom) const items = get(filteredCharacterItemsAtom)
for(const item of items) { for (const item of items) {
cur[0].set(item.item.id, item.item.item_count) cur[0].set(item.item.id, item.item.item_count)
} }
set(currentItemSelectionAtom, [cur[0], cur[1] + 1]) set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
@ -261,7 +281,7 @@ export const itemSelectionSelectAllFilterActionAtom = atom(null, (get, set) => {
export const itemSelectionSelectAllPageActionAtom = atom(null, (get, set) => { export const itemSelectionSelectAllPageActionAtom = atom(null, (get, set) => {
const cur = get(currentItemSelectionAtom) const cur = get(currentItemSelectionAtom)
const items = get(inventoryItemsCurrentPageAtom) const items = get(inventoryItemsCurrentPageAtom)
for(const item of items) { for (const item of items) {
cur[0].set(item.item.id, item.item.item_count) cur[0].set(item.item.id, item.item.item_count)
} }
set(currentItemSelectionAtom, [cur[0], cur[1] + 1]) set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
@ -271,30 +291,30 @@ export const paginateInventoryActionAtom = atom(null, (get, set, pages: number |
const inventoryRange = get(inventoryPageRangeAtom) const inventoryRange = get(inventoryPageRangeAtom)
const pageSize = get(inventoryDisplaySettingsAtoms.pageSize) const pageSize = get(inventoryDisplaySettingsAtoms.pageSize)
const filteredItems = get(filteredCharacterItemsAtom) const filteredItems = get(filteredCharacterItemsAtom)
if(pages === undefined) { if (pages === undefined) {
set(inventoryPageRangeAtom, { set(inventoryPageRangeAtom, {
start: 0, start: 0,
end: pageSize, end: pageSize,
}) })
return return
} }
if(pageSize > filteredItems.length) { if (pageSize > filteredItems.length) {
set(inventoryPageRangeAtom, { set(inventoryPageRangeAtom, {
start: 0, start: 0,
end: filteredItems.length, end: filteredItems.length,
}) })
return return
} }
if(pages > 0) { if (pages > 0) {
if(inventoryRange.end >= filteredItems.length) { if (inventoryRange.end >= filteredItems.length) {
set(inventoryPageRangeAtom, { set(inventoryPageRangeAtom, {
start: 0, start: 0,
end: pageSize, end: pageSize,
}) })
return return
} }
}else if(pages < 0) { } else if (pages < 0) {
if(inventoryRange.start <= 0) { if (inventoryRange.start <= 0) {
set(inventoryPageRangeAtom, { set(inventoryPageRangeAtom, {
start: filteredItems.length - pageSize, start: filteredItems.length - pageSize,
end: filteredItems.length, end: filteredItems.length,
@ -305,10 +325,10 @@ export const paginateInventoryActionAtom = atom(null, (get, set, pages: number |
const delta = pages * pageSize const delta = pages * pageSize
let newStart = inventoryRange.start + delta let newStart = inventoryRange.start + delta
let newEnd = inventoryRange.end + delta let newEnd = inventoryRange.end + delta
if(newEnd > filteredItems.length) { if (newEnd > filteredItems.length) {
newEnd = filteredItems.length newEnd = filteredItems.length
} }
if(newEnd - newStart != pageSize) { if (newEnd - newStart !== pageSize) {
newStart = newEnd - pageSize newStart = newEnd - pageSize
} }

View File

@ -1,12 +1,12 @@
import { defineStore, storeToRefs } from 'pinia' import { defineStore, storeToRefs } from 'pinia'
import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns' import { BasicColumns, ColumnInfo, ColumnName, DetailsColumns, MoveColumns } from '../lib/columns'
import { OrderTracker } from '../lib/lifeto/order_manager' import { OrderTracker } from '../lib/lifeto/order_manager'
import { StoreAccounts, StoreChars, StoreColSet, StoreStr } from '../lib/storage' import { StoreAccounts, StoreChars, StoreColSet, StoreStr } from '../lib/storage'
import { ColumnSet } from '../lib/table' import { ColumnSet } from '../lib/table'
import { TricksterAccount, TricksterCharacter, TricksterInventory } from '../lib/trickster' import { TricksterAccount, TricksterCharacter, TricksterInventory } from '../lib/trickster'
import { nameCookie} from '../session_storage' import { nameCookie } from '../session_storage'
const _defaultColumn:(ColumnInfo| ColumnName)[] = [ const _defaultColumn: (ColumnInfo | ColumnName)[] = [
...BasicColumns, ...BasicColumns,
...MoveColumns, ...MoveColumns,
...DetailsColumns, ...DetailsColumns,
@ -20,11 +20,11 @@ export const StoreReviver = {
screen: StoreStr, screen: StoreStr,
columns: StoreColSet, columns: StoreColSet,
tags: StoreColSet, tags: StoreColSet,
// orders: StoreSerializable(OrderTracker) // orders: StoreSerializable(OrderTracker)
} }
export interface StoreProps { export interface StoreProps {
invs: Map<string,TricksterInventory> invs: Map<string, TricksterInventory>
chars: Map<string, TricksterCharacter> chars: Map<string, TricksterCharacter>
accs: Map<string, TricksterAccount> accs: Map<string, TricksterAccount>
orders: OrderTracker orders: OrderTracker
@ -37,53 +37,50 @@ export interface StoreProps {
} }
export const useStore = defineStore('state', { export const useStore = defineStore('state', {
state: ()=> { state: () => {
let store = { const store = {
invs: new Map() as Map<string,TricksterInventory>, invs: new Map() as Map<string, TricksterInventory>,
chars: new Map() as Map<string,TricksterCharacter>, chars: new Map() as Map<string, TricksterCharacter>,
accs: new Map() as Map<string,TricksterAccount>, accs: new Map() as Map<string, TricksterAccount>,
orders: new OrderTracker(), orders: new OrderTracker(),
activeTable: "none", activeTable: 'none',
screen: "default", screen: 'default',
columns:new ColumnSet(_defaultColumn), columns: new ColumnSet(_defaultColumn),
tags: new ColumnSet(), tags: new ColumnSet(),
dirty: 0, dirty: 0,
currentSearch: "", currentSearch: '',
} }
return store return store
} },
}) })
export const loadStore = () => {
export const loadStore = ()=> { const store = useStoreRef()
let store = useStoreRef() for (const [k, v] of Object.entries(StoreReviver)) {
for(const [k, v] of Object.entries(StoreReviver)){ const coke = localStorage.getItem(nameCookie(`last_${k}`))
const coke = localStorage.getItem(nameCookie("last_"+k)) if (coke) {
if(coke){ if (store[k as keyof RefStore] !== undefined) {
if((store[k as keyof RefStore]) != undefined){
store[k as keyof RefStore].value = v.Revive(coke) as any store[k as keyof RefStore].value = v.Revive(coke) as any
} }
} }
} }
} }
export const saveStore = ()=> { export const saveStore = () => {
let store = useStoreRef() const store = useStoreRef()
for(const [k, v] of Object.entries(StoreReviver)){ for (const [k, v] of Object.entries(StoreReviver)) {
let coke; let coke
if((store[k as keyof RefStore]) != undefined){ if (store[k as keyof RefStore] !== undefined) {
coke = v.Murder(store[k as keyof RefStore].value as any) coke = v.Murder(store[k as keyof RefStore].value as any)
} }
if(coke){ if (coke) {
localStorage.setItem(nameCookie("last_"+k),coke) localStorage.setItem(nameCookie(`last_${k}`), coke)
} }
} }
} }
export const useStoreRef = ()=>{ export const useStoreRef = () => {
const refs = storeToRefs(useStore()) const refs = storeToRefs(useStore())
return refs return refs
}; }
export type RefStore = ReturnType<typeof useStoreRef>;
export type RefStore = ReturnType<typeof useStoreRef>

View File

@ -1,4 +1,9 @@
import { AsyncStorage, AsyncStringStorage, SyncStorage, SyncStringStorage } from "jotai/vanilla/utils/atomWithStorage" import {
AsyncStorage,
AsyncStringStorage,
SyncStorage,
SyncStringStorage,
} from 'jotai/vanilla/utils/atomWithStorage'
import superjson from 'superjson' import superjson from 'superjson'
const isPromiseLike = (x: unknown): x is PromiseLike<unknown> => const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
@ -19,16 +24,12 @@ type StringSubscribe = (
export function createSuperjsonStorage<Value>(): SyncStorage<Value> export function createSuperjsonStorage<Value>(): SyncStorage<Value>
export function createSuperjsonStorage<Value>( export function createSuperjsonStorage<Value>(
getStringStorage: () => getStringStorage: () => AsyncStringStorage | SyncStringStorage | undefined = () => {
| AsyncStringStorage
| SyncStringStorage
| undefined = () => {
try { try {
return window.localStorage return window.localStorage
} catch (e) { } catch (_e) {
if (import.meta.env?.MODE !== 'production') { if (import.meta.env?.MODE !== 'production') {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
console.warn(e)
} }
} }
return undefined return undefined
@ -58,18 +59,14 @@ export function createSuperjsonStorage<Value>(
} }
return parse(str) as never return parse(str) as never
}, },
setItem: (key, newValue) => setItem: (key, newValue) => getStringStorage()?.setItem(key, superjson.stringify(newValue)),
getStringStorage()?.setItem( removeItem: key => getStringStorage()?.removeItem(key),
key,
superjson.stringify(newValue),
),
removeItem: (key) => getStringStorage()?.removeItem(key),
} }
const createHandleSubscribe = const createHandleSubscribe =
(subscriber: StringSubscribe): Subscribe<Value> => (subscriber: StringSubscribe): Subscribe<Value> =>
(key, callback, initialValue) => (key, callback, initialValue) =>
subscriber(key, (v) => { subscriber(key, v => {
let newValue: Value let newValue: Value
try { try {
newValue = superjson.parse(v || '') newValue = superjson.parse(v || '')

View File

@ -7,7 +7,7 @@
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"types": [ "node" ], "types": ["node"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -21,13 +21,7 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src", "app", "index"],
"src", "exclude": ["node_modules"],
"app",
"index",
],
"exclude": [
"node_modules",
],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -7,4 +7,3 @@
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

@ -1,10 +1,12 @@
import { defineConfig } from 'vite' // ignore the type error onthe next line
// @ts-ignore
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'path' import path from 'path'
import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react(), tailwindcss()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@ -14,12 +16,12 @@ export default defineConfig({
proxy: { proxy: {
// with options // with options
'/lifeto': { '/lifeto': {
target: "https://beta.lifeto.co/", target: 'https://beta.lifeto.co/',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/lifeto/, ''), rewrite: path => path.replace(/^\/lifeto/, ''),
}, },
} },
} },
}) })
// https://vitejs.dev/config/ // https://vitejs.dev/config/

4637
yarn.lock

File diff suppressed because it is too large Load Diff