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
|
||||
WORKDIR /wd
|
||||
COPY . .
|
||||
RUN npm install
|
||||
RUN npx vite build
|
||||
|
||||
FROM alpine:3.16
|
||||
FROM caddyserver/caddy:2.10-alpine
|
||||
WORKDIR /wd
|
||||
COPY --from=GOBUILDER /wd/app.exe app.exe
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --from=NODEBUILDER /wd/dist dist
|
||||
ENTRYPOINT [ "/wd/app.exe" ]
|
||||
|
||||
|
||||
60
package.json
60
package.json
@ -8,40 +8,46 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@handsontable/react": "^14.5.0",
|
||||
"@tanstack/react-query": "^5.51.21",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"axios": "^0.27.2",
|
||||
"eslint": "^9.8.0",
|
||||
"@floating-ui/react": "^0.27.8",
|
||||
"@handsontable/react": "^15.3.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@tanstack/react-query": "^5.76.0",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"axios": "^1.9.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"handsontable": "^14.5.0",
|
||||
"loglevel": "^1.8.0",
|
||||
"pinia": "^2.0.14",
|
||||
"prettier": "^3.3.3",
|
||||
"qs": "^6.10.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-spinners": "^0.14.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",
|
||||
"handsontable": "^15.3.0",
|
||||
"jotai": "^2.12.4",
|
||||
"jotai-tanstack-query": "^0.9.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"pinia": "^3.0.2",
|
||||
"prettier": "^3.5.3",
|
||||
"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",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"uuid": "^10.0.0"
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.6",
|
||||
"postcss": "^8.4.40",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5"
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"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 = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col p-4 h-full">
|
||||
<div className="grid grid-cols-6 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<LoginWidget/>
|
||||
<div className="flex flex-col mx-auto p-4 w-full">
|
||||
<div className="flex flex-row max-w-6xl">
|
||||
<div className="flex flex-row justify-end w-full">
|
||||
<LoginWidget/>
|
||||
</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">
|
||||
<CharacterRoulette/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="flex flex-col border border-gray-400">
|
||||
<LoginWidget/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 h-full">
|
||||
<div className="col-span-1">
|
||||
@ -25,6 +45,4 @@ export const App: FC = () => {
|
||||
</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 { useLtoContext } from "../context/LtoContext"
|
||||
import { JobNumberToString, TricksterAccount, TricksterCharacter } from "../lib/trickster"
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query"
|
||||
import { TricksterCharacter } from "../lib/trickster"
|
||||
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}:{
|
||||
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 <>
|
||||
<div onClick={()=>{
|
||||
setActiveTable(character.path)
|
||||
setSelectedCharacter(character)
|
||||
}}
|
||||
|
||||
ref={refs.setReference} {...getReferenceProps()}
|
||||
className={`
|
||||
flex flex-col border border-black
|
||||
hover:cursor-pointer
|
||||
hover:bg-blue-100
|
||||
h-full
|
||||
p-2 ${character.path === activeTable ? `bg-blue-200 hover:bg-blue-100 border-double border-4` : ""}`}>
|
||||
<div className="flex"></div>
|
||||
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-row justify-center text-md">
|
||||
<span>{character.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center">
|
||||
<div className="flex flex-row justify-center"
|
||||
>
|
||||
{character.base_job === -8 ?
|
||||
<img src={`https://knowledge.lifeto.co/animations/npc/npc041_5.png`}/>
|
||||
:
|
||||
<img
|
||||
src={`https://knowledge.lifeto.co/animations/character/chr00${character.base_job}_13.png`}/>
|
||||
<img
|
||||
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${
|
||||
(character.current_job - character.base_job - 1).toString().padStart(3,"0")
|
||||
}_13.png`}/>
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<span>class: </span>
|
||||
<span>{JobNumberToString(character.current_job)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 text-xs">
|
||||
<span>path: </span>
|
||||
<span>{character.path}</span>
|
||||
<FloatingPortal>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="Tooltip"
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<div className="flex flex-col gap-1 bg-white">
|
||||
{character.base_job === -8 ? "bank" : character.name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FloatingPortal>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -53,30 +110,53 @@ const PleaseLogin = () => {
|
||||
}
|
||||
|
||||
export const CharacterRoulette = ()=>{
|
||||
const {API, loggedIn} = useLtoContext()
|
||||
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 [{data: rawCharacters}] = useAtom(charactersAtom)
|
||||
|
||||
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) {
|
||||
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 <>
|
||||
<div className="flex flex-row overflow-x-scroll gap-4 h-full">
|
||||
{characters.map(x=>{
|
||||
return <CharacterCard key={x.id} character={x} />
|
||||
})}
|
||||
<div className="flex flex-col gap-1">
|
||||
<input
|
||||
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>
|
||||
</>
|
||||
|
||||
|
||||
@ -1,116 +1,237 @@
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query"
|
||||
import { TricksterCharacter } from "../lib/trickster"
|
||||
import { useSessionContext } from "../context/SessionContext"
|
||||
import { useLtoContext } from "../context/LtoContext"
|
||||
|
||||
import 'handsontable/dist/handsontable.full.min.css';
|
||||
|
||||
import { registerAllModules } from 'handsontable/registry';
|
||||
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 { DotLoader } from "react-spinners";
|
||||
import { useDebounceCallback, useResizeObserver } from "usehooks-ts";
|
||||
import { useResizeObserver } from "@mantine/hooks";
|
||||
import { Columns } from "../lib/columns";
|
||||
import { OrderDetails, OrderSender } from "../lib/lifeto/order_manager";
|
||||
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();
|
||||
type Size = {
|
||||
width?: 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 = () => {
|
||||
const {activeTable, columns, tags, orders} = useSessionContext()
|
||||
const {API, loggedIn} = useLtoContext()
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [{ height }, setSize] = useState<Size>({
|
||||
width: 100,
|
||||
height: 100,
|
||||
})
|
||||
const [ref, {height}] = useResizeObserver({})
|
||||
|
||||
const onResize = useDebounceCallback(setSize, 200)
|
||||
|
||||
useResizeObserver({
|
||||
ref,
|
||||
onResize,
|
||||
})
|
||||
|
||||
const hotTableComponent = useRef<HotTableClass>(null);
|
||||
const {data:character, isLoading, isFetching } = useQuery({
|
||||
queryKey:["inventory", activeTable],
|
||||
queryFn: async ()=> {
|
||||
return API.GetInventory(activeTable)
|
||||
},
|
||||
enabled: loggedIn,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
const {data:characters} = useQuery({
|
||||
queryKey:["characters", API.s.user],
|
||||
queryFn: async ()=> {
|
||||
return API.GetAccounts().then(x=>{
|
||||
return x.flatMap(x=>{return x.characters})
|
||||
})
|
||||
},
|
||||
enabled: loggedIn,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if(!character) {
|
||||
hotTableComponent.current?.hotInstance?.updateSettings({
|
||||
data: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
const it = new InventoryTable(character, {
|
||||
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 {data:character, isLoading, isFetching } = useAtomValue(currentCharacterInventoryAtom)
|
||||
//const sendOrders = useCallback(()=>{
|
||||
// if(!hotTableComponent.current?.hotInstance){
|
||||
// return
|
||||
// }
|
||||
// const hott = hotTableComponent.current?.hotInstance
|
||||
// const headers = hott.getColHeader()
|
||||
// const dat = hott.getData()
|
||||
// const idxNumber = headers.indexOf(Columns.MoveCount.displayName)
|
||||
// const idxTarget = headers.indexOf(Columns.Move.displayName)
|
||||
// const origin = activeTable
|
||||
// const pending:OrderDetails[] = [];
|
||||
// for(const row of dat) {
|
||||
// try{
|
||||
// const nm = Number(row[idxNumber].replace("x",""))
|
||||
// const target = (row[idxTarget] as string).replaceAll("-","").trim()
|
||||
// if(!isNaN(nm) && nm > 0 && target.length > 0){
|
||||
// const info:OrderDetails = {
|
||||
// item_uid: row[0].toString(),
|
||||
// count: nm,
|
||||
// origin_path: activeTable,
|
||||
// target_path: target,
|
||||
// }
|
||||
// pending.push(info)
|
||||
// }
|
||||
// }catch(e){
|
||||
// }
|
||||
// }
|
||||
// log.debug("OrderDetails", pending)
|
||||
// const chars = new Map<string,TricksterCharacter>()
|
||||
// const manager = new OrderSender(orders, chars)
|
||||
// for(const d of pending){
|
||||
// const order = manager.send(d)
|
||||
// //order.tick(api)
|
||||
// }
|
||||
//}, [orders])
|
||||
|
||||
const Loading = ()=>{
|
||||
return <div role="status" className="flex align-center justify-center">
|
||||
@ -119,26 +240,27 @@ export const Inventory = () => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const items = useAtomValue(currentCharacterItemsAtom)
|
||||
|
||||
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
|
||||
onClick={(e)=>{
|
||||
sendOrders()
|
||||
// sendOrders()
|
||||
}}
|
||||
className="
|
||||
hover:cursor-pointer
|
||||
border border-black-1
|
||||
bg-green-200
|
||||
px-2 py-1
|
||||
">ayy lmao button</div>
|
||||
</div>
|
||||
{(isLoading || isFetching) ? <Loading/> : <></> }
|
||||
<div
|
||||
className={`${isLoading || isFetching ? "invisible" : ""}`}>
|
||||
<HotTable
|
||||
ref={hotTableComponent as any}
|
||||
licenseKey="non-commercial-and-evaluation"
|
||||
/>
|
||||
">Move Selected</div>
|
||||
</div>
|
||||
{(isLoading || isFetching) ? <Loading/> : <>
|
||||
<div>
|
||||
total: {items.size}
|
||||
</div>
|
||||
</> }
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -1,60 +1,78 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useLtoContext } from "../context/LtoContext"
|
||||
import { useState } from "react"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
|
||||
import { useAtom } from "jotai"
|
||||
import { loginStatusAtom } from "../state/atoms"
|
||||
import { LoginHelper } from "../lib/session"
|
||||
|
||||
export const LoginWidget = () => {
|
||||
const {loggedIn, login, logout} = useLtoContext()
|
||||
|
||||
const [username, setUsername] = useLocalStorage("input_username","", {syncData: false})
|
||||
const [password, setPassword] = useState("")
|
||||
return <>
|
||||
<div className="flex flex-col border border-gray-400">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row bg-blue-400">
|
||||
<span className="text-white pb-1 pl-2 m-y-1">
|
||||
account
|
||||
</span>
|
||||
|
||||
const [{data:loginState, refetch: refetchLoginState}] = useAtom(loginStatusAtom)
|
||||
|
||||
const [loginError, setLoginError] = useState("")
|
||||
|
||||
if(loginState?.logged_in){
|
||||
return <>
|
||||
<div className="flex flex-row justify-between px-2">
|
||||
<div>
|
||||
{loginState.community_name}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap gap-1 p-2 justify-center">
|
||||
<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>
|
||||
<div className="flex flex-row gap-2">
|
||||
<button
|
||||
onClick={()=>{
|
||||
logout()
|
||||
LoginHelper.logout().finally(()=>{
|
||||
refetchLoginState()
|
||||
})
|
||||
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
|
||||
</button>
|
||||
</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>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { LtoContextProvider } from "./LtoContext";
|
||||
import { SessionContextProvider } from "./SessionContext";
|
||||
|
||||
interface IContext {
|
||||
@ -9,7 +8,6 @@ function AppContext(props: IContext): any {
|
||||
const { children } = props;
|
||||
const providers = [
|
||||
SessionContextProvider,
|
||||
LtoContextProvider
|
||||
];
|
||||
const res = providers.reduceRight(
|
||||
(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 { LTOApiv0 } from "../lib/lifeto";
|
||||
|
||||
type Setter<T> = React.Dispatch<React.SetStateAction<T | undefined>>;
|
||||
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 Core from "handsontable/core";
|
||||
|
||||
export const BasicColumns = [
|
||||
"uid","Image","Name","Count",
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import { trace } from "loglevel"
|
||||
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 type BankEndpoint = typeof BankEndpoints[number]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { dummyChar, TricksterAccount, TricksterInventory, TricksterItem, TricksterWallet } from "../trickster"
|
||||
import { TricksterAccount, TricksterAccountInfo, TricksterInventory, TricksterItem} from "../trickster"
|
||||
import { BankEndpoint, LTOApi } from "./api"
|
||||
|
||||
export const pathIsBank = (path:string):boolean => {
|
||||
@ -47,7 +47,7 @@ export class LTOApiv0 implements LTOApi {
|
||||
char_path = char_path.replace(":","")
|
||||
}
|
||||
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
|
||||
log.debug("GetInventory", o)
|
||||
let name = "bank"
|
||||
@ -71,7 +71,7 @@ export class LTOApiv0 implements LTOApi {
|
||||
id,
|
||||
path: char_path,
|
||||
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)
|
||||
return [k, v]
|
||||
})),
|
||||
@ -86,7 +86,8 @@ export class LTOApiv0 implements LTOApi {
|
||||
return {
|
||||
name: x.name,
|
||||
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 {
|
||||
account_name:x.name,
|
||||
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 { getCookie, removeCookie } from "typescript-cookie";
|
||||
import { TricksterAccountInfo } from "./trickster";
|
||||
|
||||
|
||||
export const SITE_ROOT = "/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/"
|
||||
|
||||
|
||||
const raw_endpoint = (name:string):string =>{
|
||||
return SITE_ROOT+name
|
||||
}
|
||||
const login_endpoint = (name:string)=>{
|
||||
return SITE_ROOT + name + "?canonical=1"
|
||||
}
|
||||
@ -32,56 +37,53 @@ export const EndpointCreators = [
|
||||
export type EndpointCreator = typeof EndpointCreators[number]
|
||||
|
||||
export interface Session {
|
||||
user:string
|
||||
xsrf:string
|
||||
csrf:string
|
||||
request:(verb:Method,url:string,data:any,c?:EndpointCreator)=>Promise<any>
|
||||
}
|
||||
|
||||
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(){
|
||||
}
|
||||
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(()=>{})
|
||||
}
|
||||
}
|
||||
const sleep = async(ms:number)=> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
|
||||
export class TokenSession implements Session {
|
||||
csrf:string
|
||||
xsrf:string
|
||||
user:string
|
||||
constructor(name:string, csrf:string, xsrf: string){
|
||||
this.user = name
|
||||
this.csrf = csrf
|
||||
this.xsrf = xsrf;
|
||||
constructor(){
|
||||
}
|
||||
|
||||
request = async (verb:string,url:string,data:any, c:EndpointCreator = api_endpoint):Promise<AxiosResponse> => {
|
||||
@ -101,21 +103,7 @@ export class TokenSession implements Session {
|
||||
default:
|
||||
promise = axios.get(c(url),this.genHeaders())
|
||||
}
|
||||
return promise.then(x=>{
|
||||
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
|
||||
})
|
||||
return promise
|
||||
}
|
||||
genHeaders = ()=>{
|
||||
const out = {
|
||||
@ -125,9 +113,6 @@ export class TokenSession implements Session {
|
||||
},
|
||||
withCredentials:true
|
||||
}
|
||||
if(this.xsrf){
|
||||
(out.headers as any)["X-XSRF-TOKEN"] = this.xsrf.replace("%3D","=")
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,11 @@ export interface TricksterItem {
|
||||
stats?: {[key: string]:any}
|
||||
}
|
||||
|
||||
export interface TricksterAccountInfo {
|
||||
community_name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface TricksterAccount {
|
||||
name:string
|
||||
characters: TricksterCharacter[]
|
||||
@ -37,7 +42,7 @@ export interface TricksterCharacter extends Identifier {
|
||||
|
||||
export interface TricksterInventory extends Identifier{
|
||||
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'
|
||||
|
||||
|
||||
@ -11,23 +10,12 @@ export const nameCookie = (...s:string[]):string=>{
|
||||
|
||||
export class Storage {
|
||||
GetSession():Session {
|
||||
const {user, xsrf, csrf} = {
|
||||
user: getCookie(nameCookie("user"))!,
|
||||
xsrf: getCookie(nameCookie("xsrf"))!,
|
||||
csrf: getCookie(nameCookie("csrf"))!
|
||||
}
|
||||
return new TokenSession(user, xsrf, csrf)
|
||||
return new TokenSession()
|
||||
}
|
||||
|
||||
RemoveSession() {
|
||||
removeCookie(nameCookie("user"))
|
||||
removeCookie(nameCookie("xsrf"))
|
||||
removeCookie(nameCookie("csrf"))
|
||||
}
|
||||
AddSession(s:Session) {
|
||||
setCookie(nameCookie("user"),s.user)
|
||||
setCookie(nameCookie("xsrf"),s.xsrf)
|
||||
setCookie(nameCookie("csrf"),s.csrf)
|
||||
// setCookie(nameCookie("xsrf"),s.xsrf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns'
|
||||
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 { TricksterAccount, TricksterCharacter, TricksterInventory } from '../lib/trickster'
|
||||
import { nameCookie} from '../session_storage'
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user