Compare commits
No commits in common. "master" and "master" have entirely different histories.
@ -1,8 +0,0 @@
|
|||||||
dist
|
|
||||||
dist/**
|
|
||||||
**/vendor/**
|
|
||||||
**/locales/**
|
|
||||||
generated.*
|
|
||||||
node_modules
|
|
||||||
*.min.js
|
|
||||||
*.min.css
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
# flyctl launch added from .gitignore
|
|
||||||
# Logs
|
|
||||||
**/logs
|
|
||||||
**/*.log
|
|
||||||
**/npm-debug.log*
|
|
||||||
**/yarn-debug.log*
|
|
||||||
**/yarn-error.log*
|
|
||||||
**/pnpm-debug.log*
|
|
||||||
**/lerna-debug.log*
|
|
||||||
|
|
||||||
**/node_modules
|
|
||||||
**/dist
|
|
||||||
**/dist-ssr
|
|
||||||
**/*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
**/.vscode/*
|
|
||||||
!**/.vscode/extensions.json
|
|
||||||
**/.idea
|
|
||||||
**/.DS_Store
|
|
||||||
**/*.suo
|
|
||||||
**/*.ntvs*
|
|
||||||
**/*.njsproj
|
|
||||||
**/*.sln
|
|
||||||
**/*.sw?
|
|
||||||
fly.toml
|
|
||||||
74
CLAUDE.md
74
CLAUDE.md
@ -1,74 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Essential Commands
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
yarn dev # Start Vite development server on port 5173
|
|
||||||
yarn preview # Preview production build locally
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build & Deploy
|
|
||||||
```bash
|
|
||||||
yarn build # Create production build with Vite
|
|
||||||
make build # Build Docker image (tuxpa.in/a/lto:v0.0.2)
|
|
||||||
make push # Push Docker image to registry
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
```bash
|
|
||||||
yarn lint # Check code with Biome
|
|
||||||
yarn lint:fix # Auto-fix linting issues
|
|
||||||
yarn format # Format code with Biome
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
This is a React-based inventory management system for the game "Trickster Online" via the lifeto.co platform.
|
|
||||||
|
|
||||||
### Key Technologies
|
|
||||||
- **React 19** with TypeScript
|
|
||||||
- **Vite** for bundling and dev server
|
|
||||||
- **Jotai** for atomic state management
|
|
||||||
- **TanStack Query** for server state and caching
|
|
||||||
- **Tailwind CSS** for styling
|
|
||||||
- **Axios** for HTTP requests
|
|
||||||
|
|
||||||
### Core Architecture
|
|
||||||
|
|
||||||
1. **State Management Pattern**:
|
|
||||||
- Jotai atoms in `src/state/atoms.ts` handle all application state
|
|
||||||
- Uses `atomWithQuery` for server data integration
|
|
||||||
- Persistent storage via `atomWithStorage` with superjson serialization
|
|
||||||
- Actions are implemented as write-only atoms (e.g., `doLoginAtom`, `orderManagerAtom`)
|
|
||||||
|
|
||||||
2. **API Integration**:
|
|
||||||
- All API calls go through `LTOApi` interface (`src/lib/lifeto/api.ts`)
|
|
||||||
- Token-based authentication via `TokenSession`
|
|
||||||
- Development: Vite proxy to `https://beta.lifeto.co`
|
|
||||||
- Production: Caddy reverse proxy configuration
|
|
||||||
|
|
||||||
3. **Component Structure**:
|
|
||||||
- Entry: `src/index.tsx` → `App.tsx`
|
|
||||||
- Main sections: Login, Character Selection, Inventory Management
|
|
||||||
- Components follow atomic design with clear separation of concerns
|
|
||||||
|
|
||||||
4. **Business Logic**:
|
|
||||||
- Domain models in `src/lib/trickster.ts` (Character, Item, Inventory)
|
|
||||||
- Order management via `OrderManager` class for item transfers
|
|
||||||
- Item filtering uses Fuse.js for fuzzy search
|
|
||||||
|
|
||||||
5. **Data Flow**:
|
|
||||||
```
|
|
||||||
User Action → Component → Jotai Action Atom → API Call →
|
|
||||||
Server Response → Query Cache → Atom Update → UI Re-render
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Notes
|
|
||||||
|
|
||||||
- The app uses a proxy setup to avoid CORS issues with the lifeto.co API
|
|
||||||
- All API responses are strongly typed with TypeScript interfaces
|
|
||||||
- State persistence allows users to maintain their session and preferences
|
|
||||||
- The inventory system supports multi-character management with bulk operations
|
|
||||||
31
Caddyfile
31
Caddyfile
@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
admin off
|
|
||||||
log {
|
|
||||||
include http.log.access http.handlers.reverse_proxy
|
|
||||||
output stdout
|
|
||||||
format console
|
|
||||||
level debug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:{$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 Host {upstream_hostport}
|
|
||||||
header_up X-Forwarded-For {remote_host}
|
|
||||||
header_up User-Agent "LifetoShop/1.0"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
Dockerfile
24
Dockerfile
@ -1,13 +1,19 @@
|
|||||||
FROM node:24-alpine as NODEBUILDER
|
FROM golang:1.18.2-alpine as GOBUILDER
|
||||||
WORKDIR /wd
|
WORKDIR /wd
|
||||||
# Copy only package files first for better caching
|
COPY go.mod go.sum ./
|
||||||
COPY package.json yarn.lock ./
|
COPY app ./app
|
||||||
RUN corepack yarn install
|
RUN go mod tidy
|
||||||
# Copy source code after dependencies are installed
|
RUN go build -o app.exe ./app
|
||||||
COPY . .
|
|
||||||
RUN corepack yarn build
|
|
||||||
|
|
||||||
FROM caddy:2.10-alpine
|
FROM node:18.1-alpine as NODEBUILDER
|
||||||
WORKDIR /wd
|
WORKDIR /wd
|
||||||
COPY Caddyfile /etc/caddy/Caddyfile
|
COPY . .
|
||||||
|
RUN npm install
|
||||||
|
RUN npx vite build
|
||||||
|
|
||||||
|
FROM alpine:3.16
|
||||||
|
WORKDIR /wd
|
||||||
|
COPY --from=GOBUILDER /wd/app.exe app.exe
|
||||||
COPY --from=NODEBUILDER /wd/dist dist
|
COPY --from=NODEBUILDER /wd/dist dist
|
||||||
|
ENTRYPOINT [ "/wd/app.exe" ]
|
||||||
|
|
||||||
|
|||||||
7
Makefile
7
Makefile
@ -1,9 +1,6 @@
|
|||||||
VERSION=v0.0.2
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker build -t tuxpa.in/a/lto:${VERSION} .
|
docker build -t cr.aaaaa.news/lto:latest .
|
||||||
|
|
||||||
push:
|
push:
|
||||||
docker push tuxpa.in/a/lto:${VERSION}
|
docker push cr.aaaaa.news/lto:latest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
113
app/main.go
Normal file
113
app/main.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gfx.cafe/open/gun"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Config struct {
|
||||||
|
Port string
|
||||||
|
Root string
|
||||||
|
|
||||||
|
Remote string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gun.Load(&Config)
|
||||||
|
if Config.Port == "" {
|
||||||
|
Config.Port = "8080"
|
||||||
|
}
|
||||||
|
if Config.Root == "" {
|
||||||
|
Config.Root = "dist"
|
||||||
|
}
|
||||||
|
if Config.Remote == "" {
|
||||||
|
Config.Remote = "https://beta.lifeto.co"
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.Root = path.Clean(Config.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hopHeaders = []string{
|
||||||
|
"Connection",
|
||||||
|
"Keep-Alive",
|
||||||
|
"Proxy-Authenticate",
|
||||||
|
"Proxy-Authorization",
|
||||||
|
"Te",
|
||||||
|
"Trailers",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"Upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendHostToXForwardHeader(header http.Header, host string) {
|
||||||
|
if prior, ok := header["X-Forwarded-For"]; ok {
|
||||||
|
host = strings.Join(prior, ", ") + ", " + host
|
||||||
|
}
|
||||||
|
header.Set("X-Forwarded-For", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyHeader(dst, src http.Header) {
|
||||||
|
for k, vv := range src {
|
||||||
|
for _, v := range vv {
|
||||||
|
dst.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delHopHeaders(header http.Header) {
|
||||||
|
for _, h := range hopHeaders {
|
||||||
|
header.Del(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
p ProxyHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/lifeto") {
|
||||||
|
http.StripPrefix("/lifeto", &h.p).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.handleSite(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyHandler struct {
|
||||||
|
c http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.RequestURI = ""
|
||||||
|
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||||
|
appendHostToXForwardHeader(r.Header, clientIP)
|
||||||
|
}
|
||||||
|
r.URL, _ = url.Parse(Config.Remote + r.URL.Path)
|
||||||
|
r.Host = r.URL.Host
|
||||||
|
|
||||||
|
resp, err := h.c.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Server Error", http.StatusInternalServerError)
|
||||||
|
log.Println("ServeHTTP:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
delHopHeaders(resp.Header)
|
||||||
|
copyHeader(w.Header(), resp.Header)
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleSite(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.FileServer(http.Dir(Config.Root)).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Printf("starting with config: %+v", Config)
|
||||||
|
http.ListenAndServe(":"+Config.Port, &Handler{})
|
||||||
|
}
|
||||||
60
biome.json
60
biome.json
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": true,
|
|
||||||
"includes": ["src/**/*.{ts,tsx,js,jsx}"]
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"style": {
|
|
||||||
"noUselessElse": "error",
|
|
||||||
"useConst": "warn",
|
|
||||||
"useImportType": "off",
|
|
||||||
"useNodejsImportProtocol": "off"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noConsole": "error",
|
|
||||||
"noRedeclare": "off",
|
|
||||||
"noDoubleEquals": "warn",
|
|
||||||
"noExplicitAny": "off"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"noUndeclaredVariables": "off",
|
|
||||||
"useExhaustiveDependencies": "off",
|
|
||||||
"noUnusedImports": "warn"
|
|
||||||
},
|
|
||||||
"complexity": {
|
|
||||||
"noExtraBooleanCast": "warn",
|
|
||||||
"noBannedTypes": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"formatWithErrors": false,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"parser": {
|
|
||||||
"unsafeParameterDecoratorsEnabled": true
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"jsxQuoteStyle": "double",
|
|
||||||
"semicolons": "asNeeded",
|
|
||||||
"trailingCommas": "all",
|
|
||||||
"arrowParentheses": "asNeeded"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"json": {
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
fly.toml
22
fly.toml
@ -1,22 +0,0 @@
|
|||||||
# fly.toml app configuration file generated for lifeto on 2025-06-23T01:16:55-05:00
|
|
||||||
#
|
|
||||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
|
||||||
#
|
|
||||||
|
|
||||||
app = 'lifeto'
|
|
||||||
primary_region = 'ord'
|
|
||||||
|
|
||||||
[build]
|
|
||||||
|
|
||||||
[http_service]
|
|
||||||
internal_port = 8080
|
|
||||||
force_https = true
|
|
||||||
auto_stop_machines = 'stop'
|
|
||||||
auto_start_machines = true
|
|
||||||
min_machines_running = 0
|
|
||||||
processes = ['app']
|
|
||||||
|
|
||||||
[[vm]]
|
|
||||||
memory = '1gb'
|
|
||||||
cpu_kind = 'shared'
|
|
||||||
cpus = 1
|
|
||||||
@ -4,10 +4,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>lto inventory</title>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="overflow-y: hidden;" class="w-screen h-screen">
|
<body style="overflow-y: hidden;">
|
||||||
<div id="app" class="w-full h-full"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
67
package.json
67
package.json
@ -2,58 +2,29 @@
|
|||||||
"name": "lifeto-shop",
|
"name": "lifeto-shop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"lint": "biome check .",
|
|
||||||
"lint:fix": "biome check --write .",
|
|
||||||
"format": "biome format --write ."
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.27.8",
|
"@handsontable/vue3": "^12.0.1",
|
||||||
"@handsontable/react": "^15.3.0",
|
"@types/qs": "^6.9.7",
|
||||||
"@mantine/hooks": "^8.0.0",
|
"@types/uuid": "^8.3.4",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@vueuse/core": "^8.7.5",
|
||||||
"@tanstack/react-query": "^5.76.0",
|
"axios": "^0.27.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"handsontable": "^12.0.1",
|
||||||
"@types/qs": "^6.9.18",
|
"loglevel": "^1.8.0",
|
||||||
"@types/react": "^19.1.4",
|
"pinia": "^2.0.14",
|
||||||
"@types/react-dom": "^19.1.5",
|
"qs": "^6.10.5",
|
||||||
"@types/uuid": "^10.0.0",
|
"typescript-cookie": "^1.0.4",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"uuid": "^8.3.2",
|
||||||
"arktype": "^2.1.20",
|
"vue": "^3.2.25"
|
||||||
"axios": "^1.9.0",
|
|
||||||
"fuse.js": "^7.1.0",
|
|
||||||
"handsontable": "^15.3.0",
|
|
||||||
"jotai": "^2.12.4",
|
|
||||||
"jotai-optics": "^0.4.0",
|
|
||||||
"jotai-tanstack-query": "^0.9.0",
|
|
||||||
"loglevel": "^1.9.2",
|
|
||||||
"optics-ts": "^2.4.1",
|
|
||||||
"pinia": "^3.0.2",
|
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"qs": "^6.14.0",
|
|
||||||
"react": "^19.1.0",
|
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-select": "^5.10.1",
|
|
||||||
"react-spinners": "^0.17.0",
|
|
||||||
"superjson": "^2.2.2",
|
|
||||||
"typescript-cookie": "^1.0.6",
|
|
||||||
"use-local-storage": "^3.0.0",
|
|
||||||
"usehooks-ts": "^3.1.1",
|
|
||||||
"uuid": "^11.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.0.0",
|
"@vitejs/plugin-vue": "^2.3.3",
|
||||||
"@tailwindcss/postcss": "^4.1.6",
|
"typescript": "^4.5.4",
|
||||||
"@types/node": "^22.15.18",
|
"vite": "^2.9.9",
|
||||||
"postcss": "^8.5.3",
|
"vue-tsc": "^0.34.7"
|
||||||
"tailwindcss": "^4.1.6",
|
}
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vite": "^6.3.5"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 593 B |
44
src/App.tsx
44
src/App.tsx
@ -1,44 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { CharacterRoulette } from './components/characters'
|
|
||||||
import { Inventory } from './components/inventory/index'
|
|
||||||
import { LoginWidget } from './components/login'
|
|
||||||
|
|
||||||
export const App: FC = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row mx-auto p-4 gap-8 w-full h-full">
|
|
||||||
<div className="flex flex-col max-w-64">
|
|
||||||
<LoginWidget />
|
|
||||||
<CharacterRoulette />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<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">
|
|
||||||
</div>
|
|
||||||
<div className="col-span-5 h-full">
|
|
||||||
<div className="overflow-hidden h-5/6">
|
|
||||||
<Inventory/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
*/
|
|
||||||
106
src/App.vue
Normal file
106
src/App.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CharacterInventory from "./components/CharacterInventory.vue"
|
||||||
|
import Login from "./pages/login.vue"
|
||||||
|
import CharacterRoulette from "./components/CharacterRoulette.vue";
|
||||||
|
import Sidebar from "./components/Sidebar.vue";
|
||||||
|
import { loadStore } from "./state/state";
|
||||||
|
import OrderDisplay from "./components/OrderDisplay.vue";
|
||||||
|
loadStore()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="parent">
|
||||||
|
<div class="splash">
|
||||||
|
<Login />
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<CharacterInventory />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<Sidebar/>
|
||||||
|
</div>
|
||||||
|
<div class="select">
|
||||||
|
<CharacterRoulette />
|
||||||
|
</div>
|
||||||
|
<div class="login">
|
||||||
|
<OrderDisplay/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.handsontable th {
|
||||||
|
border-right: 0px !important;
|
||||||
|
border-left: 0px !important;
|
||||||
|
border-top: 1px white !important;
|
||||||
|
border-bottom: 1px white !important;
|
||||||
|
line-height: 0 !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
text-align: left !important;
|
||||||
|
min-width: 10px !important;
|
||||||
|
width: 20px !important;
|
||||||
|
}
|
||||||
|
.handsontable td {
|
||||||
|
border-right: 0px !important;
|
||||||
|
border-left: 0px !important;
|
||||||
|
border-top: 1px white !important;
|
||||||
|
border-bottom: 1px white !important;
|
||||||
|
background-color: #F7F7F7 !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
.handsontable tr {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
.handsontable .changeType {
|
||||||
|
margin: 0px !important;
|
||||||
|
border:0px !important;
|
||||||
|
float: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.main {
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
grid-template-columns: 1fr 4fr 3fr;
|
||||||
|
grid-template-rows: 1fr repeat(2, 3fr) 1fr;
|
||||||
|
grid-column-gap: 0px;
|
||||||
|
grid-row-gap: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash {
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
}
|
||||||
|
.main{
|
||||||
|
grid-area: 2 / 2 / 5 / 4;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
grid-area: 2 / 1 / 5 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
grid-area: 1 / 2 / 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
grid-area: 1 / 3 / 2 / 4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
src/ColumnCheckbox.vue
Normal file
35
src/ColumnCheckbox.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<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,()=>{
|
||||||
|
console.log("changed")
|
||||||
|
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>
|
||||||
100
src/components/CharacterCard.vue
Normal file
100
src/components/CharacterCard.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<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
|
||||||
|
}
|
||||||
|
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>
|
||||||
127
src/components/CharacterInventory.vue
Normal file
127
src/components/CharacterInventory.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<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>
|
||||||
60
src/components/CharacterRoulette.vue
Normal file
60
src/components/CharacterRoulette.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<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>
|
||||||
34
src/components/ColumnCheckbox.vue
Normal file
34
src/components/ColumnCheckbox.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<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>
|
||||||
122
src/components/ColumnCheckboxGroup.vue
Normal file
122
src/components/ColumnCheckboxGroup.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<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>
|
||||||
35
src/components/FilterCheckbox.vue
Normal file
35
src/components/FilterCheckbox.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<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>
|
||||||
123
src/components/FilterCheckboxGroup.vue
Normal file
123
src/components/FilterCheckboxGroup.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<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>
|
||||||
127
src/components/OrderDisplay.vue
Normal file
127
src/components/OrderDisplay.vue
Normal 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>
|
||||||
34
src/components/Sidebar.vue
Normal file
34
src/components/Sidebar.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<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,260 +0,0 @@
|
|||||||
import {
|
|
||||||
autoUpdate,
|
|
||||||
FloatingPortal,
|
|
||||||
flip,
|
|
||||||
offset,
|
|
||||||
shift,
|
|
||||||
useDismiss,
|
|
||||||
useFloating,
|
|
||||||
useFocus,
|
|
||||||
useHover,
|
|
||||||
useInteractions,
|
|
||||||
useRole,
|
|
||||||
} from '@floating-ui/react'
|
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
|
||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { TricksterCharacter } from '../lib/trickster'
|
|
||||||
import { charactersAtom, selectedCharacterAtom } from '../state/atoms'
|
|
||||||
|
|
||||||
export const CharacterCard = ({ character, noTopBorder = false }: { character: TricksterCharacter; noTopBorder?: boolean }) => {
|
|
||||||
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 isBank = character.base_job === -8
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCharacter(character)
|
|
||||||
}}
|
|
||||||
ref={refs.setReference}
|
|
||||||
{...getReferenceProps()}
|
|
||||||
className={`
|
|
||||||
flex flex-col ${noTopBorder ? 'border-l border-r border-b' : 'border'} border-black
|
|
||||||
hover:cursor-pointer
|
|
||||||
hover:bg-blue-100
|
|
||||||
p-2 ${character.path === selectedCharacter?.path ? `bg-blue-200 hover:bg-blue-100` : ''}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col justify-between h-full">
|
|
||||||
<div className="flex flex-col gap-1 items-center">
|
|
||||||
<div className="flex flex-row justify-center">
|
|
||||||
{isBank ? (
|
|
||||||
<img
|
|
||||||
className="h-8"
|
|
||||||
src="https://beta.lifeto.co/item_img/gel.nri.003.000.png"
|
|
||||||
alt="Gel character"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
className="h-16 bg-gray-200"
|
|
||||||
src=""
|
|
||||||
alt={`Character ${character.name}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-semibold text-center">
|
|
||||||
{isBank ? 'Bank' : character.name}
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectedCharacterDisplay = () => {
|
|
||||||
const selectedCharacter = useAtomValue(selectedCharacterAtom)
|
|
||||||
const [{ data: rawCharacters }] = useAtom(charactersAtom)
|
|
||||||
|
|
||||||
if (!selectedCharacter) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center gap-2 p-4 border border-gray-300 rounded bg-gray-50 h-[140px] justify-center">
|
|
||||||
<div className="text-sm text-gray-400">No character selected</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCharacter.base_job === -8) {
|
|
||||||
// Find the character associated with this bank
|
|
||||||
const characterPair = rawCharacters?.find(pair => pair.bank.id === selectedCharacter.id)
|
|
||||||
const associatedCharacter = characterPair?.character
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center gap-2 p-4 border border-gray-300 rounded bg-gray-50 h-[140px] justify-center">
|
|
||||||
<div className="text-sm font-bold">Bank for: {associatedCharacter?.name || 'Unknown'}</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<img
|
|
||||||
className="h-24"
|
|
||||||
src="https://knowledge.lifeto.co/animations/npc/npc041_4.png"
|
|
||||||
alt="Bank NPC"
|
|
||||||
/>
|
|
||||||
{associatedCharacter && (
|
|
||||||
<img
|
|
||||||
className="h-24"
|
|
||||||
src={`https://knowledge.lifeto.co/animations/character/chr${(
|
|
||||||
associatedCharacter.current_type || associatedCharacter.base_job
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.padStart(3, '0')}_13.png`}
|
|
||||||
alt={`${associatedCharacter.name} walking`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center gap-2 p-4 border border-gray-300 rounded bg-gray-50 h-[140px] justify-center">
|
|
||||||
<div className="text-sm font-bold">Selected: {selectedCharacter.name}</div>
|
|
||||||
<img
|
|
||||||
className="h-24"
|
|
||||||
src={`https://knowledge.lifeto.co/animations/character/chr${(
|
|
||||||
selectedCharacter.current_type || selectedCharacter.base_job
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.padStart(3, '0')}_13.png`}
|
|
||||||
alt={`${selectedCharacter.name} walking`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CharacterRow = ({ characterPair }: { characterPair: { bank: TricksterCharacter; character: TricksterCharacter } }) => {
|
|
||||||
const [selectedCharacter, setSelectedCharacter] = useAtom(selectedCharacterAtom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedCharacter(characterPair.bank)}
|
|
||||||
className={`flex items-center justify-center px-1 py-0.5 border border-black hover:bg-blue-100 ${
|
|
||||||
characterPair.bank.path === selectedCharacter?.path ? 'bg-blue-200' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/gel.nri.003.000.png"
|
|
||||||
alt="Bank"
|
|
||||||
className="h-6"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedCharacter(characterPair.character)}
|
|
||||||
className={`flex items-center justify-start px-2 py-1 border-l-0 border-t border-r border-b border-black hover:bg-blue-100 flex-1 ${
|
|
||||||
characterPair.character.path === selectedCharacter?.path ? 'bg-blue-200' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={`https://beta.lifeto.co/img/job/${characterPair.character.current_type - 1}.png`}
|
|
||||||
alt="Class icon"
|
|
||||||
className="h-4 w-4 mr-2"
|
|
||||||
/>
|
|
||||||
{characterPair.character.name}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CharacterRoulette = () => {
|
|
||||||
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])
|
|
||||||
|
|
||||||
// Return nothing when no characters
|
|
||||||
if (!characters || characters.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const searchResults = fuse
|
|
||||||
.search(search || '!-----', {
|
|
||||||
limit: 20,
|
|
||||||
})
|
|
||||||
.map(x => {
|
|
||||||
return (
|
|
||||||
<CharacterRow key={`${x.item.character.account_id}`} characterPair={x.item} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<SelectedCharacterDisplay />
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<input
|
|
||||||
className="border border-black-1 bg-gray-100 placeholder-gray-600 p-1"
|
|
||||||
placeholder="search character..."
|
|
||||||
value={search}
|
|
||||||
onChange={e => {
|
|
||||||
setSearch(e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{searchResults.length > 0 ? searchResults : (
|
|
||||||
search ? <div className="text-sm text-gray-500 p-2">No characters matched search</div> : null
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FloatingPortal,
|
|
||||||
autoUpdate,
|
|
||||||
flip,
|
|
||||||
offset,
|
|
||||||
shift,
|
|
||||||
useFloating,
|
|
||||||
useHover,
|
|
||||||
useInteractions,
|
|
||||||
} from '@floating-ui/react'
|
|
||||||
import { inventoryFilterAtom, setInventoryFilterTabActionAtom } from '@/state/atoms'
|
|
||||||
|
|
||||||
const sections = [
|
|
||||||
{ name: 'all', value: '' },
|
|
||||||
{ name: 'consume', value: '1' },
|
|
||||||
{ name: 'equip', value: '2' },
|
|
||||||
{ name: 'drill', value: '3' },
|
|
||||||
{ name: 'pet', value: '4' },
|
|
||||||
{ name: 'etc', value: '5' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const cardSections = [
|
|
||||||
{ name: 'skill', value: '10' },
|
|
||||||
{ name: 'char', value: '11' },
|
|
||||||
{ name: 'mon', value: '12' },
|
|
||||||
{ name: 'fortune', value: '13' },
|
|
||||||
{ name: 'secret', value: '14' },
|
|
||||||
{ name: 'arcana', value: '15' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const InventoryFilters = () => {
|
|
||||||
const inventoryFilter = useAtomValue(inventoryFilterAtom)
|
|
||||||
const setInventoryFilterTab = useSetAtom(setInventoryFilterTabActionAtom)
|
|
||||||
const [isCardDropdownOpen, setIsCardDropdownOpen] = useState(false)
|
|
||||||
|
|
||||||
const sharedStyle = 'hover:cursor-pointer hover:bg-gray-200 px-2 pr-4 border-t border-l border-r border-gray-200'
|
|
||||||
const selectedStyle = 'bg-gray-200 border-b-2 border-black-1'
|
|
||||||
|
|
||||||
const { refs, floatingStyles, context } = useFloating({
|
|
||||||
open: isCardDropdownOpen,
|
|
||||||
onOpenChange: setIsCardDropdownOpen,
|
|
||||||
middleware: [offset(5), flip(), shift()],
|
|
||||||
whileElementsMounted: autoUpdate,
|
|
||||||
placement: 'bottom-start',
|
|
||||||
})
|
|
||||||
|
|
||||||
const hover = useHover(context, {
|
|
||||||
delay: { open: 100, close: 300 },
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
|
|
||||||
|
|
||||||
// Check if any card section is selected
|
|
||||||
const isCardSectionSelected = cardSections.some(x => x.value === inventoryFilter.tab)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row gap-1">
|
|
||||||
{sections.map(x => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setInventoryFilterTab(x.value)
|
|
||||||
}}
|
|
||||||
key={x.name}
|
|
||||||
className={`${sharedStyle} ${inventoryFilter.tab === x.value ? selectedStyle : ''}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{x.value === '' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/gel.nri.003.000.png"
|
|
||||||
alt="All"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '1' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/itm000.nri.00c.000.png"
|
|
||||||
alt="Consume"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '2' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/itm_cm_wp_106.nri.000.000.png"
|
|
||||||
alt="Equip"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '3' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/dri001.nri.000.000.png"
|
|
||||||
alt="Drill"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '4' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/pet_inv001.nri.015.000.png"
|
|
||||||
alt="Pet"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '5' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/itm_cm_ear_020.nri.001.000.png"
|
|
||||||
alt="Etc"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
ref={refs.setReference}
|
|
||||||
type="button"
|
|
||||||
className={`${sharedStyle} ${isCardSectionSelected ? selectedStyle : ''}`}
|
|
||||||
{...getReferenceProps()}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/card_com_001.nri.000.000.png"
|
|
||||||
alt="Card"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
card
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{isCardDropdownOpen && (
|
|
||||||
<FloatingPortal>
|
|
||||||
<div
|
|
||||||
ref={refs.setFloating}
|
|
||||||
style={floatingStyles}
|
|
||||||
className="bg-white border border-gray-300 shadow-lg rounded-md py-1 z-50"
|
|
||||||
{...getFloatingProps()}
|
|
||||||
>
|
|
||||||
{cardSections.map(x => (
|
|
||||||
<button
|
|
||||||
key={x.name}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setInventoryFilterTab(x.value)
|
|
||||||
setIsCardDropdownOpen(false)
|
|
||||||
}}
|
|
||||||
className={`block w-full text-left px-4 py-2 hover:bg-gray-100 ${
|
|
||||||
inventoryFilter.tab === x.value ? 'bg-gray-200 font-semibold' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{x.value === '10' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/card_skill_c_202.nri.000.000.png"
|
|
||||||
alt="Skill"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '11' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/cardch001.nri.006.000.png"
|
|
||||||
alt="Character"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '12' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/cardmo001.nri.019.000.png"
|
|
||||||
alt="Monster"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '13' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/card_ftn_001.nri.000.000.png"
|
|
||||||
alt="Fortune"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '14' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/card_scr_001.nri.000.000.png"
|
|
||||||
alt="Secret"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.value === '15' && (
|
|
||||||
<img
|
|
||||||
src="https://beta.lifeto.co/item_img/card_ear_002.nri.001.000.png"
|
|
||||||
alt="Arcana"
|
|
||||||
className="w-4 h-4 object-contain"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{x.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</FloatingPortal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
import {
|
|
||||||
FloatingFocusManager,
|
|
||||||
FloatingOverlay,
|
|
||||||
FloatingPortal,
|
|
||||||
useClick,
|
|
||||||
useDismiss,
|
|
||||||
useFloating,
|
|
||||||
useInteractions,
|
|
||||||
useRole,
|
|
||||||
} from '@floating-ui/react'
|
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
|
||||||
clearItemSelectionActionAtom,
|
|
||||||
closeMoveConfirmationAtom,
|
|
||||||
type MoveItemsResult,
|
|
||||||
moveConfirmationAtom,
|
|
||||||
moveSelectedItemsAtom,
|
|
||||||
} from '@/state/atoms'
|
|
||||||
|
|
||||||
export function MoveConfirmationPopup() {
|
|
||||||
const [confirmationState] = useAtom(moveConfirmationAtom)
|
|
||||||
const closeConfirmation = useSetAtom(closeMoveConfirmationAtom)
|
|
||||||
const moveItems = useSetAtom(moveSelectedItemsAtom)
|
|
||||||
const clearSelection = useSetAtom(clearItemSelectionActionAtom)
|
|
||||||
const [isMoving, setIsMoving] = useState(false)
|
|
||||||
const [moveResult, setMoveResult] = useState<MoveItemsResult | null>(null)
|
|
||||||
|
|
||||||
const { refs, context } = useFloating({
|
|
||||||
open: confirmationState.isOpen,
|
|
||||||
onOpenChange: open => {
|
|
||||||
if (!open && !isMoving) {
|
|
||||||
closeConfirmation()
|
|
||||||
setMoveResult(null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const click = useClick(context)
|
|
||||||
const dismiss = useDismiss(context, {
|
|
||||||
outsidePressEvent: 'mousedown',
|
|
||||||
escapeKey: !isMoving,
|
|
||||||
})
|
|
||||||
const role = useRole(context)
|
|
||||||
|
|
||||||
const { getFloatingProps } = useInteractions([click, dismiss, role])
|
|
||||||
|
|
||||||
if (!confirmationState.isOpen) return null
|
|
||||||
|
|
||||||
const { selectedItems, sourceCharacter, targetCharacter } = confirmationState
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
setIsMoving(true)
|
|
||||||
try {
|
|
||||||
const result = await moveItems()
|
|
||||||
setMoveResult(result)
|
|
||||||
if (result.failedCount === 0) {
|
|
||||||
clearSelection()
|
|
||||||
setTimeout(() => {
|
|
||||||
closeConfirmation()
|
|
||||||
setMoveResult(null)
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
// Error handled in UI
|
|
||||||
} finally {
|
|
||||||
setIsMoving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
if (!isMoving) {
|
|
||||||
closeConfirmation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderItemPreview = () => {
|
|
||||||
const itemsArray = Array.from(selectedItems.values())
|
|
||||||
const totalUniqueItems = itemsArray.length
|
|
||||||
const totalQuantity = itemsArray.reduce((sum, { count }) => sum + count, 0)
|
|
||||||
|
|
||||||
if (totalUniqueItems > 5) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-4">
|
|
||||||
<p className="text-lg font-semibold">Moving {totalUniqueItems} different items</p>
|
|
||||||
<p className="text-sm text-gray-600">Total quantity: {totalQuantity.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
|
||||||
{itemsArray.map(({ item, count }) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="flex items-center justify-between px-2 py-1 hover:bg-gray-50 rounded"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img
|
|
||||||
src={item.item_image || ''}
|
|
||||||
alt={item.item_name}
|
|
||||||
className="w-6 h-6 object-contain"
|
|
||||||
/>
|
|
||||||
<span className="text-sm">{item.item_name}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-gray-600">×{count.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FloatingPortal>
|
|
||||||
<FloatingOverlay className="grid place-items-center bg-black/50 z-50" lockScroll>
|
|
||||||
<FloatingFocusManager context={context} initialFocus={-1}>
|
|
||||||
<div
|
|
||||||
ref={refs.setFloating}
|
|
||||||
className="bg-white rounded-lg shadow-xl border border-gray-200 p-6 max-w-md w-full mx-4"
|
|
||||||
{...getFloatingProps()}
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold mb-4">Confirm Item Movement</h2>
|
|
||||||
|
|
||||||
<div className="mb-4 space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-gray-600">From:</span>
|
|
||||||
<span className="font-medium">{sourceCharacter?.name || 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-gray-600">To:</span>
|
|
||||||
<span className="font-medium">{targetCharacter?.name || 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-b border-gray-200 py-4 mb-4">{renderItemPreview()}</div>
|
|
||||||
|
|
||||||
{moveResult && (
|
|
||||||
<div
|
|
||||||
className={`mb-4 p-3 rounded ${moveResult.failedCount > 0 ? 'bg-yellow-50' : 'bg-green-50'}`}
|
|
||||||
>
|
|
||||||
<p className="text-sm font-medium">
|
|
||||||
{moveResult.failedCount === 0
|
|
||||||
? `Successfully moved ${moveResult.successCount} items!`
|
|
||||||
: `Moved ${moveResult.successCount} of ${moveResult.totalItems} items`}
|
|
||||||
</p>
|
|
||||||
{moveResult.errors.length > 0 && (
|
|
||||||
<div className="mt-2 text-xs text-red-600">
|
|
||||||
{moveResult.errors.slice(0, 3).map(error => (
|
|
||||||
<p key={error.itemId}>{error.error}</p>
|
|
||||||
))}
|
|
||||||
{moveResult.errors.length > 3 && (
|
|
||||||
<p>...and {moveResult.errors.length - 3} more errors</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-3 justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isMoving}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={isMoving || moveResult !== null}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isMoving ? 'Moving...' : 'Confirm Move'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FloatingFocusManager>
|
|
||||||
</FloatingOverlay>
|
|
||||||
</FloatingPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
|
||||||
import {
|
|
||||||
clearItemSelectionActionAtom,
|
|
||||||
currentCharacterInventoryAtom,
|
|
||||||
filteredCharacterItemsAtom,
|
|
||||||
inventoryPageRangeAtom,
|
|
||||||
itemSelectionSelectAllFilterActionAtom,
|
|
||||||
itemSelectionSelectAllPageActionAtom,
|
|
||||||
moveSelectedItemsAtom,
|
|
||||||
openMoveConfirmationAtom,
|
|
||||||
paginateInventoryActionAtom,
|
|
||||||
preferenceInventorySearch,
|
|
||||||
selectedCharacterAtom,
|
|
||||||
} from '@/state/atoms'
|
|
||||||
import { MoveConfirmationPopup } from './MoveConfirmationPopup'
|
|
||||||
import { InventoryTargetSelector } from './movetarget'
|
|
||||||
import { InventoryTable } from './table'
|
|
||||||
import { InventoryFilters } from './InventoryFilters'
|
|
||||||
|
|
||||||
const InventoryRangeDisplay = () => {
|
|
||||||
const inventoryRange = useAtomValue(inventoryPageRangeAtom)
|
|
||||||
const items = useAtomValue(filteredCharacterItemsAtom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center px-2 bg-yellow-100 border border-black-1 whitespace-pre select-none">
|
|
||||||
{inventoryRange.start}..{inventoryRange.end}/{items.length}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const Inventory = () => {
|
|
||||||
const selectedCharacter = useAtomValue(selectedCharacterAtom)
|
|
||||||
const clearItemSelection = useSetAtom(clearItemSelectionActionAtom)
|
|
||||||
const { refetch: refetchInventory } = useAtomValue(currentCharacterInventoryAtom)
|
|
||||||
|
|
||||||
const addPageItemSelection = useSetAtom(itemSelectionSelectAllPageActionAtom)
|
|
||||||
const addFilterItemSelection = useSetAtom(itemSelectionSelectAllFilterActionAtom)
|
|
||||||
const [search, setSearch] = useAtom(preferenceInventorySearch)
|
|
||||||
|
|
||||||
const paginateInventory = useSetAtom(paginateInventoryActionAtom)
|
|
||||||
const openMoveConfirmation = useSetAtom(openMoveConfirmationAtom)
|
|
||||||
const moveSelectedItems = useSetAtom(moveSelectedItemsAtom)
|
|
||||||
|
|
||||||
// Add keyboard navigation
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
// Don't paginate if user is typing in an input
|
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'ArrowLeft') {
|
|
||||||
e.preventDefault()
|
|
||||||
paginateInventory(-1)
|
|
||||||
} else if (e.key === 'ArrowRight') {
|
|
||||||
e.preventDefault()
|
|
||||||
paginateInventory(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
|
||||||
}
|
|
||||||
}, [paginateInventory])
|
|
||||||
|
|
||||||
if (!selectedCharacter) {
|
|
||||||
return <div>select a character</div>
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-col h-full w-full`}>
|
|
||||||
<div className="flex flex-col py-2 flex-0 justify-between h-full">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex flex-row gap-0 items-center justify-between">
|
|
||||||
<div className="flex flex-row gap-0 items-stretch">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:cursor-pointer border border-black-1 bg-blue-200 hover:bg-blue-300 px-2 py-1"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedCharacter) {
|
|
||||||
refetchInventory()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Refresh inventory"
|
|
||||||
>
|
|
||||||
↻
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
className="border border-black-1 px-2 py-1"
|
|
||||||
placeholder="search..."
|
|
||||||
onChange={e => {
|
|
||||||
setSearch(e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 flex items-center justify-center"
|
|
||||||
onClick={() => {
|
|
||||||
paginateInventory(-1)
|
|
||||||
}}
|
|
||||||
aria-label="Previous page"
|
|
||||||
title="Previous page (← arrow key)"
|
|
||||||
>
|
|
||||||
<FaArrowLeft />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:cursor-pointer border border-black-1 bg-green-200 hover:bg-green-300 px-2 flex items-center justify-center"
|
|
||||||
onClick={() => {
|
|
||||||
paginateInventory(1)
|
|
||||||
}}
|
|
||||||
aria-label="Next page"
|
|
||||||
title="Next page (→ arrow key)"
|
|
||||||
>
|
|
||||||
<FaArrowRight />
|
|
||||||
</button>
|
|
||||||
<InventoryRangeDisplay />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-0">
|
|
||||||
<InventoryTargetSelector />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async e => {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
// Shift+click: skip confirmation
|
|
||||||
const result = await moveSelectedItems()
|
|
||||||
if (result.successCount > 0) {
|
|
||||||
clearItemSelection()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal click: show confirmation
|
|
||||||
openMoveConfirmation()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="hover:cursor-pointer whitespace-preborder border-black-1 bg-orange-200 hover:bg-orange-300 px-2 py-1"
|
|
||||||
title="Click to move with confirmation, Shift+Click to move immediately"
|
|
||||||
>
|
|
||||||
Move Selected
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="whitespace-pre bg-purple-200 px-2 py-1 hover:cursor-pointer hover:bg-purple-300 border border-black-1"
|
|
||||||
onClick={() => {
|
|
||||||
addFilterItemSelection()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
select filtered
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="whitespace-pre bg-cyan-200 px-2 py-1 hover:cursor-pointer hover:bg-cyan-300 border border-black-1"
|
|
||||||
onClick={() => {
|
|
||||||
addPageItemSelection()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
select page
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="whitespace-pre bg-red-200 px-2 py-1 hover:cursor-pointer hover:bg-red-300 border border-black-1"
|
|
||||||
onClick={() => {
|
|
||||||
clearItemSelection()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row justify-between items-center">
|
|
||||||
<InventoryFilters />
|
|
||||||
<InventoryRangeDisplay />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col flex-1 h-full border border-black-2">
|
|
||||||
<InventoryTable />
|
|
||||||
</div>
|
|
||||||
<MoveConfirmationPopup />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,235 +0,0 @@
|
|||||||
import {
|
|
||||||
autoUpdate,
|
|
||||||
FloatingFocusManager,
|
|
||||||
FloatingPortal,
|
|
||||||
flip,
|
|
||||||
size,
|
|
||||||
useDismiss,
|
|
||||||
useFloating,
|
|
||||||
useInteractions,
|
|
||||||
useListNavigation,
|
|
||||||
useRole,
|
|
||||||
} from '@floating-ui/react'
|
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
|
||||||
import { forwardRef, useId, useMemo, useRef, useState } from 'react'
|
|
||||||
import { charactersAtom, selectedCharacterAtom, selectedTargetInventoryAtom } from '@/state/atoms'
|
|
||||||
|
|
||||||
interface AccountInventorySelectorItemProps {
|
|
||||||
children: React.ReactNode
|
|
||||||
active: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountInventorySelectorItem = forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
AccountInventorySelectorItemProps & React.HTMLProps<HTMLDivElement>
|
|
||||||
>(({ children, active, ...rest }, ref) => {
|
|
||||||
const id = useId()
|
|
||||||
const isDisabled = rest['aria-disabled']
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
// biome-ignore lint/a11y/useSemanticElements: Custom autocomplete component needs role="option"
|
|
||||||
role="option"
|
|
||||||
id={id}
|
|
||||||
aria-selected={active}
|
|
||||||
aria-disabled={isDisabled}
|
|
||||||
tabIndex={-1}
|
|
||||||
{...rest}
|
|
||||||
style={{
|
|
||||||
background: active && !isDisabled ? 'lightblue' : 'none',
|
|
||||||
padding: 4,
|
|
||||||
cursor: isDisabled ? 'not-allowed' : 'default',
|
|
||||||
opacity: isDisabled ? 0.5 : 1,
|
|
||||||
...rest.style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
export 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: `${Math.max(rects.reference.width * 2, 400)}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 selectedCharacter = useAtomValue(selectedCharacterAtom)
|
|
||||||
|
|
||||||
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
|
|
||||||
|
|
||||||
const searcher = useMemo(() => {
|
|
||||||
const allInventories = subaccounts?.flatMap(x => [x.bank, x.character]) || []
|
|
||||||
// Don't filter out current character, we'll disable it in the UI
|
|
||||||
return new Fuse(allInventories, {
|
|
||||||
keys: ['path', 'name'],
|
|
||||||
findAllMatches: true,
|
|
||||||
threshold: 0.8,
|
|
||||||
useExtendedSearch: true,
|
|
||||||
})
|
|
||||||
}, [subaccounts])
|
|
||||||
|
|
||||||
const items = inputValue
|
|
||||||
? searcher.search(inputValue, { limit: 10 }).map(x => x.item)
|
|
||||||
: subaccounts?.flatMap(x => [x.bank, x.character]).slice(0, 10) || []
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
className={`border border-black-1 placeholder-gray-600 ${
|
|
||||||
selectedTargetInventory ? 'bg-green-100' : inputValue ? 'bg-yellow-200' : 'bg-gray-300'
|
|
||||||
}`}
|
|
||||||
{...getReferenceProps({
|
|
||||||
ref: refs.setReference,
|
|
||||||
onChange,
|
|
||||||
value:
|
|
||||||
selectedTargetInventory !== undefined
|
|
||||||
? !selectedTargetInventory.path.includes('/')
|
|
||||||
? `[Bank] ${selectedTargetInventory.account_name}`
|
|
||||||
: selectedTargetInventory.name
|
|
||||||
: inputValue,
|
|
||||||
placeholder: 'Target Inventory',
|
|
||||||
'aria-autocomplete': 'list',
|
|
||||||
onFocus() {
|
|
||||||
setOpen(true)
|
|
||||||
},
|
|
||||||
onKeyDown(event) {
|
|
||||||
if (event.key === 'Enter' && activeIndex != null && items[activeIndex]) {
|
|
||||||
setSelectedTargetInventory(items[activeIndex])
|
|
||||||
setInputValue('')
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '10px', padding: '5px' }}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
{items
|
|
||||||
.filter(item => item.path.includes('/'))
|
|
||||||
.map(item => {
|
|
||||||
const actualIndex = items.indexOf(item)
|
|
||||||
const isDisabled = item.path === selectedCharacter?.path
|
|
||||||
return (
|
|
||||||
<AccountInventorySelectorItem
|
|
||||||
key={item.path}
|
|
||||||
{...getItemProps({
|
|
||||||
ref(node) {
|
|
||||||
listRef.current[actualIndex] = node
|
|
||||||
},
|
|
||||||
onClick() {
|
|
||||||
if (!isDisabled) {
|
|
||||||
setInputValue('')
|
|
||||||
setSelectedTargetInventory(item)
|
|
||||||
setOpen(false)
|
|
||||||
refs.domReference.current?.focus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
active={activeIndex === actualIndex}
|
|
||||||
aria-disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</AccountInventorySelectorItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
{items
|
|
||||||
.filter(item => !item.path.includes('/'))
|
|
||||||
.map(item => {
|
|
||||||
const actualIndex = items.indexOf(item)
|
|
||||||
const isDisabled = item.path === selectedCharacter?.path
|
|
||||||
return (
|
|
||||||
<AccountInventorySelectorItem
|
|
||||||
key={item.path}
|
|
||||||
{...getItemProps({
|
|
||||||
ref(node) {
|
|
||||||
listRef.current[actualIndex] = node
|
|
||||||
},
|
|
||||||
onClick() {
|
|
||||||
if (!isDisabled) {
|
|
||||||
setInputValue('')
|
|
||||||
setSelectedTargetInventory(item)
|
|
||||||
setOpen(false)
|
|
||||||
refs.domReference.current?.focus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
active={activeIndex === actualIndex}
|
|
||||||
aria-disabled={isDisabled}
|
|
||||||
>
|
|
||||||
[Bank] {item.account_name}
|
|
||||||
</AccountInventorySelectorItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FloatingFocusManager>
|
|
||||||
</FloatingPortal>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
|
||||||
import { atom, useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import { useEffect, useMemo } from 'react'
|
|
||||||
import { StatsColumns } from '@/lib/columns'
|
|
||||||
import { ItemWithSelection } from '@/lib/table/defs'
|
|
||||||
import { InventoryColumns } from '@/lib/table/tanstack'
|
|
||||||
import {
|
|
||||||
inventoryItemsCurrentPageAtom,
|
|
||||||
mouseDragSelectionStateAtom,
|
|
||||||
preferenceInventoryTab,
|
|
||||||
} from '@/state/atoms'
|
|
||||||
|
|
||||||
const columnVisibilityAtom = atom(get => {
|
|
||||||
const itemTab = get(preferenceInventoryTab)
|
|
||||||
if (!['2', '4'].includes(itemTab)) {
|
|
||||||
return Object.fromEntries([...StatsColumns.map(x => [`stats.${x}`, false]), ['slots', false]])
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
export const InventoryTable = () => {
|
|
||||||
const items = useAtomValue(inventoryItemsCurrentPageAtom)
|
|
||||||
const setDragState = useSetAtom(mouseDragSelectionStateAtom)
|
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
|
||||||
return [...Object.values(InventoryColumns)]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const columnVisibility = useAtomValue(columnVisibilityAtom)
|
|
||||||
|
|
||||||
const table = useReactTable<ItemWithSelection>({
|
|
||||||
getRowId: row => row.item.unique_id.toString(),
|
|
||||||
data: items,
|
|
||||||
state: {
|
|
||||||
columnVisibility,
|
|
||||||
},
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle global mouse up to end drag selection
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
setDragState(prev => ({ ...prev, isDragging: false }))
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
}
|
|
||||||
}, [setDragState])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-y-auto h-full mb-32">
|
|
||||||
<table
|
|
||||||
onContextMenu={e => {
|
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}}
|
|
||||||
className="border-spacing-x-2 border-separate"
|
|
||||||
>
|
|
||||||
<thead className="sticky top-0 z-10 select-none bg-white">
|
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
|
||||||
<tr className="" key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map(header => (
|
|
||||||
<th key={header.id} className="text-left">
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
{table.getRowModel().rows.map(row => (
|
|
||||||
<tr key={row.id} className={''}>
|
|
||||||
{row.getVisibleCells().map(cell => (
|
|
||||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import { useAtom } from 'jotai'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import useLocalStorage from 'use-local-storage'
|
|
||||||
import { login, logout } from '../lib/session'
|
|
||||||
import { loginStatusAtom } from '../state/atoms'
|
|
||||||
|
|
||||||
export const LoginWidget = () => {
|
|
||||||
const [username, setUsername] = useLocalStorage('input_username', '', { syncData: false })
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
const [{ data: loginState, refetch: refetchLoginState }] = useAtom(loginStatusAtom)
|
|
||||||
|
|
||||||
const [loginError, setLoginError] = useState('')
|
|
||||||
|
|
||||||
// Handle logged in state
|
|
||||||
if (loginState?.logged_in) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row justify-between items-center px-4 py-2 bg-green-50 border border-green-200 rounded">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-green-600">●</span>
|
|
||||||
<span className="font-medium">{loginState.community_name}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsLoading(true)
|
|
||||||
logout().finally(() => {
|
|
||||||
refetchLoginState()
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="text-blue-600 text-sm hover:text-blue-800 hover:underline disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Logging out...' : 'Logout'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle server maintenance (503) state
|
|
||||||
if (loginState?.code === 503) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 p-4 bg-yellow-50 border border-yellow-200 rounded">
|
|
||||||
<div className="flex items-center gap-2 justify-center">
|
|
||||||
<span className="text-yellow-600">⚠️</span>
|
|
||||||
<span className="font-medium text-yellow-800">Server Maintenance</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-yellow-700 ml-4">
|
|
||||||
The server is currently unavailable.{' '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsLoading(true)
|
|
||||||
refetchLoginState()
|
|
||||||
// Add a small delay to show loading state
|
|
||||||
setTimeout(() => setIsLoading(false), 500)
|
|
||||||
}}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="text-blue-600 hover:text-blue-800 hover:underline disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Checking...' : 'Retry'}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login form (code < 200 or no code)
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setLoginError('')
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await login(username, password)
|
|
||||||
setPassword('') // Clear password on success
|
|
||||||
} catch (error) {
|
|
||||||
setLoginError(error instanceof Error ? error.message : 'Login failed')
|
|
||||||
} finally {
|
|
||||||
refetchLoginState()
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 bg-gray-50 border border-gray-200 rounded">
|
|
||||||
<form onSubmit={handleLogin} className="flex flex-col gap-3">
|
|
||||||
<h3 className="font-medium text-gray-700">Lifeto Login</h3>
|
|
||||||
|
|
||||||
{loginError && (
|
|
||||||
<div className="text-red-600 text-sm bg-red-50 border border-red-200 rounded p-2">
|
|
||||||
{loginError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={username}
|
|
||||||
onChange={e => setUsername(e.target.value)}
|
|
||||||
placeholder="Email"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
placeholder="Password"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
disabled={isLoading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || !username || !password}
|
|
||||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Logging in...' : 'Login'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
html {
|
|
||||||
cursor: url(/assets/cursor.png), auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--cursor-default: url(/assets/cursor.png), auto !important;
|
|
||||||
--cursor-pointer: url(/assets/cursor.png), pointer !important;
|
|
||||||
--cursor-text: url(/assets/cursor.png), pointer !important;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
|
||||||
so we've added these compatibility styles to make sure everything still
|
|
||||||
looks the same as it did with Tailwind CSS v3.
|
|
||||||
|
|
||||||
If we ever want to remove these styles, we need to add an explicit border
|
|
||||||
color utility to any element that depends on these defaults.
|
|
||||||
*/
|
|
||||||
@layer base {
|
|
||||||
*,
|
|
||||||
::after,
|
|
||||||
::before,
|
|
||||||
::backdrop,
|
|
||||||
::file-selector-button {
|
|
||||||
border-color: var(--color-gray-200, currentcolor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import { App } from './App'
|
|
||||||
|
|
||||||
import './lib/superjson'
|
|
||||||
import './index.css'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
||||||
import { Provider } from 'jotai'
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<Provider>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<App />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</Provider>
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
||||||
@ -1,33 +1,38 @@
|
|||||||
import { TricksterItem } from '../trickster'
|
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'] as const
|
export const BasicColumns = [
|
||||||
|
"uid","Image","Name","Count",
|
||||||
export const DetailsColumns = ['Desc', 'Use'] as const
|
|
||||||
|
|
||||||
export const MoveColumns = ['MoveCount', 'Move'] as const
|
|
||||||
|
|
||||||
export const TagColumns = ['All', 'Equip', 'Drill', 'Card', 'Quest', 'Consume', 'Compound'] as const
|
|
||||||
|
|
||||||
export const EquipmentColumns = ['MinLvl', 'Slots', 'RefineNumber', 'RefineState'] as const
|
|
||||||
|
|
||||||
export const StatsColumns = [
|
|
||||||
'HV',
|
|
||||||
'AC',
|
|
||||||
'LK',
|
|
||||||
'WT',
|
|
||||||
'HP',
|
|
||||||
'MA',
|
|
||||||
'DP',
|
|
||||||
'DX',
|
|
||||||
'MP',
|
|
||||||
'AP',
|
|
||||||
'MD',
|
|
||||||
'DA',
|
|
||||||
'GunAP',
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const DebugColumns = []
|
export const DetailsColumns = [
|
||||||
export const HackColumns = [] as const
|
"Desc","Use",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const MoveColumns = [
|
||||||
|
"MoveCount","Move",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const TagColumns = [
|
||||||
|
"All","Equip","Drill","Card","Quest","Consume", "Compound"
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const EquipmentColumns = [
|
||||||
|
"MinLvl","Slots","RefineNumber","RefineState",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const StatsColumns = [
|
||||||
|
"AP","GunAP","AC","DX","MP","MA","MD","WT","DA","LK","HP","DP","HV",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
|
||||||
|
export const DebugColumns = [
|
||||||
|
]
|
||||||
|
export const HackColumns = [
|
||||||
|
] as const
|
||||||
|
|
||||||
export const ColumnNames = [
|
export const ColumnNames = [
|
||||||
...BasicColumns,
|
...BasicColumns,
|
||||||
@ -39,34 +44,35 @@ export const ColumnNames = [
|
|||||||
...HackColumns,
|
...HackColumns,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type ColumnName = (typeof ColumnNames)[number]
|
export type ColumnName = typeof ColumnNames[number]
|
||||||
|
|
||||||
const c = (a: ColumnName | ColumnInfo): ColumnName => {
|
const c = (a:ColumnName | ColumnInfo):ColumnName => {
|
||||||
switch (typeof a) {
|
switch(typeof a) {
|
||||||
case 'string':
|
case "string":
|
||||||
return a
|
return a
|
||||||
case 'object':
|
case "object":
|
||||||
return a.name
|
return a.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const LazyColumn = c
|
export const LazyColumn = c;
|
||||||
|
|
||||||
export const ColumnSorter = (a: ColumnName | ColumnInfo, b: ColumnName | ColumnInfo): number => {
|
export const ColumnSorter = (a:ColumnName | ColumnInfo, b: ColumnName | ColumnInfo):number => {
|
||||||
const n1 = ColumnNames.indexOf(c(a))
|
let n1 = ColumnNames.indexOf(c(a))
|
||||||
const n2 = ColumnNames.indexOf(c(b))
|
let n2 = ColumnNames.indexOf(c(b))
|
||||||
if (n1 === n2) {
|
if(n1 == n2) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return n1 > n2 ? 1 : -1
|
return n1 > n2 ? 1 : -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnInfo {
|
export interface ColumnInfo {
|
||||||
name: ColumnName
|
name: ColumnName
|
||||||
displayName: string
|
displayName:string
|
||||||
|
|
||||||
options?: (s: string[]) => string[]
|
options?:(s:string[])=>string[]
|
||||||
renderer?: any
|
renderer?:any
|
||||||
filtering?: boolean
|
filtering?:boolean
|
||||||
writable?: boolean
|
writable?:boolean
|
||||||
getter(item: TricksterItem): string | number
|
getter(item:TricksterItem):(string | number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,511 +1,464 @@
|
|||||||
import Handsontable from 'handsontable'
|
import Handsontable from "handsontable"
|
||||||
import Core from 'handsontable/core'
|
import Core from "handsontable/core"
|
||||||
import { textRenderer } from 'handsontable/renderers'
|
import { textRenderer } from "handsontable/renderers"
|
||||||
import numbro from 'numbro'
|
import numbro from "numbro"
|
||||||
import { TricksterItem } from '../trickster'
|
import { TricksterItem } from "../trickster"
|
||||||
import { ColumnInfo, ColumnName } from './column'
|
import {ColumnName, ColumnInfo} from "./column"
|
||||||
|
|
||||||
export const ColumnByNames = (...n: ColumnName[]) => {
|
export const ColumnByNames = (...n:ColumnName[]) => {
|
||||||
return n.map(ColumnByName)
|
return n.map(ColumnByName)
|
||||||
}
|
}
|
||||||
export const ColumnByName = (n: ColumnName) => {
|
export const ColumnByName = (n:ColumnName) => {
|
||||||
return Columns[n]
|
return Columns[n]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Image implements ColumnInfo {
|
class Image implements ColumnInfo {
|
||||||
name: ColumnName = 'Image'
|
name:ColumnName = 'Image'
|
||||||
displayName = ' '
|
displayName = " "
|
||||||
renderer = coverRenderer
|
renderer = coverRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number) {
|
||||||
return item.item_image ? item.item_image : ''
|
return item.image ? item.image : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function coverRenderer(
|
function coverRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
|
||||||
_row: any,
|
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
if (stringifiedValue.startsWith('http')) {
|
if (stringifiedValue.startsWith('http')) {
|
||||||
const img: any = document.createElement('IMG')
|
const img:any = document.createElement('IMG');
|
||||||
img.src = value
|
img.src = value;
|
||||||
Handsontable.dom.addEvent(img, 'mousedown', event => {
|
Handsontable.dom.addEvent(img, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(img)
|
td.appendChild(img);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Name implements ColumnInfo {
|
class Name implements ColumnInfo {
|
||||||
name: ColumnName = 'Name'
|
name:ColumnName = "Name"
|
||||||
displayName = 'Name'
|
displayName = "Name"
|
||||||
filtering = true
|
filtering = true
|
||||||
renderer = nameRenderer
|
renderer = nameRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_name
|
return item.item_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function nameRenderer(
|
function nameRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
let showText = stringifiedValue;
|
||||||
_row: any,
|
const div= document.createElement('div');
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
const showText = stringifiedValue
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerHTML = showText
|
div.innerHTML = showText
|
||||||
div.title = showText
|
div.title = showText
|
||||||
div.style.maxWidth = '20ch'
|
div.style.maxWidth = "20ch"
|
||||||
div.style.textOverflow = 'ellipsis'
|
div.style.textOverflow = "ellipsis"
|
||||||
div.style.overflow = 'hidden'
|
div.style.overflow= "hidden"
|
||||||
div.style.whiteSpace = 'nowrap'
|
div.style.whiteSpace= "nowrap"
|
||||||
Handsontable.dom.addEvent(div, 'mousedown', event => {
|
Handsontable.dom.addEvent(div, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(div)
|
td.appendChild(div);
|
||||||
td.classList.add('htLeft')
|
td.classList.add("htLeft")
|
||||||
}
|
}
|
||||||
|
|
||||||
class Count implements ColumnInfo {
|
class Count implements ColumnInfo {
|
||||||
name: ColumnName = 'Count'
|
name:ColumnName = "Count"
|
||||||
displayName = 'Count'
|
displayName = "Count"
|
||||||
renderer = 'numeric'
|
renderer = "numeric"
|
||||||
filtering = true
|
filtering = true
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_count
|
return item.item_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const spacer = '-----------'
|
const spacer = "-----------"
|
||||||
|
|
||||||
const getMoveTargets = (invs: string[]): string[] => {
|
|
||||||
const out: string[] = []
|
|
||||||
out.push(spacer)
|
|
||||||
for (const k of invs) {
|
|
||||||
out.push(k)
|
|
||||||
}
|
|
||||||
out.push('')
|
|
||||||
out.push('')
|
|
||||||
out.push('TRASH')
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
class Move implements ColumnInfo {
|
class Move implements ColumnInfo {
|
||||||
name: ColumnName = 'Move'
|
name:ColumnName = "Move"
|
||||||
displayName = 'Target'
|
displayName = "Target"
|
||||||
writable = true
|
writable = true
|
||||||
options = getMoveTargets
|
options = getMoveTargets
|
||||||
getter(_item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return spacer
|
return spacer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMoveTargets = (invs: string[]):string[] => {
|
||||||
|
let out:string[] = [];
|
||||||
|
out.push(spacer)
|
||||||
|
for(const k of invs){
|
||||||
|
out.push(k)
|
||||||
|
}
|
||||||
|
out.push("")
|
||||||
|
out.push("")
|
||||||
|
out.push("TRASH")
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
class MoveCount implements ColumnInfo {
|
class MoveCount implements ColumnInfo {
|
||||||
name: ColumnName = 'MoveCount'
|
name:ColumnName = "MoveCount"
|
||||||
displayName = 'Move #'
|
displayName = "Move #"
|
||||||
renderer = moveCountRenderer
|
renderer = moveCountRenderer
|
||||||
writable = true
|
writable = true
|
||||||
getter(_item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return ''
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCountRenderer(
|
function moveCountRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) {
|
||||||
instance: Core,
|
let newValue = value;
|
||||||
td: any,
|
|
||||||
row: number,
|
|
||||||
col: number,
|
|
||||||
prop: any,
|
|
||||||
value: any,
|
|
||||||
cellProperties: any,
|
|
||||||
) {
|
|
||||||
let newValue = value
|
|
||||||
|
|
||||||
if (Handsontable.helper.isNumeric(newValue)) {
|
if (Handsontable.helper.isNumeric(newValue)) {
|
||||||
const numericFormat = cellProperties.numericFormat
|
const numericFormat = cellProperties.numericFormat;
|
||||||
const cellCulture = numericFormat?.culture || '-'
|
const cellCulture = numericFormat && numericFormat.culture || '-';
|
||||||
const cellFormatPattern = numericFormat?.pattern
|
const cellFormatPattern = numericFormat && numericFormat.pattern;
|
||||||
const className = cellProperties.className || ''
|
const className = cellProperties.className || '';
|
||||||
const classArr = className.length ? className.split(' ') : []
|
const classArr = className.length ? className.split(' ') : [];
|
||||||
if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) {
|
if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) {
|
||||||
const shortTag: any = cellCulture.replace('-', '')
|
const shortTag:any = cellCulture.replace('-', '');
|
||||||
const langData = (numbro as any)[shortTag]
|
const langData = (numbro as any)[shortTag];
|
||||||
|
|
||||||
if (langData) {
|
if (langData) {
|
||||||
numbro.registerLanguage(langData)
|
numbro.registerLanguage(langData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const totalCount = Number(instance.getCell(row, col - 1)?.innerHTML)
|
const totalCount = Number(instance.getCell(row,col-1)?.innerHTML)
|
||||||
numbro.setLanguage(cellCulture)
|
numbro.setLanguage(cellCulture);
|
||||||
const num = numbro(newValue)
|
const num = numbro(newValue)
|
||||||
if (totalCount < num.value()) {
|
if(totalCount < num.value()) {
|
||||||
const newNum = numbro(totalCount)
|
const newNum = numbro(totalCount)
|
||||||
newValue = newNum.format(cellFormatPattern || '0')
|
newValue = newNum.format(cellFormatPattern || '0');
|
||||||
} else {
|
}else {
|
||||||
newValue = num.format(cellFormatPattern || '0')
|
newValue = num.format(cellFormatPattern || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (classArr.indexOf('htLeft') < 0 && classArr.indexOf('htCenter') < 0 &&
|
||||||
classArr.indexOf('htLeft') < 0 &&
|
classArr.indexOf('htRight') < 0 && classArr.indexOf('htJustify') < 0) {
|
||||||
classArr.indexOf('htCenter') < 0 &&
|
classArr.push('htRight');
|
||||||
classArr.indexOf('htRight') < 0 &&
|
|
||||||
classArr.indexOf('htJustify') < 0
|
|
||||||
) {
|
|
||||||
classArr.push('htRight')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classArr.indexOf('htNumeric') < 0) {
|
if (classArr.indexOf('htNumeric') < 0) {
|
||||||
classArr.push('htNumeric')
|
classArr.push('htNumeric');
|
||||||
}
|
}
|
||||||
cellProperties.className = classArr.join(' ')
|
cellProperties.className = classArr.join(' ');
|
||||||
|
|
||||||
td.dir = 'ltr'
|
td.dir = 'ltr';
|
||||||
newValue = `${newValue}x`
|
newValue = newValue + "x"
|
||||||
} else {
|
}else {
|
||||||
newValue = ''
|
newValue = ""
|
||||||
}
|
}
|
||||||
textRenderer(instance, td, row, col, prop, newValue, cellProperties)
|
textRenderer(instance, td, row, col, prop, newValue, cellProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Equip implements ColumnInfo {
|
class Equip implements ColumnInfo {
|
||||||
name: ColumnName = 'Equip'
|
name:ColumnName = "Equip"
|
||||||
displayName = 'equip'
|
displayName = "equip"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.is_equip ? 1 : 0
|
return item.is_equip ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Drill implements ColumnInfo {
|
class Drill implements ColumnInfo {
|
||||||
name: ColumnName = 'Drill'
|
name:ColumnName = "Drill"
|
||||||
displayName = 'drill'
|
displayName = "drill"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.is_drill ? 1 : 0
|
return item.is_drill ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class All implements ColumnInfo {
|
class All implements ColumnInfo {
|
||||||
name: ColumnName = 'All'
|
name:ColumnName = "All"
|
||||||
displayName = 'swap'
|
displayName = "swap"
|
||||||
getter(_: TricksterItem): string | number {
|
getter(_:TricksterItem):(string|number){
|
||||||
return -10000
|
return -10000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class uid implements ColumnInfo {
|
class uid implements ColumnInfo {
|
||||||
name: ColumnName = 'uid'
|
name:ColumnName = "uid"
|
||||||
displayName = 'id'
|
displayName = "id"
|
||||||
renderer = invisibleRenderer
|
renderer = invisibleRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.unique_id
|
return item.unique_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function invisibleRenderer(
|
function invisibleRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: Core,
|
Handsontable.dom.empty(td);
|
||||||
td: any,
|
|
||||||
_row: number,
|
|
||||||
_col: number,
|
|
||||||
_prop: any,
|
|
||||||
_value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
Handsontable.dom.empty(td)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Card implements ColumnInfo {
|
class Card implements ColumnInfo {
|
||||||
name: ColumnName = 'Card'
|
name:ColumnName = "Card"
|
||||||
displayName = 'card'
|
displayName = "card"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return cardFilter(item) ? 1 : 0
|
return cardFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardFilter = (item: TricksterItem): boolean => {
|
const cardFilter= (item:TricksterItem): boolean => {
|
||||||
return item.item_name.endsWith(' Card') || item.item_name.startsWith('Star Card')
|
return (item.item_name.endsWith(" Card") || item.item_name.startsWith("Star Card"))
|
||||||
}
|
}
|
||||||
class Compound implements ColumnInfo {
|
class Compound implements ColumnInfo {
|
||||||
name: ColumnName = 'Compound'
|
name:ColumnName = "Compound"
|
||||||
displayName = 'comp'
|
displayName = "comp"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return compFilter(item) ? 1 : 0
|
return compFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const compFilter = (item: TricksterItem): boolean => {
|
const compFilter= (item:TricksterItem): boolean => {
|
||||||
return item.item_comment.toLowerCase().includes('compound item')
|
return (item.item_desc.toLowerCase().includes("compound item"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Quest implements ColumnInfo {
|
class Quest implements ColumnInfo {
|
||||||
name: ColumnName = 'Quest'
|
name:ColumnName = "Quest"
|
||||||
displayName = 'quest'
|
displayName = "quest"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return questFilter(item) ? 1 : 0
|
return questFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const questFilter = (_item: TricksterItem): boolean => {
|
const questFilter= (item:TricksterItem): boolean => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
class Consume implements ColumnInfo {
|
class Consume implements ColumnInfo {
|
||||||
name: ColumnName = 'Consume'
|
name:ColumnName = "Consume"
|
||||||
displayName = 'eat'
|
displayName = "eat"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return consumeFilter(item) ? 1 : 0
|
return consumeFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const consumeFilter = (item: TricksterItem): boolean => {
|
const consumeFilter= (item:TricksterItem): boolean => {
|
||||||
const tl = item.item_use.toLowerCase()
|
const tl = item.item_use.toLowerCase()
|
||||||
return tl.includes('recover') || tl.includes('restores')
|
return tl.includes("recover") || tl.includes("restores")
|
||||||
}
|
}
|
||||||
|
|
||||||
class AP implements ColumnInfo {
|
class AP implements ColumnInfo {
|
||||||
name: ColumnName = 'AP'
|
name:ColumnName = "AP"
|
||||||
displayName = 'AP'
|
displayName = "AP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.AP : ''
|
return item.stats ? item.stats["AP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GunAP implements ColumnInfo {
|
class GunAP implements ColumnInfo {
|
||||||
name: ColumnName = 'GunAP'
|
name:ColumnName = "GunAP"
|
||||||
displayName = 'Gun AP'
|
displayName = "Gun AP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats['Gun AP'] : ''
|
return item.stats ? item.stats["Gun AP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AC implements ColumnInfo {
|
class AC implements ColumnInfo {
|
||||||
name: ColumnName = 'AC'
|
name:ColumnName = "AC"
|
||||||
displayName = 'AC'
|
displayName = "AC"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.AC : ''
|
return item.stats ? item.stats["AC"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DX implements ColumnInfo {
|
class DX implements ColumnInfo {
|
||||||
name: ColumnName = 'DX'
|
name:ColumnName = "DX"
|
||||||
displayName = 'DX'
|
displayName = "DX"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.DX : ''
|
return item.stats ? item.stats["DX"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MP implements ColumnInfo {
|
class MP implements ColumnInfo {
|
||||||
name: ColumnName = 'MP'
|
name:ColumnName = "MP"
|
||||||
displayName = 'MP'
|
displayName = "MP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.MP : ''
|
return item.stats ? item.stats["MP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MA implements ColumnInfo {
|
class MA implements ColumnInfo {
|
||||||
name: ColumnName = 'MA'
|
name:ColumnName = "MA"
|
||||||
displayName = 'MA'
|
displayName = "MA"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.MA : ''
|
return item.stats ? item.stats["MA"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MD implements ColumnInfo {
|
class MD implements ColumnInfo {
|
||||||
name: ColumnName = 'MD'
|
name:ColumnName = "MD"
|
||||||
displayName = 'MD'
|
displayName = "MD"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.MD : ''
|
return item.stats ? item.stats["MD"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WT implements ColumnInfo {
|
class WT implements ColumnInfo {
|
||||||
name: ColumnName = 'WT'
|
name:ColumnName = "WT"
|
||||||
displayName = 'WT'
|
displayName = "WT"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.WT : ''
|
return item.stats ? item.stats["WT"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DA implements ColumnInfo {
|
class DA implements ColumnInfo {
|
||||||
name: ColumnName = 'DA'
|
name:ColumnName = "DA"
|
||||||
displayName = 'DA'
|
displayName = "DA"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.DA : ''
|
return item.stats ? item.stats["DA"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LK implements ColumnInfo {
|
class LK implements ColumnInfo {
|
||||||
name: ColumnName = 'LK'
|
name:ColumnName = "LK"
|
||||||
displayName = 'LK'
|
displayName = "LK"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.LK : ''
|
return item.stats ? item.stats["LK"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HP implements ColumnInfo {
|
class HP implements ColumnInfo {
|
||||||
name: ColumnName = 'HP'
|
name:ColumnName = "HP"
|
||||||
displayName = 'HP'
|
displayName = "HP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.HP : ''
|
return item.stats ? item.stats["HP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DP implements ColumnInfo {
|
class DP implements ColumnInfo {
|
||||||
name: ColumnName = 'DP'
|
name:ColumnName = "DP"
|
||||||
displayName = 'DP'
|
displayName = "DP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.DP : ''
|
return item.stats ? item.stats["DP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class HV implements ColumnInfo {
|
class HV implements ColumnInfo {
|
||||||
name: ColumnName = 'HV'
|
name:ColumnName = "HV"
|
||||||
displayName = 'HV'
|
displayName = "HV"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.HV : ''
|
return item.stats ? item.stats["HV"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MinLvl implements ColumnInfo {
|
class MinLvl implements ColumnInfo {
|
||||||
name: ColumnName = 'MinLvl'
|
name:ColumnName = "MinLvl"
|
||||||
displayName = 'lvl'
|
displayName = "lvl"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
//TODO:
|
//TODO:
|
||||||
return item.item_min_level ? item.item_min_level : ''
|
return item.item_min_level? item.item_min_level:""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Slots implements ColumnInfo {
|
class Slots implements ColumnInfo {
|
||||||
name: ColumnName = 'Slots'
|
name:ColumnName = "Slots"
|
||||||
displayName = 'slots'
|
displayName = "slots"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
//TODO:
|
//TODO:
|
||||||
return item.item_slots ? item.item_slots : ''
|
return item.item_slots ? item.item_slots : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RefineNumber implements ColumnInfo {
|
class RefineNumber implements ColumnInfo {
|
||||||
name: ColumnName = 'RefineNumber'
|
name:ColumnName = "RefineNumber"
|
||||||
displayName = 'refine'
|
displayName = "refine"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.refine_level ? item.refine_level : 0
|
return item.refine_level ? item.refine_level : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RefineState implements ColumnInfo {
|
class RefineState implements ColumnInfo {
|
||||||
name: ColumnName = 'RefineState'
|
name:ColumnName = "RefineState"
|
||||||
displayName = 'bork'
|
displayName = "bork"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.refine_state ? item.refine_state : 0
|
return item.refine_state ? item.refine_state : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Desc implements ColumnInfo {
|
class Desc implements ColumnInfo {
|
||||||
name: ColumnName = 'Desc'
|
name:ColumnName = "Desc"
|
||||||
displayName = 'desc'
|
displayName = "desc"
|
||||||
renderer = descRenderer
|
renderer = descRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_comment
|
return item.item_desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function descRenderer(
|
function descRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
let showText = stringifiedValue;
|
||||||
_row: any,
|
const div= document.createElement('div');
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
const showText = stringifiedValue
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerHTML = showText
|
div.innerHTML = showText
|
||||||
div.title = showText
|
div.title = showText
|
||||||
div.style.maxWidth = '30ch'
|
div.style.maxWidth = "30ch"
|
||||||
div.style.textOverflow = 'ellipsis'
|
div.style.textOverflow = "ellipsis"
|
||||||
div.style.overflow = 'hidden'
|
div.style.overflow= "hidden"
|
||||||
div.style.whiteSpace = 'nowrap'
|
div.style.whiteSpace= "nowrap"
|
||||||
Handsontable.dom.addEvent(div, 'mousedown', event => {
|
Handsontable.dom.addEvent(div, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(div)
|
td.appendChild(div);
|
||||||
td.classList.add('htLeft')
|
td.classList.add("htLeft")
|
||||||
}
|
}
|
||||||
|
|
||||||
class Use implements ColumnInfo {
|
class Use implements ColumnInfo {
|
||||||
name: ColumnName = 'Use'
|
name:ColumnName = "Use"
|
||||||
displayName = 'use'
|
displayName = "use"
|
||||||
renderer = useRenderer
|
renderer= useRenderer;
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_use
|
return item.item_use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function useRenderer(
|
function useRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
let showText = stringifiedValue;
|
||||||
_row: any,
|
const div= document.createElement('div');
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
const showText = stringifiedValue
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.title = showText
|
div.title = showText
|
||||||
div.innerHTML = showText
|
div.innerHTML = showText
|
||||||
div.style.maxWidth = '30ch'
|
div.style.maxWidth = "30ch"
|
||||||
div.style.textOverflow = 'ellipsis'
|
div.style.textOverflow = "ellipsis"
|
||||||
div.style.overflow = 'hidden'
|
div.style.overflow= "hidden"
|
||||||
div.style.whiteSpace = 'nowrap'
|
div.style.whiteSpace= "nowrap"
|
||||||
Handsontable.dom.addEvent(div, 'mousedown', event => {
|
Handsontable.dom.addEvent(div, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(div)
|
td.appendChild(div);
|
||||||
td.classList.add('htLeft')
|
td.classList.add("htLeft")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Columns: { [Property in ColumnName]: ColumnInfo } = {
|
export const Columns:{[Property in ColumnName]:ColumnInfo}= {
|
||||||
Use: new Use(),
|
Use: new Use(),
|
||||||
Desc: new Desc(),
|
Desc: new Desc(),
|
||||||
Image: new Image(),
|
Image: new Image(),
|
||||||
Name: new Name(),
|
Name: new Name(),
|
||||||
Count: new Count(),
|
Count: new Count(),
|
||||||
Move: new Move(),
|
Move: new Move(),
|
||||||
MoveCount: new MoveCount(),
|
MoveCount: new MoveCount(),
|
||||||
Equip: new Equip(),
|
Equip: new Equip(),
|
||||||
Drill: new Drill(),
|
Drill: new Drill(),
|
||||||
Card: new Card(),
|
Card: new Card(),
|
||||||
Quest: new Quest(),
|
Quest: new Quest(),
|
||||||
Consume: new Consume(),
|
Consume: new Consume(),
|
||||||
AP: new AP(),
|
AP: new AP(),
|
||||||
GunAP: new GunAP(),
|
GunAP: new GunAP(),
|
||||||
AC: new AC(),
|
AC: new AC(),
|
||||||
DX: new DX(),
|
DX: new DX(),
|
||||||
MP: new MP(),
|
MP: new MP(),
|
||||||
MA: new MA(),
|
MA: new MA(),
|
||||||
MD: new MD(),
|
MD: new MD(),
|
||||||
WT: new WT(),
|
WT: new WT(),
|
||||||
DA: new DA(),
|
DA: new DA(),
|
||||||
LK: new LK(),
|
LK: new LK(),
|
||||||
HP: new HP(),
|
HP: new HP(),
|
||||||
DP: new DP(),
|
DP: new DP(),
|
||||||
HV: new HV(),
|
HV: new HV(),
|
||||||
MinLvl: new MinLvl(),
|
MinLvl: new MinLvl(),
|
||||||
Slots: new Slots(),
|
Slots: new Slots(),
|
||||||
RefineNumber: new RefineNumber(),
|
RefineNumber: new RefineNumber(),
|
||||||
RefineState: new RefineState(),
|
RefineState: new RefineState(),
|
||||||
All: new All(),
|
All: new All(),
|
||||||
Compound: new Compound(),
|
Compound: new Compound(),
|
||||||
uid: new uid(),
|
uid: new uid(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './column'
|
export * from "./column"
|
||||||
export * from './column_impl'
|
export * from "./column_impl"
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
import { TricksterAccount, TricksterInventory } from '../trickster'
|
import { trace } from "loglevel"
|
||||||
|
import { TricksterAccount, TricksterInventory } from "../trickster"
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
|
||||||
export const BankEndpoints = [
|
export const BankEndpoints = ["internal-xfer-item", "bank-item", "sell-item","buy-from-order","cancel-order"] as const
|
||||||
'internal-xfer-item',
|
export type BankEndpoint = typeof BankEndpoints[number]
|
||||||
'bank-item',
|
|
||||||
'sell-item',
|
|
||||||
'buy-from-order',
|
|
||||||
'cancel-order',
|
|
||||||
] as const
|
|
||||||
export type BankEndpoint = (typeof BankEndpoints)[number]
|
|
||||||
|
|
||||||
export interface LTOApi {
|
export interface LTOApi {
|
||||||
GetInventory: (path: string) => Promise<TricksterInventory>
|
GetInventory:(path:string)=>Promise<TricksterInventory>
|
||||||
GetAccounts: () => Promise<Array<TricksterAccount>>
|
GetAccounts:() =>Promise<Array<TricksterAccount>>
|
||||||
GetLoggedin: () => Promise<boolean>
|
GetLoggedin:() =>Promise<boolean>
|
||||||
|
|
||||||
BankAction: <T, D>(e: BankEndpoint, t: T) => Promise<D>
|
BankAction:<T, D>(e:BankEndpoint, t:T) => Promise<D>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from './api'
|
export * from "./lifeto"
|
||||||
export * from './lifeto'
|
export * from "./api"
|
||||||
export * from './stateful'
|
export * from "./stateful"
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
import { LTOApi } from './api'
|
|
||||||
|
|
||||||
export interface InternalXferParams {
|
|
||||||
itemUid: string | 'galders'
|
|
||||||
count: number
|
|
||||||
targetCharId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BankItemParams {
|
|
||||||
itemUid: string | 'galders'
|
|
||||||
count: number
|
|
||||||
targetAccount: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoveResult {
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
data?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ItemMover {
|
|
||||||
constructor(private api: LTOApi) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transfer items between characters
|
|
||||||
* Uses internal-xfer-item API
|
|
||||||
*/
|
|
||||||
async internalXfer(params: InternalXferParams): Promise<MoveResult> {
|
|
||||||
try {
|
|
||||||
const request = {
|
|
||||||
item_uid: params.itemUid,
|
|
||||||
qty: params.count.toString(),
|
|
||||||
new_char: params.targetCharId,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.api.BankAction<any, any>('internal-xfer-item', request)
|
|
||||||
|
|
||||||
if (response.status !== 'success') {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: response.message || 'Failed to transfer item',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: response.data,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error in internalXfer',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move items to bank
|
|
||||||
* Uses bank-item API
|
|
||||||
*/
|
|
||||||
async bankItem(params: BankItemParams): Promise<MoveResult> {
|
|
||||||
try {
|
|
||||||
const request = {
|
|
||||||
item_uid: params.itemUid,
|
|
||||||
qty: params.count.toString(),
|
|
||||||
account: params.targetAccount,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.api.BankAction<any, any>('bank-item', request)
|
|
||||||
|
|
||||||
if (response.status !== 'success') {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: response.message || 'Failed to bank item',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: response.data,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error in bankItem',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* High-level function that determines whether to use bankItem or internalXfer
|
|
||||||
* based on whether targetAccountId is provided (bank) or targetCharId (character)
|
|
||||||
*/
|
|
||||||
async moveItem(
|
|
||||||
itemUid: string | 'galders',
|
|
||||||
count: number,
|
|
||||||
targetCharId?: string,
|
|
||||||
targetAccountId?: string,
|
|
||||||
): Promise<MoveResult> {
|
|
||||||
if (targetAccountId) {
|
|
||||||
// Use bank-item when moving to bank (targetAccountId is provided)
|
|
||||||
return this.bankItem({
|
|
||||||
itemUid,
|
|
||||||
count,
|
|
||||||
targetAccount: targetAccountId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (targetCharId) {
|
|
||||||
// Use internal-xfer when moving between characters
|
|
||||||
return this.internalXfer({
|
|
||||||
itemUid,
|
|
||||||
count,
|
|
||||||
targetCharId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Either targetCharId or targetAccountId must be provided',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,137 +1,108 @@
|
|||||||
import { AxiosResponse, Method } from 'axios'
|
import { Axios, AxiosResponse, Method } from "axios"
|
||||||
import log from 'loglevel'
|
import log, { debug } from "loglevel"
|
||||||
import { bank_endpoint, EndpointCreator, market_endpoint, Session } from '../session'
|
import { bank_endpoint, EndpointCreator, market_endpoint, Session } from "../session"
|
||||||
import { TricksterAccount, TricksterInventory, TricksterItem } from '../trickster'
|
import { dummyChar, TricksterAccount, TricksterInventory, TricksterItem, TricksterWallet } from "../trickster"
|
||||||
import { BankEndpoint, LTOApi } from './api'
|
import { BankEndpoint, LTOApi } from "./api"
|
||||||
|
|
||||||
export const pathIsBank = (path: string): boolean => {
|
export const pathIsBank = (path:string):boolean => {
|
||||||
if (path.includes('/')) {
|
if(path.includes("/")) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const splitPath = (path: string): [string, string] => {
|
export const splitPath = (path:string):[string,string]=>{
|
||||||
const spl = path.split('/')
|
const spl = path.split("/")
|
||||||
switch (spl.length) {
|
switch(spl.length) {
|
||||||
case 1:
|
case 1:
|
||||||
return [spl[0], '']
|
return [spl[0], ""]
|
||||||
case 2:
|
case 2:
|
||||||
return [spl[0], spl[1]]
|
return [spl[0],spl[1]]
|
||||||
}
|
}
|
||||||
return ['', '']
|
return ["",""]
|
||||||
}
|
}
|
||||||
export class LTOApiv0 implements LTOApi {
|
export class LTOApiv0 implements LTOApi {
|
||||||
s: Session
|
s: Session
|
||||||
constructor(s: Session) {
|
constructor(s:Session) {
|
||||||
this.s = s
|
this.s = s
|
||||||
}
|
}
|
||||||
|
|
||||||
BankAction = async <T, D>(e: BankEndpoint, t: T): Promise<D> => {
|
BankAction = async <T,D>(e: BankEndpoint, t: T):Promise<D> => {
|
||||||
let VERB: Method | 'POSTFORM' = 'POST'
|
let VERB:Method | "POSTFORM" = "POST"
|
||||||
let endpoint: EndpointCreator = bank_endpoint
|
let endpoint:EndpointCreator = bank_endpoint
|
||||||
switch (e) {
|
switch(e){
|
||||||
case 'buy-from-order':
|
case "buy-from-order":
|
||||||
case 'cancel-order':
|
case "cancel-order":
|
||||||
endpoint = market_endpoint
|
endpoint = market_endpoint
|
||||||
break
|
case "sell-item":
|
||||||
case 'sell-item':
|
VERB = "POSTFORM"
|
||||||
//case 'internal-xfer-item':
|
|
||||||
VERB = 'POSTFORM'
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
break
|
|
||||||
}
|
}
|
||||||
return this.s.request(VERB as any, e, t, endpoint).then(x => {
|
return this.s.request(VERB as any,e,t,endpoint).then((x)=>{
|
||||||
return x.data
|
return x.data
|
||||||
})
|
|
||||||
}
|
|
||||||
GetInventory = async (char_path: string): Promise<TricksterInventory> => {
|
|
||||||
if (char_path.startsWith(':')) {
|
|
||||||
char_path = char_path.replace(':', '')
|
|
||||||
}
|
|
||||||
const type = char_path.includes('/') ? 'char' : 'account'
|
|
||||||
return this.s
|
|
||||||
.request('GET', `v3/item-manager/items/${type}/${char_path}`, undefined)
|
|
||||||
.then((ans: AxiosResponse) => {
|
|
||||||
const o = ans.data
|
|
||||||
log.debug('GetInventory', o)
|
|
||||||
let name = 'bank'
|
|
||||||
let id = 0
|
|
||||||
let galders = 0
|
|
||||||
if (pathIsBank(char_path)) {
|
|
||||||
const [char, val] = Object.entries(o.characters)[0] as [string, any]
|
|
||||||
name = val.name
|
|
||||||
id = Number(char)
|
|
||||||
galders = 0
|
|
||||||
} else {
|
|
||||||
const [char, val] = Object.entries(o.characters)[0] as [string, any]
|
|
||||||
name = val.name
|
|
||||||
id = Number(char)
|
|
||||||
galders = val.galders
|
|
||||||
}
|
|
||||||
const out: TricksterInventory = {
|
|
||||||
account_name: o.account.account_gid,
|
|
||||||
account_id: o.account.account_code,
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
path: char_path,
|
|
||||||
galders,
|
|
||||||
items: new Map(
|
|
||||||
(Object.entries(o.items) as any).map(
|
|
||||||
([k, v]: [string, TricksterItem]): [string, TricksterItem] => {
|
|
||||||
v.unique_id = Number(k)
|
|
||||||
v.id = k
|
|
||||||
return [k, v]
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
GetAccounts = async (): Promise<TricksterAccount[]> => {
|
GetInventory = async (char_path: string):Promise<TricksterInventory> =>{
|
||||||
return this.s.request('GET', 'characters/list', undefined).then((ans: AxiosResponse) => {
|
if(char_path.startsWith(":")) {
|
||||||
log.debug('GetAccounts', ans.data)
|
char_path = char_path.replace(":","")
|
||||||
return ans.data.map((x: any): TricksterAccount => {
|
}
|
||||||
|
return this.s.request("GET", `item-manager/items/account/${char_path}`,undefined).then((ans:AxiosResponse)=>{
|
||||||
|
const o = ans.data
|
||||||
|
log.debug("GetInventory", o)
|
||||||
|
let name = "bank"
|
||||||
|
let id = 0
|
||||||
|
let galders = 0
|
||||||
|
if(pathIsBank(char_path)){
|
||||||
|
let [char, val] = Object.entries(o.characters)[0] as [string,any]
|
||||||
|
name = val.name
|
||||||
|
id = Number(char)
|
||||||
|
galders = 0
|
||||||
|
}else {
|
||||||
|
let [char, val] = Object.entries(o.characters)[0] as [string,any]
|
||||||
|
name = val.name
|
||||||
|
id = Number(char)
|
||||||
|
galders = val.galders
|
||||||
|
}
|
||||||
|
let out = {
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
path: char_path,
|
||||||
|
galders,
|
||||||
|
items: Object.fromEntries((Object.entries(o.items) as any).map(([k, v]: [string, TricksterItem]):[string, TricksterItem]=>{
|
||||||
|
v.unique_id = Number(k)
|
||||||
|
return [k, v]
|
||||||
|
})),
|
||||||
|
} as TricksterInventory
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
}
|
||||||
|
GetAccounts = async ():Promise<TricksterAccount[]> => {
|
||||||
|
return this.s.request("GET", "characters/list",undefined).then((ans:AxiosResponse)=>{
|
||||||
|
log.debug("GetAccounts", ans.data)
|
||||||
|
return ans.data.map((x:any)=>{
|
||||||
return {
|
return {
|
||||||
name: x.name,
|
name: x.name,
|
||||||
characters: [
|
characters: [{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,
|
|
||||||
id: x.id,
|
|
||||||
account_id: x.id,
|
account_id: x.id,
|
||||||
path: x.name,
|
id: z.id,
|
||||||
name: `${x.name}/bank`,
|
name: z.name,
|
||||||
class: -8,
|
path: x.name+"/"+z.name,
|
||||||
base_job: -8,
|
class: z.class,
|
||||||
current_job: -8,
|
base_job: z.base_job,
|
||||||
current_type: -8,
|
current_job: z.current_job,
|
||||||
},
|
}
|
||||||
...Object.values(x.characters).map((z: any) => {
|
})],
|
||||||
return {
|
} as TricksterAccount
|
||||||
account_name: x.name,
|
|
||||||
account_id: x.id,
|
|
||||||
id: z.id,
|
|
||||||
name: z.name,
|
|
||||||
path: `${x.name}/${z.name}`,
|
|
||||||
class: z.class,
|
|
||||||
base_job: z.base_job,
|
|
||||||
current_job: z.current_job,
|
|
||||||
current_type: z.current_type,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
GetLoggedin = async (): Promise<boolean> => {
|
GetLoggedin = async ():Promise<boolean> => {
|
||||||
return this.s.request('POST', 'accounts/list', undefined).then((ans: AxiosResponse) => {
|
return this.s.request("POST", "accounts/list",undefined).then((ans:AxiosResponse)=>{
|
||||||
if (ans.status === 401) {
|
if(ans.status == 401) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (ans.status === 200) {
|
if(ans.status == 200) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -1,55 +1,56 @@
|
|||||||
import { debug } from 'loglevel'
|
import { LTOApi } from "./api"
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { RefStore } from '../../state/state'
|
import { RefStore } from "../../state/state";
|
||||||
import { LTOApi } from './api'
|
import { debug } from "loglevel";
|
||||||
|
|
||||||
export const TxnStates = ['PENDING', 'INFLIGHT', 'WORKING', 'ERROR', 'SUCCESS'] as const
|
export const TxnStates = ["PENDING","INFLIGHT","WORKING","ERROR","SUCCESS"] as const
|
||||||
|
|
||||||
export type TxnState = (typeof TxnStates)[number]
|
export type TxnState = typeof TxnStates[number]
|
||||||
|
|
||||||
export interface TxnDetails {
|
export interface TxnDetails {
|
||||||
item_uid: string | 'galders'
|
item_uid: string | "galders"
|
||||||
count: number
|
count:number
|
||||||
origin: string
|
origin:string
|
||||||
target: string
|
target:string
|
||||||
|
|
||||||
origin_path: string
|
origin_path:string
|
||||||
target_path: string
|
target_path:string
|
||||||
|
|
||||||
origin_account: string
|
origin_account:string
|
||||||
target_account: string
|
target_account:string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Envelope<REQ, RESP> {
|
export interface Envelope<REQ,RESP> {
|
||||||
req: REQ
|
req: REQ
|
||||||
resp: RESP
|
resp: RESP
|
||||||
state: TxnState
|
state: TxnState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export abstract class Order {
|
export abstract class Order {
|
||||||
action_id: string
|
action_id: string
|
||||||
details?: TxnDetails
|
details?:TxnDetails
|
||||||
created: Date
|
created:Date
|
||||||
state: TxnState
|
state: TxnState
|
||||||
constructor(details?: TxnDetails) {
|
constructor(details?:TxnDetails) {
|
||||||
this.state = 'PENDING'
|
this.state = "PENDING"
|
||||||
this.details = details
|
this.details = details
|
||||||
this.created = new Date()
|
this.created = new Date()
|
||||||
this.action_id = uuidv4()
|
this.action_id = uuidv4();
|
||||||
}
|
}
|
||||||
|
|
||||||
mark(t: TxnState) {
|
mark(t:TxnState) {
|
||||||
this.state = t
|
this.state = t
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract tick(r: RefStore, api: LTOApi): Promise<any>
|
abstract tick(r:RefStore, api:LTOApi):Promise<any>
|
||||||
abstract status(): string
|
abstract status():string
|
||||||
abstract progress(): [number, number]
|
abstract progress():[number, number]
|
||||||
abstract error(): string
|
abstract error():string
|
||||||
|
|
||||||
abstract order_type: OrderType
|
abstract order_type:OrderType
|
||||||
|
|
||||||
parse(i: any): Order {
|
parse(i:any):Order {
|
||||||
this.action_id = i.action_id
|
this.action_id = i.action_id
|
||||||
this.details = i.details
|
this.details = i.details
|
||||||
this.created = new Date(i.created)
|
this.created = new Date(i.created)
|
||||||
@ -62,20 +63,20 @@ export abstract class BasicOrder extends Order {
|
|||||||
stage: number
|
stage: number
|
||||||
err?: string
|
err?: string
|
||||||
|
|
||||||
constructor(details: TxnDetails) {
|
constructor(details:TxnDetails) {
|
||||||
super(details)
|
super(details)
|
||||||
this.stage = 0
|
this.stage = 0
|
||||||
}
|
}
|
||||||
progress(): [number, number] {
|
progress():[number,number]{
|
||||||
return [this.stage, 1]
|
return [this.stage, 1]
|
||||||
}
|
}
|
||||||
status(): string {
|
status():string {
|
||||||
return this.state
|
return this.state
|
||||||
}
|
}
|
||||||
error(): string {
|
error():string {
|
||||||
return this.err ? this.err : ''
|
return this.err ? this.err : ""
|
||||||
}
|
}
|
||||||
parse(i: any): BasicOrder {
|
parse(i:any):BasicOrder {
|
||||||
this.stage = i.stage
|
this.stage = i.stage
|
||||||
this.err = i.err
|
this.err = i.err
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
@ -84,38 +85,31 @@ export abstract class BasicOrder extends Order {
|
|||||||
}
|
}
|
||||||
/// start user defined
|
/// start user defined
|
||||||
|
|
||||||
export const OrderTypes = [
|
export const OrderTypes = ["InvalidOrder","BankItem","InternalXfer", "PrivateMarket","MarketMove", "MarketMoveToChar"]
|
||||||
'InvalidOrder',
|
export type OrderType = typeof OrderTypes[number]
|
||||||
'BankItem',
|
|
||||||
'InternalXfer',
|
|
||||||
'PrivateMarket',
|
|
||||||
'MarketMove',
|
|
||||||
'MarketMoveToChar',
|
|
||||||
]
|
|
||||||
export type OrderType = (typeof OrderTypes)[number]
|
|
||||||
|
|
||||||
export class InvalidOrder extends Order {
|
export class InvalidOrder extends Order{
|
||||||
order_type = 'InvalidOrder'
|
order_type = "InvalidOrder"
|
||||||
|
|
||||||
msg: string
|
msg:string
|
||||||
constructor(msg: string) {
|
constructor(msg: string){
|
||||||
super(undefined)
|
super(undefined)
|
||||||
this.msg = msg
|
this.msg = msg
|
||||||
this.mark('ERROR')
|
this.mark("ERROR")
|
||||||
}
|
}
|
||||||
status(): string {
|
status():string {
|
||||||
return 'ERROR'
|
return "ERROR"
|
||||||
}
|
}
|
||||||
progress(): [number, number] {
|
progress():[number, number] {
|
||||||
return [0, 0]
|
return [0,0]
|
||||||
}
|
}
|
||||||
error(): string {
|
error(): string {
|
||||||
return this.msg
|
return this.msg
|
||||||
}
|
}
|
||||||
async tick(_r: RefStore, _api: LTOApi): Promise<void> {
|
async tick(r:RefStore, api:LTOApi):Promise<void> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parse(i: any): InvalidOrder {
|
parse(i:any):InvalidOrder {
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
this.msg = i.msg
|
this.msg = i.msg
|
||||||
return this
|
return this
|
||||||
@ -123,68 +117,60 @@ export class InvalidOrder extends Order {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BasicResponse {
|
export interface BasicResponse {
|
||||||
status: string
|
status: number
|
||||||
data: any
|
data: any
|
||||||
message?: string
|
msg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface InternalXferRequest {
|
export interface InternalXferRequest {
|
||||||
item_uid: string
|
item_uid:string
|
||||||
qty: string
|
qty:string
|
||||||
account: string
|
account:string
|
||||||
new_char: string
|
new_char:string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalXferResponse extends BasicResponse {}
|
export interface InternalXferResponse extends BasicResponse {}
|
||||||
|
|
||||||
export class InternalXfer extends BasicOrder {
|
export class InternalXfer extends BasicOrder{
|
||||||
order_type = 'InternalXfer'
|
order_type = "InternalXfer"
|
||||||
|
|
||||||
originalRequest: InternalXferRequest
|
originalRequest:InternalXferRequest
|
||||||
originalResponse?: InternalXferResponse
|
originalResponse?:InternalXferResponse
|
||||||
constructor(details: TxnDetails) {
|
constructor(details:TxnDetails) {
|
||||||
super(details)
|
super(details)
|
||||||
this.originalRequest = {
|
this.originalRequest = {
|
||||||
item_uid: details.item_uid,
|
item_uid: details.item_uid,
|
||||||
qty: details.count.toString(),
|
qty: details.count.toString(),
|
||||||
new_char: details.target,
|
new_char: details.target,
|
||||||
account: details.origin,
|
account: details.origin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async tick(r: RefStore, api: LTOApi): Promise<void> {
|
async tick(r:RefStore, api:LTOApi):Promise<void> {
|
||||||
if (this.state !== 'PENDING') {
|
if(this.state !== "PENDING") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.mark('WORKING')
|
this.mark("WORKING")
|
||||||
return api
|
return api.BankAction<InternalXferRequest, InternalXferResponse>("internal-xfer-item",this.originalRequest)
|
||||||
.BankAction<InternalXferRequest, InternalXferResponse>(
|
.then((x:InternalXferResponse)=>{
|
||||||
'internal-xfer-item',
|
if(x.status == 200){
|
||||||
this.originalRequest,
|
this.originalResponse = x
|
||||||
)
|
|
||||||
.then((x: InternalXferResponse) => {
|
|
||||||
if (x.status === 'success') {
|
|
||||||
this.originalResponse = x
|
|
||||||
this.stage = 1
|
|
||||||
this.mark('SUCCESS')
|
|
||||||
if (this.details?.origin_path && this.details?.item_uid && this.details?.count) {
|
|
||||||
const inventory = r.invs.value.get(this.details.origin_path)
|
|
||||||
const origin_item = inventory?.items[this.details.item_uid]
|
|
||||||
if (origin_item) {
|
|
||||||
origin_item.item_count = origin_item.item_count - this.details.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw x.message
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
debug('InternalXfer', e)
|
|
||||||
this.stage = 1
|
this.stage = 1
|
||||||
this.err = e
|
this.mark("SUCCESS")
|
||||||
this.mark('ERROR')
|
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
|
||||||
})
|
origin_item.item_count = origin_item.item_count - this.details?.count!
|
||||||
|
}else{
|
||||||
|
throw x.msg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
debug("InternalXfer",e)
|
||||||
|
this.stage = 1
|
||||||
|
this.err = e
|
||||||
|
this.mark("ERROR")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
parse(i: any): InternalXfer {
|
parse(i:any):InternalXfer {
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
this.originalRequest = i.originalRequest
|
this.originalRequest = i.originalRequest
|
||||||
this.originalResponse = i.originalResponse
|
this.originalResponse = i.originalResponse
|
||||||
@ -193,58 +179,52 @@ export class InternalXfer extends BasicOrder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BankItemRequest {
|
export interface BankItemRequest {
|
||||||
item_uid: string
|
item_uid:string
|
||||||
qty: string
|
qty:string
|
||||||
account: string
|
account:string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BankItemResponse extends BasicResponse {}
|
export interface BankItemResponse extends BasicResponse {}
|
||||||
|
|
||||||
export class BankItem extends BasicOrder {
|
export class BankItem extends BasicOrder{
|
||||||
order_type = 'BankItem'
|
order_type = "BankItem";
|
||||||
|
|
||||||
originalRequest: BankItemRequest
|
originalRequest:BankItemRequest
|
||||||
originalResponse?: BankItemResponse
|
originalResponse?:BankItemResponse
|
||||||
constructor(details: TxnDetails) {
|
constructor(details:TxnDetails) {
|
||||||
super(details)
|
super(details)
|
||||||
this.originalRequest = {
|
this.originalRequest = {
|
||||||
item_uid: details.item_uid,
|
item_uid: details.item_uid,
|
||||||
qty: details.count.toString(),
|
qty: details.count.toString(),
|
||||||
account: details.target,
|
account: details.target,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async tick(r: RefStore, api: LTOApi): Promise<void> {
|
async tick(r:RefStore, api:LTOApi):Promise<void> {
|
||||||
if (this.state !== 'PENDING') {
|
if(this.state !== "PENDING" ){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.mark('WORKING')
|
this.mark("WORKING")
|
||||||
return api
|
return api.BankAction<BankItemRequest, BankItemResponse>("bank-item",this.originalRequest)
|
||||||
.BankAction<BankItemRequest, BankItemResponse>('bank-item', this.originalRequest)
|
.then((x)=>{
|
||||||
.then(x => {
|
debug("BankItem",x)
|
||||||
debug('BankItem', x)
|
if(x.status == 200){
|
||||||
if (x.status === 'success') {
|
|
||||||
this.stage = 1
|
|
||||||
this.originalResponse = x
|
|
||||||
this.mark('SUCCESS')
|
|
||||||
if (this.details?.origin_path && this.details?.item_uid && this.details?.count) {
|
|
||||||
const inventory = r.invs.value.get(this.details.origin_path)
|
|
||||||
const origin_item = inventory?.items[this.details.item_uid]
|
|
||||||
if (origin_item) {
|
|
||||||
origin_item.item_count = origin_item.item_count - this.details.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw x.message ? x.message : 'unknown error'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.stage = 1
|
this.stage = 1
|
||||||
this.err = e
|
this.originalResponse = x
|
||||||
this.mark('ERROR')
|
this.mark("SUCCESS")
|
||||||
})
|
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
|
||||||
|
origin_item.item_count = origin_item.item_count - this.details?.count!
|
||||||
|
}else {
|
||||||
|
throw x.msg ? x.msg : "unknown error"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
this.stage = 1
|
||||||
|
this.err = e
|
||||||
|
this.mark("ERROR")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(i: any): BankItem {
|
parse(i:any):BankItem {
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
this.originalRequest = i.originalRequest
|
this.originalRequest = i.originalRequest
|
||||||
this.originalResponse = i.originalResponse
|
this.originalResponse = i.originalResponse
|
||||||
@ -252,71 +232,69 @@ export class BankItem extends BasicOrder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface PrivateMarketRequest {
|
export interface PrivateMarketRequest {
|
||||||
item_uid: string
|
item_uid:string
|
||||||
qty: string
|
qty:string
|
||||||
account: string
|
account:string
|
||||||
currency: string
|
currency:string
|
||||||
price: number
|
price:number
|
||||||
private: number
|
private:number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrivateMarketResponse extends BasicResponse {}
|
export interface PrivateMarketResponse extends BasicResponse {}
|
||||||
|
|
||||||
export class PrivateMarket extends BasicOrder {
|
export class PrivateMarket extends BasicOrder{
|
||||||
order_type = 'PrivateMarket'
|
order_type = "PrivateMarket";
|
||||||
|
|
||||||
originalRequest: PrivateMarketRequest
|
originalRequest:PrivateMarketRequest
|
||||||
originalResponse?: PrivateMarketResponse
|
originalResponse?:PrivateMarketResponse
|
||||||
|
|
||||||
listingId?: string
|
listingId?: string
|
||||||
listingHash?: string
|
listingHash?: string
|
||||||
|
|
||||||
constructor(details: TxnDetails) {
|
constructor(details:TxnDetails) {
|
||||||
super(details)
|
super(details)
|
||||||
this.originalRequest = {
|
this.originalRequest = {
|
||||||
item_uid: details.item_uid,
|
item_uid: details.item_uid,
|
||||||
qty: details.count.toString(),
|
qty: details.count.toString(),
|
||||||
account: details.origin_account,
|
account: details.origin_account,
|
||||||
private: 1,
|
private: 1,
|
||||||
currency: '0',
|
currency: "0",
|
||||||
price: 1,
|
price: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async tick(r: RefStore, api: LTOApi): Promise<void> {
|
async tick(r:RefStore, api:LTOApi):Promise<void> {
|
||||||
if (this.state !== 'PENDING') {
|
if(this.state !== "PENDING" ){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.mark('WORKING')
|
this.mark("WORKING")
|
||||||
return api
|
return api.BankAction<PrivateMarketRequest, PrivateMarketResponse>("sell-item",this.originalRequest)
|
||||||
.BankAction<PrivateMarketRequest, PrivateMarketResponse>('sell-item', this.originalRequest)
|
.then((x)=>{
|
||||||
.then(x => {
|
debug("PrivateMarket",x)
|
||||||
debug('PrivateMarket', x)
|
if(x.status == 200){
|
||||||
if (x.status === 'success') {
|
|
||||||
this.stage = 1
|
|
||||||
this.originalResponse = x
|
|
||||||
this.mark('SUCCESS')
|
|
||||||
this.listingId = x.data.listing_id
|
|
||||||
this.listingHash = x.data.hash
|
|
||||||
if (this.details?.origin_path && this.details?.item_uid && this.details?.count) {
|
|
||||||
const inventory = r.invs.value.get(this.details.origin_path)
|
|
||||||
const origin_item = inventory?.items[this.details.item_uid]
|
|
||||||
if (origin_item) {
|
|
||||||
origin_item.item_count = origin_item.item_count - this.details.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw x.message ? x.message : 'unknown error'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.stage = 1
|
this.stage = 1
|
||||||
this.err = e
|
this.originalResponse = x
|
||||||
this.mark('ERROR')
|
this.mark("SUCCESS")
|
||||||
})
|
this.listingId = x.data["listing_id"]
|
||||||
|
this.listingHash = x.data["hash"]
|
||||||
|
try{
|
||||||
|
const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]!
|
||||||
|
origin_item.item_count = origin_item.item_count - this.details?.count!
|
||||||
|
}catch(e){
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
throw x.msg ? x.msg : "unknown error"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
this.stage = 1
|
||||||
|
this.err = e
|
||||||
|
this.mark("ERROR")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(i: any): PrivateMarket {
|
parse(i:any):PrivateMarket {
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
this.originalRequest = i.originalRequest
|
this.originalRequest = i.originalRequest
|
||||||
this.originalResponse = i.originalResponse
|
this.originalResponse = i.originalResponse
|
||||||
@ -326,10 +304,11 @@ export class PrivateMarket extends BasicOrder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface MarketMoveRequest {
|
export interface MarketMoveRequest {
|
||||||
listing_id?: string
|
listing_id?: string
|
||||||
qty: string
|
qty:string
|
||||||
account: string
|
account:string
|
||||||
character: string
|
character: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,77 +316,77 @@ export interface MarketMoveResponse extends BasicResponse {
|
|||||||
item_uid: string
|
item_uid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class MarketMove extends PrivateMarket {
|
export class MarketMove extends PrivateMarket {
|
||||||
order_type = 'MarketMove'
|
order_type = "MarketMove";
|
||||||
|
|
||||||
moveRequest: MarketMoveRequest
|
moveRequest:MarketMoveRequest
|
||||||
moveResponse?: MarketMoveResponse
|
moveResponse?:MarketMoveResponse
|
||||||
|
|
||||||
moveStage: number
|
moveStage:number
|
||||||
moveState: TxnState
|
moveState: TxnState
|
||||||
|
|
||||||
newUid: string
|
newUid: string
|
||||||
|
|
||||||
constructor(details: TxnDetails) {
|
constructor(details:TxnDetails) {
|
||||||
super(details)
|
super(details)
|
||||||
this.moveStage = 0
|
this.moveStage = 0
|
||||||
this.moveState = 'PENDING'
|
this.moveState = "PENDING"
|
||||||
this.newUid = ''
|
this.newUid = ""
|
||||||
this.moveRequest = {
|
this.moveRequest = {
|
||||||
qty: details.count.toString(),
|
qty: details.count.toString(),
|
||||||
account: details.target_account,
|
account: details.target_account,
|
||||||
character: details.target_path.includes('/') ? details.target : '0',
|
character: (details.target_path.includes("/")) ? details.target : "0" ,
|
||||||
listing_id: '', // not initially populated
|
listing_id: "", // not initially populated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async tick(r: RefStore, api: LTOApi): Promise<void> {
|
async tick(r:RefStore, api:LTOApi):Promise<void> {
|
||||||
try {
|
try {
|
||||||
await super.tick(r, api)
|
await super.tick(r, api)
|
||||||
} catch (_e) {
|
}catch(e){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch (super.status()) {
|
switch(super.status()) {
|
||||||
case 'SUCCESS':
|
case "SUCCESS":
|
||||||
break
|
break;
|
||||||
case 'ERROR':
|
case "ERROR":
|
||||||
this.moveState = 'ERROR'
|
this.moveState = "ERROR"
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.moveState !== 'PENDING') {
|
if(this.moveState !== "PENDING" ){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.moveRequest.listing_id = `${this.listingId}-${this.listingHash}`
|
this.moveRequest.listing_id = `${this.listingId}-${this.listingHash}`
|
||||||
this.moveState = 'WORKING'
|
this.moveState = "WORKING"
|
||||||
return api
|
return api.BankAction<MarketMoveRequest, MarketMoveResponse>("buy-from-order",this.moveRequest)
|
||||||
.BankAction<MarketMoveRequest, MarketMoveResponse>('buy-from-order', this.moveRequest)
|
.then((x)=>{
|
||||||
.then(x => {
|
debug("MarketMove",x)
|
||||||
debug('MarketMove', x)
|
this.moveResponse = x
|
||||||
this.moveResponse = x
|
if(x.status == 200){
|
||||||
if (x.status === 'success') {
|
|
||||||
this.moveStage = 1
|
|
||||||
this.moveState = 'SUCCESS'
|
|
||||||
this.newUid = x.item_uid
|
|
||||||
} else {
|
|
||||||
throw x ? x : 'unknown error'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.moveStage = 1
|
this.moveStage = 1
|
||||||
this.err = e
|
this.moveState = "SUCCESS"
|
||||||
this.moveState = 'ERROR'
|
this.newUid = x.item_uid
|
||||||
})
|
}else {
|
||||||
|
throw x ? x : "unknown error"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
this.moveStage = 1
|
||||||
|
this.err = e
|
||||||
|
this.moveState = "ERROR"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
progress(): [number, number] {
|
progress():[number,number]{
|
||||||
return [this.stage + this.moveStage, 2]
|
return [this.stage + this.moveStage, 2]
|
||||||
}
|
}
|
||||||
status(): string {
|
status():string {
|
||||||
return this.moveState
|
return this.moveState
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(i: any): MarketMove {
|
parse(i:any):MarketMove {
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
this.moveRequest = i.moveRequest
|
this.moveRequest = i.moveRequest
|
||||||
this.moveResponse = i.moveResponse
|
this.moveResponse = i.moveResponse
|
||||||
@ -418,72 +397,71 @@ export class MarketMove extends PrivateMarket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MarketMoveToChar extends MarketMove {
|
export class MarketMoveToChar extends MarketMove {
|
||||||
order_type = 'MarketMoveToChar'
|
order_type = "MarketMoveToChar";
|
||||||
|
|
||||||
charRequest: InternalXferRequest
|
charRequest:InternalXferRequest
|
||||||
charResponse?: InternalXferResponse
|
charResponse?:InternalXferResponse
|
||||||
|
|
||||||
charStage: number
|
charStage:number
|
||||||
charState: TxnState
|
charState: TxnState
|
||||||
|
|
||||||
constructor(details: TxnDetails) {
|
constructor(details:TxnDetails) {
|
||||||
super(details)
|
super(details)
|
||||||
this.charStage = 0
|
this.charStage = 0
|
||||||
this.charState = 'PENDING'
|
this.charState = "PENDING"
|
||||||
this.charRequest = {
|
this.charRequest = {
|
||||||
item_uid: '',
|
item_uid: "",
|
||||||
qty: details.count.toString(),
|
qty: details.count.toString(),
|
||||||
new_char: details.target,
|
new_char: details.target,
|
||||||
account: details.target_account,
|
account: details.target_account,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async tick(r: RefStore, api: LTOApi): Promise<void> {
|
async tick(r:RefStore, api:LTOApi):Promise<void> {
|
||||||
try {
|
try {
|
||||||
await super.tick(r, api)
|
await super.tick(r, api)
|
||||||
} catch (_e) {
|
}catch(e){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch (super.status()) {
|
switch(super.status()) {
|
||||||
case 'SUCCESS':
|
case "SUCCESS":
|
||||||
break
|
break;
|
||||||
case 'ERROR':
|
case "ERROR":
|
||||||
this.charState = 'ERROR'
|
this.charState = "ERROR"
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.charState !== 'PENDING') {
|
if(this.charState !== "PENDING" ){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.charState = 'WORKING'
|
this.charState = "WORKING"
|
||||||
this.charRequest.item_uid = this.newUid
|
this.charRequest.item_uid = this.newUid
|
||||||
return api
|
return api.BankAction<InternalXferRequest, InternalXferResponse>("internal-xfer-item",this.charRequest)
|
||||||
.BankAction<InternalXferRequest, InternalXferResponse>('internal-xfer-item', this.charRequest)
|
.then((x)=>{
|
||||||
.then(x => {
|
debug("MarketMoveToChar",x)
|
||||||
debug('MarketMoveToChar', x)
|
this.charResponse = x
|
||||||
this.charResponse = x
|
if(x.status == 200){
|
||||||
if (x.status === 'success') {
|
|
||||||
this.charStage = 1
|
|
||||||
this.charState = 'SUCCESS'
|
|
||||||
} else {
|
|
||||||
throw x ? x : 'unknown error'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
this.charStage = 1
|
this.charStage = 1
|
||||||
this.err = e
|
this.charState = "SUCCESS"
|
||||||
this.charState = 'ERROR'
|
}else {
|
||||||
})
|
throw x ? x : "unknown error"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
this.charStage = 1
|
||||||
|
this.err = e
|
||||||
|
this.charState = "ERROR"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
progress(): [number, number] {
|
progress():[number,number]{
|
||||||
return [this.stage + this.moveStage + this.charStage, 3]
|
return [this.stage +this.moveStage+ this.charStage, 3]
|
||||||
}
|
}
|
||||||
status(): string {
|
status():string {
|
||||||
return this.charState
|
return this.charState
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(i: any): MarketMoveToChar {
|
parse(i:any):MarketMoveToChar {
|
||||||
super.parse(i)
|
super.parse(i)
|
||||||
this.charRequest = i.charRequest
|
this.charRequest = i.charRequest
|
||||||
this.charResponse = i.charResponse
|
this.charResponse = i.charResponse
|
||||||
|
|||||||
@ -1,79 +1,72 @@
|
|||||||
import { RefStore } from '../../state/state'
|
import { RefStore } from "../../state/state";
|
||||||
import { Serializable } from '../storage'
|
import { Serializable } from "../storage";
|
||||||
import { TricksterCharacter } from '../trickster'
|
import { LTOApi } from "./api";
|
||||||
import { LTOApi } from './api'
|
import { pathIsBank, splitPath } from "./lifeto";
|
||||||
import { pathIsBank, splitPath } from './lifeto'
|
import { BankItem, InternalXfer, InvalidOrder, MarketMove, Order,MarketMoveToChar, TxnDetails } from "./order";
|
||||||
import {
|
|
||||||
BankItem,
|
|
||||||
InternalXfer,
|
|
||||||
InvalidOrder,
|
|
||||||
MarketMove,
|
|
||||||
MarketMoveToChar,
|
|
||||||
Order,
|
|
||||||
TxnDetails,
|
|
||||||
} from './order'
|
|
||||||
|
|
||||||
export interface OrderDetails {
|
export interface OrderDetails {
|
||||||
item_uid: string | 'galders'
|
item_uid: string | "galders"
|
||||||
count: number
|
count:number
|
||||||
origin_path: string
|
origin_path:string
|
||||||
target_path: string
|
target_path:string
|
||||||
}
|
}
|
||||||
|
|
||||||
const notSupported = new InvalidOrder('not supported yet')
|
const notSupported = new InvalidOrder("not supported yet")
|
||||||
const notFound = new InvalidOrder('character not found')
|
const notFound = new InvalidOrder("character not found")
|
||||||
|
|
||||||
export class OrderTracker implements Serializable<OrderTracker> {
|
export class OrderTracker implements Serializable<OrderTracker> {
|
||||||
orders: { [key: string]: Order } = {}
|
orders: {[key:string]:Order} = {}
|
||||||
|
|
||||||
async tick(r: RefStore, api: LTOApi): Promise<any> {
|
async tick(r:RefStore, api:LTOApi):Promise<any> {
|
||||||
let hasDirty = false
|
let hasDirty = false
|
||||||
for (const [id, order] of Object.entries(this.orders)) {
|
console.log("ticking")
|
||||||
if (order.status() === 'SUCCESS' || order.status() === 'ERROR') {
|
for(const [id, order] of Object.entries(this.orders)) {
|
||||||
|
if(order.status() == "SUCCESS" || order.status() == "ERROR") {
|
||||||
|
console.log("finished order", order)
|
||||||
hasDirty = true
|
hasDirty = true
|
||||||
delete this.orders[id]
|
delete this.orders[id]
|
||||||
}
|
}
|
||||||
order.tick(r, api)
|
order.tick(r,api)
|
||||||
}
|
}
|
||||||
if (hasDirty) {
|
if(hasDirty){
|
||||||
r.dirty.value++
|
r.dirty.value++
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(s: any): OrderTracker {
|
parse(s: any): OrderTracker {
|
||||||
if (s === undefined) {
|
if(s == undefined) {
|
||||||
return new OrderTracker()
|
return new OrderTracker()
|
||||||
}
|
}
|
||||||
if (s.orders === undefined) {
|
if(s.orders == undefined) {
|
||||||
return new OrderTracker()
|
return new OrderTracker()
|
||||||
}
|
}
|
||||||
this.orders = {}
|
this.orders = {}
|
||||||
const raw: Order[] = Object.values(s.orders)
|
const raw: Order[] = Object.values(s.orders)
|
||||||
for (const o of raw) {
|
for(const o of raw) {
|
||||||
let newOrder: Order | undefined
|
let newOrder:Order | undefined = undefined
|
||||||
if (o.details) {
|
console.log("loading", o)
|
||||||
if (o.status() === 'SUCCESS' || o.status() === 'ERROR') {
|
if(o.details){
|
||||||
|
if(o.status() == "SUCCESS" || o.status() == "ERROR") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch (o.order_type) {
|
switch(o.order_type) {
|
||||||
case 'InternalXfer':
|
case "InternalXfer":
|
||||||
newOrder = new InternalXfer(o.details).parse(o)
|
newOrder = new InternalXfer(o.details).parse(o)
|
||||||
break
|
break;
|
||||||
case 'BankItem':
|
case "BankItem":
|
||||||
newOrder = new BankItem(o.details).parse(o)
|
newOrder = new BankItem(o.details).parse(o)
|
||||||
break
|
break;
|
||||||
case 'MarketMove':
|
case "MarketMove":
|
||||||
newOrder = new MarketMove(o.details).parse(o)
|
newOrder = new MarketMove(o.details).parse(o)
|
||||||
break
|
case "MarketMoveToChar":
|
||||||
case 'MarketMoveToChar':
|
|
||||||
newOrder = new MarketMoveToChar(o.details).parse(o)
|
newOrder = new MarketMoveToChar(o.details).parse(o)
|
||||||
break
|
break;
|
||||||
case 'InvalidOrder':
|
case "InvalidOrder":
|
||||||
newOrder = new InvalidOrder('').parse(o)
|
newOrder = new InvalidOrder("").parse(o)
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
if (newOrder) {
|
if(newOrder) {
|
||||||
this.orders[newOrder.action_id] = newOrder
|
this.orders[newOrder.action_id] = newOrder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,85 +76,81 @@ export class OrderTracker implements Serializable<OrderTracker> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class OrderSender {
|
export class OrderSender {
|
||||||
constructor(
|
r: RefStore
|
||||||
private orders: OrderTracker,
|
constructor(r:RefStore) {
|
||||||
private chars: Map<string, TricksterCharacter>,
|
this.r = r
|
||||||
) {}
|
}
|
||||||
|
|
||||||
send(o: OrderDetails): Order {
|
send(o:OrderDetails):Order {
|
||||||
const formed = this.form(o)
|
const formed = this.form(o)
|
||||||
this.orders.orders[formed.action_id] = formed
|
this.r.orders.value.orders[formed.action_id] = formed
|
||||||
return formed
|
return formed
|
||||||
}
|
}
|
||||||
|
|
||||||
form(o: OrderDetails): Order {
|
form(o:OrderDetails):Order {
|
||||||
// bank to bank
|
// bank to bank
|
||||||
if (pathIsBank(o.origin_path) && pathIsBank(o.target_path)) {
|
if(pathIsBank(o.origin_path) && pathIsBank(o.target_path)) {
|
||||||
return this.bank_to_bank(o)
|
return this.bank_to_bank(o)
|
||||||
}
|
}
|
||||||
// bank to user
|
// bank to user
|
||||||
if (pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) {
|
if(pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) {
|
||||||
return this.bank_to_user(o)
|
return this.bank_to_user(o)
|
||||||
}
|
}
|
||||||
// user to bank
|
// user to bank
|
||||||
if (!pathIsBank(o.origin_path) && pathIsBank(o.target_path)) {
|
if(!pathIsBank(o.origin_path) && pathIsBank(o.target_path)) {
|
||||||
return this.user_to_bank(o)
|
return this.user_to_bank(o)
|
||||||
}
|
}
|
||||||
// user to user
|
// user to user
|
||||||
if (!pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) {
|
if(!pathIsBank(o.origin_path) && !pathIsBank(o.target_path)) {
|
||||||
return this.user_to_user(o)
|
return this.user_to_user(o)
|
||||||
}
|
}
|
||||||
return notSupported
|
return notSupported
|
||||||
}
|
}
|
||||||
bank_to_bank(o: OrderDetails): Order {
|
bank_to_bank(o:OrderDetails): Order{
|
||||||
const origin = this.chars.get(o.origin_path)
|
const origin = this.r.chars.value.get(o.origin_path)
|
||||||
const target = this.chars.get(o.target_path)
|
const target = this.r.chars.value.get(o.target_path)
|
||||||
if (!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
return new MarketMove(this.transformInternalOrder(o))
|
return new MarketMove(this.transformInternalOrder(o))
|
||||||
}
|
}
|
||||||
bank_to_user(o: OrderDetails): Order {
|
bank_to_user(o:OrderDetails): Order{
|
||||||
// get the uid of the bank
|
// get the uid of the bank
|
||||||
const origin = this.chars.get(o.origin_path)
|
const origin = this.r.chars.value.get(o.origin_path)
|
||||||
const target = this.chars.get(o.target_path)
|
const target = this.r.chars.value.get(o.target_path)
|
||||||
if (!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
const [_account, _name] = splitPath(target.path)
|
const [account, name] = splitPath(target.path)
|
||||||
/*if(account != origin.path) {
|
if(account != origin.path) {
|
||||||
return new MarketMoveToChar(this.transformInternalOrder(o))
|
return new MarketMoveToChar(this.transformInternalOrder(o))
|
||||||
}*/
|
}
|
||||||
return new InternalXfer(this.transformInternalOrder(o))
|
return new InternalXfer(this.transformInternalOrder(o))
|
||||||
}
|
}
|
||||||
user_to_bank(o: OrderDetails): Order {
|
user_to_bank(o:OrderDetails): Order{
|
||||||
const origin = this.chars.get(o.origin_path)
|
const origin = this.r.chars.value.get(o.origin_path)
|
||||||
const target = this.chars.get(o.target_path)
|
const target = this.r.chars.value.get(o.target_path)
|
||||||
if (!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
const [_account, _name] = splitPath(origin.path)
|
const [account, name] = splitPath(origin.path)
|
||||||
/*if(account != target.path) {
|
if(account != target.path) {
|
||||||
return new MarketMove(this.transformInternalOrder(o))
|
return new MarketMove(this.transformInternalOrder(o))
|
||||||
}*/
|
}
|
||||||
return new BankItem(this.transformInternalOrder(o))
|
return new BankItem(this.transformInternalOrder(o))
|
||||||
}
|
}
|
||||||
user_to_user(o: OrderDetails): Order {
|
user_to_user(o:OrderDetails): Order{
|
||||||
const origin = this.chars.get(o.origin_path)
|
const origin = this.r.chars.value.get(o.origin_path)
|
||||||
const target = this.chars.get(o.target_path)
|
const target = this.r.chars.value.get(o.target_path)
|
||||||
if (!(origin && target)) {
|
if(!(origin && target)) {
|
||||||
return notFound
|
return notFound
|
||||||
}
|
}
|
||||||
// return new MarketMoveToChar(this.transformInternalOrder(o))
|
return new MarketMoveToChar(this.transformInternalOrder(o))
|
||||||
return new InternalXfer(this.transformInternalOrder(o))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformInternalOrder(o: OrderDetails): TxnDetails {
|
private transformInternalOrder(o:OrderDetails):TxnDetails {
|
||||||
const origin = this.chars.get(o.origin_path)
|
const origin = this.r.chars.value.get(o.origin_path)!
|
||||||
const target = this.chars.get(o.target_path)
|
const target = this.r.chars.value.get(o.target_path)!
|
||||||
if (!origin || !target) {
|
|
||||||
throw new Error(`Character not found: origin=${o.origin_path}, target=${o.target_path}`)
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
origin: origin.id.toString(),
|
origin: origin.id.toString(),
|
||||||
target: target.id.toString(),
|
target: target.id.toString(),
|
||||||
@ -174,3 +163,4 @@ export class OrderSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,53 +1,49 @@
|
|||||||
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 { BankEndpoint, LTOApi } from './api'
|
import { BankEndpoint, LTOApi } from "./api";
|
||||||
|
|
||||||
export interface SessionBinding {
|
export interface SessionBinding {
|
||||||
new (s: Session): LTOApi
|
new(s:Session):LTOApi
|
||||||
}
|
}
|
||||||
export const getLTOState = <A extends LTOApi>(
|
export const getLTOState = <A extends LTOApi>(c: new (s:Session) => A,s:Session, r:RefStore): LTOApi => {
|
||||||
c: new (s: Session) => A,
|
return new StatefulLTOApi(new c(s),r);
|
||||||
s: Session,
|
|
||||||
r: RefStore,
|
|
||||||
): LTOApi => {
|
|
||||||
return new StatefulLTOApi(new c(s), r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StatefulLTOApi implements LTOApi {
|
export class StatefulLTOApi implements LTOApi {
|
||||||
u: LTOApi
|
u: LTOApi
|
||||||
r: RefStore
|
r: RefStore
|
||||||
constructor(s: LTOApi, r: RefStore) {
|
constructor(s:LTOApi, r:RefStore){
|
||||||
this.u = s
|
this.u = s
|
||||||
this.r = r
|
this.r=r
|
||||||
}
|
}
|
||||||
BankAction = <T, D>(e: BankEndpoint, t: T): Promise<D> => {
|
BankAction = <T,D>(e: BankEndpoint, t: T):Promise<D> => {
|
||||||
return this.u.BankAction(e, t)
|
return this.u.BankAction(e,t)
|
||||||
}
|
}
|
||||||
GetInventory = async (path: string): Promise<TricksterInventory> => {
|
GetInventory = async (path:string):Promise<TricksterInventory>=>{
|
||||||
const inv = await this.u.GetInventory(path)
|
const inv = await this.u.GetInventory(path)
|
||||||
const existingInv = this.r.invs.value.get(inv.path)
|
if(this.r.invs.value.get(inv.path)){
|
||||||
if (existingInv) {
|
this.r.invs.value.get(inv.path)!.items = inv.items
|
||||||
existingInv.items = inv.items
|
}else{
|
||||||
if (inv.galders) {
|
this.r.invs.value.set(inv.path,inv)
|
||||||
existingInv.galders = inv.galders
|
}
|
||||||
}
|
if(inv.galders) {
|
||||||
} else {
|
this.r.invs.value.get(inv.path)!.galders = inv.galders
|
||||||
this.r.invs.value.set(inv.path, inv)
|
|
||||||
}
|
}
|
||||||
this.r.dirty.value = this.r.dirty.value + 1
|
this.r.dirty.value = this.r.dirty.value + 1
|
||||||
return inv
|
return inv
|
||||||
}
|
}
|
||||||
GetAccounts = async (): Promise<TricksterAccount[]> => {
|
GetAccounts = async ():Promise<TricksterAccount[]> => {
|
||||||
const xs = await this.u.GetAccounts()
|
const xs = await this.u.GetAccounts()
|
||||||
xs.forEach(x => {
|
xs.forEach((x)=>{
|
||||||
x.characters.forEach(ch => {
|
x.characters.forEach((ch)=>{
|
||||||
this.r.chars.value.set(ch.path, ch)
|
this.r.chars.value.set(ch.path,ch)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return xs
|
return xs
|
||||||
}
|
}
|
||||||
GetLoggedin = async (): Promise<boolean> => {
|
GetLoggedin= async ():Promise<boolean>=>{
|
||||||
return this.u.GetLoggedin()
|
return this.u.GetLoggedin()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
623
src/lib/old.ts
623
src/lib/old.ts
@ -1,36 +1,37 @@
|
|||||||
import Handsontable from 'handsontable'
|
import Handsontable from "handsontable"
|
||||||
import Core from 'handsontable/core'
|
import numbro from 'numbro';
|
||||||
import { textRenderer } from 'handsontable/renderers'
|
import { textRenderer } from "handsontable/renderers"
|
||||||
import numbro from 'numbro'
|
import { TricksterInventory, TricksterItem } from "./trickster"
|
||||||
import { TricksterItem } from './trickster'
|
import Core from "handsontable/core";
|
||||||
|
import { RefStore } from "../state/state";
|
||||||
|
|
||||||
export const BasicColumns = ['Image', 'Name', 'Count'] as const
|
|
||||||
|
|
||||||
export const DetailsColumns = ['Desc', 'Use'] as const
|
export const BasicColumns = [
|
||||||
|
"Image","Name","Count",
|
||||||
export const MoveColumns = ['MoveCount', 'Move'] as const
|
|
||||||
|
|
||||||
export const TagColumns = ['All', 'Equip', 'Drill', 'Card', 'Quest', 'Consume', 'Compound'] as const
|
|
||||||
|
|
||||||
export const EquipmentColumns = ['MinLvl', 'Slots', 'RefineNumber', 'RefineState'] as const
|
|
||||||
|
|
||||||
export const StatsColumns = [
|
|
||||||
'AP',
|
|
||||||
'GunAP',
|
|
||||||
'AC',
|
|
||||||
'DX',
|
|
||||||
'MP',
|
|
||||||
'MA',
|
|
||||||
'MD',
|
|
||||||
'WT',
|
|
||||||
'DA',
|
|
||||||
'LK',
|
|
||||||
'HP',
|
|
||||||
'DP',
|
|
||||||
'HV',
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const HackColumns = [] as const
|
export const DetailsColumns = [
|
||||||
|
"Desc","Use",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const MoveColumns = [
|
||||||
|
"MoveCount","Move",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const TagColumns = [
|
||||||
|
"All","Equip","Drill","Card","Quest","Consume", "Compound"
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const EquipmentColumns = [
|
||||||
|
"MinLvl","Slots","RefineNumber","RefineState",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const StatsColumns = [
|
||||||
|
"AP","GunAP","AC","DX","MP","MA","MD","WT","DA","LK","HP","DP","HV",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const HackColumns = [
|
||||||
|
] as const
|
||||||
|
|
||||||
export const ColumnNames = [
|
export const ColumnNames = [
|
||||||
...BasicColumns,
|
...BasicColumns,
|
||||||
@ -42,520 +43,478 @@ export const ColumnNames = [
|
|||||||
...HackColumns,
|
...HackColumns,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type ColumnName = (typeof ColumnNames)[number]
|
export type ColumnName = typeof ColumnNames[number]
|
||||||
|
|
||||||
const c = (a: ColumnName | ColumnInfo): ColumnName => {
|
const c = (a:ColumnName | ColumnInfo):ColumnName => {
|
||||||
switch (typeof a) {
|
switch(typeof a) {
|
||||||
case 'string':
|
case "string":
|
||||||
return a
|
return a
|
||||||
case 'object':
|
case "object":
|
||||||
return a.name
|
return a.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const LazyColumn = c
|
export const LazyColumn = c;
|
||||||
|
|
||||||
export const ColumnSorter = (a: ColumnName | ColumnInfo, b: ColumnName | ColumnInfo): number => {
|
export const ColumnSorter = (a:ColumnName | ColumnInfo, b: ColumnName | ColumnInfo):number => {
|
||||||
const n1 = ColumnNames.indexOf(c(a))
|
let n1 = ColumnNames.indexOf(c(a))
|
||||||
const n2 = ColumnNames.indexOf(c(b))
|
let n2 = ColumnNames.indexOf(c(b))
|
||||||
if (n1 === n2) {
|
if(n1 == n2) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return n1 > n2 ? 1 : -1
|
return n1 > n2 ? 1 : -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnInfo {
|
export interface ColumnInfo {
|
||||||
name: ColumnName
|
name: ColumnName
|
||||||
displayName: string
|
displayName:string
|
||||||
|
|
||||||
options?: (s: string[]) => string[]
|
options?:(s:string[])=>string[]
|
||||||
renderer?: any
|
renderer?:any
|
||||||
filtering?: boolean
|
filtering?:boolean
|
||||||
writable?: boolean
|
writable?:boolean
|
||||||
getter(item: TricksterItem): string | number
|
getter(item:TricksterItem):(string | number)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Image implements ColumnInfo {
|
class Image implements ColumnInfo {
|
||||||
name: ColumnName = 'Image'
|
name:ColumnName = 'Image'
|
||||||
displayName = ' '
|
displayName = " "
|
||||||
renderer = coverRenderer
|
renderer = coverRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number) {
|
||||||
return item.item_image ? item.item_image : ''
|
return item.image ? item.image : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function coverRenderer(
|
function coverRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
|
||||||
_row: any,
|
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
if (stringifiedValue.startsWith('http')) {
|
if (stringifiedValue.startsWith('http')) {
|
||||||
const img: any = document.createElement('IMG')
|
const img:any = document.createElement('IMG');
|
||||||
img.src = value
|
img.src = value;
|
||||||
Handsontable.dom.addEvent(img, 'mousedown', event => {
|
Handsontable.dom.addEvent(img, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(img)
|
td.appendChild(img);
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Name implements ColumnInfo {
|
class Name implements ColumnInfo {
|
||||||
name: ColumnName = 'Name'
|
name:ColumnName = "Name"
|
||||||
displayName = 'Name'
|
displayName = "Name"
|
||||||
filtering = true
|
filtering = true
|
||||||
renderer = nameRenderer
|
renderer = nameRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_name
|
return item.item_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function nameRenderer(
|
function nameRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
let showText = stringifiedValue;
|
||||||
_row: any,
|
const div= document.createElement('div');
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
const showText = stringifiedValue
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerHTML = showText
|
div.innerHTML = showText
|
||||||
div.title = showText
|
div.title = showText
|
||||||
div.style.maxWidth = '20ch'
|
div.style.maxWidth = "20ch"
|
||||||
div.style.textOverflow = 'ellipsis'
|
div.style.textOverflow = "ellipsis"
|
||||||
div.style.overflow = 'hidden'
|
div.style.overflow= "hidden"
|
||||||
div.style.whiteSpace = 'nowrap'
|
div.style.whiteSpace= "nowrap"
|
||||||
Handsontable.dom.addEvent(div, 'mousedown', event => {
|
Handsontable.dom.addEvent(div, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(div)
|
td.appendChild(div);
|
||||||
td.classList.add('htLeft')
|
td.classList.add("htLeft")
|
||||||
}
|
}
|
||||||
|
|
||||||
class Count implements ColumnInfo {
|
class Count implements ColumnInfo {
|
||||||
name: ColumnName = 'Count'
|
name:ColumnName = "Count"
|
||||||
displayName = 'Count'
|
displayName = "Count"
|
||||||
renderer = 'numeric'
|
renderer = "numeric"
|
||||||
filtering = true
|
filtering = true
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_count
|
return item.item_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMoveTargets = (invs: string[]): string[] => {
|
class Move implements ColumnInfo {
|
||||||
const out: string[] = []
|
name:ColumnName = "Move"
|
||||||
for (const k of invs) {
|
displayName = "Target"
|
||||||
|
writable = true
|
||||||
|
options = getMoveTargets
|
||||||
|
getter(item:TricksterItem):(string|number){
|
||||||
|
return "---------------------------------------------"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMoveTargets = (invs: string[]):string[] => {
|
||||||
|
let out:string[] = [];
|
||||||
|
for(const k of invs){
|
||||||
out.push(k)
|
out.push(k)
|
||||||
}
|
}
|
||||||
out.push('')
|
out.push("")
|
||||||
out.push('')
|
out.push("")
|
||||||
out.push('!TRASH')
|
out.push("!TRASH")
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
class Move implements ColumnInfo {
|
|
||||||
name: ColumnName = 'Move'
|
|
||||||
displayName = 'Target'
|
|
||||||
writable = true
|
|
||||||
options = getMoveTargets
|
|
||||||
getter(_item: TricksterItem): string | number {
|
|
||||||
return '---------------------------------------------'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MoveCount implements ColumnInfo {
|
class MoveCount implements ColumnInfo {
|
||||||
name: ColumnName = 'MoveCount'
|
name:ColumnName = "MoveCount"
|
||||||
displayName = 'Move #'
|
displayName = "Move #"
|
||||||
renderer = moveCountRenderer
|
renderer = moveCountRenderer
|
||||||
writable = true
|
writable = true
|
||||||
getter(_item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return ''
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCountRenderer(
|
function moveCountRenderer(instance:Core, td:any, row:number, col:number, prop:any, value:any, cellProperties:any) {
|
||||||
instance: Core,
|
let newValue = value;
|
||||||
td: any,
|
|
||||||
row: number,
|
|
||||||
col: number,
|
|
||||||
prop: any,
|
|
||||||
value: any,
|
|
||||||
cellProperties: any,
|
|
||||||
) {
|
|
||||||
let newValue = value
|
|
||||||
|
|
||||||
if (Handsontable.helper.isNumeric(newValue)) {
|
if (Handsontable.helper.isNumeric(newValue)) {
|
||||||
const numericFormat = cellProperties.numericFormat
|
const numericFormat = cellProperties.numericFormat;
|
||||||
const cellCulture = numericFormat?.culture || '-'
|
const cellCulture = numericFormat && numericFormat.culture || '-';
|
||||||
const cellFormatPattern = numericFormat?.pattern
|
const cellFormatPattern = numericFormat && numericFormat.pattern;
|
||||||
const className = cellProperties.className || ''
|
const className = cellProperties.className || '';
|
||||||
const classArr = className.length ? className.split(' ') : []
|
const classArr = className.length ? className.split(' ') : [];
|
||||||
if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) {
|
if (typeof cellCulture !== 'undefined' && !numbro.languages()[cellCulture]) {
|
||||||
const shortTag: any = cellCulture.replace('-', '')
|
const shortTag:any = cellCulture.replace('-', '');
|
||||||
const langData = (numbro as any)[shortTag]
|
const langData = (numbro as any)[shortTag];
|
||||||
|
|
||||||
if (langData) {
|
if (langData) {
|
||||||
numbro.registerLanguage(langData)
|
numbro.registerLanguage(langData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const totalCount = Number(instance.getCell(row, col - 1)?.innerHTML)
|
const totalCount = Number(instance.getCell(row,col-1)?.innerHTML)
|
||||||
numbro.setLanguage(cellCulture)
|
numbro.setLanguage(cellCulture);
|
||||||
const num = numbro(newValue)
|
const num = numbro(newValue)
|
||||||
if (totalCount < num.value()) {
|
if(totalCount < num.value()) {
|
||||||
const newNum = numbro(totalCount)
|
const newNum = numbro(totalCount)
|
||||||
newValue = newNum.format(cellFormatPattern || '0')
|
newValue = newNum.format(cellFormatPattern || '0');
|
||||||
} else {
|
}else {
|
||||||
newValue = num.format(cellFormatPattern || '0')
|
newValue = num.format(cellFormatPattern || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (classArr.indexOf('htLeft') < 0 && classArr.indexOf('htCenter') < 0 &&
|
||||||
classArr.indexOf('htLeft') < 0 &&
|
classArr.indexOf('htRight') < 0 && classArr.indexOf('htJustify') < 0) {
|
||||||
classArr.indexOf('htCenter') < 0 &&
|
classArr.push('htRight');
|
||||||
classArr.indexOf('htRight') < 0 &&
|
|
||||||
classArr.indexOf('htJustify') < 0
|
|
||||||
) {
|
|
||||||
classArr.push('htRight')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classArr.indexOf('htNumeric') < 0) {
|
if (classArr.indexOf('htNumeric') < 0) {
|
||||||
classArr.push('htNumeric')
|
classArr.push('htNumeric');
|
||||||
}
|
}
|
||||||
cellProperties.className = classArr.join(' ')
|
cellProperties.className = classArr.join(' ');
|
||||||
|
|
||||||
td.dir = 'ltr'
|
td.dir = 'ltr';
|
||||||
newValue = `${newValue}x`
|
newValue = newValue + "x"
|
||||||
} else {
|
}else {
|
||||||
newValue = ''
|
newValue = ""
|
||||||
}
|
}
|
||||||
textRenderer(instance, td, row, col, prop, newValue, cellProperties)
|
textRenderer(instance, td, row, col, prop, newValue, cellProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Equip implements ColumnInfo {
|
class Equip implements ColumnInfo {
|
||||||
name: ColumnName = 'Equip'
|
name:ColumnName = "Equip"
|
||||||
displayName = 'equip'
|
displayName = "equip"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.is_equip ? 1 : 0
|
return item.is_equip ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Drill implements ColumnInfo {
|
class Drill implements ColumnInfo {
|
||||||
name: ColumnName = 'Drill'
|
name:ColumnName = "Drill"
|
||||||
displayName = 'drill'
|
displayName = "drill"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.is_drill ? 1 : 0
|
return item.is_drill ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class All implements ColumnInfo {
|
class All implements ColumnInfo {
|
||||||
name: ColumnName = 'All'
|
name:ColumnName = "All"
|
||||||
displayName = 'swap'
|
displayName = "swap"
|
||||||
getter(_: TricksterItem): string | number {
|
getter(_:TricksterItem):(string|number){
|
||||||
return -10000
|
return -10000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Card implements ColumnInfo {
|
class Card implements ColumnInfo {
|
||||||
name: ColumnName = 'Card'
|
name:ColumnName = "Card"
|
||||||
displayName = 'card'
|
displayName = "card"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return cardFilter(item) ? 1 : 0
|
return cardFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardFilter = (item: TricksterItem): boolean => {
|
const cardFilter= (item:TricksterItem): boolean => {
|
||||||
return item.item_name.endsWith(' Card') || item.item_name.startsWith('Star Card')
|
return (item.item_name.endsWith(" Card") || item.item_name.startsWith("Star Card"))
|
||||||
}
|
}
|
||||||
class Compound implements ColumnInfo {
|
class Compound implements ColumnInfo {
|
||||||
name: ColumnName = 'Compound'
|
name:ColumnName = "Compound"
|
||||||
displayName = 'comp'
|
displayName = "comp"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return compFilter(item) ? 1 : 0
|
return compFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const compFilter = (item: TricksterItem): boolean => {
|
const compFilter= (item:TricksterItem): boolean => {
|
||||||
return item.item_comment.toLowerCase().includes('compound item')
|
return (item.item_desc.toLowerCase().includes("compound item"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Quest implements ColumnInfo {
|
class Quest implements ColumnInfo {
|
||||||
name: ColumnName = 'Quest'
|
name:ColumnName = "Quest"
|
||||||
displayName = 'quest'
|
displayName = "quest"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return questFilter(item) ? 1 : 0
|
return questFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const questFilter = (_item: TricksterItem): boolean => {
|
const questFilter= (item:TricksterItem): boolean => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
class Consume implements ColumnInfo {
|
class Consume implements ColumnInfo {
|
||||||
name: ColumnName = 'Consume'
|
name:ColumnName = "Consume"
|
||||||
displayName = 'eat'
|
displayName = "eat"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return consumeFilter(item) ? 1 : 0
|
return consumeFilter(item) ? 1 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const consumeFilter = (item: TricksterItem): boolean => {
|
const consumeFilter= (item:TricksterItem): boolean => {
|
||||||
const tl = item.item_use.toLowerCase()
|
const tl = item.item_use.toLowerCase()
|
||||||
return tl.includes('recover') || tl.includes('restores')
|
return tl.includes("recover") || tl.includes("restores")
|
||||||
}
|
}
|
||||||
|
|
||||||
class AP implements ColumnInfo {
|
class AP implements ColumnInfo {
|
||||||
name: ColumnName = 'AP'
|
name:ColumnName = "AP"
|
||||||
displayName = 'AP'
|
displayName = "AP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.AP : ''
|
return item.stats ? item.stats["AP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GunAP implements ColumnInfo {
|
class GunAP implements ColumnInfo {
|
||||||
name: ColumnName = 'GunAP'
|
name:ColumnName = "GunAP"
|
||||||
displayName = 'Gun AP'
|
displayName = "Gun AP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats['Gun AP'] : ''
|
return item.stats ? item.stats["Gun AP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AC implements ColumnInfo {
|
class AC implements ColumnInfo {
|
||||||
name: ColumnName = 'AC'
|
name:ColumnName = "AC"
|
||||||
displayName = 'AC'
|
displayName = "AC"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.AC : ''
|
return item.stats ? item.stats["AC"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DX implements ColumnInfo {
|
class DX implements ColumnInfo {
|
||||||
name: ColumnName = 'DX'
|
name:ColumnName = "DX"
|
||||||
displayName = 'DX'
|
displayName = "DX"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.DX : ''
|
return item.stats ? item.stats["DX"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MP implements ColumnInfo {
|
class MP implements ColumnInfo {
|
||||||
name: ColumnName = 'MP'
|
name:ColumnName = "MP"
|
||||||
displayName = 'MP'
|
displayName = "MP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.MP : ''
|
return item.stats ? item.stats["MP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MA implements ColumnInfo {
|
class MA implements ColumnInfo {
|
||||||
name: ColumnName = 'MA'
|
name:ColumnName = "MA"
|
||||||
displayName = 'MA'
|
displayName = "MA"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.MA : ''
|
return item.stats ? item.stats["MA"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MD implements ColumnInfo {
|
class MD implements ColumnInfo {
|
||||||
name: ColumnName = 'MD'
|
name:ColumnName = "MD"
|
||||||
displayName = 'MD'
|
displayName = "MD"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.MD : ''
|
return item.stats ? item.stats["MD"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WT implements ColumnInfo {
|
class WT implements ColumnInfo {
|
||||||
name: ColumnName = 'WT'
|
name:ColumnName = "WT"
|
||||||
displayName = 'WT'
|
displayName = "WT"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.WT : ''
|
return item.stats ? item.stats["WT"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DA implements ColumnInfo {
|
class DA implements ColumnInfo {
|
||||||
name: ColumnName = 'DA'
|
name:ColumnName = "DA"
|
||||||
displayName = 'DA'
|
displayName = "DA"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.DA : ''
|
return item.stats ? item.stats["DA"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LK implements ColumnInfo {
|
class LK implements ColumnInfo {
|
||||||
name: ColumnName = 'LK'
|
name:ColumnName = "LK"
|
||||||
displayName = 'LK'
|
displayName = "LK"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.LK : ''
|
return item.stats ? item.stats["LK"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HP implements ColumnInfo {
|
class HP implements ColumnInfo {
|
||||||
name: ColumnName = 'HP'
|
name:ColumnName = "HP"
|
||||||
displayName = 'HP'
|
displayName = "HP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.HP : ''
|
return item.stats ? item.stats["HP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DP implements ColumnInfo {
|
class DP implements ColumnInfo {
|
||||||
name: ColumnName = 'DP'
|
name:ColumnName = "DP"
|
||||||
displayName = 'DP'
|
displayName = "DP"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.DP : ''
|
return item.stats ? item.stats["DP"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class HV implements ColumnInfo {
|
class HV implements ColumnInfo {
|
||||||
name: ColumnName = 'HV'
|
name:ColumnName = "HV"
|
||||||
displayName = 'HV'
|
displayName = "HV"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.stats ? item.stats.HV : ''
|
return item.stats ? item.stats["HV"] : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MinLvl implements ColumnInfo {
|
class MinLvl implements ColumnInfo {
|
||||||
name: ColumnName = 'MinLvl'
|
name:ColumnName = "MinLvl"
|
||||||
displayName = 'lvl'
|
displayName = "lvl"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
//TODO:
|
//TODO:
|
||||||
return item.item_min_level ? item.item_min_level : ''
|
return item.item_min_level? item.item_min_level:""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Slots implements ColumnInfo {
|
class Slots implements ColumnInfo {
|
||||||
name: ColumnName = 'Slots'
|
name:ColumnName = "Slots"
|
||||||
displayName = 'slots'
|
displayName = "slots"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
//TODO:
|
//TODO:
|
||||||
return item.item_slots ? item.item_slots : ''
|
return item.item_slots ? item.item_slots : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RefineNumber implements ColumnInfo {
|
class RefineNumber implements ColumnInfo {
|
||||||
name: ColumnName = 'RefineNumber'
|
name:ColumnName = "RefineNumber"
|
||||||
displayName = 'refine'
|
displayName = "refine"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.refine_level ? item.refine_level : 0
|
return item.refine_level ? item.refine_level : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RefineState implements ColumnInfo {
|
class RefineState implements ColumnInfo {
|
||||||
name: ColumnName = 'RefineState'
|
name:ColumnName = "RefineState"
|
||||||
displayName = 'bork'
|
displayName = "bork"
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.refine_state ? item.refine_state : 0
|
return item.refine_state ? item.refine_state : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Desc implements ColumnInfo {
|
class Desc implements ColumnInfo {
|
||||||
name: ColumnName = 'Desc'
|
name:ColumnName = "Desc"
|
||||||
displayName = 'desc'
|
displayName = "desc"
|
||||||
renderer = descRenderer
|
renderer = descRenderer
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_comment
|
return item.item_desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function descRenderer(
|
function descRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
let showText = stringifiedValue;
|
||||||
_row: any,
|
const div= document.createElement('div');
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
const showText = stringifiedValue
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerHTML = showText
|
div.innerHTML = showText
|
||||||
div.title = showText
|
div.title = showText
|
||||||
div.style.maxWidth = '30ch'
|
div.style.maxWidth = "30ch"
|
||||||
div.style.textOverflow = 'ellipsis'
|
div.style.textOverflow = "ellipsis"
|
||||||
div.style.overflow = 'hidden'
|
div.style.overflow= "hidden"
|
||||||
div.style.whiteSpace = 'nowrap'
|
div.style.whiteSpace= "nowrap"
|
||||||
Handsontable.dom.addEvent(div, 'mousedown', event => {
|
Handsontable.dom.addEvent(div, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(div)
|
td.appendChild(div);
|
||||||
td.classList.add('htLeft')
|
td.classList.add("htLeft")
|
||||||
}
|
}
|
||||||
|
|
||||||
class Use implements ColumnInfo {
|
class Use implements ColumnInfo {
|
||||||
name: ColumnName = 'Use'
|
name:ColumnName = "Use"
|
||||||
displayName = 'use'
|
displayName = "use"
|
||||||
renderer = useRenderer
|
renderer= useRenderer;
|
||||||
getter(item: TricksterItem): string | number {
|
getter(item:TricksterItem):(string|number){
|
||||||
return item.item_use
|
return item.item_use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function useRenderer(
|
function useRenderer(instance:any, td:any, row:any, col:any, prop:any, value:any, cellProperties:any) {
|
||||||
_instance: any,
|
const stringifiedValue = Handsontable.helper.stringify(value);
|
||||||
td: any,
|
let showText = stringifiedValue;
|
||||||
_row: any,
|
const div= document.createElement('div');
|
||||||
_col: any,
|
|
||||||
_prop: any,
|
|
||||||
value: any,
|
|
||||||
_cellProperties: any,
|
|
||||||
) {
|
|
||||||
const stringifiedValue = Handsontable.helper.stringify(value)
|
|
||||||
const showText = stringifiedValue
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.title = showText
|
div.title = showText
|
||||||
div.innerHTML = showText
|
div.innerHTML = showText
|
||||||
div.style.maxWidth = '30ch'
|
div.style.maxWidth = "30ch"
|
||||||
div.style.textOverflow = 'ellipsis'
|
div.style.textOverflow = "ellipsis"
|
||||||
div.style.overflow = 'hidden'
|
div.style.overflow= "hidden"
|
||||||
div.style.whiteSpace = 'nowrap'
|
div.style.whiteSpace= "nowrap"
|
||||||
Handsontable.dom.addEvent(div, 'mousedown', event => {
|
Handsontable.dom.addEvent(div, 'mousedown', event =>{
|
||||||
event?.preventDefault()
|
event!.preventDefault();
|
||||||
})
|
});
|
||||||
Handsontable.dom.empty(td)
|
Handsontable.dom.empty(td);
|
||||||
td.appendChild(div)
|
td.appendChild(div);
|
||||||
td.classList.add('htLeft')
|
td.classList.add("htLeft")
|
||||||
}
|
}
|
||||||
export const ColumnByNames = (...n: ColumnName[]) => {
|
export const ColumnByNames = (...n:ColumnName[]) => {
|
||||||
return n.map(ColumnByName)
|
return n.map(ColumnByName)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColumnByName = (n: ColumnName) => {
|
export const ColumnByName = (n:ColumnName) => {
|
||||||
return Columns[n]
|
return Columns[n]
|
||||||
}
|
}
|
||||||
export const test = <T extends ColumnInfo>(n: new () => T): [string, T] => {
|
export const test = <T extends ColumnInfo>(n:(new ()=>T)):[string,T] => {
|
||||||
const nn = new n()
|
let nn = new n()
|
||||||
return [nn.name, nn]
|
return [nn.name, nn]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Columns: { [Property in ColumnName]: ColumnInfo } = {
|
export const Columns:{[Property in ColumnName]:ColumnInfo}= {
|
||||||
Use: new Use(),
|
Use: new Use(),
|
||||||
Desc: new Desc(),
|
Desc: new Desc(),
|
||||||
Image: new Image(),
|
Image: new Image(),
|
||||||
Name: new Name(),
|
Name: new Name(),
|
||||||
Count: new Count(),
|
Count: new Count(),
|
||||||
Move: new Move(),
|
Move: new Move(),
|
||||||
MoveCount: new MoveCount(),
|
MoveCount: new MoveCount(),
|
||||||
Equip: new Equip(),
|
Equip: new Equip(),
|
||||||
Drill: new Drill(),
|
Drill: new Drill(),
|
||||||
Card: new Card(),
|
Card: new Card(),
|
||||||
Quest: new Quest(),
|
Quest: new Quest(),
|
||||||
Consume: new Consume(),
|
Consume: new Consume(),
|
||||||
AP: new AP(),
|
AP: new AP(),
|
||||||
GunAP: new GunAP(),
|
GunAP: new GunAP(),
|
||||||
AC: new AC(),
|
AC: new AC(),
|
||||||
DX: new DX(),
|
DX: new DX(),
|
||||||
MP: new MP(),
|
MP: new MP(),
|
||||||
MA: new MA(),
|
MA: new MA(),
|
||||||
MD: new MD(),
|
MD: new MD(),
|
||||||
WT: new WT(),
|
WT: new WT(),
|
||||||
DA: new DA(),
|
DA: new DA(),
|
||||||
LK: new LK(),
|
LK: new LK(),
|
||||||
HP: new HP(),
|
HP: new HP(),
|
||||||
DP: new DP(),
|
DP: new DP(),
|
||||||
HV: new HV(),
|
HV: new HV(),
|
||||||
MinLvl: new MinLvl(),
|
MinLvl: new MinLvl(),
|
||||||
Slots: new Slots(),
|
Slots: new Slots(),
|
||||||
RefineNumber: new RefineNumber(),
|
RefineNumber: new RefineNumber(),
|
||||||
RefineState: new RefineState(),
|
RefineState: new RefineState(),
|
||||||
All: new All(),
|
All: new All(),
|
||||||
Compound: new Compound(),
|
Compound: new Compound(),
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,128 +1,132 @@
|
|||||||
import axios, { AxiosError, AxiosResponse, Method } from 'axios'
|
import axios, { AxiosResponse, Method } from "axios";
|
||||||
import qs from 'qs'
|
import qs from "qs";
|
||||||
import { TricksterAccountInfo } from './trickster'
|
import { getCookie, removeCookie } from "typescript-cookie";
|
||||||
|
|
||||||
export const SITE_ROOT = '/lifeto/'
|
|
||||||
|
|
||||||
export const API_ROOT = 'api/lifeto/'
|
export const SITE_ROOT = "/lifeto/"
|
||||||
export const BANK_ROOT = 'v3/item-manager/'
|
|
||||||
export const MARKET_ROOT = 'marketplace-api/'
|
|
||||||
|
|
||||||
const raw_endpoint = (name: string): string => {
|
export const API_ROOT = "api/lifeto/"
|
||||||
|
export const BANK_ROOT = "item-manager-action/"
|
||||||
|
export const MARKET_ROOT = "marketplace-api/"
|
||||||
|
|
||||||
|
const login_endpoint = (name:string)=>{
|
||||||
return SITE_ROOT + name
|
return SITE_ROOT + name
|
||||||
}
|
}
|
||||||
const login_endpoint = (name: string) => {
|
export const api_endpoint = (name:string):string =>{
|
||||||
return `${SITE_ROOT + name}?canonical=1`
|
return SITE_ROOT+API_ROOT + name
|
||||||
}
|
}
|
||||||
export const api_endpoint = (name: string): string => {
|
export const bank_endpoint = (name:string):string =>{
|
||||||
return SITE_ROOT + API_ROOT + name
|
return SITE_ROOT+BANK_ROOT + name
|
||||||
}
|
|
||||||
export const bank_endpoint = (name: string): string => {
|
|
||||||
return SITE_ROOT + API_ROOT + BANK_ROOT + name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const market_endpoint = (name: string): string => {
|
export const market_endpoint = (name:string):string =>{
|
||||||
return SITE_ROOT + MARKET_ROOT + name
|
return SITE_ROOT+MARKET_ROOT+ name
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EndpointCreators = [api_endpoint, bank_endpoint, market_endpoint]
|
export const EndpointCreators = [
|
||||||
|
api_endpoint,
|
||||||
|
bank_endpoint,
|
||||||
|
market_endpoint,
|
||||||
|
]
|
||||||
|
|
||||||
export type EndpointCreator = (typeof EndpointCreators)[number]
|
export type EndpointCreator = typeof EndpointCreators[number]
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
request: (verb: Method, url: string, data: any, c?: EndpointCreator) => Promise<any>
|
user:string
|
||||||
|
xsrf:string
|
||||||
|
csrf:string
|
||||||
|
request:(verb:Method,url:string,data:any,c?:EndpointCreator)=>Promise<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const login = async (user: string, pass: string): Promise<TokenSession> => {
|
export class LoginHelper {
|
||||||
return axios
|
user:string
|
||||||
.get(login_endpoint('login'), {
|
pass:string
|
||||||
withCredentials: false,
|
csrf?:string
|
||||||
maxRedirects: 0,
|
constructor(user:string, pass:string){
|
||||||
xsrfCookieName: 'XSRF-TOKEN',
|
this.user = user;
|
||||||
})
|
this.pass = pass;
|
||||||
.then(async () => {
|
}
|
||||||
return axios.post(
|
login = async ():Promise<TokenSession> =>{
|
||||||
login_endpoint('login'),
|
return axios.get(login_endpoint("login"),{withCredentials:false})
|
||||||
{
|
.then(async (x)=>{
|
||||||
login: user,
|
return axios.post(login_endpoint("login"),{
|
||||||
password: pass,
|
login:this.user,
|
||||||
redirectTo: 'lifeto',
|
password:this.pass,
|
||||||
},
|
redirectTo:"lifeto"
|
||||||
{
|
},{withCredentials:false})
|
||||||
withCredentials: false,
|
.then(async (x)=>{
|
||||||
maxRedirects: 0,
|
await sleep(100)
|
||||||
xsrfCookieName: 'XSRF-TOKEN',
|
let xsrf= getCookie("XSRF-TOKEN")
|
||||||
},
|
return new TokenSession(this.user,this.csrf!, xsrf!)
|
||||||
)
|
})
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
return new TokenSession()
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
if (e instanceof AxiosError) {
|
|
||||||
if (e.code === 'ERR_BAD_REQUEST') {
|
|
||||||
throw new Error('invalid username/password')
|
|
||||||
}
|
|
||||||
throw new Error(e.message)
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAccountInfo = async (): Promise<TricksterAccountInfo> => {
|
export class LogoutHelper{
|
||||||
return axios
|
constructor(){
|
||||||
.get(raw_endpoint('settings/info'), { withCredentials: false })
|
}
|
||||||
.then((ans: AxiosResponse) => {
|
logout = async ():Promise<void> =>{
|
||||||
return ans.data
|
return axios.get(login_endpoint("logout"),{withCredentials:false}).catch((e)=>{})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
const sleep = async(ms:number)=> {
|
||||||
export const logout = async (): Promise<void> => {
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
return axios
|
|
||||||
.get(login_endpoint('logout'), { withCredentials: false })
|
|
||||||
.catch(() => {})
|
|
||||||
.then(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep LoginHelper for backwards compatibility
|
|
||||||
export const LoginHelper = {
|
|
||||||
login,
|
|
||||||
info: getAccountInfo,
|
|
||||||
logout,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TokenSession implements Session {
|
export class TokenSession implements Session {
|
||||||
request = async (
|
csrf:string
|
||||||
verb: string,
|
xsrf:string
|
||||||
url: string,
|
user:string
|
||||||
data: any,
|
constructor(name:string, csrf:string, xsrf: string){
|
||||||
c: EndpointCreator = api_endpoint,
|
this.user = name
|
||||||
): Promise<AxiosResponse> => {
|
this.csrf = csrf
|
||||||
let promise: Promise<AxiosResponse>
|
this.xsrf = xsrf;
|
||||||
switch (verb.toLowerCase()) {
|
|
||||||
case 'post':
|
|
||||||
promise = axios.post(c(url), data, this.genHeaders())
|
|
||||||
break
|
|
||||||
case 'postform':
|
|
||||||
promise = axios.postForm(c(url), data)
|
|
||||||
break
|
|
||||||
case 'postraw': {
|
|
||||||
const querystring = qs.stringify(data)
|
|
||||||
promise = axios.post(c(url), querystring, this.genHeaders())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
promise = axios.get(c(url), this.genHeaders())
|
|
||||||
}
|
|
||||||
return promise
|
|
||||||
}
|
}
|
||||||
genHeaders = () => {
|
|
||||||
|
request = async (verb:string,url:string,data:any, c:EndpointCreator = api_endpoint):Promise<AxiosResponse> => {
|
||||||
|
let promise
|
||||||
|
switch (verb.toLowerCase()){
|
||||||
|
case "post":
|
||||||
|
promise = axios.post(c(url),data,this.genHeaders())
|
||||||
|
break;
|
||||||
|
case "postform":
|
||||||
|
promise = axios.postForm(c(url),data)
|
||||||
|
break;
|
||||||
|
case "postraw":
|
||||||
|
const querystring = qs.stringify(data)
|
||||||
|
promise = axios.post(c(url),querystring,this.genHeaders())
|
||||||
|
break;
|
||||||
|
case "get":
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
genHeaders = ()=>{
|
||||||
const out = {
|
const out = {
|
||||||
headers: {
|
headers:{
|
||||||
Accept: 'application/json',
|
Accept: "application/json",
|
||||||
'Update-Insecure-Requests': 1,
|
"Update-Insecure-Requests": 1,
|
||||||
},
|
},
|
||||||
withCredentials: true,
|
withCredentials:true
|
||||||
|
}
|
||||||
|
if(this.xsrf){
|
||||||
|
(out.headers as any)["X-XSRF-TOKEN"] = this.xsrf.replace("%3D","=")
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/lib/storage.ts
Normal file
76
src/lib/storage.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//class helper {
|
||||||
|
// Revive<T>(t:string, _type:string):string {
|
||||||
|
// return t
|
||||||
|
// }
|
||||||
|
// Revive<T>(t:string, _type:string[]):string[]{
|
||||||
|
// return t.split(",")
|
||||||
|
// }
|
||||||
|
// Revive<T>(t:string, _type:number):number {
|
||||||
|
// return Number(t)
|
||||||
|
// }
|
||||||
|
// Revive<T>(t:string, _type:number[]):number[]{
|
||||||
|
// return t.split(",").map(Number)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
import { ColumnSet } from "./table"
|
||||||
|
import { TricksterAccount, TricksterCharacter, TricksterInventory } from "./trickster"
|
||||||
|
|
||||||
|
export const ARRAY_SEPERATOR = ","
|
||||||
|
|
||||||
|
let as = ARRAY_SEPERATOR
|
||||||
|
|
||||||
|
export interface Reviver<T> {
|
||||||
|
Murder(t:T):string
|
||||||
|
Revive(s:string):T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreStr= {
|
||||||
|
Murder: (s:string):string=>s,
|
||||||
|
Revive: (s:string):string=>s
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreNum = {
|
||||||
|
Murder: (s:number):string=>s.toString(),
|
||||||
|
Revive: (s:string):number=>Number(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreStrSet = {
|
||||||
|
Murder: (s:Set<string>):string=>Array.from(s).join(as),
|
||||||
|
Revive: (s:string):Set<string>=>new Set(s.split(as))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreColSet = {
|
||||||
|
Murder: (s:ColumnSet):string=>Array.from(s.s.values()).join(as),
|
||||||
|
Revive: (s:string):ColumnSet=>new ColumnSet(s.split(as) as any)
|
||||||
|
}
|
||||||
|
export const StoreChars = {
|
||||||
|
Murder: (s:Map<string,TricksterCharacter>):string=>{
|
||||||
|
let o = JSON.stringify(Array.from(s.entries()))
|
||||||
|
return o
|
||||||
|
},
|
||||||
|
Revive: (s:string):Map<string,TricksterCharacter>=>new Map(JSON.parse(s)),
|
||||||
|
}
|
||||||
|
export const StoreAccounts = {
|
||||||
|
Murder: (s:Map<string,TricksterAccount>):string=>{
|
||||||
|
let o = JSON.stringify(Array.from(s.entries()))
|
||||||
|
return o
|
||||||
|
},
|
||||||
|
Revive: (s:string):Map<string,TricksterAccount>=>new Map(JSON.parse(s)),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreJsonable = {
|
||||||
|
Murder: <T>(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
198
src/lib/table.ts
198
src/lib/table.ts
@ -1,6 +1,12 @@
|
|||||||
import { HotTableProps } from '@handsontable/react'
|
import { HotTableProps } from "@handsontable/vue3/types"
|
||||||
import { ColumnInfo, ColumnName, ColumnSorter, Columns, LazyColumn } from './columns'
|
import { TricksterInventory } from "./trickster"
|
||||||
import { TricksterInventory } from './trickster'
|
import {ColumnInfo, ColumnName, Columns, ColumnSorter, LazyColumn} from "./columns"
|
||||||
|
import { ColumnSettings } from "handsontable/settings"
|
||||||
|
import { PredefinedMenuItemKey } from "handsontable/plugins/contextMenu"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import Handsontable from "handsontable"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface InventoryTableOptions {
|
export interface InventoryTableOptions {
|
||||||
columns: ColumnSet
|
columns: ColumnSet
|
||||||
@ -10,16 +16,16 @@ export interface InventoryTableOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Mappable<T> {
|
export interface Mappable<T> {
|
||||||
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]
|
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
|
||||||
}
|
}
|
||||||
export class ColumnSet implements Mappable<ColumnInfo> {
|
export class ColumnSet implements Set<ColumnInfo>, Mappable<ColumnInfo>{
|
||||||
s: Set<ColumnName> = new Set()
|
s: Set<ColumnName> = new Set()
|
||||||
size: number
|
size: number;
|
||||||
dirty = 0
|
dirty = ref(0)
|
||||||
constructor(i?: Iterable<ColumnInfo | ColumnName>) {
|
constructor(i?:Iterable<ColumnInfo | ColumnName>){
|
||||||
if (i) {
|
if(i){
|
||||||
for (const a of i) {
|
for (const a of i) {
|
||||||
if (Columns[LazyColumn(a)]) {
|
if(Columns[LazyColumn(a)]){
|
||||||
this.s.add(LazyColumn(a))
|
this.s.add(LazyColumn(a))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,46 +33,33 @@ export class ColumnSet implements Mappable<ColumnInfo> {
|
|||||||
this.size = 0
|
this.size = 0
|
||||||
this.mark()
|
this.mark()
|
||||||
}
|
}
|
||||||
map<U>(
|
map<U>(callbackfn: (value: ColumnInfo, index: number, array: ColumnInfo[]) => U, thisArg?: any): U[] {
|
||||||
callbackfn: (value: ColumnInfo, index: number, array: ColumnInfo[]) => U,
|
|
||||||
thisArg?: any,
|
|
||||||
): U[] {
|
|
||||||
return Array.from(this.values()).map(callbackfn, thisArg)
|
return Array.from(this.values()).map(callbackfn, thisArg)
|
||||||
}
|
}
|
||||||
[Symbol.iterator](): IterableIterator<ColumnInfo> {
|
[Symbol.iterator](): IterableIterator<ColumnInfo>{
|
||||||
return this.values()
|
return this.values()
|
||||||
}
|
}
|
||||||
[Symbol.toStringTag] = 'ColumnSet'
|
[Symbol.toStringTag] = "ColumnSet";
|
||||||
entries(): IterableIterator<[ColumnInfo, ColumnInfo]> {
|
entries(): IterableIterator<[ColumnInfo, ColumnInfo]>{
|
||||||
return Array.from(this.values())
|
return Array.from(this.values()).map((x):[ColumnInfo,ColumnInfo]=>{return [x,x]}).values()
|
||||||
.map((x): [ColumnInfo, ColumnInfo] => {
|
|
||||||
return [x, x]
|
|
||||||
})
|
|
||||||
.values()
|
|
||||||
}
|
}
|
||||||
keys(): IterableIterator<ColumnInfo> {
|
keys(): IterableIterator<ColumnInfo>{
|
||||||
return this.values()
|
return this.values()
|
||||||
}
|
}
|
||||||
forEach(
|
forEach(callbackfn: (value: ColumnInfo, value2: ColumnInfo, set: Set<ColumnInfo>) => void, thisArg?: any): void{
|
||||||
callbackfn: (value: ColumnInfo, value2: ColumnInfo, set: Set<ColumnInfo>) => void,
|
Array.from(this.values()).forEach((v)=>{
|
||||||
thisArg?: any,
|
if(this.has(v)) {
|
||||||
): void {
|
|
||||||
Array.from(this.values()).forEach(v => {
|
|
||||||
if (this.has(v)) {
|
|
||||||
callbackfn(v, v, new Set(this.values()))
|
callbackfn(v, v, new Set(this.values()))
|
||||||
}
|
}
|
||||||
}, thisArg)
|
}, thisArg)
|
||||||
}
|
}
|
||||||
values(): IterableIterator<ColumnInfo> {
|
values(): IterableIterator<ColumnInfo>{
|
||||||
return Array.from(this.s.values())
|
return Array.from(this.s.values()).sort(ColumnSorter).map((a, b)=>{
|
||||||
.sort(ColumnSorter)
|
return Columns[a]
|
||||||
.map((a, _b) => {
|
}).values()
|
||||||
return Columns[a]
|
|
||||||
})
|
|
||||||
.values()
|
|
||||||
}
|
}
|
||||||
mark() {
|
mark() {
|
||||||
this.dirty = this.dirty + 1
|
this.dirty.value = this.dirty.value + 1
|
||||||
this.size = this.s.size
|
this.size = this.s.size
|
||||||
}
|
}
|
||||||
add(value: ColumnInfo | ColumnName): this {
|
add(value: ColumnInfo | ColumnName): this {
|
||||||
@ -78,70 +71,89 @@ export class ColumnSet implements Mappable<ColumnInfo> {
|
|||||||
this.mark()
|
this.mark()
|
||||||
this.s.clear()
|
this.s.clear()
|
||||||
}
|
}
|
||||||
delete(value: ColumnInfo | ColumnName): boolean {
|
delete(value: ColumnInfo | ColumnName): boolean{
|
||||||
this.mark()
|
this.mark()
|
||||||
return this.s.delete(LazyColumn(value))
|
return this.s.delete(LazyColumn(value))
|
||||||
}
|
}
|
||||||
has(value: ColumnInfo | ColumnName): boolean {
|
has(value: ColumnInfo | ColumnName): boolean{
|
||||||
return this.s.has(LazyColumn(value))
|
return this.s.has(LazyColumn(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InventoryTable {
|
export class InventoryTable {
|
||||||
inv!: TricksterInventory
|
inv!:TricksterInventory
|
||||||
o!: InventoryTableOptions
|
o!: InventoryTableOptions
|
||||||
|
|
||||||
constructor(inv: TricksterInventory, o: InventoryTableOptions) {
|
constructor(inv:TricksterInventory, o:InventoryTableOptions) {
|
||||||
this.setInv(inv)
|
this.setInv(inv)
|
||||||
this.setOptions(o)
|
this.setOptions(o)
|
||||||
}
|
}
|
||||||
setOptions(o: InventoryTableOptions) {
|
setOptions(o:InventoryTableOptions) {
|
||||||
this.o = o
|
this.o = o
|
||||||
}
|
}
|
||||||
setInv(inv: TricksterInventory) {
|
setInv(inv:TricksterInventory) {
|
||||||
this.inv = inv
|
this.inv = inv
|
||||||
}
|
}
|
||||||
getTableColumnNames(): string[] {
|
getTableColumnNames(): string[] {
|
||||||
return this.o.columns.map(x => x.displayName)
|
return this.o.columns.map(x=>x.displayName)
|
||||||
}
|
}
|
||||||
getTableColumnSettings() {}
|
getTableColumnSettings(): ColumnSettings[] {
|
||||||
getTableRows(): any[][] {
|
return this.o.columns.map(x=>{
|
||||||
return Object.values(this.inv.items)
|
let out:any = {
|
||||||
.filter((item): boolean => {
|
renderer: x.renderer ? x.renderer : "text",
|
||||||
if (item.item_count <= 0) {
|
filters: true,
|
||||||
return false
|
dropdownMenu: x.filtering ? DefaultDropdownItems() : false,
|
||||||
|
readOnly: x.writable ? false : true,
|
||||||
|
selectionMode: (x.writable ? "multiple" : 'single') as any,
|
||||||
|
}
|
||||||
|
if(x.options) {
|
||||||
|
out.type = 'dropdown'
|
||||||
|
if(typeof x.options == "function") {
|
||||||
|
out.source = x.options(this.o.accounts)
|
||||||
|
}else {
|
||||||
|
out.source = x.options
|
||||||
}
|
}
|
||||||
let found = true
|
}
|
||||||
const hasAll = this.o.tags.has('All')
|
return out
|
||||||
if (this.o.tags.s.size > 0) {
|
})
|
||||||
found = hasAll
|
}
|
||||||
for (const tag of this.o.tags.values()) {
|
getTableRows():any[][] {
|
||||||
if (tag.name === 'All') {
|
return Object.values(this.inv.items)
|
||||||
continue
|
.filter((item):boolean=>{
|
||||||
}
|
if(item.item_count <= 0) {
|
||||||
if (tag.getter(item) === 1) {
|
return false
|
||||||
|
}
|
||||||
|
let found = true
|
||||||
|
let hasAll = this.o.tags.has("All")
|
||||||
|
if(this.o.tags.s.size > 0) {
|
||||||
|
found = hasAll
|
||||||
|
for(const tag of this.o.tags.values()) {
|
||||||
|
if(tag.name =="All") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if(tag.getter(item) === 1) {
|
||||||
return !hasAll
|
return !hasAll
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return found
|
}
|
||||||
})
|
return found
|
||||||
.map(item => {
|
})
|
||||||
return this.o.columns.map(x => {
|
.map((item)=>{
|
||||||
return x.getter(item)
|
return this.o.columns.map(x=>{
|
||||||
})
|
return x.getter(item)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
BuildTable(): TableRecipe {
|
BuildTable():TableRecipe {
|
||||||
const s = DefaultSettings()
|
const s = DefaultSettings()
|
||||||
const dat = this.getTableRows()
|
const dat = this.getTableRows()
|
||||||
return {
|
return {
|
||||||
data: dat,
|
data: dat,
|
||||||
settings: {
|
settings: {
|
||||||
data: dat,
|
data: dat,
|
||||||
colHeaders: this.getTableColumnNames(),
|
colHeaders:this.getTableColumnNames(),
|
||||||
columns: this.getTableColumnSettings(),
|
columns:this.getTableColumnSettings(),
|
||||||
...s,
|
...s
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,3 +163,49 @@ export interface TableRecipe {
|
|||||||
data: any[][]
|
data: any[][]
|
||||||
settings: HotTableProps
|
settings: HotTableProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultDropdownItems = ():PredefinedMenuItemKey[]=>['filter_by_condition' , 'filter_operators' ,'filter_by_condition2' , 'filter_by_value' , 'filter_action_bar']
|
||||||
|
export const DefaultSettings = ():HotTableProps=>{
|
||||||
|
return {
|
||||||
|
trimDropdown: true,
|
||||||
|
filters: true,
|
||||||
|
manualRowMove: false,
|
||||||
|
manualColumnMove: false,
|
||||||
|
allowInsertRow: false,
|
||||||
|
allowInsertColumn: false,
|
||||||
|
allowRemoveRow: false,
|
||||||
|
allowRemoveColumn: false,
|
||||||
|
allowHtml: true,
|
||||||
|
disableVisualSelection: false,
|
||||||
|
columnSorting: {
|
||||||
|
indicator: true,
|
||||||
|
headerAction: true,
|
||||||
|
},
|
||||||
|
hiddenColumns: {
|
||||||
|
columns: [0],
|
||||||
|
},
|
||||||
|
// renderAllRows: true,
|
||||||
|
viewportColumnRenderingOffset: 3,
|
||||||
|
viewportRowRenderingOffset: 10,
|
||||||
|
// 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',
|
||||||
|
contextMenu: false,
|
||||||
|
readOnlyCellClassName: "",
|
||||||
|
licenseKey:"non-commercial-and-evaluation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import { TricksterItem } from '../trickster'
|
|
||||||
|
|
||||||
export interface ItemSelectionStatus {
|
|
||||||
selected: boolean
|
|
||||||
amount?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ItemWithSelection {
|
|
||||||
item: TricksterItem
|
|
||||||
status?: ItemSelectionStatus
|
|
||||||
}
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
import { createColumnHelper } from '@tanstack/react-table'
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import {
|
|
||||||
currentItemSelectionAtom,
|
|
||||||
itemSelectionSetActionAtom,
|
|
||||||
mouseDragSelectionStateAtom,
|
|
||||||
} from '@/state/atoms'
|
|
||||||
import { StatsColumns } from '../columns'
|
|
||||||
import { ItemWithSelection } from './defs'
|
|
||||||
|
|
||||||
const ch = createColumnHelper<ItemWithSelection>()
|
|
||||||
|
|
||||||
const columns = {
|
|
||||||
icon: ch.display({
|
|
||||||
id: 'icon',
|
|
||||||
header: function Component(_col) {
|
|
||||||
return <div className="flex flex-row justify-center"></div>
|
|
||||||
},
|
|
||||||
cell: function Component({ row }) {
|
|
||||||
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
|
|
||||||
const c = useAtomValue(currentItemSelectionAtom)
|
|
||||||
const [dragState, setDragState] = useAtom(mouseDragSelectionStateAtom)
|
|
||||||
const selected = useMemo(() => {
|
|
||||||
return c[0].has(row.original.item.id)
|
|
||||||
}, [c])
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const newSelected = !selected
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: newSelected ? row.original.item.item_count : undefined,
|
|
||||||
})
|
|
||||||
setDragState({
|
|
||||||
isDragging: true,
|
|
||||||
lastAction: newSelected ? 'select' : 'deselect',
|
|
||||||
lastItemId: row.original.item.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (dragState.isDragging && dragState.lastItemId !== row.original.item.id) {
|
|
||||||
if (dragState.lastAction === 'select' && !selected) {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: row.original.item.item_count,
|
|
||||||
})
|
|
||||||
} else if (dragState.lastAction === 'deselect' && selected) {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`no-select flex flex-row ${row.original.status?.selected ? 'animate-pulse' : ''}`}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
>
|
|
||||||
<div className="flex flex-row w-6 h-6 justify-center">
|
|
||||||
<img
|
|
||||||
src={row.original.item.item_image || ''}
|
|
||||||
alt="icon"
|
|
||||||
className="select-none object-contain pointer-events-none"
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
count: ch.display({
|
|
||||||
id: 'count',
|
|
||||||
header: function Component(_col) {
|
|
||||||
return <div className="flex flex-row justify-center">#</div>
|
|
||||||
},
|
|
||||||
cell: function Component({ row }) {
|
|
||||||
const c = useAtomValue(currentItemSelectionAtom)
|
|
||||||
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
|
|
||||||
const dragState = useAtomValue(mouseDragSelectionStateAtom)
|
|
||||||
const currentValue = useMemo(() => {
|
|
||||||
const got = c[0].get(row.original.item.id)
|
|
||||||
if (got !== undefined) {
|
|
||||||
return got.toString()
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}, [c])
|
|
||||||
const itemCount = row.original.item.item_count
|
|
||||||
const selected = useMemo(() => {
|
|
||||||
return c[0].has(row.original.item.id)
|
|
||||||
}, [c])
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (dragState.isDragging && dragState.lastItemId !== row.original.item.id) {
|
|
||||||
if (dragState.lastAction === 'select' && !selected) {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: row.original.item.item_count,
|
|
||||||
})
|
|
||||||
} else if (dragState.lastAction === 'deselect' && selected) {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
// biome-ignore lint/a11y/useSemanticElements: Using div for layout with input child
|
|
||||||
// biome-ignore lint/a11y/noStaticElementInteractions: Mouse interaction needed for drag select
|
|
||||||
<div
|
|
||||||
className={`flex flex-row select-none ${row.original.status?.selected ? 'bg-gray-200' : ''}`}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="w-10 text-center "
|
|
||||||
value={currentValue}
|
|
||||||
onChange={e => {
|
|
||||||
if (e.target.value === '') {
|
|
||||||
setItemSelection({ [row.original.item.id]: undefined })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.target.value === '-') {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: itemCount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let parsedInt = parseInt(e.target.value)
|
|
||||||
if (Number.isNaN(parsedInt)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (parsedInt > itemCount) {
|
|
||||||
parsedInt = itemCount
|
|
||||||
}
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: parsedInt,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
placeholder={itemCount.toString()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
name: ch.display({
|
|
||||||
id: 'name',
|
|
||||||
header: _col => {
|
|
||||||
return <div className="flex flex-row text-sm">name</div>
|
|
||||||
},
|
|
||||||
cell: function Component({ row }) {
|
|
||||||
const c = useAtomValue(currentItemSelectionAtom)
|
|
||||||
const setItemSelection = useSetAtom(itemSelectionSetActionAtom)
|
|
||||||
const [dragState, setDragState] = useAtom(mouseDragSelectionStateAtom)
|
|
||||||
const selected = useMemo(() => {
|
|
||||||
return c[0].has(row.original.item.id)
|
|
||||||
}, [c])
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (dragState.isDragging && dragState.lastItemId !== row.original.item.id) {
|
|
||||||
if (dragState.lastAction === 'select' && !selected) {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: row.original.item.item_count,
|
|
||||||
})
|
|
||||||
} else if (dragState.lastAction === 'deselect' && selected) {
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const newSelected = !selected
|
|
||||||
setItemSelection({
|
|
||||||
[row.original.item.id]: newSelected ? row.original.item.item_count : undefined,
|
|
||||||
})
|
|
||||||
setDragState({
|
|
||||||
isDragging: true,
|
|
||||||
lastAction: newSelected ? 'select' : 'deselect',
|
|
||||||
lastItemId: row.original.item.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
// biome-ignore lint/a11y/useSemanticElements: Using div for text content
|
|
||||||
// biome-ignore lint/a11y/noStaticElementInteractions: Mouse interaction needed for drag select
|
|
||||||
<div
|
|
||||||
className="flex flex-row whitespace-pre cursor-pointer select-none hover:bg-gray-100"
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
>
|
|
||||||
<span>{row.original.item.item_name}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
slots: ch.display({
|
|
||||||
id: 'slots',
|
|
||||||
header: _col => {
|
|
||||||
return <div className="flex flex-row text-sm">slots</div>
|
|
||||||
},
|
|
||||||
cell: function Component({ row }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row justify-center">
|
|
||||||
<span>{row.original.item.item_slots}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
stats: ch.group({
|
|
||||||
id: 'stats',
|
|
||||||
header: _col => {
|
|
||||||
return <div className="flex flex-row text-sm">stats</div>
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
...StatsColumns.map(c => {
|
|
||||||
return ch.display({
|
|
||||||
id: `stats.${c}`,
|
|
||||||
header: _col => {
|
|
||||||
return <div className="flex flex-row text-sm justify-center">{c}</div>
|
|
||||||
},
|
|
||||||
cell: function Component({ row }) {
|
|
||||||
const stats = row.original.item.stats
|
|
||||||
const stat = stats ? stats[c] : ''
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row justify-start border-l border-r h-full">
|
|
||||||
<span>{stat}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const InventoryColumns = columns
|
|
||||||
@ -1,36 +1,33 @@
|
|||||||
export interface TricksterItem {
|
export interface ItemExpireTime {
|
||||||
id: string
|
text: string
|
||||||
unique_id: number
|
us: string
|
||||||
item_name: string
|
|
||||||
item_count: number
|
|
||||||
item_comment: string
|
|
||||||
item_use: string
|
|
||||||
item_slots?: number
|
|
||||||
item_tab: number
|
|
||||||
item_type: number
|
|
||||||
item_min_level?: number
|
|
||||||
is_equip?: boolean
|
|
||||||
is_drill?: boolean
|
|
||||||
item_expire_time?: string
|
|
||||||
refine_level?: number
|
|
||||||
refine_type?: number
|
|
||||||
refine_state?: number
|
|
||||||
item_image?: string
|
|
||||||
stats?: { [key: string]: any }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TricksterAccountInfo {
|
export interface TricksterItem {
|
||||||
community_name: string
|
unique_id: number;
|
||||||
email: string
|
item_name: string;
|
||||||
|
item_id: number;
|
||||||
|
item_count: number;
|
||||||
|
item_desc: string;
|
||||||
|
item_use: string;
|
||||||
|
item_slots?: number;
|
||||||
|
item_min_level?: number;
|
||||||
|
is_equip?: boolean;
|
||||||
|
is_drill?: boolean;
|
||||||
|
item_expire_time?: ItemExpireTime;
|
||||||
|
refine_level?: number;
|
||||||
|
refine_type?: number;
|
||||||
|
refine_state?: number;
|
||||||
|
image?: string;
|
||||||
|
stats?: {[key: string]:any}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TricksterAccount {
|
export interface TricksterAccount {
|
||||||
name: string
|
name:string
|
||||||
characters: TricksterCharacter[]
|
characters: TricksterCharacter[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Identifier {
|
export interface Identifier {
|
||||||
account_name: string
|
|
||||||
account_id: number
|
account_id: number
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@ -41,58 +38,58 @@ export interface TricksterCharacter extends Identifier {
|
|||||||
class: number
|
class: number
|
||||||
base_job: number
|
base_job: number
|
||||||
current_job: number
|
current_job: number
|
||||||
current_type: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TricksterInventory extends Identifier {
|
export interface TricksterInventory extends Identifier{
|
||||||
galders?: number
|
galders?:number
|
||||||
items: Map<string, TricksterItem>
|
items:{[key:string]:TricksterItem}
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobMap: { [key: number]: string } = {
|
|
||||||
|
const jobMap:{[key:number]:string} = {
|
||||||
//---- job 1, fm
|
//---- job 1, fm
|
||||||
1: 'schoolgirl',
|
1: "schoolgirl",
|
||||||
2: 'fighter',
|
2: "fighter",
|
||||||
3: 'librarian',
|
3: "librarian",
|
||||||
4: 'shaman',
|
4: "shaman",
|
||||||
5: 'archeologist',
|
5: "archeologist",
|
||||||
6: 'engineer',
|
6: "engineer",
|
||||||
7: 'model',
|
7: "model",
|
||||||
8: 'teacher',
|
8: "teacher",
|
||||||
//---- job 2 fm
|
//---- job 2 fm
|
||||||
9: 'boxer',
|
9: "boxer",
|
||||||
10: 'warrior',
|
10: "warrior",
|
||||||
11: 'bard',
|
11: "bard",
|
||||||
12: 'magician',
|
12: "magician",
|
||||||
13: 'explorer',
|
13: "explorer",
|
||||||
14: 'inventor',
|
14: "inventor",
|
||||||
15: 'entertainer',
|
15: "entertainer",
|
||||||
16: 'card master',
|
16: "card master",
|
||||||
//----
|
//----
|
||||||
17: 'champion',
|
17: "champion",
|
||||||
18: 'duelist',
|
18: "duelist",
|
||||||
19: 'mercinary',
|
19: "mercinary",
|
||||||
20: 'gladiator',
|
20: "gladiator",
|
||||||
21: 'soul master',
|
21: "soul master",
|
||||||
22: 'witch',
|
22: "witch",
|
||||||
23: 'wizard',
|
23: "wizard",
|
||||||
24: 'dark lord',
|
24: "dark lord",
|
||||||
25: 'priest',
|
25: "priest",
|
||||||
26: 'thief master',
|
26: "thief master",
|
||||||
27: 'hunter lord',
|
27: "hunter lord",
|
||||||
28: 'cyber hunter',
|
28: "cyber hunter",
|
||||||
29: 'scientist',
|
29: "scientist",
|
||||||
30: 'primadonna',
|
30: "primadonna",
|
||||||
31: 'diva',
|
31: "diva",
|
||||||
32: 'duke',
|
32: "duke",
|
||||||
33: 'gambler',
|
33: "gambler",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JobNumberToString = (n: number): string => {
|
export const JobNumberToString = (n:number):string=> {
|
||||||
if (n === -8) {
|
if(n == -8) {
|
||||||
return 'bank'
|
return "bank"
|
||||||
}
|
}
|
||||||
if (jobMap[n] !== undefined) {
|
if(jobMap[n] != undefined) {
|
||||||
return jobMap[n]
|
return jobMap[n]
|
||||||
}
|
}
|
||||||
return n.toString()
|
return n.toString()
|
||||||
|
|||||||
12
src/main.ts
Normal file
12
src/main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
|
log.setLevel("debug")
|
||||||
|
const pinia = createPinia()
|
||||||
|
createApp(App).
|
||||||
|
use(pinia).
|
||||||
|
mount('#app')
|
||||||
91
src/pages/login.vue
Normal file
91
src/pages/login.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<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") {
|
||||||
|
alert("invalid username/password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert("unknown error, please report")
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,19 +1,36 @@
|
|||||||
|
import { Cookies, getCookie, removeCookie, setCookie} from 'typescript-cookie'
|
||||||
import { Session, TokenSession } from './lib/session'
|
import { Session, TokenSession } from './lib/session'
|
||||||
|
|
||||||
export const LIFETO_COOKIE_PREFIX = 'LIFETO_PANEL_'
|
|
||||||
|
|
||||||
export const nameCookie = (...s: string[]): string => {
|
|
||||||
return LIFETO_COOKIE_PREFIX + s.join('_').toUpperCase()
|
export const LIFETO_COOKIE_PREFIX="LIFETO_PANEL_"
|
||||||
|
|
||||||
|
export const nameCookie = (...s:string[]):string=>{
|
||||||
|
return LIFETO_COOKIE_PREFIX+s.join("_").toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
GetSession(): Session {
|
GetSession():Session {
|
||||||
return new TokenSession()
|
const {user, xsrf, csrf} = {
|
||||||
|
user: getCookie(nameCookie("user"))!,
|
||||||
|
xsrf: getCookie(nameCookie("xsrf"))!,
|
||||||
|
csrf: getCookie(nameCookie("csrf"))!
|
||||||
|
}
|
||||||
|
return new TokenSession(user, xsrf, csrf)
|
||||||
}
|
}
|
||||||
RemoveSession() {}
|
|
||||||
AddSession(_s: Session) {
|
RemoveSession() {
|
||||||
// setCookie(nameCookie("xsrf"),s.xsrf)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = new Storage()
|
export const storage = new Storage()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
// Re-export all atoms from the separate files for backward compatibility
|
|
||||||
|
|
||||||
// Auth-related atoms
|
|
||||||
export {
|
|
||||||
LTOApi,
|
|
||||||
loginStatusAtom,
|
|
||||||
charactersAtom,
|
|
||||||
selectedCharacterAtom,
|
|
||||||
} from './auth.atoms'
|
|
||||||
|
|
||||||
// Inventory-related atoms
|
|
||||||
export {
|
|
||||||
selectedTargetInventoryAtom,
|
|
||||||
currentFilter,
|
|
||||||
currentCharacterInventoryAtom,
|
|
||||||
inventoryDisplaySettingsAtoms,
|
|
||||||
currentCharacterItemsAtom,
|
|
||||||
type InventoryFilter,
|
|
||||||
inventoryFilterAtom,
|
|
||||||
preferenceInventorySearch,
|
|
||||||
preferenceInventoryTab,
|
|
||||||
preferenceInventorySort,
|
|
||||||
preferenceInventorySortReverse,
|
|
||||||
setInventoryFilterTabActionAtom,
|
|
||||||
inventoryPageRangeAtom,
|
|
||||||
nextInventoryPageActionAtom,
|
|
||||||
currentItemSelectionAtom,
|
|
||||||
currentInventorySearchQueryAtom,
|
|
||||||
filteredCharacterItemsAtom,
|
|
||||||
inventoryItemsCurrentPageAtom,
|
|
||||||
rowSelectionLastActionAtom,
|
|
||||||
mouseDragSelectionStateAtom,
|
|
||||||
clearItemSelectionActionAtom,
|
|
||||||
itemSelectionSetActionAtom,
|
|
||||||
itemSelectionSelectAllFilterActionAtom,
|
|
||||||
itemSelectionSelectAllPageActionAtom,
|
|
||||||
paginateInventoryActionAtom,
|
|
||||||
type MoveItemsResult,
|
|
||||||
type MoveConfirmationState,
|
|
||||||
moveConfirmationAtom,
|
|
||||||
openMoveConfirmationAtom,
|
|
||||||
closeMoveConfirmationAtom,
|
|
||||||
moveSelectedItemsAtom,
|
|
||||||
} from './inventory.atoms'
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import { AxiosError } from 'axios'
|
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
|
||||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
|
||||||
import { LTOApiv0 } from '../lib/lifeto'
|
|
||||||
import { LoginHelper, TokenSession } from '../lib/session'
|
|
||||||
import { TricksterCharacter } 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: '...',
|
|
||||||
code: 102,
|
|
||||||
},
|
|
||||||
queryFn: async () => {
|
|
||||||
return LoginHelper.info()
|
|
||||||
.then(info => {
|
|
||||||
return {
|
|
||||||
logged_in: true,
|
|
||||||
community_name: info.community_name,
|
|
||||||
code: 200,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
if (e instanceof AxiosError) {
|
|
||||||
return {
|
|
||||||
logged_in: false,
|
|
||||||
community_name: '...',
|
|
||||||
code: e.response?.status || 500,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const charactersAtom = atomWithQuery((get) => {
|
|
||||||
const { data: loginStatus } = get(loginStatusAtom)
|
|
||||||
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 = atomWithStorage<TricksterCharacter | undefined>(
|
|
||||||
'lto_state.selected_character',
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
@ -1,428 +0,0 @@
|
|||||||
import Fuse from 'fuse.js'
|
|
||||||
import { atom } from 'jotai'
|
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
|
||||||
import { focusAtom } from 'jotai-optics'
|
|
||||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
|
||||||
import { ItemWithSelection } from '@/lib/table/defs'
|
|
||||||
import { ItemMover } from '../lib/lifeto/item_mover'
|
|
||||||
import { TricksterCharacter, TricksterItem } from '../lib/trickster'
|
|
||||||
import { createSuperjsonStorage } from './storage'
|
|
||||||
import { LTOApi, selectedCharacterAtom } from './auth.atoms'
|
|
||||||
|
|
||||||
export const selectedTargetInventoryAtom = atom<TricksterCharacter | undefined>(undefined)
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const inventoryDisplaySettings = atomWithStorage<{
|
|
||||||
page_size: number
|
|
||||||
}>(
|
|
||||||
'preference.inventory_display_settings',
|
|
||||||
{
|
|
||||||
page_size: 25,
|
|
||||||
},
|
|
||||||
createSuperjsonStorage(),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const inventoryDisplaySettingsAtoms = {
|
|
||||||
pageSize: focusAtom(inventoryDisplaySettings, x => x.prop('page_size')),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const currentCharacterItemsAtom = atom(get => {
|
|
||||||
const { data: inventory } = get(currentCharacterInventoryAtom)
|
|
||||||
const items = inventory?.items || new Map<string, TricksterItem>()
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
searcher: new Fuse(Array.from(items.values()), {
|
|
||||||
keys: ['item_name'],
|
|
||||||
useExtendedSearch: true,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface InventoryFilter {
|
|
||||||
search: string
|
|
||||||
tab: string
|
|
||||||
sort: string
|
|
||||||
sort_reverse: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const inventoryFilterAtom = atomWithStorage<InventoryFilter>(
|
|
||||||
'preference.inventory_filter',
|
|
||||||
{
|
|
||||||
search: '',
|
|
||||||
tab: '',
|
|
||||||
sort: '',
|
|
||||||
sort_reverse: false,
|
|
||||||
},
|
|
||||||
createSuperjsonStorage(),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const preferenceInventorySearch = focusAtom(inventoryFilterAtom, x => x.prop('search'))
|
|
||||||
export const preferenceInventoryTab = focusAtom(inventoryFilterAtom, x => x.prop('tab'))
|
|
||||||
export const preferenceInventorySort = focusAtom(inventoryFilterAtom, x => x.prop('sort'))
|
|
||||||
export const preferenceInventorySortReverse = focusAtom(inventoryFilterAtom, x =>
|
|
||||||
x.prop('sort_reverse'),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const setInventoryFilterTabActionAtom = atom(null, (get, set, tab: string) => {
|
|
||||||
set(inventoryFilterAtom, x => {
|
|
||||||
return {
|
|
||||||
...x,
|
|
||||||
tab,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Reset pagination to first page when switching tabs
|
|
||||||
const pageSize = get(inventoryDisplaySettingsAtoms.pageSize)
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: 0,
|
|
||||||
end: pageSize,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const inventoryPageRangeAtom = atom({
|
|
||||||
start: 0,
|
|
||||||
end: 25,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const nextInventoryPageActionAtom = atom(null, (get, set) => {
|
|
||||||
const { start, end } = get(inventoryPageRangeAtom)
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: start + end,
|
|
||||||
end: end + end,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const currentItemSelectionAtom = atom<[Map<string, number>, number]>([
|
|
||||||
new Map<string, number>(),
|
|
||||||
0,
|
|
||||||
])
|
|
||||||
export const currentInventorySearchQueryAtom = atom('')
|
|
||||||
|
|
||||||
export const filteredCharacterItemsAtom = atom(get => {
|
|
||||||
const { items } = get(currentCharacterItemsAtom)
|
|
||||||
const [selection] = get(currentItemSelectionAtom)
|
|
||||||
const filter = get(inventoryFilterAtom)
|
|
||||||
const out: ItemWithSelection[] = []
|
|
||||||
for (const [_, value] of items.entries()) {
|
|
||||||
if (filter.search !== '') {
|
|
||||||
if (!value.item_name.toLowerCase().includes(filter.search)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filter.tab !== '') {
|
|
||||||
if (value.item_tab !== parseInt(filter.tab)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let status: { selected: boolean } | undefined
|
|
||||||
if (selection.has(value.id)) {
|
|
||||||
status = {
|
|
||||||
selected: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push({ item: value, status })
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (filter.sort) {
|
|
||||||
case 'count':
|
|
||||||
out.sort((a, b) => {
|
|
||||||
return b.item.item_count - a.item.item_count
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'type':
|
|
||||||
out.sort((a, b) => {
|
|
||||||
return a.item.item_tab - b.item.item_tab
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'name':
|
|
||||||
out.sort((a, b) => {
|
|
||||||
return a.item.item_name.localeCompare(b.item.item_name)
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (filter.sort && filter.sort_reverse) {
|
|
||||||
out.reverse()
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
|
|
||||||
export const inventoryItemsCurrentPageAtom = atom(get => {
|
|
||||||
const items = get(filteredCharacterItemsAtom)
|
|
||||||
const { start, end } = get(inventoryPageRangeAtom)
|
|
||||||
return items.slice(start, end).map((item): ItemWithSelection => {
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const rowSelectionLastActionAtom = atom<
|
|
||||||
| {
|
|
||||||
index: number
|
|
||||||
action: 'add' | 'remove'
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
export const mouseDragSelectionStateAtom = atom({
|
|
||||||
isDragging: false,
|
|
||||||
lastAction: null as 'select' | 'deselect' | null,
|
|
||||||
lastItemId: null as string | null,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const clearItemSelectionActionAtom = atom(null, (_get, set) => {
|
|
||||||
set(currentItemSelectionAtom, [new Map<string, number>(), 0])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const itemSelectionSetActionAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, arg: Record<string, number | undefined>) => {
|
|
||||||
const cur = get(currentItemSelectionAtom)
|
|
||||||
for (const [key, value] of Object.entries(arg)) {
|
|
||||||
if (value === undefined) {
|
|
||||||
cur[0].delete(key)
|
|
||||||
} else {
|
|
||||||
cur[0].set(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const itemSelectionSelectAllFilterActionAtom = atom(null, (get, set) => {
|
|
||||||
const cur = get(currentItemSelectionAtom)
|
|
||||||
const items = get(filteredCharacterItemsAtom)
|
|
||||||
for (const item of items) {
|
|
||||||
cur[0].set(item.item.id, item.item.item_count)
|
|
||||||
}
|
|
||||||
set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const itemSelectionSelectAllPageActionAtom = atom(null, (get, set) => {
|
|
||||||
const cur = get(currentItemSelectionAtom)
|
|
||||||
const items = get(inventoryItemsCurrentPageAtom)
|
|
||||||
for (const item of items) {
|
|
||||||
cur[0].set(item.item.id, item.item.item_count)
|
|
||||||
}
|
|
||||||
set(currentItemSelectionAtom, [cur[0], cur[1] + 1])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const paginateInventoryActionAtom = atom(null, (get, set, pages: number | undefined) => {
|
|
||||||
const inventoryRange = get(inventoryPageRangeAtom)
|
|
||||||
const pageSize = get(inventoryDisplaySettingsAtoms.pageSize)
|
|
||||||
const filteredItems = get(filteredCharacterItemsAtom)
|
|
||||||
if (pages === undefined) {
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: 0,
|
|
||||||
end: pageSize,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (pageSize > filteredItems.length) {
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: 0,
|
|
||||||
end: filteredItems.length,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (pages > 0) {
|
|
||||||
if (inventoryRange.end >= filteredItems.length) {
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: 0,
|
|
||||||
end: pageSize,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if (pages < 0) {
|
|
||||||
if (inventoryRange.start <= 0) {
|
|
||||||
// Wrap around to the last page
|
|
||||||
const lastPageStart = Math.max(0, filteredItems.length - pageSize)
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: lastPageStart,
|
|
||||||
end: filteredItems.length,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const delta = pages * pageSize
|
|
||||||
let newStart = inventoryRange.start + delta
|
|
||||||
let newEnd = inventoryRange.end + delta
|
|
||||||
|
|
||||||
// Handle negative start
|
|
||||||
if (newStart < 0) {
|
|
||||||
newStart = 0
|
|
||||||
newEnd = Math.min(pageSize, filteredItems.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle end beyond items length
|
|
||||||
if (newEnd > filteredItems.length) {
|
|
||||||
newEnd = filteredItems.length
|
|
||||||
newStart = Math.max(0, newEnd - pageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
set(inventoryPageRangeAtom, {
|
|
||||||
start: newStart,
|
|
||||||
end: newEnd,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface MoveItemsResult {
|
|
||||||
totalItems: number
|
|
||||||
successCount: number
|
|
||||||
failedCount: number
|
|
||||||
errors: Array<{ itemId: string; error: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoveConfirmationState {
|
|
||||||
isOpen: boolean
|
|
||||||
selectedItems: Map<string, { item: TricksterItem; count: number }>
|
|
||||||
sourceCharacter?: TricksterCharacter
|
|
||||||
targetCharacter?: TricksterCharacter
|
|
||||||
}
|
|
||||||
|
|
||||||
export const moveConfirmationAtom = atom<MoveConfirmationState>({
|
|
||||||
isOpen: false,
|
|
||||||
selectedItems: new Map(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const openMoveConfirmationAtom = atom(null, (get, set) => {
|
|
||||||
const [selectedItems] = get(currentItemSelectionAtom)
|
|
||||||
const sourceCharacter = get(selectedCharacterAtom)
|
|
||||||
const targetCharacter = get(selectedTargetInventoryAtom)
|
|
||||||
const { data: inventory } = get(currentCharacterInventoryAtom)
|
|
||||||
|
|
||||||
if (!sourceCharacter || !targetCharacter || !inventory) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsWithDetails = new Map<string, { item: TricksterItem; count: number }>()
|
|
||||||
|
|
||||||
selectedItems.forEach((count, itemId) => {
|
|
||||||
const item = inventory.items.get(itemId)
|
|
||||||
if (item) {
|
|
||||||
itemsWithDetails.set(itemId, { item, count })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
set(moveConfirmationAtom, {
|
|
||||||
isOpen: true,
|
|
||||||
selectedItems: itemsWithDetails,
|
|
||||||
sourceCharacter,
|
|
||||||
targetCharacter,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const closeMoveConfirmationAtom = atom(null, (_get, set) => {
|
|
||||||
set(moveConfirmationAtom, {
|
|
||||||
isOpen: false,
|
|
||||||
selectedItems: new Map(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const moveSelectedItemsAtom = atom(null, async (get, _set): Promise<MoveItemsResult> => {
|
|
||||||
const itemMover = new ItemMover(LTOApi)
|
|
||||||
const confirmationState = get(moveConfirmationAtom)
|
|
||||||
const selectedItems = confirmationState.isOpen
|
|
||||||
? new Map(
|
|
||||||
Array.from(confirmationState.selectedItems.entries()).map(([id, { count }]) => [id, count]),
|
|
||||||
)
|
|
||||||
: get(currentItemSelectionAtom)[0]
|
|
||||||
const sourceCharacter = confirmationState.sourceCharacter || get(selectedCharacterAtom)
|
|
||||||
const targetCharacter = confirmationState.targetCharacter || get(selectedTargetInventoryAtom)
|
|
||||||
const { data: sourceInventory } = get(currentCharacterInventoryAtom)
|
|
||||||
|
|
||||||
const result: MoveItemsResult = {
|
|
||||||
totalItems: selectedItems.size,
|
|
||||||
successCount: 0,
|
|
||||||
failedCount: 0,
|
|
||||||
errors: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourceCharacter || !targetCharacter) {
|
|
||||||
throw new Error('Source or target character not selected')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourceInventory) {
|
|
||||||
throw new Error('Source inventory not loaded')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedItems.size === 0) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track successful moves to update counts
|
|
||||||
const successfulMoves: Array<{ itemId: string; count: number }> = []
|
|
||||||
|
|
||||||
// Process each selected item
|
|
||||||
const movePromises = Array.from(selectedItems.entries()).map(async ([itemId, count]) => {
|
|
||||||
const item = sourceInventory.items.get(itemId)
|
|
||||||
if (!item) {
|
|
||||||
result.errors.push({ itemId, error: 'Item not found in inventory' })
|
|
||||||
result.failedCount++
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isTargetBank = !targetCharacter.path.includes('/')
|
|
||||||
const moveResult = await itemMover.moveItem(
|
|
||||||
item.unique_id.toString(),
|
|
||||||
count,
|
|
||||||
isTargetBank ? undefined : targetCharacter.id.toString(),
|
|
||||||
isTargetBank ? targetCharacter.account_id.toString() : undefined,
|
|
||||||
)
|
|
||||||
if (moveResult.success) {
|
|
||||||
result.successCount++
|
|
||||||
successfulMoves.push({ itemId, count })
|
|
||||||
} else {
|
|
||||||
result.errors.push({ itemId, error: moveResult.error || 'Unknown error' })
|
|
||||||
result.failedCount++
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
result.errors.push({
|
|
||||||
itemId,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
})
|
|
||||||
result.failedCount++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(movePromises)
|
|
||||||
|
|
||||||
// Update the inventory optimistically
|
|
||||||
if (successfulMoves.length > 0 && sourceInventory) {
|
|
||||||
const updatedItems = new Map(sourceInventory.items)
|
|
||||||
|
|
||||||
for (const { itemId, count } of successfulMoves) {
|
|
||||||
const item = updatedItems.get(itemId)
|
|
||||||
if (item) {
|
|
||||||
const newCount = item.item_count - count
|
|
||||||
if (newCount <= 0) {
|
|
||||||
// Remove item if count reaches 0
|
|
||||||
updatedItems.delete(itemId)
|
|
||||||
} else {
|
|
||||||
// Update item count
|
|
||||||
updatedItems.set(itemId, { ...item, item_count: newCount })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the local inventory state
|
|
||||||
sourceInventory.items = updatedItems
|
|
||||||
|
|
||||||
// Trigger a refetch to sync with server
|
|
||||||
const { refetch } = get(currentCharacterInventoryAtom)
|
|
||||||
refetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
89
src/state/state.ts
Normal file
89
src/state/state.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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 { ColumnSet } from '../lib/table'
|
||||||
|
import { TricksterAccount, TricksterCharacter, TricksterInventory } from '../lib/trickster'
|
||||||
|
import { nameCookie} from '../session_storage'
|
||||||
|
|
||||||
|
const _defaultColumn:(ColumnInfo| ColumnName)[] = [
|
||||||
|
...BasicColumns,
|
||||||
|
...MoveColumns,
|
||||||
|
...DetailsColumns,
|
||||||
|
]
|
||||||
|
|
||||||
|
// if you wish for the thing to persist
|
||||||
|
export const StoreReviver = {
|
||||||
|
chars: StoreChars,
|
||||||
|
accs: StoreAccounts,
|
||||||
|
activeTable: StoreStr,
|
||||||
|
screen: StoreStr,
|
||||||
|
columns: StoreColSet,
|
||||||
|
tags: StoreColSet,
|
||||||
|
// orders: StoreSerializable(OrderTracker)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreProps {
|
||||||
|
invs: Map<string,TricksterInventory>
|
||||||
|
chars: Map<string, TricksterCharacter>
|
||||||
|
accs: Map<string, TricksterAccount>
|
||||||
|
orders: OrderTracker
|
||||||
|
activeTable: string
|
||||||
|
screen: string
|
||||||
|
columns: ColumnSet
|
||||||
|
tags: ColumnSet
|
||||||
|
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>,
|
||||||
|
accs: new Map() as Map<string,TricksterAccount>,
|
||||||
|
orders: new OrderTracker(),
|
||||||
|
activeTable: "none",
|
||||||
|
screen: "default",
|
||||||
|
columns:new ColumnSet(_defaultColumn),
|
||||||
|
tags: new ColumnSet(),
|
||||||
|
dirty: 0,
|
||||||
|
currentSearch: "",
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const loadStore = ()=> {
|
||||||
|
let store = useStoreRef()
|
||||||
|
for(const [k, v] of Object.entries(StoreReviver)){
|
||||||
|
const coke = localStorage.getItem(nameCookie("last_"+k))
|
||||||
|
if(coke){
|
||||||
|
if((store[k as keyof RefStore]) != undefined){
|
||||||
|
store[k as keyof RefStore].value = v.Revive(coke) as any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const saveStore = ()=> {
|
||||||
|
let store = useStoreRef()
|
||||||
|
for(const [k, v] of Object.entries(StoreReviver)){
|
||||||
|
let coke;
|
||||||
|
if((store[k as keyof RefStore]) != undefined){
|
||||||
|
coke = v.Murder(store[k as keyof RefStore].value as any)
|
||||||
|
}
|
||||||
|
if(coke){
|
||||||
|
localStorage.setItem(nameCookie("last_"+k),coke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStoreRef = ()=>{
|
||||||
|
const refs = storeToRefs(useStore())
|
||||||
|
return refs
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefStore = ReturnType<typeof useStoreRef>;
|
||||||
|
|
||||||
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import {
|
|
||||||
AsyncStorage,
|
|
||||||
AsyncStringStorage,
|
|
||||||
SyncStorage,
|
|
||||||
SyncStringStorage,
|
|
||||||
} from 'jotai/vanilla/utils/atomWithStorage'
|
|
||||||
import superjson from 'superjson'
|
|
||||||
|
|
||||||
const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
|
|
||||||
typeof (x as any)?.then === 'function'
|
|
||||||
|
|
||||||
type Unsubscribe = () => void
|
|
||||||
|
|
||||||
type Subscribe<Value> = (
|
|
||||||
key: string,
|
|
||||||
callback: (value: Value) => void,
|
|
||||||
initialValue: Value,
|
|
||||||
) => Unsubscribe | undefined
|
|
||||||
|
|
||||||
type StringSubscribe = (
|
|
||||||
key: string,
|
|
||||||
callback: (value: string | null) => void,
|
|
||||||
) => Unsubscribe | undefined
|
|
||||||
|
|
||||||
export function createSuperjsonStorage<Value>(): SyncStorage<Value>
|
|
||||||
export function createSuperjsonStorage<Value>(
|
|
||||||
getStringStorage: () => AsyncStringStorage | SyncStringStorage | undefined = () => {
|
|
||||||
try {
|
|
||||||
return window.localStorage
|
|
||||||
} catch (_e) {
|
|
||||||
if (import.meta.env?.MODE !== 'production') {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
): AsyncStorage<Value> | SyncStorage<Value> {
|
|
||||||
let lastStr: string | undefined
|
|
||||||
let lastValue: Value
|
|
||||||
|
|
||||||
const storage: AsyncStorage<Value> | SyncStorage<Value> = {
|
|
||||||
getItem: (key, initialValue) => {
|
|
||||||
const parse = (str: string | null) => {
|
|
||||||
str = str || ''
|
|
||||||
if (lastStr !== str) {
|
|
||||||
try {
|
|
||||||
lastValue = superjson.parse(str)
|
|
||||||
} catch {
|
|
||||||
return initialValue
|
|
||||||
}
|
|
||||||
lastStr = str
|
|
||||||
}
|
|
||||||
return lastValue
|
|
||||||
}
|
|
||||||
const str = getStringStorage()?.getItem(key) ?? null
|
|
||||||
if (isPromiseLike(str)) {
|
|
||||||
return str.then(parse) as never
|
|
||||||
}
|
|
||||||
return parse(str) as never
|
|
||||||
},
|
|
||||||
setItem: (key, newValue) => getStringStorage()?.setItem(key, superjson.stringify(newValue)),
|
|
||||||
removeItem: key => getStringStorage()?.removeItem(key),
|
|
||||||
}
|
|
||||||
|
|
||||||
const createHandleSubscribe =
|
|
||||||
(subscriber: StringSubscribe): Subscribe<Value> =>
|
|
||||||
(key, callback, initialValue) =>
|
|
||||||
subscriber(key, v => {
|
|
||||||
let newValue: Value
|
|
||||||
try {
|
|
||||||
newValue = superjson.parse(v || '')
|
|
||||||
} catch {
|
|
||||||
newValue = initialValue
|
|
||||||
}
|
|
||||||
callback(newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
let subscriber: StringSubscribe | undefined
|
|
||||||
try {
|
|
||||||
subscriber = getStringStorage()?.subscribe
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!subscriber &&
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
typeof window.addEventListener === 'function' &&
|
|
||||||
window.Storage
|
|
||||||
) {
|
|
||||||
subscriber = (key, callback) => {
|
|
||||||
if (!(getStringStorage() instanceof window.Storage)) {
|
|
||||||
return () => {}
|
|
||||||
}
|
|
||||||
const storageEventCallback = (e: StorageEvent) => {
|
|
||||||
if (e.storageArea === getStringStorage() && e.key === key) {
|
|
||||||
callback(e.newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('storage', storageEventCallback)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('storage', storageEventCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriber) {
|
|
||||||
storage.subscribe = createHandleSubscribe(subscriber)
|
|
||||||
}
|
|
||||||
return storage
|
|
||||||
}
|
|
||||||
|
|
||||||
export const superJsonStorage = createSuperjsonStorage()
|
|
||||||
@ -1,27 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"module": "esnext",
|
||||||
"paths": {
|
"moduleResolution": "node",
|
||||||
"@/*": ["./src/*"]
|
|
||||||
},
|
|
||||||
"types": ["node"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"module": "ESNext",
|
"jsx": "preserve",
|
||||||
"moduleResolution": "bundler",
|
"sourceMap": true,
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": false,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "react-jsx"
|
"lib": ["esnext", "dom"],
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["src", "app", "index"],
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"module": "ESNext",
|
"module": "esnext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "node"
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,17 @@
|
|||||||
// ignore the type error onthe next line
|
|
||||||
// @ts-ignore
|
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
import path from 'path'
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [vue()],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, './src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
// with options
|
// with options
|
||||||
'/lifeto': {
|
'/lifeto': {
|
||||||
target: 'https://beta.lifeto.co/',
|
target: "https://beta.lifeto.co/",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: path => path.replace(/^\/lifeto/, ''),
|
rewrite: (path) => path.replace(/^\/lifeto/, ''),
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
//export default defineConfig({
|
|
||||||
// plugins: [vue()],
|
|
||||||
// server: {
|
|
||||||
// proxy: {
|
|
||||||
// // with options
|
|
||||||
// '/lifeto': {
|
|
||||||
// target: "https://beta.lifeto.co/",
|
|
||||||
// changeOrigin: true,
|
|
||||||
// rewrite: (path) => path.replace(/^\/lifeto/, ''),
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//})
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user