This commit is contained in:
Alexander Drozdov
2021-01-11 12:05:21 +02:00
parent d939235e75
commit cea799a680
11 changed files with 249 additions and 194 deletions
+3 -10
View File
@@ -1,14 +1,7 @@
declare module 'vue-trend-chart' {
export default function install(): void
}
declare module 'vue-virtual-scroller' {
export default function install(): void
}
declare module 'vue-popperjs' {
import Vue from 'vue'
export default Vue
import type { Plugin, Plugin, DefineComponent } from 'vue'
const plugin: Plugin
export default plugin
}
declare module '*.json' {
+3 -2
View File
@@ -1,4 +1,5 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+16 -1
View File
@@ -31,10 +31,25 @@ export interface WidgetManager {
hide (wmId: number): void
remove (wmId: number): void
bringToTop (wmId: number): void
create (wmType: string): void
create (wmType: string): void
showBrowser (wmId: number, url: string): void
}
export interface WidgetMenu extends Widget {
anchor: Anchor
alwaysShow: boolean
}
export interface PriceCheckWidget extends Widget {
chaosPriceThreshold: number
}
export interface MapCheckWidget extends Widget {
selectedStats: Array<{
matcher: string
invert: boolean
valueWarning: string
valueDanger: string
valueDesirable: string
}>
}
+6 -4
View File
@@ -16,13 +16,15 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
props: {
position: {
type: Object,
type: Object as PropType<{ x: number, y: number }>,
required: true
}
}
}
})
</script>
+75 -56
View File
@@ -14,24 +14,24 @@
<div class="flex">
<ui-input-debounced class="search-num-input rounded-tl mr-px" :placeholder="$t('min')" :min="filter.boundMin" :max="filter.boundMax" step="any" type="number" :class="{ 'rounded-bl': !showQ20Notice }"
v-if="showMinmaxInput" ref="inputMin"
v-model.number="filter.min" @focus.native="inputFocus($event, 'min')" :delay="0" />
v-model.number="filter.min" @focus="inputFocus($event, 'min')" :delay="0" />
<ui-input-debounced class="search-num-input rounded-tr" :placeholder="$t('max')" :min="filter.boundMin" :max="filter.boundMax" step="any" type="number" :class="{ 'rounded-br': !showQ20Notice }"
v-if="showMinmaxInput" ref="inputMax"
v-model.number="filter.max" @focus.native="inputFocus($event, 'max')" :delay="0" />
v-model.number="filter.max" @focus="inputFocus($event, 'max')" :delay="0" />
</div>
</div>
<div class="flex">
<div class="w-5 flex items-start">
<ui-popper v-if="filter.hidden" tag-name="div" class="flex" :options="{ placement: 'right-start' }" boundaries-selector="#price-window">
<template slot="reference">
<ui-popover v-if="filter.hidden" placement="right-start" boundary="#price-window">
<template #target>
<span class="text-xs leading-none text-gray-600 cursor-pointer">
<i class="fas fa-eye-slash" :class="{ 'faa-ring': !filter.disabled }"></i>
</span>
</template>
<div class="popper">
<template #content>
<div style="max-width: 18.5rem;">{{ $t(filter.hidden) }}</div>
</div>
</ui-popper>
</template>
</ui-popover>
</div>
<div class="flex-1 flex items-start">
<span v-if="showTypeTags"
@@ -73,98 +73,117 @@
</div>
</template>
<script>
import ItemModifierText from '../../ui/ItemModifierText'
<script lang="ts">
import { defineComponent, PropType, computed, ref, nextTick, ComponentPublicInstance } from 'vue'
import ItemModifierText from '../../ui/ItemModifierText.vue'
import { Config } from '@/web/Config'
import { ParsedItem } from '@/parser'
import { StatFilter } from './interfaces'
export default {
export default defineComponent({
components: { ItemModifierText },
emits: ['submit'],
props: {
filter: {
type: Object,
type: Object as PropType<StatFilter>,
required: true
},
item: {
type: Object,
type: Object as PropType<ParsedItem>,
required: true
}
},
computed: {
showMinmaxInput () {
setup (props, ctx) {
const showMinmaxInput = computed(() => {
if (
this.filter.option != null ||
(this.filter.roll == null && this.filter.min == null && this.filter.max == null)
props.filter.option != null ||
(props.filter.roll == null && props.filter.min == null && props.filter.max == null)
) return false
return true
},
showTypeTags () {
if (this.filter.boundMin !== undefined || this.filter.variant) {
})
const showTypeTags = computed(() => {
if (props.filter.boundMin !== undefined || props.filter.variant) {
return false
}
return this.filter.type !== 'armour' &&
this.filter.type !== 'weapon'
},
showQ20Notice () {
return props.filter.type !== 'armour' &&
props.filter.type !== 'weapon'
})
const showQ20Notice = computed(() => {
return [
'armour.armour',
'armour.evasion_rating',
'armour.energy_shield',
'weapon.total_dps',
'weapon.physical_dps'
].includes(this.filter.tradeId[0])
},
sliderValue: {
].includes(props.filter.tradeId[0])
})
const inputMin = ref<ComponentPublicInstance | null>(null)
const inputMax = ref<ComponentPublicInstance | null>(null)
const sliderValue = computed<Array<number>>({
get () {
return [
typeof this.filter.min === 'number' ? this.filter.min : this.filter.boundMin,
typeof this.filter.max === 'number' ? this.filter.max : this.filter.boundMax
typeof props.filter.min === 'number' ? props.filter.min : props.filter.boundMin!,
typeof props.filter.max === 'number' ? props.filter.max : props.filter.boundMax!
]
},
set (value) {
if (this.filter.min !== value[0]) {
this.filter.min = value[0]
this.$nextTick(() => {
this.$refs.inputMin.$el.focus()
if (props.filter.min !== value[0]) {
props.filter.min = value[0]
nextTick(() => {
(inputMin.value!.$el as HTMLInputElement).focus()
})
} else if (this.filter.max !== value[1]) {
this.filter.max = value[1]
this.$nextTick(() => {
this.$refs.inputMax.$el.focus()
} else if (props.filter.max !== value[1]) {
props.filter.max = value[1]
nextTick(() => {
(inputMax.value!.$el as HTMLInputElement).focus()
})
}
this.filter.disabled = false
props.filter.disabled = false
}
},
fontSize () {
return Config.store.fontSize
}
},
methods: {
inputFocus (e, type) {
if (e.target.value === '') {
})
function inputFocus (e: FocusEvent, type: 'min' | 'max') {
const target = e.target as HTMLInputElement
if (target.value === '') {
if (type === 'max') {
this.filter.max = this.filter.defaultMax
props.filter.max = props.filter.defaultMax
} else if (type === 'min') {
this.filter.min = this.filter.defaultMin
props.filter.min = props.filter.defaultMin
}
this.$nextTick(() => {
e.target.select()
nextTick(() => {
target.select()
})
} else {
e.target.select()
target.select()
}
this.filter.disabled = false
},
toggleFilter (e) {
props.filter.disabled = false
}
function toggleFilter (e: MouseEvent) {
if (e.detail === 0) {
this.$emit('submit')
ctx.emit('submit')
} else {
this.filter.disabled = !this.filter.disabled
props.filter.disabled = !props.filter.disabled
}
}
return {
showMinmaxInput,
showTypeTags,
showQ20Notice,
inputMin,
inputMax,
sliderValue,
fontSize: computed(() => Config.store.fontSize),
inputFocus
}
}
}
})
</script>
<style lang="postcss">
@@ -1,11 +1,11 @@
<template>
<ui-popper trigger="clickToToggle" boundaries-selector="#price-window" tag-name="div">
<template slot="reference">
<ui-popover trigger="click" boundary="#price-window">
<template #target>
<button class="bg-gray-700 px-2 opacity-25" :class="{ 'rounded-l': option === 'low', 'rounded-r': option === 'high' }"
>{{ option }}</button>
</template>
<div class="popper">
<form @submit.prevent="submit" class="w-64 text-left p-2">
<template #content>
<form @submit.prevent="submit" class="w-64 p-2">
<div>{{ text }}</div>
<textarea v-if="option !== 'fair'"
v-model="feedbackText"
@@ -13,52 +13,57 @@
rows="5" class="w-full bg-gray-700 text-gray-100 p-1"></textarea>
<button class="btn" type="submit">Send feedback</button>
</form>
</div>
</ui-popper>
</template>
</ui-popover>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue'
import { ParsedItem } from '@/parser'
import { sendFeedback } from './poeprices'
export default {
export default defineComponent({
emits: ['sent'],
props: {
option: {
type: String,
type: String as PropType<'fair' | 'low' | 'high'>,
required: true
},
prediction: {
type: Object,
type: Object as PropType<{ min: number, max: number, currency: 'chaos' | 'exalt' }>,
required: true
},
item: {
type: Object,
type: Object as PropType<ParsedItem>,
required: true
}
},
data () {
return {
feedbackText: ''
}
},
computed: {
text () {
if (this.option === 'low') {
setup (props, ctx) {
const feedbackText = ref('')
const text = computed(() => {
if (props.option === 'low') {
return 'Predicted price is too low.'
} else if (this.option === 'high') {
} else if (props.option === 'high') {
return 'Predicted price is too high.'
} else {
return 'Predicted price is fair.'
}
}
},
methods: {
submit () {
this.$emit('sent')
})
function submit () {
ctx.emit('sent')
sendFeedback({
text: this.feedbackText,
option: this.option
}, this.prediction, this.item)
text: feedbackText.value,
option: props.option
}, props.prediction, props.item)
}
return {
feedbackText,
text,
submit
}
}
}
})
</script>
@@ -56,47 +56,51 @@
</div>
</template>
<script>
import { requestPoeprices } from './poeprices'
import FeedbackOption from './FeedbackOption'
import ItemQuickPrice from '@/web/ui/ItemQuickPrice'
<script lang="ts">
import { defineComponent, watch, ref, PropType } from 'vue'
import { RareItemPrice, requestPoeprices } from './poeprices'
import FeedbackOption from './FeedbackOption.vue'
import ItemQuickPrice from '@/web/ui/ItemQuickPrice.vue'
import { ParsedItem } from '@/parser'
export default {
export default defineComponent({
name: 'PricePrediction',
components: { FeedbackOption, ItemQuickPrice },
props: {
item: {
type: Object,
type: Object as PropType<ParsedItem>,
required: true
}
},
data () {
return {
price: null,
error: null,
loading: false,
showContrib: false,
feedbackSent: false
}
},
watch: {
item: {
immediate: true,
async handler (item) {
try {
this.loading = true
this.error = null
this.price = null
this.showContrib = false
this.feedbackSent = false
this.price = await requestPoeprices(this.item)
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
setup (props) {
const price = ref<RareItemPrice | null>(null)
const error = ref<string | null>(null)
const loading = ref(false)
const showContrib = ref(false)
const feedbackSent = ref(false)
watch(props.item, async (item) => {
try {
loading.value = true
error.value = null
price.value = null
showContrib.value = false
feedbackSent.value = false
price.value = await requestPoeprices(props.item)
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}, { immediate: true })
return {
price,
error,
loading,
showContrib,
feedbackSent
}
}
}
})
</script>
@@ -14,7 +14,7 @@ interface PoepricesApiResponse { /* eslint-disable camelcase */
pred_explanation: Array<[string, number]>
}
interface RareItemPrice {
export interface RareItemPrice {
max: number
min: number
confidence: number
+73 -57
View File
@@ -86,88 +86,104 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, PropType, inject, ref, computed, watch } from 'vue'
import { DateTime } from 'luxon'
import { MainProcess } from '@/ipc/main-process-bindings'
import { execBulkSearch } from './pathofexile-bulk'
import { BulkSearch, execBulkSearch } from './pathofexile-bulk'
import { tradeTag, getTradeEndpoint } from './common'
import { TRADE_TAG_BY_NAME } from '@/assets/data'
import { Leagues } from '../Leagues'
import { Config } from '@/web/Config'
import { ItemFilters } from '../filters/interfaces'
import { ParsedItem } from '@/parser'
import { PriceCheckWidget, WidgetManager } from '@/web/overlay/interfaces'
export default {
function useBulkApi () {
let searchId = 0
const error = ref<string | null>(null)
const result = ref<BulkSearch | null>(null)
async function search (item: ParsedItem, filters: ItemFilters) {
try {
searchId += 1
error.value = null
result.value = null
const _searchId = searchId
const _result = await execBulkSearch(item, filters)
if (_searchId === searchId) {
result.value = _result
}
} catch (err) {
error.value = err.message
}
}
return { error, result, search }
}
export default defineComponent({
props: {
filters: {
type: Object,
type: Object as PropType<ItemFilters>,
required: true
},
item: {
type: Object,
type: Object as PropType<ParsedItem>,
required: true
}
},
inject: ['wm', 'widget'],
data () {
return {
searchId: 0,
loading: false,
error: null,
result: null,
selectedCurr: 'chaos'
}
},
computed: {
selectedResults () {
const arr = Array(20)
if (!this.result) return arr
setup (props) {
const wm = inject<WidgetManager>('wm')!
const widget = inject<{ config: PriceCheckWidget }>('widget')!
const { error, result, search } = useBulkApi()
const listed = this.result[this.selectedCurr].listed
const selectedCurr = ref<'chaos' | 'exa'>('chaos')
const selectedResults = computed(() => {
const arr = Array(20)
if (!result.value) return arr
const listed = result.value[selectedCurr.value].listed
arr.splice(0, listed.length, ...listed)
return arr
},
config () {
return Config.store
}
},
methods: {
async execSearch () {
try {
this.searchId += 1
const searchId = this.searchId
})
this.loading = true
this.error = null
this.result = null
const result = await execBulkSearch(this.item, this.filters)
if (this.searchId !== searchId) return
this.result = result
this.selectedCurr = (result.exa.total > result.chaos.total) ? 'exa' : 'chaos'
watch(result, () => {
if (result.value) {
const { exa, chaos } = result.value
selectedCurr.value = (exa.total > chaos.total) ? 'exa' : 'chaos'
// override, because at league start many players set wrong price, and this breaks auto-detection
if (tradeTag(this.item) === TRADE_TAG_BY_NAME.get('Chaos Orb')) {
this.selectedCurr = 'exa'
} else if (tradeTag(this.item) === TRADE_TAG_BY_NAME.get('Exalted Orb')) {
this.selectedCurr = 'chaos'
if (tradeTag(props.item) === TRADE_TAG_BY_NAME.get('Chaos Orb')) {
selectedCurr.value = 'exa'
} else if (tradeTag(props.item) === TRADE_TAG_BY_NAME.get('Exalted Orb')) {
selectedCurr.value = 'chaos'
}
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
},
getRelativeTime (iso) {
return DateTime.fromISO(iso).toRelative({ style: 'short' })
},
openTradeLink (isExternal) {
const link = `https://${getTradeEndpoint()}/trade/exchange/${Leagues.selected}/${this.result[this.selectedCurr].queryId}`
if (isExternal) {
MainProcess.openSystemBrowser(link)
} else {
this.wm.showBrowser(this.widget.config.wmId, link)
})
return {
error,
result,
selectedResults,
selectedCurr,
execSearch: () => { search(props.item, props.filters) },
config: computed(() => Config.store),
openTradeLink (isExternal: boolean) {
const link = `https://${getTradeEndpoint()}/trade/exchange/${Leagues.selected}/${result.value![selectedCurr.value].queryId}`
if (isExternal) {
MainProcess.openSystemBrowser(link)
} else {
wm.showBrowser(widget.config.wmId, link)
}
},
getRelativeTime (iso: string) {
return DateTime.fromISO(iso).toRelative({ style: 'short' })
}
}
}
}
})
</script>
<style lang="postcss">
@@ -91,7 +91,7 @@ async function requestResults (queryId: string, resultIds: string[]): Promise<Pr
})
}
interface BulkSearch {
export interface BulkSearch {
exa: {
queryId: string
total: number
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div
ref="el"
style="position: relative; overflow: auto;"
style="position: relative; overflow-y: auto;"
@scroll.passive="handleScroll"
>
<div :style="{ height: `${fullHeight}px` }">