3 Commits
9 changed files with 111 additions and 47 deletions
+3 -4
View File
@@ -41,12 +41,11 @@ useEventListener('keyup', e => {
<template>
<div ref="root" class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="[doAutoClose, { ignore: [floating] }]">
<button @click="isOpen = !isOpen" class="cursor-pointer">
<button @click="isOpen = !isOpen" class="flex items-center cursor-pointer border-none bg-transparent hover:bg-transparent p-0">
<Transition
mode="out-in"
enter-active-class="transition-transform"
enter-from-class="rotate-180"
leave-active-class="hidden"
leave-to-class="rotate-180">
enter-from-class="rotate-180">
<ChevronDownIcon v-if="!isOpen" class="chevron" />
<ChevronUpIcon v-else class="chevron" />
</Transition>
+7 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts" generic="T">
import {vOnClickOutside} from '@vueuse/components';
import {useVirtualList} from '@vueuse/core';
import {computed, ref} from 'vue';
import {computed, ref, watch} from 'vue';
interface Props {
items: T[];
@@ -15,6 +15,12 @@ const props = withDefaults(defineProps<Props>(), {
const modelValue = defineModel<T>();
const isOpen = ref(false);
watch(() => props.items, items => {
if (items.length > 0) {
isOpen.value = true;
}
});
const currentIndex = ref(-1);
const {list, scrollTo, containerProps, wrapperProps} = useVirtualList(computed(() => props.items), {
itemHeight: () => props.itemHeight,
+12 -7
View File
@@ -3,6 +3,7 @@ import {getMarketTypes, getPrices, MarketTypePrice} from "@/market";
import {ref, watch} from 'vue';
import {ArrowPathIcon} from '@heroicons/vue/24/outline';
import {useAutoRefresh} from '@/composables';
import {MarketLocationInput, useMarketLocationStore} from '@/market/location';
import {AcquiredType} from './AcquiredType';
import {RawAcquiredType} from './acquisition';
import AcquisitionResultTable from './AcquisitionResultTable.vue';
@@ -20,6 +21,7 @@ const props = defineProps<Props>();
const buyModal = ref<typeof BuyModal>();
const sellModal = ref<typeof SellModal>();
const marketLocationStore = useMarketLocationStore();
const enriched = ref<AcquiredType[]>([]);
const refresh = async (itms: RawAcquiredType[] = props.items) => {
@@ -29,7 +31,7 @@ const refresh = async (itms: RawAcquiredType[] = props.items) => {
}
const types = await getMarketTypes([...new Set(itms.map(i => i.type))]);
const prices = await getPrices(types);
const prices = await getPrices(types, marketLocationStore.location.id);
enriched.value = itms.map(i => {
const price = prices.find(p => p.type.id === i.type) as MarketTypePrice;
@@ -43,18 +45,21 @@ const refresh = async (itms: RawAcquiredType[] = props.items) => {
});
};
watch(() => props.items, refresh, {immediate: true});
watch([() => props.items, () => marketLocationStore.location], ([itms]) => refresh(itms), {immediate: true});
const {active: autoRefresh, toggle: toggleAutoRefresh} = useAutoRefresh(() => refresh(), 5 * 60 * 1000);
</script>
<template>
<div class="flex">
<div class="ms-auto flex">
<button class="rounded-e-none" @click="refresh()">Refresh</button>
<button class="rounded-s-none border-s-0 flex items-center" :class="{ 'bg-slate-700': autoRefresh }" title="Auto refresh" @click="toggleAutoRefresh">
<ArrowPathIcon class="w-5 h-5" :class="{ 'animate-spin': autoRefresh }" />
</button>
<div class="ms-auto flex items-center gap-2">
<MarketLocationInput v-model="marketLocationStore.location" />
<div class="flex">
<button class="rounded-e-none" @click="refresh()">Refresh</button>
<button class="rounded-s-none border-s-0 flex items-center" :class="{ 'bg-slate-700': autoRefresh }" title="Auto refresh" @click="toggleAutoRefresh">
<ArrowPathIcon class="w-5 h-5" :class="{ 'animate-spin': autoRefresh }" />
</button>
</div>
</div>
</div>
<template v-if="enriched.length > 0">
+1
View File
@@ -1,2 +1,3 @@
export * from './location';
export * from './MarketLocation';
export { default as MarketLocationInput } from './MarketLocationInput.vue';
+18
View File
@@ -0,0 +1,18 @@
import {useLocalStorage} from "@vueuse/core";
import {defineStore} from "pinia";
import type {MarketLocation} from "./MarketLocation";
const defaultLocation: MarketLocation = {
id: 60003760,
name: 'Jita IV - Moon 4 - Caldari Navy Assembly Plant',
systemId: 30000142,
systemName: 'Jita',
regionId: 10000002,
regionName: 'The Forge',
};
export const useMarketLocationStore = defineStore("marketLocation", () => {
const location = useLocalStorage<MarketLocation>("market-location", defaultLocation);
return {location};
});
+3 -1
View File
@@ -3,9 +3,11 @@ import {RouterLink, RouterView} from 'vue-router';
import {appraiseItemBalances, getLedgerBalance, isMain, LedgerLabel, useLedgerParam} from "@/ledger";
import {routeNames} from "@/routes.ts";
import {getMarketTypes, getPrices, IskLabel} from "@/market";
import {useMarketLocationStore} from '@/market/location';
import {computedAsync} from "@vueuse/core";
const {ledgerId, ledger} = useLedgerParam();
const marketLocationStore = useMarketLocationStore();
const itemAppraisal = computedAsync(async () => {
if (!ledgerId.value) {
@@ -19,7 +21,7 @@ const itemAppraisal = computedAsync(async () => {
}
const types = await getMarketTypes([...new Set(itemBalances.map(i => i.typeId))]);
const prices = await getPrices(types);
const prices = await getPrices(types, marketLocationStore.location.id);
const appraisal = appraiseItemBalances(itemBalances, prices);
return appraisal.buy === 0 && appraisal.sell === 0 ? undefined : appraisal;
+9 -1
View File
@@ -1,14 +1,16 @@
<script setup lang="ts">
import {Dropdown} from '@/components';
import {AppraisalItemValue, AppraisalResultTable, appraise} from '@/market/appraisal';
import {MarketLocationInput, useMarketLocationStore} from '@/market/location';
import {ref} from 'vue';
const items = ref('');
const showInputs = ref(true);
const marketLocationStore = useMarketLocationStore();
const result = ref<AppraisalItemValue[]>([]);
const send = async () => result.value = await appraise(items.value);
const send = async () => result.value = await appraise(items.value, marketLocationStore.location.id);
</script>
<template>
@@ -20,6 +22,12 @@ const send = async () => result.value = await appraise(items.value);
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
</div>
</div>
<div class="mx-1 mt-2">
<span>Location</span>
<div class="mt-1">
<MarketLocationInput v-model="marketLocationStore.location" />
</div>
</div>
<div class="grid my-2">
<button class="justify-self-end" @click="send" :disabled="!items.trim()">Send</button>
</div>
+5 -2
View File
@@ -2,6 +2,7 @@
import {ClipboardButton} from '@/components';
import {getMarketType, getPrice, MarketType, MarketTypeInput, useMarketTaxStore} from "@/market";
import {AcquisitionResultTable, BuyModal} from '@/market/acquisition';
import {MarketLocationInput, useMarketLocationStore} from '@/market/location';
import {ScanResultTable, toScanResult} from '@/market/scan';
import {acquisitionApi, marketApi} from "@/mammon";
import {ShoppingCartIcon} from '@heroicons/vue/24/outline';
@@ -19,8 +20,9 @@ const item = ref<MarketType>();
const inputItem = ref<MarketType>();
const marketTaxStore = useMarketTaxStore();
const marketLocationStore = useMarketLocationStore();
const days = useStorage('market-scan-days', 365);
const price = computedAsync(() => item.value ? getPrice(item.value) : undefined);
const price = computedAsync(() => item.value ? getPrice(item.value, marketLocationStore.location.id) : undefined);
const result = computedAsync(async () => {
if (!item.value) {
return undefined;
@@ -83,10 +85,11 @@ watch(useRoute(), async route => {
<template>
<div class="grid mb-2 mt-4">
<div class="w-auto flex">
<div class="w-auto flex items-center">
<span>Item: </span>
<MarketTypeInput class="ms-2" v-model="inputItem" @submit="view"/>
<button class="justify-self-end ms-2" @click="view">View</button>
<MarketLocationInput class="ms-auto" v-model="marketLocationStore.location" />
</div>
</div>
<template v-if="item">
+53 -31
View File
@@ -1,14 +1,16 @@
<script setup lang="ts">
import {computed, ref, watchEffect} from "vue";
import {computedAsync, useEventListener} from "@vueuse/core";
import {useEventListener} from "@vueuse/core";
import log from "loglevel";
import {ScriptEditor, useRuleBookStore} from "@/rules";
import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline";
import {Dropdown} from "@/components";
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
import {useCharactersStore} from "@/characters";
import {CorporationLabel, useCorporationsStore} from "@/corporations";
import {RuleScriptRequest} from "@/generated/mammon";
import {toast} from "@/toast";
import {confirm} from "@/confirm";
type LedgerScopeEntry = { ref: string; ledger: Ledger };
type CharacterScopeEntry = { ref: string; characterId: number };
@@ -33,6 +35,11 @@ const ledgersToUse = computed(() => [systemLedger, ...ledgersStore.ledgers.filte
const scripts = ref<ScriptDraft[]>([]);
const selectedIndex = ref(0);
const scopesExpanded = ref(true);
const ledgersExpanded = ref(true);
const charactersExpanded = ref(true);
const corporationsExpanded = ref(true);
const walletDivisionsExpanded = ref(true);
const selected = computed<ScriptDraft | undefined>(() => scripts.value[selectedIndex.value]);
watchEffect(() => {
@@ -71,7 +78,13 @@ const addScript = () => {
selectedIndex.value = scripts.value.length - 1;
};
const removeScript = (index: number) => {
const removeScript = async (index: number) => {
const script = scripts.value[index];
if (script?.script.trim() && !await confirm({message: `Delete script "${script.name || '(unnamed)'}"? It still has content.`, danger: true})) {
return;
}
scripts.value = scripts.value.toSpliced(index, 1);
if (selectedIndex.value >= scripts.value.length) {
selectedIndex.value = Math.max(0, scripts.value.length - 1);
@@ -90,7 +103,18 @@ const removeCorporationScope = (index: number) => { if (selected.value) selected
const addWalletDivisionScope = () => selected.value?.walletDivisions.push({ref: '', corporationId: 0, division: 1});
const removeWalletDivisionScope = (index: number) => { if (selected.value) selected.value.walletDivisions = selected.value.walletDivisions.toSpliced(index, 1); };
const resolvedCorporation = (corporationId: number) => computedAsync(async () => corporationId ? await corporationsStore.findById(corporationId) : undefined, undefined);
watchEffect(() => {
const corporationIds = new Set([
...(selected.value?.corporations.map(entry => entry.corporationId) ?? []),
...(selected.value?.walletDivisions.map(entry => entry.corporationId) ?? []),
]);
for (const corporationId of corporationIds) {
if (corporationId) {
corporationsStore.findById(corporationId);
}
}
});
const save = () => ruleBookStore.update(
scripts.value
@@ -117,29 +141,29 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
</script>
<template>
<div class="flex mb-2 mt-4 h-[calc(100vh-4.5rem)]">
<div class="flex mb-2 mt-4 h-[calc(100vh-1.5rem)]">
<div class="flex flex-col me-4 w-48 shrink-0">
<div class="grow overflow-y-auto">
<div v-for="(script, index) in scripts" :key="index"
class="flex items-center mb-1 px-1 py-0.5 cursor-pointer"
:class="{'bg-neutral-700': index === selectedIndex}"
class="flex items-center mb-1 px-1 py-0.5 border-l-4 cursor-pointer"
:class="index === selectedIndex ? 'border-emerald-500' : 'border-transparent'"
@click="selectedIndex = index">
<span class="grow truncate">{{ script.name || '(unnamed)' }}</span>
<button class="btn-icon text-amber-700 hover:text-amber-600" @click.stop="removeScript(index)"><TrashIcon /></button>
</div>
<button class="btn-icon flex items-center" @click="addScript"><PlusIcon /> Add script</button>
</div>
<button class="btn-icon mt-2" @click="addScript"><PlusIcon /> Add script</button>
</div>
<div v-if="selected" class="flex flex-col grow min-h-0">
<div class="border-b-1 mb-2">
<div class="border-b-1 mb-2 pb-2">
Name: <input class="me-1" type="text" v-model="selected.name" />
</div>
<div class="border-b-1">
Scopes:
<div class="ms-4 mt-2">
Ledgers:
<Dropdown inline :auto-close="false" v-model:open="scopesExpanded" class="mb-2">
<template #button>Scopes</template>
<Dropdown inline :auto-close="false" v-model:open="ledgersExpanded" class="ms-4 mt-2">
<template #button>Ledgers</template>
<div class="flex flex-wrap items-center mt-2">
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.ledgers" :key="index">
<input class="me-1" type="text" v-model="entry.ref" />
@@ -151,10 +175,10 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
<button class="btn-icon" @click="addLedgerScope"><PlusIcon /></button>
</div>
</div>
</div>
</Dropdown>
<div class="ms-4 mt-2">
Characters:
<Dropdown inline :auto-close="false" v-model:open="charactersExpanded" class="ms-4 mt-2">
<template #button>Characters</template>
<div class="flex flex-wrap items-center mt-2">
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.characters" :key="index">
<input class="me-1" type="text" v-model="entry.ref" />
@@ -168,55 +192,53 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
<button class="btn-icon" @click="addCharacterScope"><PlusIcon /></button>
</div>
</div>
</div>
</Dropdown>
<div class="ms-4 mt-2">
Corporations:
<Dropdown inline :auto-close="false" v-model:open="corporationsExpanded" class="ms-4 mt-2">
<template #button>Corporations</template>
<div class="flex flex-wrap items-center mt-2">
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.corporations" :key="index">
<input class="me-1" type="text" v-model="entry.ref" />
<span class="me-1">:</span>
<input class="me-1 w-32" type="number" v-model.number="entry.corporationId" placeholder="Corporation ID" />
<CorporationLabel class="me-1" :corporation-id="entry.corporationId" :corporation="resolvedCorporation(entry.corporationId).value" :size="16" v-if="entry.corporationId" />
<CorporationLabel class="me-1" :corporation-id="entry.corporationId" :corporation="corporationsStore.corporations[entry.corporationId]" :size="16" v-if="entry.corporationId" />
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeCorporationScope(index)"><TrashIcon /></button>
</div>
<div class="flex items-center mb-2">
<button class="btn-icon" @click="addCorporationScope"><PlusIcon /></button>
</div>
</div>
</div>
</Dropdown>
<div class="ms-4 mt-2 mb-2">
Wallet Divisions:
<Dropdown inline :auto-close="false" v-model:open="walletDivisionsExpanded" class="ms-4 mt-2 mb-2">
<template #button>Wallet Divisions</template>
<div class="flex flex-wrap items-center mt-2">
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.walletDivisions" :key="index">
<input class="me-1" type="text" v-model="entry.ref" />
<span class="me-1">:</span>
<input class="me-1 w-32" type="number" v-model.number="entry.corporationId" placeholder="Corporation ID" />
<input class="me-1 w-16" type="number" min="1" max="7" v-model.number="entry.division" placeholder="Division" />
<CorporationLabel class="me-1" :corporation-id="entry.corporationId" :corporation="resolvedCorporation(entry.corporationId).value" :division="entry.division" :size="16" v-if="entry.corporationId" />
<CorporationLabel class="me-1" :corporation-id="entry.corporationId" :corporation="corporationsStore.corporations[entry.corporationId]" :division="entry.division" :size="16" v-if="entry.corporationId" />
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeWalletDivisionScope(index)"><TrashIcon /></button>
</div>
<div class="flex items-center mb-2">
<button class="btn-icon" @click="addWalletDivisionScope"><PlusIcon /></button>
</div>
</div>
</div>
</div>
</Dropdown>
</Dropdown>
<div class="flex flex-col grow min-h-0 border-b-1">
Script:
<ScriptEditor class="mt-2 mb-2" v-model="scriptText" :scope="editorScope" />
</div>
<div class="mt-2 justify-end flex">
<button @click="save">Save</button>
</div>
</div>
<div v-else class="flex grow items-center justify-center text-neutral-500">
No scripts yet
</div>
<div class="mt-2 justify-end flex" v-if="selected">
<div>
<button @click="save">Save</button>
</div>
</div>
</div>
</template>