1
0
Fork 0

item moving

This commit is contained in:
a 2022-07-07 02:54:36 -05:00
parent 90106b844d
commit 8f0f0e715b
17 changed files with 807 additions and 134 deletions

View File

@ -3,10 +3,15 @@ import CharacterInventory from "./components/CharacterInventory.vue"
import Login from "./pages/login.vue" import Login from "./pages/login.vue"
import CharacterRoulette from "./components/CharacterRoulette.vue"; import CharacterRoulette from "./components/CharacterRoulette.vue";
import Sidebar from "./components/Sidebar.vue"; import Sidebar from "./components/Sidebar.vue";
import { loadStore } from "./state/state";
import OrderDisplay from "./components/OrderDisplay.vue";
loadStore()
</script> </script>
<template> <template>
<OrderDisplay/>
<div class="parent"> <div class="parent">
<div class="splash"> </div> <div class="splash">
</div>
<div class="main"> <div class="main">
<CharacterInventory /> <CharacterInventory />
</div> </div>
@ -63,6 +68,14 @@ import Sidebar from "./components/Sidebar.vue";
</style> </style>
<style> <style>
.main {
overflow-x: scroll;
overflow-y: hidden;
}
.sidebar {
overflow-y: scroll;
}
.parent { .parent {
display: grid; display: grid;
height: 100%; height: 100%;
@ -76,13 +89,9 @@ import Sidebar from "./components/Sidebar.vue";
.splash { .splash {
grid-area: 1 / 1 / 2 / 2; grid-area: 1 / 1 / 2 / 2;
} }
.main{ .main{
grid-area: 2 / 2 / 5 / 4; grid-area: 2 / 2 / 5 / 4;
overflow-x: scroll;
overflow-y: hidden;
} }
.sidebar { .sidebar {
grid-area: 2 / 1 / 5 / 2; grid-area: 2 / 1 / 5 / 2;
} }

View File

@ -2,7 +2,9 @@
<div> <div>
<div <div
v-on:click="selectCharacter()" v-on:click="selectCharacter()"
>{{name}} the <span v-html="job" /> ({{galders.toLocaleString()}}g) ({{props.character}})</div> >
{{activeTable == props.character ? "*" :""}}
{{name}} the <span v-html="job" /> ({{galders.toLocaleString()}}g) ({{props.character}}, {{items}})</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -10,16 +12,19 @@ const session = storage.GetSession()
const api:LTOApi = getLTOState(LTOApiv0, session, useStoreRef()) const api:LTOApi = getLTOState(LTOApiv0, session, useStoreRef())
const props = defineProps(['character']) const props = defineProps(['character'])
const name = ref(props.character.split("/").pop()) const name = ref("")
const job = ref("") const job = ref("")
const items = ref(0)
const galders = ref(0) const galders = ref(0)
const {invs, activeTable, chars} = useStoreRef() const {invs, activeTable, chars} = useStoreRef()
watch(invs.value,()=>{ watch(invs.value,()=>{
const currentInv = invs.value.get(props.character) const currentInv = invs.value.get(props.character)
if(currentInv){ if(currentInv){
name.value = currentInv.name! if(currentInv.galders){
galders.value = currentInv.galders! galders.value = currentInv.galders
}
items.value = Object.values(currentInv.items).length
} }
},{deep:true}) },{deep:true})
@ -29,7 +34,6 @@ if(currentChar){
job.value = JobNumberToString(currentChar.current_job) job.value = JobNumberToString(currentChar.current_job)
} }
const selectCharacter = () => { const selectCharacter = () => {
activeTable.value = props.character activeTable.value = props.character
api.GetInventory(props.character) api.GetInventory(props.character)

View File

@ -1,4 +1,9 @@
<template> <template>
<button
type="button"
id="logoutButton"
v-on:click="send_orders()"
>ayy lmao button</button>
<HotTable <HotTable
ref="hotTableComponent" ref="hotTableComponent"
:settings="DefaultSettings()" :settings="DefaultSettings()"
@ -6,11 +11,19 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { HotTable, HotColumn } from '@handsontable/vue3'; import { HotTable, HotColumn } from '@handsontable/vue3';
const {invs, activeTable, columns, tags, dirty} = useStoreRef() const storeRefs = useStoreRef()
const {invs, activeTable, columns, tags, dirty, chars, currentSearch, orders} = storeRefs
const hotTableComponent = ref<any>(null) 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 => { const updateTable = ():TableRecipe | undefined => {
if (invs.value.has(activeTable.value)) { if (invs.value.has(activeTable.value)) {
@ -19,25 +32,76 @@ const updateTable = ():TableRecipe | undefined => {
const it = new InventoryTable(chardat, { const it = new InventoryTable(chardat, {
columns: columns.value, columns: columns.value,
tags: tags.value, tags: tags.value,
accounts: Array.from(invs.value.keys()), accounts: Array.from(chars.value.keys()),
} as InventoryTableOptions) } as InventoryTableOptions)
const hot = (hotTableComponent.value.hotInstance as Handsontable)
const build = it.BuildTable() const build = it.BuildTable()
hot.updateSettings(build.settings) hott().updateSettings(build.settings)
return build return build
} }
} }
return undefined 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) {
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)
}
}
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 // register Handsontable's modules
registerAllModules(); registerAllModules();
watch([columns.value.dirty, tags.value.dirty, activeTable, dirty], () => { watch([columns.value.dirty, tags.value.dirty, activeTable, dirty], () => {
log.debug(`${dirty.value} rendering inventory`, activeTable.value) log.debug(`${dirty.value} rendering inventory`, activeTable.value)
let u = updateTable() let u = updateTable()
if(u != undefined){
saveStore() saveStore()
}
}) })
</script> </script>
@ -47,12 +111,13 @@ import { defineComponent, computed, PropType, defineProps, defineEmits, watch} f
import { registerAllModules } from 'handsontable/registry'; import { registerAllModules } from 'handsontable/registry';
import { DefaultSettings, InventoryTable, InventoryTableOptions, TableRecipe } from '../lib/table'; import { DefaultSettings, InventoryTable, InventoryTableOptions, TableRecipe } from '../lib/table';
import { Columns, ColumnByNames, ColumnInfo } from '../lib/columns'; import { Columns, ColumnByNames, ColumnInfo } from '../lib/columns';
import { TricksterItem, SampleData } from '../lib/trickster'; import { TricksterItem} from '../lib/trickster';
import Handsontable from 'handsontable'; import Handsontable from 'handsontable';
import { useStoreRef, saveStore } from '../state/state'; import { useStoreRef, saveStore } from '../state/state';
import { storage } from '../session_storage'; import { storage } from '../session_storage';
import { LTOApi, LTOApiv0 } from '../lib/lifeto'; import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto';
import log from 'loglevel'; import log, { info } from 'loglevel';
import { OrderDetails, OrderSender } from '../lib/lifeto/order_manager';
</script> </script>
<style src="handsontable/dist/handsontable.full.css"> <style src="handsontable/dist/handsontable.full.css">

View File

@ -7,7 +7,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import CharacterCard from './CharacterCard.vue'; import CharacterCard from './CharacterCard.vue';
const { chars, accounts, invs, activeTable } = useStoreRef() const { chars, invs, activeTable } = useStoreRef()
const characters = ref([] as string[]) const characters = ref([] as string[])
watch(chars, () => { watch(chars, () => {
@ -22,6 +22,7 @@ api.GetAccounts().then(xs => {
characters.value.push(...x.characters.map(x=>x.path)) characters.value.push(...x.characters.map(x=>x.path))
}) })
characters.value = [...new Set([...characters.value])] characters.value = [...new Set([...characters.value])]
saveStore();
}) })
onMounted(()=>{ onMounted(()=>{
@ -38,7 +39,7 @@ import { defineComponent, computed, PropType, defineProps, defineEmits, ref, wat
import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto'; import { getLTOState, LTOApi, LTOApiv0 } from '../lib/lifeto';
import { LoginHelper, Session } from '../lib/session'; import { LoginHelper, Session } from '../lib/session';
import { storage } from '../session_storage'; import { storage } from '../session_storage';
import { useStore, useStoreRef } from '../state/state'; import { saveStore, useStore, useStoreRef } from '../state/state';
import log from 'loglevel'; import log from 'loglevel';
</script> </script>

View File

@ -0,0 +1,127 @@
<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>

View File

@ -1,22 +1,30 @@
<template> <template>
Filters: search:
<div class="filter_field">
<input
type="text"
id="searchbox"
v-model="currentSearch"
/>
</div>
<br> <br>
<FilterCheckboxGroup :header="'tags:'" :columns="TagColumns" /> <FilterCheckboxGroup :header="'tags:'" :columns="[...TagColumns]" />
<br> <br>
Columns: Columns:
<br> <br>
<ColumnCheckboxGroup :header="'action:'" :columns="MoveColumns" :default="true" /> <ColumnCheckboxGroup :header="'action:'" :columns="[...MoveColumns]" :default="true" />
<ColumnCheckboxGroup :header="'details:'" :columns="DetailsColumns" :default="true"/> <ColumnCheckboxGroup :header="'details:'" :columns="[...DetailsColumns]" :default="true"/>
<ColumnCheckboxGroup :header="'equipment:'" :columns="EquipmentColumns" /> <ColumnCheckboxGroup :header="'equipment:'" :columns="[...EquipmentColumns]" />
<ColumnCheckboxGroup :header="'stats:'" :columns="StatsColumns" /> <ColumnCheckboxGroup :header="'stats:'" :columns="[...StatsColumns]" />
<ColumnCheckboxGroup :header="'debug:'" :columns="[...DebugColumns]" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import ColumnCheckboxGroup from './ColumnCheckboxGroup.vue'; import ColumnCheckboxGroup from './ColumnCheckboxGroup.vue';
import FilterCheckboxGroup from './FilterCheckboxGroup.vue'; import FilterCheckboxGroup from './FilterCheckboxGroup.vue';
import { StatsColumns, MoveColumns, TagColumns, EquipmentColumns, DetailsColumns } from '../lib/columns'; import { DebugColumns, StatsColumns, MoveColumns, TagColumns, EquipmentColumns, DetailsColumns } from '../lib/columns';
const { accounts, invs } = useStoreRef() const { currentSearch} = useStoreRef()
</script> </script>

View File

@ -5,7 +5,7 @@ import { TricksterItem } from "../trickster"
import Core from "handsontable/core"; import Core from "handsontable/core";
export const BasicColumns = [ export const BasicColumns = [
"Image","Name","Count", "uid","Image","Name","Count",
] as const ] as const
export const DetailsColumns = [ export const DetailsColumns = [
@ -28,6 +28,9 @@ export const StatsColumns = [
"AP","GunAP","AC","DX","MP","MA","MD","WT","DA","LK","HP","DP","HV", "AP","GunAP","AC","DX","MP","MA","MD","WT","DA","LK","HP","DP","HV",
] as const ] as const
export const DebugColumns = [
]
export const HackColumns = [ export const HackColumns = [
] as const ] as const

View File

@ -11,11 +11,6 @@ export const ColumnByNames = (...n:ColumnName[]) => {
export const ColumnByName = (n:ColumnName) => { export const ColumnByName = (n:ColumnName) => {
return Columns[n] return Columns[n]
} }
export const test = <T extends ColumnInfo>(n:(new ()=>T)):[string,T] => {
let nn = new n()
return [nn.name, nn]
}
class Image implements ColumnInfo { class Image implements ColumnInfo {
name:ColumnName = 'Image' name:ColumnName = 'Image'
@ -76,24 +71,26 @@ class Count implements ColumnInfo {
} }
} }
const spacer = "-------------------------------"
class Move implements ColumnInfo { class Move implements ColumnInfo {
name:ColumnName = "Move" name:ColumnName = "Move"
displayName = "Target" displayName = "Target"
writable = true writable = true
options = getMoveTargets options = getMoveTargets
getter(item:TricksterItem):(string|number){ getter(item:TricksterItem):(string|number){
return "---------------------------------------------" return spacer
} }
} }
const getMoveTargets = (invs: string[]):string[] => { const getMoveTargets = (invs: string[]):string[] => {
let out:string[] = []; let out:string[] = [];
out.push(spacer)
for(const k of invs){ for(const k of invs){
out.push(k) out.push(k)
} }
out.push("") out.push("")
out.push("") out.push("")
out.push("!TRASH") out.push("TRASH")
return out return out
} }
@ -175,6 +172,20 @@ class All implements ColumnInfo {
return -10000 return -10000
} }
} }
class uid implements ColumnInfo {
name:ColumnName = "uid"
displayName = "id"
renderer = invisibleRenderer
getter(item:TricksterItem):(string|number){
return item.unique_id
}
}
function invisibleRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) {
Handsontable.dom.empty(td);
}
class Card implements ColumnInfo { class Card implements ColumnInfo {
name:ColumnName = "Card" name:ColumnName = "Card"
@ -447,6 +458,7 @@ export const Columns:{[Property in ColumnName]:ColumnInfo}= {
RefineState: new RefineState(), RefineState: new RefineState(),
All: new All(), All: new All(),
Compound: new Compound(), Compound: new Compound(),
uid: new uid(),
} }

View File

@ -1,40 +1,15 @@
import { trace } from "loglevel" import { trace } from "loglevel"
import { TricksterAccount, TricksterInventory } from "../trickster" import { TricksterAccount, TricksterInventory } from "../trickster"
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
export const BankEndpoints = ["internal-xfer-item", "bank-item"] as const
export type BankEndpoint = typeof BankEndpoints[number]
export interface LTOApi { export interface LTOApi {
GetInventory:(path:string)=>Promise<TricksterInventory> GetInventory:(path:string)=>Promise<TricksterInventory>
GetAccounts:() =>Promise<Array<TricksterAccount>> GetAccounts:() =>Promise<Array<TricksterAccount>>
GetLoggedin:() =>Promise<boolean> GetLoggedin:() =>Promise<boolean>
}
export const TxnStates = ["PENDING","INFLIGHT","WAITING","ERROR","SUCCESS"] as const BankAction:<T, D>(e:BankEndpoint, t:T) => Promise<D>
export type TxnState = typeof TxnStates[number]
export interface TxnDetails {
item_uid: string | "galders"
count:number
origin:string
target:string
}
export abstract class BankReceipt {
action_id: string
details:TxnDetails
created:Date
constructor(details:TxnDetails) {
this.details = details
this.created = new Date()
this.action_id = uuidv4();
}
abstract state():Promise<TxnState>
abstract status():string
abstract progress():[number, number]
abstract error():string
}
export interface BankApi {
SendTxn: (txn:TxnDetails) => Promise<BankReceipt>
} }

View File

@ -1,27 +1,48 @@
import { Axios, AxiosResponse } from "axios" import { Axios, AxiosResponse } from "axios"
import { zhCN } from "handsontable/i18n"
import log from "loglevel" import log from "loglevel"
import { RefStore } from "../../state/state" import { bank_endpoint, Session } from "../session"
import { Session } from "../session"
import { dummyChar, TricksterAccount, TricksterInventory, TricksterItem, TricksterWallet } from "../trickster" import { dummyChar, TricksterAccount, TricksterInventory, TricksterItem, TricksterWallet } from "../trickster"
import { LTOApi } from "./api" import { BankEndpoint, LTOApi } from "./api"
export const pathIsBank = (path:string):boolean => {
if(!path.includes("/")) {
return true
}
return false
}
export const splitPath = (path:string):[string,string]=>{
const spl = path.split("/")
switch(spl.length) {
case 1:
return [spl[0], ""]
case 2:
return [spl[0],spl[1]]
}
return ["",""]
}
export class LTOApiv0 implements LTOApi { export class LTOApiv0 implements LTOApi {
s: Session s: Session
constructor(s:Session) { constructor(s:Session) {
this.s = s this.s = s
} }
BankAction = async <T,D>(e: BankEndpoint, t: T):Promise<D> => {
return this.s.request("POST",e,t,bank_endpoint).then((x)=>{
return x.data
})
}
GetInventory = async (char_path: string):Promise<TricksterInventory> =>{ GetInventory = async (char_path: string):Promise<TricksterInventory> =>{
if(char_path.startsWith(":")) { if(char_path.startsWith(":")) {
char_path = char_path.replace(":","") char_path = char_path.replace(":","")
} }
return this.s.authed_request("GET", `item-manager/items/account/${char_path}`,undefined).then((ans:AxiosResponse)=>{ return this.s.request("GET", `item-manager/items/account/${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"
let id = 0 let id = 0
let galders = 0 let galders = 0
if(char_path.includes("/")) { if(pathIsBank("char_path")){
let [char, val] = Object.entries(o.characters)[0] as [string,any] let [char, val] = Object.entries(o.characters)[0] as [string,any]
name = val.name name = val.name
id = Number(char) id = Number(char)
@ -36,16 +57,16 @@ export class LTOApiv0 implements LTOApi {
id, id,
path: char_path, path: char_path,
galders, galders,
items:(Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):TricksterItem=>{ items: Object.fromEntries((Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):[string, TricksterItem]=>{
v.unique_id = Number(k) v.unique_id = Number(k)
return v return [k, v]
}), })),
} as TricksterInventory } as TricksterInventory
return out return out
}) })
} }
GetAccounts = async ():Promise<TricksterAccount[]> => { GetAccounts = async ():Promise<TricksterAccount[]> => {
return this.s.authed_request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{ return this.s.request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{
log.debug("GetAccounts", ans.data) log.debug("GetAccounts", ans.data)
return ans.data.map((x:any)=>{ return ans.data.map((x:any)=>{
return { return {
@ -65,7 +86,7 @@ export class LTOApiv0 implements LTOApi {
}) })
} }
GetLoggedin = async ():Promise<boolean> => { GetLoggedin = async ():Promise<boolean> => {
return this.s.authed_request("POST", "accounts/list",undefined).then((ans:AxiosResponse)=>{ return this.s.request("POST", "accounts/list",undefined).then((ans:AxiosResponse)=>{
if(ans.status == 401) { if(ans.status == 401) {
return false return false
} }

226
src/lib/lifeto/order.ts Normal file
View File

@ -0,0 +1,226 @@
import { LTOApi } from "./api"
import { v4 as uuidv4 } from 'uuid';
import { RefStore } from "../../state/state";
import { debug } from "loglevel";
export const TxnStates = ["PENDING","INFLIGHT","WORKING","ERROR","SUCCESS"] as const
export type TxnState = typeof TxnStates[number]
export interface TxnDetails {
item_uid: string | "galders"
count:number
origin:string
target:string
origin_path:string
target_path:string
}
export abstract class Order {
action_id: string
details?:TxnDetails
created:Date
state: TxnState
constructor(details?:TxnDetails) {
this.state = "PENDING"
this.details = details
this.created = new Date()
this.action_id = uuidv4();
}
mark(t:TxnState) {
this.state = t
}
abstract tick(r:RefStore, api:LTOApi):Promise<any>
abstract status():string
abstract progress():[number, number]
abstract error():string
abstract order_type:OrderType
parse(i:any):Order {
this.action_id = i.action_id
this.details = i.details
this.created = new Date(i.created)
this.state = i.state
return this
}
}
export abstract class BasicOrder extends Order {
stage: number
err?: string
constructor(details:TxnDetails) {
super(details)
this.stage = 0
}
progress():[number,number]{
return [this.stage, 1]
}
status():string {
return this.state
}
error():string {
return this.err ? this.err : ""
}
parse(i:any):BasicOrder {
this.stage = i.stage
this.err = i.err
super.parse(i)
return this
}
}
/// start user defined
export const OrderTypes = ["InvalidOrder","BankItem","InternalXfer"]
export type OrderType = typeof OrderTypes[number]
export class InvalidOrder extends Order{
order_type = "InvalidOrder"
msg:string
constructor(msg: string){
super(undefined)
this.msg = msg
this.mark("ERROR")
}
status():string {
return "ERROR"
}
progress():[number, number] {
return [0,0]
}
error(): string {
return this.msg
}
async tick(r:RefStore, api:LTOApi):Promise<void> {
return
}
parse(i:any):InvalidOrder {
super.parse(i)
this.msg = i.msg
return this
}
}
export interface BasicResponse {
status: number
data: any
msg?: string
}
export interface InternalXferRequest {
item_uid:string
qty:string
account:string
new_char:string
}
export interface InternalXferResponse extends BasicResponse {}
export class InternalXfer extends BasicOrder{
order_type = "InternalXfer"
originalRequest:InternalXferRequest
originalResponse?:InternalXferResponse
constructor(details:TxnDetails) {
super(details)
this.originalRequest = {
item_uid: details.item_uid,
qty: details.count.toString(),
new_char: details.target,
account: details.origin,
}
}
async tick(r:RefStore, api:LTOApi):Promise<void> {
if(this.state !== "PENDING") {
return
}
this.mark("WORKING")
return api.BankAction<InternalXferRequest, InternalXferResponse>("internal-xfer-item",this.originalRequest)
.then((x:InternalXferResponse)=>{
if(x.status == 200){
this.originalResponse = x
this.stage = 1
this.mark("SUCCESS")
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
origin_item.item_count = origin_item.item_count - this.details?.count!
r.dirty.value++
}else{
throw x.msg
}
})
.catch((e)=>{
debug("InternalXfer",e)
this.stage = 1
this.err = e
this.mark("ERROR")
})
}
parse(i:any):InternalXfer {
super.parse(i)
this.originalRequest = i.originalRequest
this.originalResponse = i.originalResponse
return this
}
}
export interface BankItemRequest {
item_uid:string
qty:string
account:string
}
export interface BankItemResponse extends BasicResponse {}
export class BankItem extends BasicOrder{
order_type = "BankItem";
originalRequest:BankItemRequest
originalResponse?:BankItemResponse
constructor(details:TxnDetails) {
super(details)
this.originalRequest = {
item_uid: details.item_uid,
qty: details.count.toString(),
account: details.target,
}
}
async tick(r:RefStore, api:LTOApi):Promise<void> {
if(this.state !== "PENDING" ){
return
}
this.mark("WORKING")
return api.BankAction<BankItemRequest, BankItemResponse>("bank-item",this.originalRequest)
.then((x)=>{
debug("BankItem",x)
if(x.status == 200){
this.stage = 1
this.originalResponse = x
this.mark("SUCCESS")
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
origin_item.item_count = origin_item.item_count - this.details?.count!
r.dirty.value++
}else {
throw x.msg ? "unknown error" : ""
}
})
.catch((e)=>{
this.stage = 1
this.err = e
this.mark("ERROR")
})
}
parse(i:any):BankItem {
super.parse(i)
this.originalRequest = i.originalRequest
this.originalResponse = i.originalResponse
return this
}
}

View File

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

View File

@ -1,7 +1,7 @@
import { RefStore } from "../../state/state"; import { RefStore } from "../../state/state";
import { Session } from "../session"; import { bank_endpoint, Session } from "../session";
import { TricksterAccount, TricksterInventory } from "../trickster"; import { TricksterAccount, TricksterInventory } from "../trickster";
import { LTOApi } from "./api"; import { BankEndpoint, LTOApi } from "./api";
export interface SessionBinding { export interface SessionBinding {
new(s:Session):LTOApi new(s:Session):LTOApi
@ -17,9 +17,19 @@ export class StatefulLTOApi implements LTOApi {
this.u = s this.u = s
this.r=r this.r=r
} }
BankAction = <T,D>(e: BankEndpoint, t: T):Promise<D> => {
return this.u.BankAction(e,t)
}
GetInventory = async (path:string):Promise<TricksterInventory>=>{ GetInventory = async (path:string):Promise<TricksterInventory>=>{
const inv = await this.u.GetInventory(path) const inv = await this.u.GetInventory(path)
if(this.r.invs.value.get(inv.path)){
this.r.invs.value.get(inv.path)!.items = inv.items
}else{
this.r.invs.value.set(inv.path,inv) this.r.invs.value.set(inv.path,inv)
}
if(inv.galders) {
this.r.invs.value.get(inv.path)!.galders = inv.galders
}
this.r.dirty.value = this.r.dirty.value + 1 this.r.dirty.value = this.r.dirty.value + 1
return inv return inv
} }

View File

@ -2,24 +2,37 @@ import axios, { AxiosResponse, Method } from "axios";
import qs from "qs"; import qs from "qs";
import { getCookie, removeCookie } from "typescript-cookie"; import { getCookie, removeCookie } from "typescript-cookie";
export const SITE_ROOT = "/lifeto/"
export const API_ROOT = "api/lifeto/"
export const BANK_ROOT = "item-manager-action/"
const login_endpoint = (name:string)=>{
return SITE_ROOT + name
}
export const api_endpoint = (name:string):string =>{
return SITE_ROOT+API_ROOT + name
}
export const bank_endpoint = (name:string):string =>{
return SITE_ROOT+BANK_ROOT + name
}
export const EndpointCreators = [
api_endpoint,
bank_endpoint,
]
type EndpointCreator = typeof EndpointCreators[number]
export interface Session { export interface Session {
user:string user:string
xsrf:string xsrf:string
csrf:string csrf:string
authed_request:(verb:Method,url:string,data:any)=>Promise<any> request:(verb:Method,url:string,data:any,c?:EndpointCreator)=>Promise<any>
} }
export const SITE_ROOT = "/lifeto/"
export const API_ROOT = "api/lifeto/"
const login_endpoint = (name:string)=>{
return SITE_ROOT + name
}
const api_endpoint = (name:string)=>{
return SITE_ROOT+API_ROOT + name
}
export class LoginHelper { export class LoginHelper {
user:string user:string
@ -32,13 +45,6 @@ export class LoginHelper {
login = async ():Promise<TokenSession> =>{ login = async ():Promise<TokenSession> =>{
return axios.get(login_endpoint("login"),{withCredentials:false}) return axios.get(login_endpoint("login"),{withCredentials:false})
.then(async (x)=>{ .then(async (x)=>{
console.log(x)
if(x.data){
try{
this.csrf = x.data.split("csrf-token")[1].split('\">')[0].replace("\" content=\"",'')
}catch(e){
}
}
return axios.post(login_endpoint("login"),{ return axios.post(login_endpoint("login"),{
login:this.user, login:this.user,
password:this.pass, password:this.pass,
@ -74,19 +80,19 @@ export class TokenSession implements Session {
this.xsrf = xsrf; this.xsrf = xsrf;
} }
authed_request = async (verb:string,url:string,data:any):Promise<AxiosResponse> => { request = async (verb:string,url:string,data:any, c:EndpointCreator = api_endpoint):Promise<AxiosResponse> => {
let promise let promise
switch (verb.toLowerCase()){ switch (verb.toLowerCase()){
case "post": case "post":
promise = axios.post(api_endpoint(url),data,this.genHeaders()) promise = axios.post(c(url),data,this.genHeaders())
break; break;
case "postraw": case "postraw":
const querystring = qs.stringify(data) const querystring = qs.stringify(data)
promise = axios.post(api_endpoint(url),querystring,this.genHeaders()) promise = axios.post(c(url),querystring,this.genHeaders())
break; break;
case "get": case "get":
default: default:
promise = axios.get(api_endpoint(url),this.genHeaders()) promise = axios.get(c(url),this.genHeaders())
} }
return promise.then(x=>{ return promise.then(x=>{
if(x.data){ if(x.data){

View File

@ -20,9 +20,6 @@ export const ARRAY_SEPERATOR = ","
let as = ARRAY_SEPERATOR let as = ARRAY_SEPERATOR
export interface Reviver<T> { export interface Reviver<T> {
Murder(t:T):string Murder(t:T):string
Revive(s:string):T Revive(s:string):T
@ -48,6 +45,25 @@ export const StoreColSet = {
Revive: (s:string):ColumnSet=>new ColumnSet(s.split(as) as any) Revive: (s:string):ColumnSet=>new ColumnSet(s.split(as) as any)
} }
export const StoreChars = { export const StoreChars = {
Murder: (s:Map<string,TricksterCharacter>):string=>JSON.stringify(Object.entries(s)), Murder: (s:Map<string,TricksterCharacter>):string=>{
let o = JSON.stringify(Array.from(s.entries()))
return o
},
Revive: (s:string):Map<string,TricksterCharacter>=>new Map(JSON.parse(s)), Revive: (s:string):Map<string,TricksterCharacter>=>new Map(JSON.parse(s)),
} }
export const StoreJsonable = {
Murder: <T>(s:T):string=>JSON.stringify(Object.entries(s)),
Revive: <T>(s:string):T=>JSON.parse(s),
}
export interface Serializable<T> {
parse(s:any):T
}
export const StoreSerializable = <T extends Serializable<T>>(n:(new ()=>T))=>{
return {
Murder: (s:T):string=>JSON.stringify(s),
Revive: (s:string):T=>new n().parse(JSON.parse(s))
}
}

View File

@ -4,6 +4,7 @@ import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./colum
import { ColumnSettings } from "handsontable/settings" import { ColumnSettings } from "handsontable/settings"
import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu" import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu"
import { ref } from "vue" import { ref } from "vue"
import Handsontable from "handsontable"
@ -100,13 +101,18 @@ export class InventoryTable {
return this.o.columns.map(x=>{ return this.o.columns.map(x=>{
let out:any = { let out:any = {
renderer: x.renderer ? x.renderer : "text", renderer: x.renderer ? x.renderer : "text",
filters: true,
dropdownMenu: x.filtering ? DefaultDropdownItems() : false, dropdownMenu: x.filtering ? DefaultDropdownItems() : false,
readOnly: x.writable ? false : true, readOnly: x.writable ? false : true,
selectionMode: (x.writable ? "multiple" : 'single') as any, selectionMode: (x.writable ? "multiple" : 'single') as any,
} }
if(x.options) { if(x.options) {
out.type = 'dropdown' out.type = 'dropdown'
out.source = Array.from(this.o.accounts.values()) if(typeof x.options == "function") {
out.source = x.options(this.o.accounts)
}else {
out.source = x.options
}
} }
return out return out
}) })
@ -114,6 +120,9 @@ export class InventoryTable {
getTableRows():any[][] { getTableRows():any[][] {
return Object.values(this.inv.items) return Object.values(this.inv.items)
.filter((item):boolean=>{ .filter((item):boolean=>{
if(item.item_count <= 0) {
return false
}
let found = true let found = true
let hasAll = this.o.tags.has("All") let hasAll = this.o.tags.has("All")
if(this.o.tags.s.size > 0) { if(this.o.tags.s.size > 0) {
@ -172,9 +181,30 @@ export const DefaultSettings = ():HotTableProps=>{
indicator: true, indicator: true,
headerAction: true, headerAction: true,
}, },
hiddenColumns: {
columns: [0],
},
// renderAllRows: true,
viewportColumnRenderingOffset: 3,
viewportRowRenderingOffset: 100,
// dropdownMenu: DefaultDropdownItems(),
afterGetColHeader: (col, th) => {
if(!th.innerHTML.toLowerCase().includes("name")) {
const ct = th.querySelector('.changeType')
if(ct){
ct.parentElement!.removeChild(ct)
}
}
},
beforeOnCellMouseDown(event:any, coords) {
// Deselect the column after clicking on input.
if (coords.row === -1 && event.target.nodeName === 'INPUT') {
event.stopImmediatePropagation();
this.deselectCell();
}
},
className: 'htLeft', className: 'htLeft',
contextMenu: false, contextMenu: false,
dropdownMenu: false,
readOnlyCellClassName: "", readOnlyCellClassName: "",
licenseKey:"non-commercial-and-evaluation", licenseKey:"non-commercial-and-evaluation",
} }

View File

@ -2,7 +2,8 @@ import { defineStore, storeToRefs } from 'pinia'
import { getCookie, setCookie } from 'typescript-cookie' import { getCookie, setCookie } from 'typescript-cookie'
import { useCookies } from 'vue3-cookies' import { useCookies } from 'vue3-cookies'
import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns' import { BasicColumns, ColumnInfo, ColumnName, Columns, DetailsColumns, MoveColumns } from '../lib/columns'
import { Reviver, StoreChars, StoreColSet, StoreInvs, StoreStr, StoreStrSet } from '../lib/storage' import { OrderTracker } from '../lib/lifeto/order_manager'
import { Reviver, StoreChars, StoreColSet, StoreInvs, StoreSerializable, StoreStr, StoreStrSet } from '../lib/storage'
import { ColumnSet } from '../lib/table' import { ColumnSet } from '../lib/table'
import { TricksterCharacter, TricksterInventory } from '../lib/trickster' import { TricksterCharacter, TricksterInventory } from '../lib/trickster'
import { nameCookie} from '../session_storage' import { nameCookie} from '../session_storage'
@ -12,42 +13,47 @@ const _defaultColumn:(ColumnInfo| ColumnName)[] = [
...MoveColumns, ...MoveColumns,
...DetailsColumns, ...DetailsColumns,
] ]
export const useStore = defineStore('state', {
state: ()=> {
let store = {
invs: new Map() as Map<string,TricksterInventory>,
chars: new Map() as Map<string,TricksterCharacter>,
accounts: new Set() as Set<string>,
activeTable: "none",
screen: "default",
columns:new ColumnSet(_defaultColumn),
tags: new ColumnSet(),
dirty: 0,
}
loadStore();
return store
}
})
// if you wish for the thing to persist
export const StoreReviver = { export const StoreReviver = {
chars: StoreChars, chars: StoreChars,
accounts: StoreStrSet,
activeTable: StoreStr, activeTable: StoreStr,
screen: StoreStr, screen: StoreStr,
columns: StoreColSet, columns: StoreColSet,
tags: StoreColSet, tags: StoreColSet,
// orders: StoreSerializable(OrderTracker)
} }
export interface StoreProps { export interface StoreProps {
invs: Map<string,TricksterInventory> invs: Map<string,TricksterInventory>
chars: Map<string, TricksterCharacter> chars: Map<string, TricksterCharacter>
accounts: Set<string> orders: OrderTracker
activeTable: string activeTable: string
screen: string screen: string
columns: ColumnSet columns: ColumnSet
tags: ColumnSet tags: ColumnSet
dirty: number dirty: number
currentSearch: string
} }
export const useStore = defineStore('state', {
state: ()=> {
let store = {
invs: new Map() as Map<string,TricksterInventory>,
chars: new Map() as Map<string,TricksterCharacter>,
orders: new OrderTracker(),
activeTable: "none",
screen: "default",
columns:new ColumnSet(_defaultColumn),
tags: new ColumnSet(),
dirty: 0,
currentSearch: "",
}
return store
}
})
export const loadStore = ()=> { export const loadStore = ()=> {
let store = useStoreRef() let store = useStoreRef()
for(const [k, v] of Object.entries(StoreReviver)){ for(const [k, v] of Object.entries(StoreReviver)){
@ -63,7 +69,6 @@ export const saveStore = ()=> {
let store = useStoreRef() let store = useStoreRef()
for(const [k, v] of Object.entries(StoreReviver)){ for(const [k, v] of Object.entries(StoreReviver)){
let coke; let coke;
if((store[k as keyof RefStore]) != undefined){ if((store[k as keyof RefStore]) != undefined){
coke = v.Murder(store[k as keyof RefStore].value as any) coke = v.Murder(store[k as keyof RefStore].value as any)
} }