noot
This commit is contained in:
parent
908b4da72d
commit
79f90a7478
28
Caddyfile
Normal file
28
Caddyfile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
admin off
|
||||||
|
}
|
||||||
|
:{$PORT:8080} {
|
||||||
|
root * {$ROOT:./dist}
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Proxy requests with "/lifeto" prefix to the remote server
|
||||||
|
handle /lifeto/* {
|
||||||
|
uri strip_prefix /lifeto
|
||||||
|
reverse_proxy https://beta.lifeto.co {
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_down -Connection
|
||||||
|
header_down -Keep-Alive
|
||||||
|
header_down -Proxy-Authenticate
|
||||||
|
header_down -Proxy-Authorization
|
||||||
|
header_down -Te
|
||||||
|
header_down -Trailers
|
||||||
|
header_down -Transfer-Encoding
|
||||||
|
header_down -Upgrade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Dockerfile
13
Dockerfile
@ -1,19 +1,10 @@
|
|||||||
FROM golang:1.18.2-alpine as GOBUILDER
|
|
||||||
WORKDIR /wd
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
COPY app ./app
|
|
||||||
RUN go mod tidy
|
|
||||||
RUN go build -o app.exe ./app
|
|
||||||
|
|
||||||
FROM node:18.1-alpine as NODEBUILDER
|
FROM node:18.1-alpine as NODEBUILDER
|
||||||
WORKDIR /wd
|
WORKDIR /wd
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install
|
RUN npm install
|
||||||
RUN npx vite build
|
RUN npx vite build
|
||||||
|
|
||||||
FROM alpine:3.16
|
FROM caddyserver/caddy:2.10-alpine
|
||||||
WORKDIR /wd
|
WORKDIR /wd
|
||||||
COPY --from=GOBUILDER /wd/app.exe app.exe
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
COPY --from=NODEBUILDER /wd/dist dist
|
COPY --from=NODEBUILDER /wd/dist dist
|
||||||
ENTRYPOINT [ "/wd/app.exe" ]
|
|
||||||
|
|
||||||
|
|||||||
60
package.json
60
package.json
@ -8,40 +8,46 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@handsontable/react": "^14.5.0",
|
"@floating-ui/react": "^0.27.8",
|
||||||
"@tanstack/react-query": "^5.51.21",
|
"@handsontable/react": "^15.3.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@mantine/hooks": "^8.0.0",
|
||||||
"@types/react": "^18.3.3",
|
"@tanstack/react-query": "^5.76.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/qs": "^6.9.18",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/react": "^19.1.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
"axios": "^0.27.2",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"eslint": "^9.8.0",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"eslint": "^9.26.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-react": "^7.35.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.9",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"handsontable": "^14.5.0",
|
"fuse.js": "^7.1.0",
|
||||||
"loglevel": "^1.8.0",
|
"handsontable": "^15.3.0",
|
||||||
"pinia": "^2.0.14",
|
"jotai": "^2.12.4",
|
||||||
"prettier": "^3.3.3",
|
"jotai-tanstack-query": "^0.9.0",
|
||||||
"qs": "^6.10.5",
|
"loglevel": "^1.9.2",
|
||||||
"react": "^18.3.1",
|
"pinia": "^3.0.2",
|
||||||
"react-dom": "^18.3.1",
|
"prettier": "^3.5.3",
|
||||||
"react-spinners": "^0.14.1",
|
"qs": "^6.14.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-select": "^5.10.1",
|
||||||
|
"react-spinners": "^0.17.0",
|
||||||
"typescript-cookie": "^1.0.6",
|
"typescript-cookie": "^1.0.6",
|
||||||
"use-local-storage": "^3.0.0",
|
"use-local-storage": "^3.0.0",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.1",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.6",
|
"@tailwindcss/postcss": "^4.1.6",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^4.1.6",
|
"tailwindcss": "^4.1.6",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^5.3.5"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/App.tsx
32
src/App.tsx
@ -6,14 +6,34 @@ import { Inventory } from "./components/inventory";
|
|||||||
export const App: FC = () => {
|
export const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col p-4 h-full">
|
<div className="flex flex-col mx-auto p-4 w-full">
|
||||||
<div className="grid grid-cols-6 gap-x-4">
|
<div className="flex flex-row max-w-6xl">
|
||||||
<div className="col-span-1">
|
<div className="flex flex-row justify-end w-full">
|
||||||
<LoginWidget/>
|
<LoginWidget/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CharacterRoulette/>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<Inventory/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
<div className="flex flex-col p-4 h-full">
|
||||||
|
<div className="grid grid-cols-6 gap-x-4">
|
||||||
<div className="col-span-5 h-full">
|
<div className="col-span-5 h-full">
|
||||||
<CharacterRoulette/>
|
<CharacterRoulette/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<div className="flex flex-col border border-gray-400">
|
||||||
|
<LoginWidget/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-6 h-full">
|
<div className="grid grid-cols-6 h-full">
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
@ -25,6 +45,4 @@ export const App: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
*/
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="'cc_parent cc_' + job"
|
|
||||||
v-on:click="selectCharacter()"
|
|
||||||
>
|
|
||||||
<div class="cc_div1">
|
|
||||||
<span v-html="job" />
|
|
||||||
</div>
|
|
||||||
<div class="cc_div2">name: <br/> items: </div>
|
|
||||||
<div class="cc_div3"> {{name}} <br> {{items}} </div>
|
|
||||||
<div class="cc_div4">
|
|
||||||
{{galders.toLocaleString()}}g
|
|
||||||
</div>
|
|
||||||
<div class="cc_div5">
|
|
||||||
</div>
|
|
||||||
<div class="cc_div6">
|
|
||||||
{{activeTable == props.character ? "**" :""}}
|
|
||||||
</div>
|
|
||||||
<div class="cc_div7">
|
|
||||||
{{currentChar?.path.split("/")[0]}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const session = storage.GetSession()
|
|
||||||
const api:LTOApi = getLTOState(LTOApiv0, session, useStoreRef())
|
|
||||||
|
|
||||||
const props = defineProps(['character'])
|
|
||||||
const name = ref("")
|
|
||||||
const job = ref("")
|
|
||||||
const items = ref(0)
|
|
||||||
const galders = ref(0)
|
|
||||||
const {invs, activeTable, chars} = useStoreRef()
|
|
||||||
|
|
||||||
watch(invs.value,()=>{
|
|
||||||
const currentInv = invs.value.get(props.character)
|
|
||||||
if(currentInv){
|
|
||||||
if(currentInv.galders){
|
|
||||||
galders.value = currentInv.galders || currentInv.money
|
|
||||||
}
|
|
||||||
items.value = Object.values(currentInv.items).length
|
|
||||||
}
|
|
||||||
},{deep:true})
|
|
||||||
|
|
||||||
const currentChar = chars.value.get(props.character)
|
|
||||||
if(currentChar){
|
|
||||||
name.value = currentChar.name!
|
|
||||||
job.value = JobNumberToString(currentChar.current_job)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectCharacter = () => {
|
|
||||||
activeTable.value = props.character
|
|
||||||
api.GetInventory(props.character)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineProps, ref, watch} from 'vue';
|
|
||||||
import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto';
|
|
||||||
import { JobNumberToString } from '../lib/trickster';
|
|
||||||
import { storage } from '../session_storage';
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
.cc_parent {
|
|
||||||
border: 1px black solid;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
grid-template-rows: repeat(7, 1fr);
|
|
||||||
grid-column-gap: 0px;
|
|
||||||
grid-row-gap: 0px;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-left: 5px;
|
|
||||||
width: 165px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cc_div1 { grid-area: 1 / 1 / 5 / 6; }
|
|
||||||
.cc_div2 {
|
|
||||||
text-align: left;
|
|
||||||
grid-area: 5 / 2 / 6 / 3; }
|
|
||||||
.cc_div3 {
|
|
||||||
text-align: right;
|
|
||||||
grid-area: 5 / 4 / 6 / 5; }
|
|
||||||
.cc_div4 {
|
|
||||||
text-align: right;
|
|
||||||
grid-area: 6 / 4 / 7 / 5; }
|
|
||||||
.cc_div5 { grid-area: 1 / 1 / 8 / 6; }
|
|
||||||
.cc_div6 { grid-area: 7 / 1 / 8 / 3; }
|
|
||||||
.cc_div7 {
|
|
||||||
font-size: 12px;
|
|
||||||
text-align:right;
|
|
||||||
padding-right: 8px;
|
|
||||||
grid-area: 7 / 4 / 8 / 6;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="logoutButton"
|
|
||||||
v-on:click="send_orders()"
|
|
||||||
>ayy lmao button</button>
|
|
||||||
<HotTable
|
|
||||||
ref="hotTableComponent"
|
|
||||||
:settings="DefaultSettings()"
|
|
||||||
></HotTable>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { HotTable, HotColumn } from '@handsontable/vue3';
|
|
||||||
|
|
||||||
const storeRefs = useStoreRef()
|
|
||||||
const {invs, activeTable, columns, tags, dirty, chars, currentSearch, orders} = storeRefs
|
|
||||||
|
|
||||||
const hotTableComponent = ref<any>(null)
|
|
||||||
const hott = ():Handsontable =>{
|
|
||||||
return hotTableComponent.value.hotInstance as any
|
|
||||||
}
|
|
||||||
const session = storage.GetSession()
|
|
||||||
const api:LTOApi = getLTOState(LTOApiv0, session, useStoreRef())
|
|
||||||
const manager = new OrderSender(storeRefs)
|
|
||||||
|
|
||||||
const updateTable = ():TableRecipe | undefined => {
|
|
||||||
if (invs.value.has(activeTable.value)) {
|
|
||||||
const chardat = invs.value.get(activeTable.value)
|
|
||||||
if (chardat) {
|
|
||||||
const it = new InventoryTable(chardat, {
|
|
||||||
columns: columns.value,
|
|
||||||
tags: tags.value,
|
|
||||||
accounts: Array.from(chars.value.keys()),
|
|
||||||
} as InventoryTableOptions)
|
|
||||||
const build = it.BuildTable()
|
|
||||||
hott().updateSettings(build.settings)
|
|
||||||
return build
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(currentSearch, ()=>{
|
|
||||||
filterTable()
|
|
||||||
})
|
|
||||||
|
|
||||||
const send_orders = () => {
|
|
||||||
if(hott()) {
|
|
||||||
const headers = hott().getColHeader()
|
|
||||||
const dat = hott().getData()
|
|
||||||
const idxNumber = headers.indexOf(Columns.MoveCount.displayName)
|
|
||||||
const idxTarget = headers.indexOf(Columns.Move.displayName)
|
|
||||||
const origin = activeTable
|
|
||||||
const pending:OrderDetails[] = [];
|
|
||||||
for(const row of dat) {
|
|
||||||
try{
|
|
||||||
const nm = Number(row[idxNumber].replace("x",""))
|
|
||||||
const target = (row[idxTarget] as string).replaceAll("-","").trim()
|
|
||||||
if(!isNaN(nm) && nm > 0 && target.length > 0){
|
|
||||||
const info:OrderDetails = {
|
|
||||||
item_uid: row[0].toString(),
|
|
||||||
count: nm,
|
|
||||||
origin_path: origin.value,
|
|
||||||
target_path: target,
|
|
||||||
}
|
|
||||||
pending.push(info)
|
|
||||||
}
|
|
||||||
}catch(e){
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.debug("OrderDetails", pending)
|
|
||||||
for(const d of pending){
|
|
||||||
const order = manager.send(d)
|
|
||||||
order.tick(storeRefs, api)
|
|
||||||
}
|
|
||||||
saveStore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(()=>{
|
|
||||||
window.setInterval(tick_orders, 1000)
|
|
||||||
})
|
|
||||||
const tick_orders = () => {
|
|
||||||
if(orders && storeRefs && api){
|
|
||||||
orders.value.tick(storeRefs, api)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const filterTable = () => {
|
|
||||||
if(hott()){
|
|
||||||
const fp = hott().getPlugin('filters')
|
|
||||||
fp.removeConditions(2)
|
|
||||||
fp.addCondition(2,'contains', [currentSearch.value])
|
|
||||||
fp.filter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// register Handsontable's modules
|
|
||||||
registerAllModules();
|
|
||||||
|
|
||||||
watch([columns.value.dirty, tags.value.dirty, activeTable, dirty], () => {
|
|
||||||
log.debug(`${dirty.value} rendering inventory`, activeTable.value)
|
|
||||||
let u = updateTable()
|
|
||||||
saveStore()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, computed, PropType, defineProps, defineEmits, watch} from 'vue';
|
|
||||||
import { registerAllModules } from 'handsontable/registry';
|
|
||||||
import { DefaultSettings, InventoryTable, InventoryTableOptions, TableRecipe } from '../lib/table';
|
|
||||||
import { Columns, ColumnByNames, ColumnInfo } from '../lib/columns';
|
|
||||||
import { TricksterItem} from '../lib/trickster';
|
|
||||||
import Handsontable from 'handsontable';
|
|
||||||
import { useStoreRef, saveStore } from '../state/state';
|
|
||||||
import { storage } from '../session_storage';
|
|
||||||
import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto';
|
|
||||||
import log, { info } from 'loglevel';
|
|
||||||
import { OrderDetails, OrderSender } from '../lib/lifeto/order_manager';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style src="handsontable/dist/handsontable.full.css">
|
|
||||||
</style>
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="character_roulette">
|
|
||||||
<div class="single_character_card" v-for="v in characters">
|
|
||||||
<CharacterCard :character="v" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import CharacterCard from './CharacterCard.vue';
|
|
||||||
|
|
||||||
const {accs, chars, invs, activeTable } = useStoreRef()
|
|
||||||
|
|
||||||
const characters = ref([] as string[])
|
|
||||||
watch(chars, () => {
|
|
||||||
characters.value = [...new Set([...characters.value, ...invs.value.keys()])]
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
const session = storage.GetSession()
|
|
||||||
const api:LTOApi = getLTOState(LTOApiv0, session, useStoreRef())
|
|
||||||
|
|
||||||
api.GetAccounts().then(xs => {
|
|
||||||
xs.forEach(x => {
|
|
||||||
characters.value.push(...x.characters.map(x=>x.path))
|
|
||||||
accs.value.set(x.name, x)
|
|
||||||
})
|
|
||||||
characters.value = [...new Set([...characters.value])]
|
|
||||||
saveStore();
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(()=>{
|
|
||||||
let val = invs.value.get(activeTable.value)
|
|
||||||
if(!val || Object.values(val.items).length == 0) {
|
|
||||||
api.GetInventory(activeTable.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { ref, watch, onMounted } from 'vue';
|
|
||||||
import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto';
|
|
||||||
import { storage } from '../session_storage';
|
|
||||||
import { saveStore, useStoreRef } from '../state/state';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#character_roulette {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: normal;
|
|
||||||
align-items: center;
|
|
||||||
overflow-x: scroll;
|
|
||||||
width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single_character_card {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<input type="checkbox" id={{props.colname}} v-model="checked" />
|
|
||||||
<label for={{props.colname}}>{{(props.label ? props.label : Columns[props.colname].displayName)}}</label>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
|
|
||||||
const props = defineProps(["colname","label"])
|
|
||||||
const {columns} = useStoreRef()
|
|
||||||
const checked = ref(columns.value.has(props.colname))
|
|
||||||
watch(columns.value.dirty,()=>{
|
|
||||||
if(columns.value.has(props.colname)) {
|
|
||||||
checked.value = true
|
|
||||||
}else{
|
|
||||||
checked.value = false
|
|
||||||
}
|
|
||||||
},{deep:true})
|
|
||||||
|
|
||||||
watch(checked, ()=>{
|
|
||||||
if(checked.value === true) {
|
|
||||||
columns.value.add(props.colname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(checked.value === false) {
|
|
||||||
columns.value.delete(props.colname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineProps, ref, watch } from 'vue';
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
import { ColumnName, Columns } from '../lib/columns';
|
|
||||||
</script>
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="css-checkbox"
|
|
||||||
:id="'toggle-'+props.header"
|
|
||||||
v-model="show"
|
|
||||||
/>
|
|
||||||
<label :for="'toggle-'+props.header" class="css-label">
|
|
||||||
<span class="fa fa-plus">+</span>
|
|
||||||
<span class="fa fa-minus">-</span>
|
|
||||||
</label>
|
|
||||||
<label :for="'checkbox-'+props.header">{{props.header}}</label>
|
|
||||||
<input type="checkbox" :id="'checkbox-'+props.header" v-model="checked" />
|
|
||||||
<br>
|
|
||||||
<div
|
|
||||||
class="checkbox_parent"
|
|
||||||
:style="'grid-template-rows: repeat('+Math.ceil(props.columns.length/2+2)+', 1fr);'"
|
|
||||||
v-if="show"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in props.columns"
|
|
||||||
class="checkbox_child"
|
|
||||||
>
|
|
||||||
<ColumnCheckbox :colname="item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import ColumnCheckbox from './ColumnCheckbox.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
header: string
|
|
||||||
columns: ColumnName[]
|
|
||||||
default?: boolean
|
|
||||||
}>()
|
|
||||||
const {columns} = useStoreRef()
|
|
||||||
const checked = ref(props.default)
|
|
||||||
const show = ref(true)
|
|
||||||
watch(show,()=>{
|
|
||||||
})
|
|
||||||
watch(checked,()=>{
|
|
||||||
if(checked.value === true) {
|
|
||||||
props.columns.forEach(x=>columns.value.add(x))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(checked.value === false) {
|
|
||||||
props.columns.forEach(x=>columns.value.delete(x))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineProps, ref, watch } from 'vue';
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
import { ColumnName } from '../lib/columns';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.checkbox_parent {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
grid-column-gap: 0px;
|
|
||||||
grid-row-gap: 0px;
|
|
||||||
}
|
|
||||||
.checkbox_child {
|
|
||||||
align-content: left;
|
|
||||||
justify-self: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.css-label {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.css-checkbox {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.fa {
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding-top: 0px;
|
|
||||||
padding-right: 4px;
|
|
||||||
padding-left: 4px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
}
|
|
||||||
.fa-plus {
|
|
||||||
background-color: #E85764;
|
|
||||||
}
|
|
||||||
.fa-minus {
|
|
||||||
background-color: #3AC5C9;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.css-checkbox:checked + .css-label .fa-minus {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.css-checkbox:checked + .css-label .fa-plus {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<input type="checkbox" id={{props.colname}} v-model="checked" />
|
|
||||||
<label for={{props.colname}}>{{(props.label ? props.label : Columns[props.colname].displayName)}}</label>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
|
|
||||||
const props = defineProps(["colname","label"])
|
|
||||||
const {tags} = useStoreRef()
|
|
||||||
const checked = ref(tags.value.has(props.colname))
|
|
||||||
watch(tags.value.dirty,()=>{
|
|
||||||
console.log("changed")
|
|
||||||
if(tags.value.has(props.colname)) {
|
|
||||||
checked.value = true
|
|
||||||
}else{
|
|
||||||
checked.value = false
|
|
||||||
}
|
|
||||||
},{deep:true})
|
|
||||||
|
|
||||||
watch(checked, ()=>{
|
|
||||||
if(checked.value === true) {
|
|
||||||
tags.value.add(props.colname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(checked.value === false) {
|
|
||||||
tags.value.delete(props.colname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineProps, ref, watch } from 'vue';
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
import { ColumnName, Columns } from '../lib/columns';
|
|
||||||
</script>
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
<template>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="css-checkbox"
|
|
||||||
:id="'toggle-'+props.header"
|
|
||||||
v-model="show"
|
|
||||||
/>
|
|
||||||
<label :for="'toggle-'+props.header" class="css-label">
|
|
||||||
<span class="fa fa-plus">+</span>
|
|
||||||
<span class="fa fa-minus">-</span>
|
|
||||||
</label>
|
|
||||||
<label :for="'checkbox-'+props.header">{{props.header}}</label>
|
|
||||||
<input type="checkbox" :id="'checkbox-'+props.header" v-model="checked" />
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<div
|
|
||||||
class="checkbox_parent"
|
|
||||||
:style="'grid-template-rows: repeat('+Math.ceil(props.columns.length/2+2)+', 1fr);'"
|
|
||||||
v-if="show"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in props.columns"
|
|
||||||
class="checkbox_child"
|
|
||||||
>
|
|
||||||
<FilterCheckbox :colname="item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import FilterCheckbox from './FilterCheckbox.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
header: string
|
|
||||||
columns: ColumnName[]
|
|
||||||
}>()
|
|
||||||
const {tags} = useStoreRef()
|
|
||||||
const checked = ref(false)
|
|
||||||
const show = ref(true)
|
|
||||||
watch(show,()=>{
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(checked,()=>{
|
|
||||||
if(checked.value === true) {
|
|
||||||
props.columns.forEach(x=>tags.value.add(x))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(checked.value === false) {
|
|
||||||
props.columns.forEach(x=>tags.value.delete(x))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineProps, ref, watch } from 'vue';
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
import { ColumnName } from '../lib/columns';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.checkbox_parent {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
grid-column-gap: 0px;
|
|
||||||
grid-row-gap: 0px;
|
|
||||||
}
|
|
||||||
.checkbox_child {
|
|
||||||
align-content: left;
|
|
||||||
justify-self: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.css-label {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.css-checkbox {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.fa {
|
|
||||||
color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding-top: 0px;
|
|
||||||
padding-right: 4px;
|
|
||||||
padding-left: 4px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
}
|
|
||||||
.fa-plus {
|
|
||||||
background-color: #E85764;
|
|
||||||
}
|
|
||||||
.fa-minus {
|
|
||||||
background-color: #3AC5C9;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.css-checkbox:checked + .css-label .fa-minus {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.css-checkbox:checked + .css-label .fa-plus {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="order-display">
|
|
||||||
<div id="order-titlebar"></div>
|
|
||||||
<table>
|
|
||||||
<tr
|
|
||||||
v-for="v in orders.orders"
|
|
||||||
:key="dirty"
|
|
||||||
>
|
|
||||||
<td>{{v.action_id}}</td>
|
|
||||||
<td>[{{v.progress()[0]}} / {{v.progress()[1]}}]</td>
|
|
||||||
<td>{{v.order_type}}</td>
|
|
||||||
<td>{{v.state}}</td>
|
|
||||||
<td>{{(((new Date()).getTime() - new Date(v.created).getTime())/(60 *1000)).toFixed(0)}} min ago</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="logoutButton"
|
|
||||||
v-on:click="tick_order(v.action_id)"
|
|
||||||
>tick</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
|
|
||||||
const storeRefs = useStoreRef()
|
|
||||||
const {orders, dirty} = storeRefs;
|
|
||||||
const session = storage.GetSession()
|
|
||||||
const api:LTOApi = getLTOState(LTOApiv0, session, useStoreRef())
|
|
||||||
const tick_order = (action_id:string)=> {
|
|
||||||
const deet = orders.value.orders[action_id]
|
|
||||||
console.log(deet)
|
|
||||||
if(deet){
|
|
||||||
deet.tick(storeRefs, api)
|
|
||||||
}else {
|
|
||||||
console.log(`tried to send ${action_id} but undefined`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(()=>{
|
|
||||||
dragElement(document.getElementById("order-display"));
|
|
||||||
function dragElement(elmnt:any) {
|
|
||||||
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
|
||||||
document.getElementById("order-titlebar")!.onmousedown = dragMouseDown;
|
|
||||||
function dragMouseDown(e:any) {
|
|
||||||
e = e || window.event;
|
|
||||||
e.preventDefault();
|
|
||||||
// get the mouse cursor position at startup:
|
|
||||||
pos3 = e.clientX;
|
|
||||||
pos4 = e.clientY;
|
|
||||||
document.onmouseup = closeDragElement;
|
|
||||||
// call a function whenever the cursor moves:
|
|
||||||
document.onmousemove = elementDrag;
|
|
||||||
}
|
|
||||||
|
|
||||||
function elementDrag(e:any) {
|
|
||||||
e = e || window.event;
|
|
||||||
e.preventDefault();
|
|
||||||
// calculate the new cursor position:
|
|
||||||
pos1 = pos3 - e.clientX;
|
|
||||||
pos2 = pos4 - e.clientY;
|
|
||||||
pos3 = e.clientX;
|
|
||||||
pos4 = e.clientY;
|
|
||||||
// set the element's new position:
|
|
||||||
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
|
|
||||||
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDragElement() {
|
|
||||||
// stop moving when mouse button is released:
|
|
||||||
document.onmouseup = null;
|
|
||||||
document.onmousemove = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { onMounted, watch } from 'vue';
|
|
||||||
import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto';
|
|
||||||
import { storage } from '../session_storage';
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#order-display{
|
|
||||||
position: absolute;
|
|
||||||
z-index: 9;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
border: 1px solid #d3d3d3;
|
|
||||||
text-align: center;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
#order-titlebar {
|
|
||||||
padding: 10px;
|
|
||||||
cursor: move;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: #2196F3;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
search:
|
|
||||||
<div class="filter_field">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="searchbox"
|
|
||||||
v-model="currentSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<FilterCheckboxGroup :header="'tags:'" :columns="[...TagColumns]" />
|
|
||||||
<br>
|
|
||||||
Columns:
|
|
||||||
<br>
|
|
||||||
<ColumnCheckboxGroup :header="'action:'" :columns="[...MoveColumns]" :default="true" />
|
|
||||||
<ColumnCheckboxGroup :header="'details:'" :columns="[...DetailsColumns]" :default="true"/>
|
|
||||||
<ColumnCheckboxGroup :header="'equipment:'" :columns="[...EquipmentColumns]" />
|
|
||||||
<ColumnCheckboxGroup :header="'stats:'" :columns="[...StatsColumns]" />
|
|
||||||
<ColumnCheckboxGroup :header="'debug:'" :columns="[...DebugColumns]" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import ColumnCheckboxGroup from './ColumnCheckboxGroup.vue';
|
|
||||||
import FilterCheckboxGroup from './FilterCheckboxGroup.vue';
|
|
||||||
import { DebugColumns, StatsColumns, MoveColumns, TagColumns, EquipmentColumns, DetailsColumns } from '../lib/columns';
|
|
||||||
|
|
||||||
const { currentSearch} = useStoreRef()
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useStoreRef } from '../state/state';
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@ -1,46 +1,103 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { TricksterCharacter } from "../lib/trickster"
|
||||||
import { useLtoContext } from "../context/LtoContext"
|
|
||||||
import { JobNumberToString, TricksterAccount, TricksterCharacter } from "../lib/trickster"
|
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query"
|
|
||||||
import { useSessionContext } from "../context/SessionContext"
|
import { useSessionContext } from "../context/SessionContext"
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useAtom, useSetAtom } from "jotai"
|
||||||
|
import { charactersAtom, selectedCharacterAtom } from "../state/atoms"
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
useFloating,
|
||||||
|
autoUpdate,
|
||||||
|
offset,
|
||||||
|
flip,
|
||||||
|
shift,
|
||||||
|
useHover,
|
||||||
|
useFocus,
|
||||||
|
useDismiss,
|
||||||
|
useRole,
|
||||||
|
useInteractions,
|
||||||
|
FloatingPortal
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
|
||||||
export const CharacterCard = ({character}:{
|
export const CharacterCard = ({character}:{
|
||||||
character: TricksterCharacter,
|
character: TricksterCharacter,
|
||||||
})=>{
|
})=>{
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
placement: "top",
|
||||||
|
// Make sure the tooltip stays on the screen
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [
|
||||||
|
offset(5),
|
||||||
|
flip({
|
||||||
|
fallbackAxisSideDirection: "start"
|
||||||
|
}),
|
||||||
|
shift()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listeners to change the open state
|
||||||
|
const hover = useHover(context, { move: false });
|
||||||
|
const focus = useFocus(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
// Role props for screen readers
|
||||||
|
const role = useRole(context, { role: "tooltip" });
|
||||||
|
|
||||||
|
// Merge all the interactions into prop getters
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
|
hover,
|
||||||
|
focus,
|
||||||
|
dismiss,
|
||||||
|
role
|
||||||
|
]);
|
||||||
|
const [selectedCharacter, setSelectedCharacter] = useAtom(selectedCharacterAtom)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const {activeTable, setActiveTable} = useSessionContext()
|
|
||||||
return <>
|
return <>
|
||||||
<div onClick={()=>{
|
<div onClick={()=>{
|
||||||
setActiveTable(character.path)
|
setSelectedCharacter(character)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
ref={refs.setReference} {...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
|
||||||
h-full
|
p-2 ${character.path === selectedCharacter?.path? `bg-blue-200 hover:bg-blue-100` : ""}`}>
|
||||||
p-2 ${character.path === activeTable ? `bg-blue-200 hover:bg-blue-100 border-double border-4` : ""}`}>
|
|
||||||
<div className="flex"></div>
|
|
||||||
<div className="flex flex-col justify-between h-full">
|
<div className="flex flex-col justify-between h-full">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-row justify-center text-md">
|
<div className="flex flex-row justify-center"
|
||||||
<span>{character.name}</span>
|
>
|
||||||
</div>
|
|
||||||
<div className="flex flex-row justify-center">
|
|
||||||
{character.base_job === -8 ?
|
{character.base_job === -8 ?
|
||||||
<img src={`https://knowledge.lifeto.co/animations/npc/npc041_5.png`}/>
|
<img
|
||||||
:
|
className="h-8"
|
||||||
<img
|
src="https://beta.lifeto.co/item_img/gel.nri.003.000.png"
|
||||||
src={`https://knowledge.lifeto.co/animations/character/chr00${character.base_job}_13.png`}/>
|
/>
|
||||||
|
:
|
||||||
|
<img
|
||||||
|
className="h-16"
|
||||||
|
src={`https://knowledge.lifeto.co/animations/character/chr${
|
||||||
|
(character.current_job - character.base_job - 1).toString().padStart(3,"0")
|
||||||
|
}_13.png`}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<FloatingPortal>
|
||||||
<span>class: </span>
|
{isOpen && (
|
||||||
<span>{JobNumberToString(character.current_job)}</span>
|
<div
|
||||||
</div>
|
className="Tooltip"
|
||||||
</div>
|
ref={refs.setFloating}
|
||||||
<div className="flex flex-row gap-1 text-xs">
|
style={floatingStyles}
|
||||||
<span>path: </span>
|
{...getFloatingProps()}
|
||||||
<span>{character.path}</span>
|
>
|
||||||
|
<div className="flex flex-col gap-1 bg-white">
|
||||||
|
{character.base_job === -8 ? "bank" : character.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FloatingPortal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -53,30 +110,53 @@ const PleaseLogin = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CharacterRoulette = ()=>{
|
export const CharacterRoulette = ()=>{
|
||||||
const {API, loggedIn} = useLtoContext()
|
const [{data: rawCharacters}] = useAtom(charactersAtom)
|
||||||
const {data:characters} = useQuery({
|
|
||||||
queryKey:["characters", API.s.user],
|
|
||||||
queryFn: async ()=> {
|
|
||||||
return API.GetAccounts().then(x=>{
|
|
||||||
if(!x) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return x.flatMap(x=>{return x?.characters})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
enabled: loggedIn,
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const [search, setSearch] = useState("")
|
||||||
|
|
||||||
|
const { characters, fuse } = useMemo(()=>{
|
||||||
|
if(!rawCharacters) {
|
||||||
|
return {
|
||||||
|
characters: [],
|
||||||
|
fuse: new Fuse([], {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// transform characters into pairs between the bank and not bank
|
||||||
|
return {
|
||||||
|
characters: rawCharacters,
|
||||||
|
fuse: new Fuse(rawCharacters, {
|
||||||
|
findAllMatches: true,
|
||||||
|
threshold: 0.8,
|
||||||
|
useExtendedSearch: true,
|
||||||
|
keys: ["character.name"],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}, [rawCharacters])
|
||||||
if(!characters || characters.length == 0) {
|
if(!characters || characters.length == 0) {
|
||||||
return <PleaseLogin/>
|
return <PleaseLogin/>
|
||||||
}
|
}
|
||||||
|
const searchResults = fuse.search(search || "!-----", {
|
||||||
|
limit: 20,
|
||||||
|
}).map((x)=>{
|
||||||
|
return <div className="flex flex-col" key={`${x.item.character.account_id}`}>
|
||||||
|
<CharacterCard key={x.item.bank.id} character={x.item.bank} />
|
||||||
|
<CharacterCard key={x.item.character.id} character={x.item.character} />
|
||||||
|
</div>
|
||||||
|
})
|
||||||
return <>
|
return <>
|
||||||
<div className="flex flex-row overflow-x-scroll gap-4 h-full">
|
<div className="flex flex-col gap-1">
|
||||||
{characters.map(x=>{
|
<input
|
||||||
return <CharacterCard key={x.id} character={x} />
|
className="border border-black-1 bg-gray-100 placeholder-gray-600 p-1 max-w-[200px]"
|
||||||
})}
|
placeholder="search character..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e)=>{
|
||||||
|
setSearch(e.target.value)
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
<div className="flex flex-row overflow-x-scroll gap-1 h-full min-h-36">
|
||||||
|
{searchResults ? searchResults : <>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|||||||
@ -1,116 +1,237 @@
|
|||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query"
|
|
||||||
import { TricksterCharacter } from "../lib/trickster"
|
import { TricksterCharacter } from "../lib/trickster"
|
||||||
import { useSessionContext } from "../context/SessionContext"
|
import { useSessionContext } from "../context/SessionContext"
|
||||||
import { useLtoContext } from "../context/LtoContext"
|
|
||||||
|
|
||||||
import 'handsontable/dist/handsontable.full.min.css';
|
import 'handsontable/dist/handsontable.full.min.css';
|
||||||
|
|
||||||
import { registerAllModules } from 'handsontable/registry';
|
import { registerAllModules } from 'handsontable/registry';
|
||||||
import { HotTable, HotTableClass } from '@handsontable/react';
|
import { HotTable, HotTableClass } from '@handsontable/react';
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState} from "react";
|
||||||
import { InventoryTable } from "../lib/table";
|
import { InventoryTable } from "../lib/table";
|
||||||
import { DotLoader } from "react-spinners";
|
import { DotLoader } from "react-spinners";
|
||||||
import { useDebounceCallback, useResizeObserver } from "usehooks-ts";
|
import { useResizeObserver } from "@mantine/hooks";
|
||||||
import { Columns } from "../lib/columns";
|
import { Columns } from "../lib/columns";
|
||||||
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
|
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
|
import { charactersAtom, currentCharacterInventoryAtom, currentCharacterItemsAtom, LTOApi, selectedTargetInventoryAtom } from "../state/atoms";
|
||||||
|
import Select from 'react-select';
|
||||||
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, size, useDismiss, useFloating, useInteractions, useListNavigation, useRole } from "@floating-ui/react";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
registerAllModules();
|
registerAllModules();
|
||||||
type Size = {
|
type Size = {
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface InventoryItemProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InventoryItem = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
InventoryItemProps & React.HTMLProps<HTMLDivElement>
|
||||||
|
>(({ children, active, ...rest }, ref) => {
|
||||||
|
const id = useId();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="option"
|
||||||
|
id={id}
|
||||||
|
aria-selected={active}
|
||||||
|
{...rest}
|
||||||
|
style={{
|
||||||
|
background: active ? "lightblue" : "none",
|
||||||
|
padding: 4,
|
||||||
|
cursor: "default",
|
||||||
|
...rest.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const InventoryTargetSelector = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const listRef = useRef<Array<HTMLElement | null>>([]);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
open,
|
||||||
|
onOpenChange: setOpen,
|
||||||
|
middleware: [
|
||||||
|
flip({ padding: 10 }),
|
||||||
|
size({
|
||||||
|
apply({ rects, availableHeight, elements }) {
|
||||||
|
Object.assign(elements.floating.style, {
|
||||||
|
width: `${rects.reference.width}px`,
|
||||||
|
maxHeight: `${availableHeight}px`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
padding: 10,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = useRole(context, { role: "listbox" });
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const listNav = useListNavigation(context, {
|
||||||
|
listRef,
|
||||||
|
activeIndex,
|
||||||
|
onNavigate: setActiveIndex,
|
||||||
|
virtual: true,
|
||||||
|
loop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
||||||
|
[role, dismiss, listNav]
|
||||||
|
);
|
||||||
|
|
||||||
|
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = event.target.value;
|
||||||
|
setInputValue(value);
|
||||||
|
setSelectedTargetInventory(undefined)
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
setOpen(true);
|
||||||
|
setActiveIndex(0);
|
||||||
|
} else {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: subaccounts } = useAtomValue(charactersAtom)
|
||||||
|
|
||||||
|
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
|
||||||
|
|
||||||
|
const searcher = useMemo(()=>{
|
||||||
|
return new Fuse(subaccounts?.flatMap(x=>[
|
||||||
|
x.bank,
|
||||||
|
x.character,
|
||||||
|
])||[], {
|
||||||
|
keys:["path","name"],
|
||||||
|
findAllMatches: true,
|
||||||
|
threshold: 0.8,
|
||||||
|
useExtendedSearch: true,
|
||||||
|
})
|
||||||
|
}, [subaccounts])
|
||||||
|
|
||||||
|
const items = searcher.search(inputValue || "!-", {limit: 10}).map(x=>x.item)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className="border border-black-1 bg-gray-100 placeholder-gray-600"
|
||||||
|
{...getReferenceProps({
|
||||||
|
ref: refs.setReference,
|
||||||
|
onChange,
|
||||||
|
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
|
||||||
|
placeholder: "Target Inventory",
|
||||||
|
"aria-autocomplete": "list",
|
||||||
|
onKeyDown(event) {
|
||||||
|
if (
|
||||||
|
event.key === "Enter" &&
|
||||||
|
activeIndex != null &&
|
||||||
|
items[activeIndex]
|
||||||
|
) {
|
||||||
|
setSelectedTargetInventory(items[activeIndex])
|
||||||
|
setInputValue(items[activeIndex].name);
|
||||||
|
setActiveIndex(null);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{open && (
|
||||||
|
<FloatingPortal>
|
||||||
|
<FloatingFocusManager
|
||||||
|
context={context}
|
||||||
|
initialFocus={-1}
|
||||||
|
visuallyHiddenDismiss
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...getFloatingProps({
|
||||||
|
ref: refs.setFloating,
|
||||||
|
style: {
|
||||||
|
...floatingStyles,
|
||||||
|
background: "#eee",
|
||||||
|
color: "black",
|
||||||
|
overflowY: "auto",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<InventoryItem
|
||||||
|
{...getItemProps({
|
||||||
|
key: item.path,
|
||||||
|
ref(node) {
|
||||||
|
listRef.current[index] = node;
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
setInputValue(item.name);
|
||||||
|
setSelectedTargetInventory(item);
|
||||||
|
setOpen(false);
|
||||||
|
refs.domReference.current?.focus();
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
active={activeIndex === index}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</InventoryItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FloatingFocusManager>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
export const Inventory = () => {
|
export const Inventory = () => {
|
||||||
const {activeTable, columns, tags, orders} = useSessionContext()
|
const {activeTable, columns, tags, orders} = useSessionContext()
|
||||||
const {API, loggedIn} = useLtoContext()
|
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const [ref, {height}] = useResizeObserver({})
|
||||||
const [{ height }, setSize] = useState<Size>({
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
const onResize = useDebounceCallback(setSize, 200)
|
const {data:character, isLoading, isFetching } = useAtomValue(currentCharacterInventoryAtom)
|
||||||
|
//const sendOrders = useCallback(()=>{
|
||||||
useResizeObserver({
|
// if(!hotTableComponent.current?.hotInstance){
|
||||||
ref,
|
// return
|
||||||
onResize,
|
// }
|
||||||
})
|
// const hott = hotTableComponent.current?.hotInstance
|
||||||
|
// const headers = hott.getColHeader()
|
||||||
const hotTableComponent = useRef<HotTableClass>(null);
|
// const dat = hott.getData()
|
||||||
const {data:character, isLoading, isFetching } = useQuery({
|
// const idxNumber = headers.indexOf(Columns.MoveCount.displayName)
|
||||||
queryKey:["inventory", activeTable],
|
// const idxTarget = headers.indexOf(Columns.Move.displayName)
|
||||||
queryFn: async ()=> {
|
// const origin = activeTable
|
||||||
return API.GetInventory(activeTable)
|
// const pending:OrderDetails[] = [];
|
||||||
},
|
// for(const row of dat) {
|
||||||
enabled: loggedIn,
|
// try{
|
||||||
// placeholderData: keepPreviousData,
|
// const nm = Number(row[idxNumber].replace("x",""))
|
||||||
})
|
// const target = (row[idxTarget] as string).replaceAll("-","").trim()
|
||||||
const {data:characters} = useQuery({
|
// if(!isNaN(nm) && nm > 0 && target.length > 0){
|
||||||
queryKey:["characters", API.s.user],
|
// const info:OrderDetails = {
|
||||||
queryFn: async ()=> {
|
// item_uid: row[0].toString(),
|
||||||
return API.GetAccounts().then(x=>{
|
// count: nm,
|
||||||
return x.flatMap(x=>{return x.characters})
|
// origin_path: activeTable,
|
||||||
})
|
// target_path: target,
|
||||||
},
|
// }
|
||||||
enabled: loggedIn,
|
// pending.push(info)
|
||||||
placeholderData: keepPreviousData,
|
// }
|
||||||
})
|
// }catch(e){
|
||||||
|
// }
|
||||||
|
// }
|
||||||
useEffect(()=>{
|
// log.debug("OrderDetails", pending)
|
||||||
if(!character) {
|
// const chars = new Map<string,TricksterCharacter>()
|
||||||
hotTableComponent.current?.hotInstance?.updateSettings({
|
// const manager = new OrderSender(orders, chars)
|
||||||
data: [],
|
// for(const d of pending){
|
||||||
})
|
// const order = manager.send(d)
|
||||||
return
|
// //order.tick(api)
|
||||||
}
|
// }
|
||||||
const it = new InventoryTable(character, {
|
//}, [orders])
|
||||||
columns: columns,
|
|
||||||
tags: tags,
|
|
||||||
accounts: characters?.map(x=>{
|
|
||||||
return x.name
|
|
||||||
}) || [],
|
|
||||||
})
|
|
||||||
const build = it.BuildTable()
|
|
||||||
hotTableComponent.current?.hotInstance?.updateSettings(build.settings)
|
|
||||||
}, [hotTableComponent, character, height])
|
|
||||||
|
|
||||||
const sendOrders = useCallback(()=>{
|
|
||||||
if(!hotTableComponent.current?.hotInstance){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const hott = hotTableComponent.current?.hotInstance
|
|
||||||
const headers = hott.getColHeader()
|
|
||||||
const dat = hott.getData()
|
|
||||||
const idxNumber = headers.indexOf(Columns.MoveCount.displayName)
|
|
||||||
const idxTarget = headers.indexOf(Columns.Move.displayName)
|
|
||||||
const origin = activeTable
|
|
||||||
const pending:OrderDetails[] = [];
|
|
||||||
for(const row of dat) {
|
|
||||||
try{
|
|
||||||
const nm = Number(row[idxNumber].replace("x",""))
|
|
||||||
const target = (row[idxTarget] as string).replaceAll("-","").trim()
|
|
||||||
if(!isNaN(nm) && nm > 0 && target.length > 0){
|
|
||||||
const info:OrderDetails = {
|
|
||||||
item_uid: row[0].toString(),
|
|
||||||
count: nm,
|
|
||||||
origin_path: activeTable,
|
|
||||||
target_path: target,
|
|
||||||
}
|
|
||||||
pending.push(info)
|
|
||||||
}
|
|
||||||
}catch(e){
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.debug("OrderDetails", pending)
|
|
||||||
const chars = new Map<string,TricksterCharacter>()
|
|
||||||
const manager = new OrderSender(orders, chars)
|
|
||||||
for(const d of pending){
|
|
||||||
const order = manager.send(d)
|
|
||||||
//order.tick(api)
|
|
||||||
}
|
|
||||||
}, [orders])
|
|
||||||
|
|
||||||
const Loading = ()=>{
|
const Loading = ()=>{
|
||||||
return <div role="status" className="flex align-center justify-center">
|
return <div role="status" className="flex align-center justify-center">
|
||||||
@ -119,26 +240,27 @@ export const Inventory = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const items = useAtomValue(currentCharacterItemsAtom)
|
||||||
|
|
||||||
return <div ref={ref} className={``}>
|
return <div ref={ref} className={``}>
|
||||||
<div className="flex flex-row py-2 px-3">
|
<div className="flex flex-row py-2 justify-end">
|
||||||
|
<InventoryTargetSelector/>
|
||||||
<div
|
<div
|
||||||
onClick={(e)=>{
|
onClick={(e)=>{
|
||||||
sendOrders()
|
// sendOrders()
|
||||||
}}
|
}}
|
||||||
className="
|
className="
|
||||||
hover:cursor-pointer
|
hover:cursor-pointer
|
||||||
border border-black-1
|
border border-black-1
|
||||||
bg-green-200
|
bg-green-200
|
||||||
px-2 py-1
|
px-2 py-1
|
||||||
">ayy lmao button</div>
|
">Move Selected</div>
|
||||||
</div>
|
|
||||||
{(isLoading || isFetching) ? <Loading/> : <></> }
|
|
||||||
<div
|
|
||||||
className={`${isLoading || isFetching ? "invisible" : ""}`}>
|
|
||||||
<HotTable
|
|
||||||
ref={hotTableComponent as any}
|
|
||||||
licenseKey="non-commercial-and-evaluation"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{(isLoading || isFetching) ? <Loading/> : <>
|
||||||
|
<div>
|
||||||
|
total: {items.size}
|
||||||
|
</div>
|
||||||
|
</> }
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,78 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
import { useLtoContext } from "../context/LtoContext"
|
|
||||||
import useLocalStorage from "use-local-storage"
|
import useLocalStorage from "use-local-storage"
|
||||||
|
import { useAtom } from "jotai"
|
||||||
|
import { loginStatusAtom } from "../state/atoms"
|
||||||
|
import { LoginHelper } from "../lib/session"
|
||||||
|
|
||||||
export const LoginWidget = () => {
|
export const LoginWidget = () => {
|
||||||
const {loggedIn, login, logout} = useLtoContext()
|
|
||||||
|
|
||||||
const [username, setUsername] = useLocalStorage("input_username","", {syncData: false})
|
const [username, setUsername] = useLocalStorage("input_username","", {syncData: false})
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
return <>
|
|
||||||
<div className="flex flex-col border border-gray-400">
|
const [{data:loginState, refetch: refetchLoginState}] = useAtom(loginStatusAtom)
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex flex-row bg-blue-400">
|
const [loginError, setLoginError] = useState("")
|
||||||
<span className="text-white pb-1 pl-2 m-y-1">
|
|
||||||
account
|
if(loginState?.logged_in){
|
||||||
</span>
|
return <>
|
||||||
|
<div className="flex flex-row justify-between px-2">
|
||||||
|
<div>
|
||||||
|
{loginState.community_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row flex-wrap gap-1 p-2 justify-center">
|
<div className="flex flex-row gap-2">
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
onChange={(e)=>{
|
|
||||||
setUsername(e.target.value)
|
|
||||||
}}
|
|
||||||
value={username}
|
|
||||||
placeholder="username" className="w-32 pl-2 pb-1 border border-gray-600"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
onChange={(e)=>{
|
|
||||||
setPassword(e.target.value)
|
|
||||||
}}
|
|
||||||
value={password}
|
|
||||||
type="password" placeholder="password" className="w-32 pl-2 pb-1 border border-gray-600"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row p-2 justify-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={async ()=>{
|
|
||||||
login(username,password).catch((e)=>{
|
|
||||||
alert(e.toString())
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
className="border border-gray-600 px-2 py-1 hover:bg-blue-200">
|
|
||||||
login
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={()=>{
|
onClick={()=>{
|
||||||
logout()
|
LoginHelper.logout().finally(()=>{
|
||||||
|
refetchLoginState()
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}}
|
}}
|
||||||
disabled={!loggedIn} className="border border-gray-600 px-2 py-1 hover:bg-red-200 disabled:border-gray-400 disabled:text-gray-300 disabled:bg-white ">
|
className="text-blue-400 text-xs hover:cursor-pointer hover:text-blue-600">
|
||||||
logout
|
logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<form action={
|
||||||
|
()=>{
|
||||||
|
LoginHelper.login(username,password).catch((e)=>{
|
||||||
|
setLoginError(e.message)
|
||||||
|
}).finally(()=>{
|
||||||
|
refetchLoginState()
|
||||||
|
refetchLoginState()
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="flex flex-col gap-1 p-2 justify-left">
|
||||||
|
{ loginError ? (<div className="text-red-500 text-xs">
|
||||||
|
{loginError}
|
||||||
|
</div>) : null}
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
onChange={(e)=>{
|
||||||
|
setUsername(e.target.value)
|
||||||
|
}}
|
||||||
|
value={username}
|
||||||
|
id="username"
|
||||||
|
placeholder="username" className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
onChange={(e)=>{
|
||||||
|
setPassword(e.target.value)
|
||||||
|
}}
|
||||||
|
value={password}
|
||||||
|
type="password" placeholder="password" className="w-32 pl-2 pb-1 border-b border-gray-600 placeholder-gray-500"/>
|
||||||
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { LtoContextProvider } from "./LtoContext";
|
|
||||||
import { SessionContextProvider } from "./SessionContext";
|
import { SessionContextProvider } from "./SessionContext";
|
||||||
|
|
||||||
interface IContext {
|
interface IContext {
|
||||||
@ -9,7 +8,6 @@ function AppContext(props: IContext): any {
|
|||||||
const { children } = props;
|
const { children } = props;
|
||||||
const providers = [
|
const providers = [
|
||||||
SessionContextProvider,
|
SessionContextProvider,
|
||||||
LtoContextProvider
|
|
||||||
];
|
];
|
||||||
const res = providers.reduceRight(
|
const res = providers.reduceRight(
|
||||||
(acc, CurrVal) => <CurrVal>{acc as any}</CurrVal>,
|
(acc, CurrVal) => <CurrVal>{acc as any}</CurrVal>,
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from "react";
|
|
||||||
import { LTOApiv0 } from "../lib/lifeto";
|
|
||||||
import { storage } from "../session_storage";
|
|
||||||
import { LoginHelper, LogoutHelper } from "../lib/session";
|
|
||||||
|
|
||||||
interface LtoContextProps {
|
|
||||||
API: LTOApiv0;
|
|
||||||
login: (username:string, password:string)=>Promise<void>;
|
|
||||||
logout: ()=>void;
|
|
||||||
loggedIn: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const LtoContext = createContext({} as LtoContextProps);
|
|
||||||
|
|
||||||
export const LtoContextProvider = ({ children }: { children: any }) => {
|
|
||||||
const [API, setAPI] = useState(new LTOApiv0(storage.GetSession()));
|
|
||||||
const login = async (username:string , password:string ) =>{
|
|
||||||
console.log("attempting logiun", username)
|
|
||||||
return new LoginHelper(username, password).login()
|
|
||||||
.catch((e)=>{
|
|
||||||
if(e.code == "ERR_BAD_REQUEST") {
|
|
||||||
throw "invalid username/password"
|
|
||||||
}
|
|
||||||
console.warn("throwing error", e)
|
|
||||||
throw "unknown error, please report"
|
|
||||||
})
|
|
||||||
.then((session)=>{
|
|
||||||
setAPI(new LTOApiv0(session))
|
|
||||||
storage.AddSession(session)
|
|
||||||
setLoggedIn(true)
|
|
||||||
}) }
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
new LogoutHelper().logout().then(()=>{
|
|
||||||
storage.RemoveSession()
|
|
||||||
localStorage.clear()
|
|
||||||
window.location.reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const [loggedIn, setLoggedIn] = useState(false)
|
|
||||||
useEffect(()=>{
|
|
||||||
if(!API) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
API?.GetLoggedin().then((x)=>{
|
|
||||||
setLoggedIn(x)
|
|
||||||
})
|
|
||||||
}, [API])
|
|
||||||
|
|
||||||
return <LtoContext.Provider value={{
|
|
||||||
API,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
loggedIn
|
|
||||||
}}>{children}</LtoContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useLtoContext = (): LtoContextProps => {
|
|
||||||
const context = useContext<LtoContextProps>(LtoContext);
|
|
||||||
if (context === null) {
|
|
||||||
throw new Error(
|
|
||||||
'"useLtoContext" should be used inside a "LtoContextProvider"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { createContext, Dispatch, SetStateAction, useContext, useState } from "react";
|
import { createContext, Dispatch, SetStateAction, useContext, useState } from "react";
|
||||||
import { LTOApiv0 } from "../lib/lifeto";
|
|
||||||
|
|
||||||
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>>;
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import Handsontable from "handsontable"
|
|
||||||
import numbro from 'numbro';
|
|
||||||
import { textRenderer } from "handsontable/renderers"
|
|
||||||
import { TricksterItem } from "../trickster"
|
import { TricksterItem } from "../trickster"
|
||||||
import Core from "handsontable/core";
|
|
||||||
|
|
||||||
export const BasicColumns = [
|
export const BasicColumns = [
|
||||||
"uid","Image","Name","Count",
|
"uid","Image","Name","Count",
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import { trace } from "loglevel"
|
|
||||||
import { TricksterAccount, TricksterInventory } from "../trickster"
|
import { TricksterAccount, TricksterInventory } from "../trickster"
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
|
||||||
|
|
||||||
export const BankEndpoints = ["internal-xfer-item", "bank-item", "sell-item","buy-from-order","cancel-order"] as const
|
export const BankEndpoints = ["internal-xfer-item", "bank-item", "sell-item","buy-from-order","cancel-order"] as const
|
||||||
export type BankEndpoint = typeof BankEndpoints[number]
|
export type BankEndpoint = typeof BankEndpoints[number]
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Axios, AxiosResponse, Method } from "axios"
|
import { Axios, AxiosResponse, Method } from "axios"
|
||||||
import log, { debug } 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 { dummyChar, TricksterAccount, TricksterInventory, TricksterItem, TricksterWallet } from "../trickster"
|
import { TricksterAccount, TricksterAccountInfo, 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 => {
|
||||||
@ -47,7 +47,7 @@ export class LTOApiv0 implements LTOApi {
|
|||||||
char_path = char_path.replace(":","")
|
char_path = char_path.replace(":","")
|
||||||
}
|
}
|
||||||
let type = char_path.includes("/") ? "char" : "account"
|
let type = char_path.includes("/") ? "char" : "account"
|
||||||
return this.s.request("GET", `v2/item-manager/items/${type}/${char_path}`,undefined).then((ans:AxiosResponse)=>{
|
return this.s.request("GET", `v3/item-manager/items/${type}/${char_path}`,undefined).then((ans:AxiosResponse)=>{
|
||||||
const o = ans.data
|
const o = ans.data
|
||||||
log.debug("GetInventory", o)
|
log.debug("GetInventory", o)
|
||||||
let name = "bank"
|
let name = "bank"
|
||||||
@ -71,7 +71,7 @@ export class LTOApiv0 implements LTOApi {
|
|||||||
id,
|
id,
|
||||||
path: char_path,
|
path: char_path,
|
||||||
galders,
|
galders,
|
||||||
items: Object.fromEntries((Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):[string, TricksterItem]=>{
|
items: new Map((Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):[string, TricksterItem]=>{
|
||||||
v.unique_id = Number(k)
|
v.unique_id = Number(k)
|
||||||
return [k, v]
|
return [k, v]
|
||||||
})),
|
})),
|
||||||
@ -86,7 +86,8 @@ export class LTOApiv0 implements LTOApi {
|
|||||||
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,
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
import axios, { 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 { getCookie, removeCookie } from "typescript-cookie";
|
||||||
|
import { TricksterAccountInfo } from "./trickster";
|
||||||
|
|
||||||
|
|
||||||
export const SITE_ROOT = "/lifeto/"
|
export const SITE_ROOT = "/lifeto/"
|
||||||
|
|
||||||
export const API_ROOT = "api/lifeto/"
|
export const API_ROOT = "api/lifeto/"
|
||||||
export const BANK_ROOT = "api/lifeto/v2/item-manager/"
|
export const BANK_ROOT = "v2/item-manager/"
|
||||||
export const MARKET_ROOT = "marketplace-api/"
|
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"
|
||||||
}
|
}
|
||||||
@ -32,56 +37,53 @@ export const EndpointCreators = [
|
|||||||
export type EndpointCreator = typeof EndpointCreators[number]
|
export type EndpointCreator = typeof EndpointCreators[number]
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
user:string
|
|
||||||
xsrf:string
|
|
||||||
csrf:string
|
|
||||||
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 {
|
||||||
user:string
|
|
||||||
pass:string
|
|
||||||
csrf?:string
|
|
||||||
constructor(user:string, pass:string){
|
|
||||||
this.user = user;
|
|
||||||
this.pass = pass;
|
|
||||||
}
|
|
||||||
login = async ():Promise<TokenSession> =>{
|
|
||||||
return axios.get(login_endpoint("login"),{withCredentials:false})
|
|
||||||
.then(async ()=>{
|
|
||||||
return axios.post(login_endpoint("login"),{
|
|
||||||
login:this.user,
|
|
||||||
password:this.pass,
|
|
||||||
redirectTo:"lifeto"
|
|
||||||
},{withCredentials:false})
|
|
||||||
}).then(async ()=>{
|
|
||||||
await sleep(100)
|
|
||||||
let xsrf= getCookie("XSRF-TOKEN")
|
|
||||||
return new TokenSession(this.user,this.csrf!, xsrf!)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LogoutHelper{
|
|
||||||
constructor(){
|
constructor(){
|
||||||
}
|
}
|
||||||
logout = async ():Promise<void> =>{
|
static login = async (user:string, pass: string):Promise<TokenSession> =>{
|
||||||
|
return axios.get(login_endpoint("login"),{
|
||||||
|
withCredentials:false,
|
||||||
|
maxRedirects: 0,
|
||||||
|
xsrfCookieName: "XSRF-TOKEN",
|
||||||
|
})
|
||||||
|
.then(async ()=>{
|
||||||
|
return axios.post(login_endpoint("login"),{
|
||||||
|
login:user,
|
||||||
|
password:pass,
|
||||||
|
redirectTo:"lifeto"
|
||||||
|
},{
|
||||||
|
withCredentials:false,
|
||||||
|
maxRedirects: 0,
|
||||||
|
xsrfCookieName: "XSRF-TOKEN",
|
||||||
|
})
|
||||||
|
}).then(async ()=>{
|
||||||
|
return new TokenSession()
|
||||||
|
}).catch((e)=>{
|
||||||
|
if(e instanceof AxiosError) {
|
||||||
|
if(e.code == "ERR_BAD_REQUEST") {
|
||||||
|
throw "invalid username/password"
|
||||||
|
}
|
||||||
|
throw e.message
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
static info = async ():Promise<TricksterAccountInfo> =>{
|
||||||
|
return axios.get(raw_endpoint("settings/info"),{withCredentials:false}).then((ans:AxiosResponse)=>{
|
||||||
|
return ans.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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(()=>{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sleep = async(ms:number)=> {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TokenSession implements Session {
|
export class TokenSession implements Session {
|
||||||
csrf:string
|
constructor(){
|
||||||
xsrf:string
|
|
||||||
user:string
|
|
||||||
constructor(name:string, csrf:string, xsrf: string){
|
|
||||||
this.user = name
|
|
||||||
this.csrf = csrf
|
|
||||||
this.xsrf = xsrf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request = async (verb:string,url:string,data:any, c:EndpointCreator = api_endpoint):Promise<AxiosResponse> => {
|
request = async (verb:string,url:string,data:any, c:EndpointCreator = api_endpoint):Promise<AxiosResponse> => {
|
||||||
@ -101,21 +103,7 @@ export class TokenSession implements Session {
|
|||||||
default:
|
default:
|
||||||
promise = axios.get(c(url),this.genHeaders())
|
promise = axios.get(c(url),this.genHeaders())
|
||||||
}
|
}
|
||||||
return promise.then(x=>{
|
return promise
|
||||||
if(x.data){
|
|
||||||
try{
|
|
||||||
this.xsrf = x.data.split("xsrf-token")[1].split('\">')[0].replace("\" content=\"",'')
|
|
||||||
}catch(e){
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(x.headers['set-cookie']){
|
|
||||||
const cookies = x.headers['set-cookie'].map((y)=>{
|
|
||||||
return y.split("=")[1].split(";")[0];
|
|
||||||
})
|
|
||||||
this.xsrf = cookies[0]
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
genHeaders = ()=>{
|
genHeaders = ()=>{
|
||||||
const out = {
|
const out = {
|
||||||
@ -125,9 +113,6 @@ export class TokenSession implements Session {
|
|||||||
},
|
},
|
||||||
withCredentials:true
|
withCredentials:true
|
||||||
}
|
}
|
||||||
if(this.xsrf){
|
|
||||||
(out.headers as any)["X-XSRF-TOKEN"] = this.xsrf.replace("%3D","=")
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,11 @@ export interface TricksterItem {
|
|||||||
stats?: {[key: string]:any}
|
stats?: {[key: string]:any}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TricksterAccountInfo {
|
||||||
|
community_name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface TricksterAccount {
|
export interface TricksterAccount {
|
||||||
name:string
|
name:string
|
||||||
characters: TricksterCharacter[]
|
characters: TricksterCharacter[]
|
||||||
@ -37,7 +42,7 @@ export interface TricksterCharacter extends Identifier {
|
|||||||
|
|
||||||
export interface TricksterInventory extends Identifier{
|
export interface TricksterInventory extends Identifier{
|
||||||
galders?:number
|
galders?:number
|
||||||
items:{[key:string]:TricksterItem}
|
items: Map<string, TricksterItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div> {{loginString}} </div>
|
|
||||||
<section class="login_modal">
|
|
||||||
<div class="login_field">
|
|
||||||
<label for="userName">Username: </label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="login-username"
|
|
||||||
v-model="username"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="login_field">
|
|
||||||
<label for="password">Password: </label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="login-password"
|
|
||||||
v-model="password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="login_field">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="loginButton"
|
|
||||||
v-on:click="login()"
|
|
||||||
>Login</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="logoutButton"
|
|
||||||
v-on:click="logout()"
|
|
||||||
>Logout</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const username = ref("")
|
|
||||||
const password = ref("")
|
|
||||||
const loginString = ref("not logged in")
|
|
||||||
|
|
||||||
const login = () => {
|
|
||||||
new LoginHelper(username.value, password.value).login()
|
|
||||||
.then((session)=>{
|
|
||||||
console.log(session, "adding to storage")
|
|
||||||
storage.AddSession(session)
|
|
||||||
window.location.reload()
|
|
||||||
}).catch((e)=>{
|
|
||||||
if(e.code == "ERR_BAD_REQUEST") {
|
|
||||||
throw "invalid username/password"
|
|
||||||
}
|
|
||||||
console.warn(e)
|
|
||||||
throw "unknown error, please report"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
new LogoutHelper().logout().then(()=>{
|
|
||||||
storage.RemoveSession()
|
|
||||||
localStorage.clear()
|
|
||||||
window.location.reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const s = storage.GetSession()
|
|
||||||
const api = new LTOApiv0(s)
|
|
||||||
if (s != undefined) {
|
|
||||||
username.value = s.user
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateLogin = () => {
|
|
||||||
api.GetLoggedin().then((res)=>{
|
|
||||||
if(res) {
|
|
||||||
loginString.value = "logged in as " + s.user
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
updateLogin()
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, computed, PropType, defineProps, defineEmits, ref} from 'vue';
|
|
||||||
import { LTOApiv0 } from '../lib/lifeto';
|
|
||||||
import { LoginHelper, LogoutHelper, Session } from '../lib/session';
|
|
||||||
import { storage } from '../session_storage';
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { Cookies, getCookie, removeCookie, setCookie} from 'typescript-cookie'
|
|
||||||
import { Session, TokenSession } from './lib/session'
|
import { Session, TokenSession } from './lib/session'
|
||||||
|
|
||||||
|
|
||||||
@ -11,23 +10,12 @@ export const nameCookie = (...s:string[]):string=>{
|
|||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
GetSession():Session {
|
GetSession():Session {
|
||||||
const {user, xsrf, csrf} = {
|
return new TokenSession()
|
||||||
user: getCookie(nameCookie("user"))!,
|
|
||||||
xsrf: getCookie(nameCookie("xsrf"))!,
|
|
||||||
csrf: getCookie(nameCookie("csrf"))!
|
|
||||||
}
|
|
||||||
return new TokenSession(user, xsrf, csrf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveSession() {
|
RemoveSession() {
|
||||||
removeCookie(nameCookie("user"))
|
|
||||||
removeCookie(nameCookie("xsrf"))
|
|
||||||
removeCookie(nameCookie("csrf"))
|
|
||||||
}
|
}
|
||||||
AddSession(s:Session) {
|
AddSession(s:Session) {
|
||||||
setCookie(nameCookie("user"),s.user)
|
// setCookie(nameCookie("xsrf"),s.xsrf)
|
||||||
setCookie(nameCookie("xsrf"),s.xsrf)
|
|
||||||
setCookie(nameCookie("csrf"),s.csrf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
104
src/state/atoms.ts
Normal file
104
src/state/atoms.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { LTOApiv0 } from '../lib/lifeto'
|
||||||
|
import { LoginHelper, TokenSession } from '../lib/session'
|
||||||
|
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||||
|
import {atomFamily, atomWithRefresh, atomWithStorage} from "jotai/utils";
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
import { TricksterCharacter, TricksterInventory, TricksterItem } from '../lib/trickster';
|
||||||
|
|
||||||
|
export const LTOApi = new LTOApiv0(new TokenSession())
|
||||||
|
|
||||||
|
|
||||||
|
export const loginStatusAtom = atomWithQuery((get) => {
|
||||||
|
return {
|
||||||
|
queryKey: ['login_status'],
|
||||||
|
enabled: true,
|
||||||
|
placeholderData: {
|
||||||
|
logged_in: false,
|
||||||
|
community_name: "...",
|
||||||
|
},
|
||||||
|
queryFn: async () => {
|
||||||
|
return LoginHelper.info().then(info => {
|
||||||
|
return {
|
||||||
|
logged_in: true,
|
||||||
|
community_name: info.community_name,
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
if(e instanceof AxiosError) {
|
||||||
|
return {
|
||||||
|
logged_in: false,
|
||||||
|
community_name: "...",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const charactersAtom = atomWithQuery((get) => {
|
||||||
|
const {data: loginStatus} = get(loginStatusAtom)
|
||||||
|
console.log("charactersAtom", loginStatus)
|
||||||
|
return {
|
||||||
|
queryKey: ['characters', loginStatus?.community_name || "..."],
|
||||||
|
enabled: !!loginStatus?.logged_in,
|
||||||
|
refetchOnMount: true,
|
||||||
|
queryFn: async ()=> {
|
||||||
|
return LTOApi.GetAccounts().then(x=>{
|
||||||
|
if(!x) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const rawCharacters = x.flatMap(x=>{return x?.characters})
|
||||||
|
const characterPairs: Record<string, {bank?: TricksterCharacter, character?: TricksterCharacter}> = {}
|
||||||
|
rawCharacters.forEach(x=>{
|
||||||
|
let item = characterPairs[x.account_name]
|
||||||
|
if(!item) {
|
||||||
|
item = {}
|
||||||
|
}
|
||||||
|
if(x.class === -8) {
|
||||||
|
item.bank = x
|
||||||
|
} 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 true
|
||||||
|
}) as Array<{bank: TricksterCharacter, character: TricksterCharacter}>
|
||||||
|
|
||||||
|
return cleanCharacterPairs
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const selectedCharacterAtom = atom<TricksterCharacter | undefined>(undefined)
|
||||||
|
export const selectedTargetInventoryAtom = atom<TricksterCharacter | undefined>(undefined)
|
||||||
|
|
||||||
|
export const pageSize = atomWithStorage("preference.page_size", 250)
|
||||||
|
export const currentFilter = atom<undefined>(undefined)
|
||||||
|
|
||||||
|
|
||||||
|
export const currentCharacterInventoryAtom = atomWithQuery((get) => {
|
||||||
|
const currentCharacter = get(selectedCharacterAtom)
|
||||||
|
return {
|
||||||
|
queryKey:["inventory", currentCharacter?.path || "-"],
|
||||||
|
queryFn: async ()=> {
|
||||||
|
return LTOApi.GetInventory(currentCharacter?.path|| "-")
|
||||||
|
},
|
||||||
|
enabled: !!currentCharacter,
|
||||||
|
// placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const currentCharacterItemsAtom = atom((get)=>{
|
||||||
|
const {data: inventory} = get(currentCharacterInventoryAtom)
|
||||||
|
return inventory?.items || new Map<string, TricksterItem>()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineStore, storeToRefs } from 'pinia'
|
import { defineStore, storeToRefs } from 'pinia'
|
||||||
import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns'
|
import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns'
|
||||||
import { OrderTracker } from '../lib/lifeto/order_manager'
|
import { OrderTracker } from '../lib/lifeto/order_manager'
|
||||||
import { Reviver, StoreAccounts, StoreChars, StoreColSet, StoreInvs, StoreSerializable, StoreStr, StoreStrSet } 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'
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user