Compare commits

..

2 Commits

Author SHA1 Message Date
ed6bf05a59 update 2025-01-03 13:06:53 +09:00
20dc07b0e3 update 2025-01-03 09:48:27 +09:00
33 changed files with 5395 additions and 185 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -13,6 +13,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"@tauri-apps/plugin-fs": "~2",
"file-saver": "^2.0.5",
"fp-ts": "^2.16.9",
"pinia": "^2.3.0",
@ -24,6 +25,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@tauri-apps/cli": "^2.2.0",
"@tsconfig/node22": "^22.0.0",
"@types/file-saver": "^2.0.7",
"@types/node": "^22.10.2",

4
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

4834
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.3", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.2.0", features = [] }
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-fs = "2"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,20 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default",
"fs:default",
{
"identifier": "fs:scope",
"allow": [{ "path": "$DOCUMENT/My Games/Path of Exile 2" }, { "path": "$DOCUMENT/My Games/Path of Exile 2/**" }]
},
{
"identifier": "fs:allow-write-text-file",
"allow": [ { "path": "$DOCUMENT/My Games/Path of Exile 2/*" }]
}
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

17
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,17 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

37
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,37 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "poe2-loot-filter-vue",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "bun dev",
"beforeBuildCommand": "bun build"
},
"app": {
"windows": [
{
"title": "poe2-loot-filter-vue",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@ -1,15 +1,12 @@
<script setup lang="ts">
import { Button, Dialog, ConfirmPopup, Toast, Splitter, SplitterPanel, ScrollPanel, useToast } from 'primevue'
import { Button, InputText, ButtonGroup, Dialog, Listbox, ConfirmPopup, Toast, Splitter, SplitterPanel, ScrollPanel, useToast } from 'primevue'
import TreeNav from './components/TreeNav.vue'
import FilterDetail from './components/FilterDetail.vue'
import Info from './components/Info.vue'
import { computed, ref, toRaw, watch } from 'vue'
import * as O from 'fp-ts/Option'
import * as A from 'fp-ts/lib/Array';
import { pipe } from 'fp-ts/function'
import FileSaver from 'file-saver'
import { generateFilterText, type Filter, type FilterConfig, type FilterNode } from './models'
import { filterToTreeNode, treeNodeToFilter } from './services/filter'
import { BaseDirectory, writeTextFile, lstat, readDir, readTextFile } from '@tauri-apps/plugin-fs';
const darkMode = ref(document.documentElement.classList.contains('dark'))
@ -25,14 +22,9 @@ watch(darkMode, (dark) => {
localStorage.theme = dark ? 'dark' : 'light'
})
let defaultFilters: Filter[] = []
const nodes = ref(pipe(
localStorage.getItem("filters"),
O.fromNullable,
O.map((value): Filter[] => JSON.parse(value)),
O.getOrElse(() => defaultFilters),
A.map(filterToTreeNode)
))
const filterName = ref()
const nodes = ref([] as FilterNode[])
const selectedFilter = ref<FilterNode>()
function onDelete() {
@ -46,60 +38,123 @@ function onDelete() {
selectedFilter.value = undefined
}
const previewText = ref()
const POE2_FOLDER = 'My Games/Path of Exile 2'
const PREFIX = 'Clearfell'
const CONFIG_SUFFIX = "config.json"
const FILTER_SUFFIX = 'filter'
const BASE_DIR = BaseDirectory.Document
function exportFilter() {
let text = generateFilterText(toRaw(nodes.value).map(treeNodeToFilter))
console.log(text)
previewText.value = text
exportDialogVisible.value = true;
}
function download() {
var blob = new Blob([previewText.value], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, "poe2.filter")
}
const exportDialogVisible = ref(false);
function save() {
if (nodes.value) {
async function save(toastOnComplete?: boolean) {
if (nodes.value && !!filterName.value) {
const filters = toRaw(nodes.value).map(treeNodeToFilter)
localStorage.setItem("filters", JSON.stringify(filters))
let text = generateFilterText(filters)
await Promise.all([writeTextFile(`${POE2_FOLDER}/${PREFIX} ${filterName.value}.${CONFIG_SUFFIX}`, JSON.stringify({ filters }, null, 2), {
baseDir: BASE_DIR,
}), writeTextFile(`${POE2_FOLDER}/${PREFIX} ${filterName.value}.${FILTER_SUFFIX}`, text, {
baseDir: BASE_DIR,
})])
if (toastOnComplete) {
toast.add({ severity: 'success', summary: 'Success', detail: 'Content Saved', life: 3000 });
}
}
}
setInterval(save, 5000)
const CONFIG_PATTERN = `^${PREFIX} (.*)\\.config\\.json$`
const filterNames = ref([] as string[])
async function fetchFilterNames() {
const ls = await readDir(POE2_FOLDER, {
baseDir: BASE_DIR
})
filterNames.value = ls.map(f => {
if (f.isFile) {
let match = f.name.match(CONFIG_PATTERN)
if (match) {
return match[1]
}
}
return null
}).filter(f => f != null)
}
async function load() {
await fetchFilterNames()
loadDialogVisible.value = true
}
async function saveAs() {
await fetchFilterNames()
saveDialogVisible.value = true
}
const loadDialogVisible = ref(false)
const saveDialogVisible = ref(false)
async function loadFilter() {
const json = await readTextFile(`${POE2_FOLDER}/${PREFIX} ${loadName.value}.${CONFIG_SUFFIX}`, {
baseDir: BASE_DIR,
})
const filters: Filter[] = JSON.parse(json).filters
nodes.value = filters.map(f => filterToTreeNode(f))
filterName.value = loadName.value
loadDialogVisible.value = false
loadName.value = undefined
}
async function saveFilter() {
filterName.value = saveName.value
await save(true)
saveDialogVisible.value = false
saveName.value = undefined
}
const loadName = ref()
const saveName = ref()
const overwrite = computed(() => filterNames.value.includes(saveName.value))
</script>
<template>
<ConfirmPopup />
<Toast />
<Dialog v-model:visible="loadDialogVisible" modal header="Load Filter">
<Listbox v-model="loadName" :options="filterNames" />
<template #footer>
<Button label="Cancel" severity="secondary" @click="loadDialogVisible = false" autofocus />
<Button :disabled="!loadName" label="Load" severity="primary" @click="loadFilter" autofocus />
</template>
</Dialog>
<Dialog v-model:visible="saveDialogVisible" modal header="Save Filter As">
<div class="flex flex-col gap-2">
<label>Existing Filters:</label>
<Listbox v-model="saveName" :options="filterNames" />
</div>
<div class="flex flex-col gap-2 mt-4">
<label>Save Name:</label>
<InputText type="text" v-model="saveName" />
</div>
<template #footer>
<Button label="Cancel" severity="secondary" @click="saveDialogVisible = false" autofocus />
<Button :disabled="!saveName" :label="overwrite ? 'Overwrite' : 'Save'"
:severity="overwrite ? 'danger' : 'primary'" @click="saveFilter" autofocus />
</template>
</Dialog>
<div class="flex flex-row gap-2 items-center flex-grow-0">
<article class="prose dark:prose-invert p-4">
<h1>Path of Exile 2 Loot Filter Config</h1>
</article><Button :icon @click="darkMode = !darkMode" severity="secondary" variant="outlined" rounded />
<Button icon="pi pi-save" label="Save" severity="secondary" variant="outlined" rounded
@click="save(); toast.add({ severity: 'success', summary: 'Success', detail: 'Content Saved', life: 3000 });" />
<Button icon="pi pi-file-export" label="Export" severity="primary" @click="exportFilter" />
<Dialog v-model:visible="exportDialogVisible" modal header="Preview Filter Text">
<template #default>
<ScrollPanel class="h-[50vh]">
<pre>{{ previewText }}</pre>
</ScrollPanel>
</template>
<template #footer>
<Button type="button" label="Cancel" severity="secondary" @click="exportDialogVisible = false"></Button>
<Button type="button" icon="pi pi-download" label="Download" @click="download"></Button>
</template>
</Dialog>
<!-- <Button icon = "pi pi-save" severity="secondary" variant="outlined" rounded /> -->
</article>
<ButtonGroup>
<Button :icon @click="darkMode = !darkMode" severity="secondary" variant="outlined" />
<Button icon="pi pi-file-import" label="Load" severity="secondary" variant="outlined" @click="load" />
<Button icon="pi pi-save" label="Save As" severity="secondary" variant="outlined" @click="saveAs" />
<Button :disabled="!filterName" icon="pi pi-save" label="Save" severity="primary" @click="save(true)" />
</ButtonGroup>
</div>
<Splitter class="flex-1 min-h-0">
<SplitterPanel class="flex flex-col">
<TreeNav :nodes :selectedNode="selectedFilter" @nodeSelect="selectedFilter = $event"
<TreeNav :filterName :nodes :selectedNode="selectedFilter" @nodeSelect="selectedFilter = $event"
@nodeUnselect="selectedFilter = undefined" />
</SplitterPanel>
<SplitterPanel class="flex flex-col">

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { toLines, type Filter, type FilterNode, type FilterRule } from '@/models';
import { toDisplayLines, type Filter, type FilterNode, type FilterRule } from '@/models';
import { computed, ref, watchEffect } from 'vue'
import { Button, ColorPicker, ScrollPanel, Tabs, Tab, TabPanels, TabList, TabPanel, ToggleButton, InputText, InputNumber, Select, ToggleSwitch, MultiSelect } from 'primevue'
import { useConfirm } from "primevue/useconfirm";
import { useToast } from "primevue/usetoast";
import { CLASSES, COLORS, BASE_TYPES, RARITIES, SHAPES, OPERATORS, ARMOUR_TYPES } from '@/models/settings';
const confirm = useConfirm();
const toast = useToast();
@ -36,55 +37,7 @@ const props = defineProps<{
node: FilterNode,
onDelete: () => void
}>()
const OPERATORS = ['=', '==', '!=', '<', '<=', '>', '>='];
const COLORS = [
'Red',
'Green',
'Blue',
'Brown',
'White',
'Yellow',
'Cyan',
'Grey',
'Orange',
'Pink',
'Purple'
];
const SHAPES = [
'Circle',
'Diamond',
'Hexagon',
'Square',
'Star',
'Triangle',
'Cross',
'Moon',
'Raindrop',
'Kite',
'Pentagon',
'UpsideDownHouse'
];
const RARITIES = ['Normal', 'Magic', 'Rare', 'Unique'];
const CLASSES = [
'Currency', 'Stackable Currency', 'Jewel', 'Abyss Jewel', 'Divination Card',
'Gem', 'Flask', 'Map', 'Map Fragment', 'Fishing Rods', 'Amulet', 'Ring',
'Claw', 'Dagger', 'Wand', 'One Hand Sword', 'Thrusting One Hand Sword',
'One Hand Axe', 'One Hand Mace', 'Sceptre', 'Rune Dagger', 'Bow', 'Staff',
'Two Hand Sword', 'Two Hand Axe', 'Two Hand Mace', 'Warstaff', 'Body Armour',
'Boots', 'Gloves', 'Helmet', 'Shield', 'Quiver'
];
const BASE_TYPES = [
'Exalted Orb', 'Mirror of Kalandra', 'Eternal Orb', 'Divine Orb',
'Orb of Annulment', 'Chaos Orb', 'Vaal Orb', 'Regal Orb', 'Orb of Alchemy',
'Orb of Fusing', 'Blessed Orb', 'Cartographer\'s Chisel', 'Orb of Scouring',
'Jeweller\'s Orb', 'Chromatic Orb', 'Orb of Chance', 'Orb of Alteration',
'Orb of Transmutation', 'Scroll of Wisdom', 'Portal Scroll'
];
const filter = computed(() => props.node.data)
@ -156,7 +109,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
<ToggleButton class="w-24 flex-shrink-0" onIcon="pi pi-eye" offIcon="pi pi-eye-slash" v-if="filter.type === 'leaf'"
v-model="filter.show" onLabel="Show" offLabel="Hide" />
<InputText class="w-full" type="text" :placeholder="toLines(filter.rule).join(' ')" v-model="filter.name" />
<InputText class="w-full" type="text" :placeholder="toDisplayLines(filter).join(' ')" v-model="filter.name" />
<Button class="flex-shrink-0" icon="pi pi-trash" severity="danger" v-on:click="confirmDelete" />
</div>
@ -176,7 +129,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</colgroup>
<tbody>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
Class
</td>
<td class="pr-4">
@ -187,16 +140,16 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11">
<MultiSelect v-if="ruleWithLookup[1].class != 'inherit' && filter.rule.class"
v-model="filter.rule.class" display="chip" :options="CLASSES" filter placeholder="Select Classes"
class="w-full" :maxSelectedLabels=3 fluid />
v-model="filter.rule.class" display="chip" :options="CLASSES.slice()" filter
placeholder="Select Classes" class="w-full" :maxSelectedLabels=3 fluid />
<MultiSelect disabled v-if="ruleWithLookup[1].class == 'inherit'"
:model-value="ruleWithLookup[0].class" display="chip" :options="CLASSES" filter
:model-value="ruleWithLookup[0].class" display="chip" :options="CLASSES.slice()" filter
placeholder="Select Classes" class="w-full" :maxSelectedLabels=3 fluid />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
BaseType
</td>
<td class="pr-4">
@ -207,15 +160,29 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11">
<MultiSelect v-if="ruleWithLookup[1].base_type != 'inherit' && filter.rule.base_type"
v-model="filter.rule.base_type" display="chip" :options="BASE_TYPES" filter
v-model="filter.rule.base_type" display="chip" :options="BASE_TYPES.slice()" filter
placeholder="Select Base Types" class="w-full" :maxSelectedLabels=3 fluid />
<MultiSelect disabled v-if="ruleWithLookup[1].base_type == 'inherit'"
:model-value="ruleWithLookup[0].base_type" display="chip" :options="BASE_TYPES" filter
:model-value="ruleWithLookup[0].base_type" display="chip" :options="BASE_TYPES.slice()" filter
placeholder="Select Base Types" class="w-full" :maxSelectedLabels=3 fluid />
</td>
</tr>
<tr>
<tr v-if="filter.type === 'leaf'">
<td class="pr-4 whitespace-nowrap">
ArmourType
</td>
<td class="pr-4">
<ToggleSwitch class="align-middle" :model-value="filter.leafRule.armour_type != undefined"
@update:model-value="filter.leafRule.armour_type = $event ? [] : undefined" />
</td>
<td class="h-11">
<MultiSelect v-if="filter.leafRule.armour_type" v-model="filter.leafRule.armour_type" display="chip"
:options="ARMOUR_TYPES.slice()" filter placeholder="Select Base Types" class="w-full"
:maxSelectedLabels=3 fluid />
</td>
</tr>
<tr>
<td class="pr-4 whitespace-nowrap">
Rarity
</td>
<td class="pr-4">
@ -226,17 +193,17 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].rarity != 'inherit' && filter.rule.rarity"
v-model="filter.rule.rarity[0]" :options="OPERATORS" />
v-model="filter.rule.rarity[0]" :options="OPERATORS.slice()" />
<Select v-if="ruleWithLookup[1].rarity != 'inherit' && filter.rule.rarity"
v-model="filter.rule.rarity[1]" :options="RARITIES" />
v-model="filter.rule.rarity[1]" :options="RARITIES.slice()" />
<Select disabled v-if="ruleWithLookup[1].rarity == 'inherit'"
:model-value="ruleWithLookup[0].rarity![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].rarity![0]" :options="OPERATORS.slice()" />
<Select disabled v-if="ruleWithLookup[1].rarity == 'inherit'"
:model-value="ruleWithLookup[0].rarity![1]" :options="RARITIES" />
:model-value="ruleWithLookup[0].rarity![1]" :options="RARITIES.slice()" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
Sockets
</td>
<td class="pr-4">
@ -247,17 +214,17 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].sockets != 'inherit' && filter.rule.sockets"
v-model="filter.rule.sockets[0]" :options="OPERATORS" />
v-model="filter.rule.sockets[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid v-if="ruleWithLookup[1].sockets != 'inherit' && filter.rule.sockets"
v-model="filter.rule.sockets[1]" />
<Select disabled v-if="ruleWithLookup[1].sockets == 'inherit'"
:model-value="ruleWithLookup[0].sockets![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].sockets![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].sockets == 'inherit'"
:model-value="ruleWithLookup[0].sockets![1]" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
Quality
</td>
<td class="pr-4">
@ -268,17 +235,17 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].quality != 'inherit' && filter.rule.quality"
v-model="filter.rule.quality[0]" :options="OPERATORS" />
v-model="filter.rule.quality[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid v-if="ruleWithLookup[1].quality != 'inherit' && filter.rule.quality"
v-model="filter.rule.quality[1]" />
<Select disabled v-if="ruleWithLookup[1].quality == 'inherit'"
:model-value="ruleWithLookup[0].quality![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].quality![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].quality == 'inherit'"
:model-value="ruleWithLookup[0].quality![1]" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
StackSize
</td>
<td class="pr-4">
@ -289,18 +256,18 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].stack_size != 'inherit' && filter.rule.stack_size"
v-model="filter.rule.stack_size[0]" :options="OPERATORS" />
v-model="filter.rule.stack_size[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid
v-if="ruleWithLookup[1].stack_size != 'inherit' && filter.rule.stack_size"
v-model="filter.rule.stack_size[1]" />
<Select disabled v-if="ruleWithLookup[1].stack_size == 'inherit'"
:model-value="ruleWithLookup[0].stack_size![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].stack_size![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].stack_size == 'inherit'"
:model-value="ruleWithLookup[0].stack_size![1]" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
AreaLevel
</td>
<td class="pr-4">
@ -311,18 +278,18 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].area_level != 'inherit' && filter.rule.area_level"
v-model="filter.rule.area_level[0]" :options="OPERATORS" />
v-model="filter.rule.area_level[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid
v-if="ruleWithLookup[1].area_level != 'inherit' && filter.rule.area_level"
v-model="filter.rule.area_level[1]" />
<Select disabled v-if="ruleWithLookup[1].area_level == 'inherit'"
:model-value="ruleWithLookup[0].area_level![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].area_level![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].area_level == 'inherit'"
:model-value="ruleWithLookup[0].area_level![1]" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
DropLevel
</td>
<td class="pr-4">
@ -333,18 +300,18 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].drop_level != 'inherit' && filter.rule.drop_level"
v-model="filter.rule.drop_level[0]" :options="OPERATORS" />
v-model="filter.rule.drop_level[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid
v-if="ruleWithLookup[1].drop_level != 'inherit' && filter.rule.drop_level"
v-model="filter.rule.drop_level[1]" />
<Select disabled v-if="ruleWithLookup[1].drop_level == 'inherit'"
:model-value="ruleWithLookup[0].drop_level![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].drop_level![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].drop_level == 'inherit'"
:model-value="ruleWithLookup[0].drop_level![1]" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
ItemLevel
</td>
<td class="pr-4">
@ -355,18 +322,33 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].item_level != 'inherit' && filter.rule.item_level"
v-model="filter.rule.item_level[0]" :options="OPERATORS" />
v-model="filter.rule.item_level[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid
v-if="ruleWithLookup[1].item_level != 'inherit' && filter.rule.item_level"
v-model="filter.rule.item_level[1]" />
<Select disabled v-if="ruleWithLookup[1].item_level == 'inherit'"
:model-value="ruleWithLookup[0].item_level![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].item_level![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].item_level == 'inherit'"
:model-value="ruleWithLookup[0].item_level![1]" />
</td>
</tr>
<tr>
<tr v-if="filter.type === 'leaf'">
<td class="pr-4 whitespace-nowrap">
(Area - Drop)Level
</td>
<td class="pr-4">
<ToggleSwitch class="align-middle" :model-value="filter.leafRule.area_minus_drop_level != undefined"
@update:model-value="filter.leafRule.area_minus_drop_level = $event ? ['>', 0] : undefined" />
</td>
<td class="h-11 flex gap-2">
<Select v-if="filter.leafRule.area_minus_drop_level"
v-model="filter.leafRule.area_minus_drop_level[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid v-if="filter.leafRule.area_minus_drop_level"
v-model="filter.leafRule.area_minus_drop_level[1]" />
</td>
</tr>
<tr>
<td class="pr-4 whitespace-nowrap">
WaystoneTier
</td>
<td class="pr-4">
@ -377,12 +359,12 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].waystone_tier != 'inherit' && filter.rule.waystone_tier"
v-model="filter.rule.waystone_tier[0]" :options="OPERATORS" />
v-model="filter.rule.waystone_tier[0]" :options="OPERATORS.slice()" />
<InputNumber class="w-16" fluid
v-if="ruleWithLookup[1].waystone_tier != 'inherit' && filter.rule.waystone_tier"
v-model="filter.rule.waystone_tier[1]" />
<Select disabled v-if="ruleWithLookup[1].waystone_tier == 'inherit'"
:model-value="ruleWithLookup[0].waystone_tier![0]" :options="OPERATORS" />
:model-value="ruleWithLookup[0].waystone_tier![0]" :options="OPERATORS.slice()" />
<InputNumber disabled class="w-16" fluid v-if="ruleWithLookup[1].waystone_tier == 'inherit'"
:model-value="ruleWithLookup[0].waystone_tier![1]" />
</td>
@ -400,7 +382,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</colgroup>
<tbody>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
SetFontSize
</td>
<td class="pr-4">
@ -419,7 +401,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
SetTextColor
</td>
<td class="pr-4">
@ -438,7 +420,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
SetBackgroundColor
</td>
<td class="pr-4">
@ -457,7 +439,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
SetBorderColor
</td>
<td class="pr-4">
@ -476,7 +458,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
PlayAlertSound
</td>
<td class="pr-4">
@ -499,7 +481,7 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
MinimapIcon
</td>
<td class="pr-4">
@ -513,19 +495,19 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
v-if="ruleWithLookup[1].minimap_icon != 'inherit' && filter.rule.minimap_icon"
v-model="filter.rule.minimap_icon[0]" />
<Select v-if="ruleWithLookup[1].minimap_icon != 'inherit' && filter.rule.minimap_icon"
v-model="filter.rule.minimap_icon[1]" :options="COLORS" />
v-model="filter.rule.minimap_icon[1]" :options="COLORS.slice()" />
<Select v-if="ruleWithLookup[1].minimap_icon != 'inherit' && filter.rule.minimap_icon"
v-model="filter.rule.minimap_icon[2]" :options="SHAPES" />
v-model="filter.rule.minimap_icon[2]" :options="SHAPES.slice()" />
<InputNumber class="w-16" fluid v-if="ruleWithLookup[1].minimap_icon == 'inherit'" disabled
:model-value="ruleWithLookup[0].minimap_icon![0]" />
<Select v-if="ruleWithLookup[1].minimap_icon == 'inherit'" disabled
:model-value="ruleWithLookup[0].minimap_icon![1]" :options="COLORS" />
:model-value="ruleWithLookup[0].minimap_icon![1]" :options="COLORS.slice()" />
<Select v-if="ruleWithLookup[1].minimap_icon == 'inherit'" disabled
:model-value="ruleWithLookup[0].minimap_icon![2]" :options="SHAPES" />
:model-value="ruleWithLookup[0].minimap_icon![2]" :options="SHAPES.slice()" />
</td>
</tr>
<tr>
<td class="pr-4">
<td class="pr-4 whitespace-nowrap">
PlayEffect
</td>
<td class="pr-4">
@ -536,9 +518,9 @@ function mergeLookup([childValue, childLookup]: any, [parentValue, parentLookup]
</td>
<td class="h-11 flex gap-2">
<Select v-if="ruleWithLookup[1].play_effect != 'inherit' && filter.rule.play_effect"
v-model="filter.rule.play_effect" :options="COLORS" />
v-model="filter.rule.play_effect" :options="COLORS.slice()" />
<Select v-if="ruleWithLookup[1].play_effect == 'inherit'" disabled
:model-value="ruleWithLookup[0].play_effect" :options="COLORS" />
:model-value="ruleWithLookup[0].play_effect" :options="COLORS.slice()" />
</td>
</tr>
</tbody>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { defaultGroup, defaultLeaf, toLines, type FilterNode } from '@/models';
import { defaultGroup, defaultLeaf, toDisplayLines, type FilterNode } from '@/models';
import { filterToTreeNode } from '@/services/filter';
import { Button, Menu, Chip } from 'primevue';
import { ref } from 'vue'
@ -42,7 +42,7 @@ const toggle = (event: MouseEvent) => {
<template>
<div class="flex items-center w-full flex-1">
<div class="flex-1 flex truncate">
<Chip v-if="!node.data.name" class="text-xs py-1.5 px-2" v-for="line in toLines(node.data.rule)">
<Chip v-if="!node.data.name" class="text-xs py-1.5 px-2" v-for="line in toDisplayLines(node.data)">
<span class="truncate max-w-40">{{ line }}</span>
</Chip>
<label v-else>{{ node.data.name }}</label>

View File

@ -10,6 +10,7 @@ import { filterToTreeNode } from '@/services/filter';
const props = defineProps<{
nodes: FilterNodeType[]
selectedNode?: FilterNodeType
filterName?: string
}>()
const emit = defineEmits(['nodeSelect', 'nodeUnselect'])
const selectedKey = ref()
@ -144,7 +145,7 @@ const expandedKeys
<template>
<div class="flex flex-shrink-0 p-4 items-center">
<article class="prose dark:prose-invert flex-1">
<h2>Rules:</h2>
<h2>{{ filterName }}</h2>
</article>
<ButtonGroup>
<Button class="flex-shrink-0" :disabled="selectedPosition == undefined || selectedPosition[0] <= 0"

View File

@ -1,5 +1,13 @@
import { show } from 'fp-ts'
import * as uuid from 'uuid'
import type {
ARMOUR_TYPES,
BASE_TYPES,
CLASSES,
COLORS,
OPERATORS,
RARITIES,
SHAPES,
} from './settings'
interface _Filter {
id: string
@ -16,6 +24,7 @@ export function defaultLeaf(): FilterLeaf {
enabled: true,
rule: {},
show: true,
leafRule: {},
}
}
@ -37,6 +46,7 @@ export interface FilterConfig {
export type FilterLeaf = _Filter & {
type: 'leaf'
show: boolean
leafRule: LeafRule
}
export type FilterGroup = _Filter & {
@ -46,39 +56,37 @@ export type FilterGroup = _Filter & {
export type Filter = FilterLeaf | FilterGroup
export type ItemClass = any // Replace with actual type definition
export type ItemBaseType = any // Replace with actual type definition
export type Op = any // Replace with actual type definition
export type Level = RangedNumber<1, 100> // Replace with actual type definition
export type ItemRarity = string // Replace with actual type definition
export type GameColor = any // Replace with actual type definition
export type MinimapIconShape = any // Replace with actual type definition
export interface RangedNumber<T extends number, U extends number> {
value: number
min: T
max: U
}
export interface Color {
r: number
g: number
b: number
}
export interface FilterRule {
class?: string[] //
base_type?: string[] //
area_level?: [string, number] //
drop_level?: [string, number] //
item_level?: [string, number] //
rarity?: [string, string] //
sockets?: [string, number] //
quality?: [string, number] //
stack_size?: [string, number] //
export type Class = (typeof CLASSES)[number]
export type BaseType = (typeof BASE_TYPES)[number]
export type ArmourType = (typeof ARMOUR_TYPES)[number]
export type Op = (typeof OPERATORS)[number]
export type Rarity = (typeof RARITIES)[number]
export type GameColor = (typeof COLORS)[number]
export type Shape = (typeof SHAPES)[number]
export interface LeafRule {
armour_type?: ArmourType[] // special addition
area_minus_drop_level?: [Op, number] // special addition
}
export interface FilterRule {
class?: Class[] //
base_type?: BaseType[] //
area_level?: [Op, number] //
drop_level?: [Op, number] //
item_level?: [Op, number] //
rarity?: [Op, Rarity] //
sockets?: [Op, number] //
quality?: [Op, number] //
stack_size?: [Op, number] //
// waystones
waystone_tier?: [string, number] //
waystone_tier?: [Op, number] //
// effects
set_font_size?: number //
@ -86,8 +94,8 @@ export interface FilterRule {
set_border_color?: Color //
set_background_color?: Color //
play_alert_sound?: [number, number] //
play_effect?: string //
minimap_icon?: [number, string, string] //
play_effect?: GameColor //
minimap_icon?: [number, GameColor, Shape] //
}
export interface FilterNode {
@ -97,7 +105,22 @@ export interface FilterNode {
children?: FilterNode[]
}
export function toLines(rule: FilterRule): string[] {
export function toDisplayLines(filter: Filter) {
let r: string[] = toLines(filter.rule)
if (filter.type === 'leaf') {
if (filter.leafRule.armour_type) {
r.push(`ArmourType ${filter.leafRule.armour_type.map((c) => `"${c}"`).join(' ')}`)
}
if (filter.leafRule.area_minus_drop_level) {
r.push(
`Area-DropLevel ${filter.leafRule.area_minus_drop_level[0]} ${filter.leafRule.area_minus_drop_level[1]}`,
)
}
}
return r
}
export function toLines(rule: FilterRuleRaw): string[] {
let r: string[] = []
if (rule.class && rule.class.length > 0) {
r.push(`Class ${rule.class.map((c) => `"${c}"`).join(' ')}`)
@ -105,6 +128,15 @@ export function toLines(rule: FilterRule): string[] {
if (rule.base_type && rule.base_type.length > 0) {
r.push(`BaseType ${rule.base_type.map((c) => `"${c}"`).join(' ')}`)
}
if (rule.base_armour) {
r.push(`BaseArmour ${rule.base_armour[0]} ${rule.base_armour[1]}`)
}
if (rule.base_evasion) {
r.push(`BaseEvasion ${rule.base_evasion[0]} ${rule.base_evasion[1]}`)
}
if (rule.base_energy_shield) {
r.push(`BaseEnergyShield ${rule.base_energy_shield[0]} ${rule.base_energy_shield[1]}`)
}
if (rule.rarity) {
r.push(`Rarity ${rule.rarity[0]} ${rule.rarity[1]}`)
}
@ -174,7 +206,7 @@ export function generateFilterText(filters: Filter[]): string {
.join('\n\n')
}
function flatten(filters: Filter[]): Omit<FilterLeaf, 'id' | 'name' | 'type' | 'enabled'>[] {
function flatten(filters: Filter[]): { show: boolean; rule: FilterRuleRaw }[] {
return filters.flatMap((f) => {
if (!f.enabled) {
return []
@ -194,7 +226,85 @@ function flatten(filters: Filter[]): Omit<FilterLeaf, 'id' | 'name' | 'type' | '
}
})
} else {
return [{ show: f.show, rule: f.rule }]
return expandLeaf(f).map((r) => ({ show: f.show, rule: r }))
}
})
}
type FilterRuleRaw = FilterRule & {
base_armour?: [Op, number]
base_evasion?: [Op, number]
base_energy_shield?: [Op, number]
}
function expandLeaf(leaf: FilterLeaf): FilterRuleRaw[] {
let result = [leaf.rule]
if (leaf.leafRule.area_minus_drop_level) {
result = []
for (let area_level = 1; area_level <= 100; area_level++) {
let rule = { ...leaf.rule }
let op: Op
switch (leaf.leafRule.area_minus_drop_level[0]) {
case '<':
op = '>'
break
case '<=':
op = '>='
break
case '>':
op = '<'
break
case '>=':
op = '<='
break
default:
op = leaf.leafRule.area_minus_drop_level[0]
}
rule.area_level = ['==', area_level]
rule.drop_level = [op, Math.max(0, area_level - leaf.leafRule.area_minus_drop_level[1])]
result.push(rule)
}
}
if (leaf.leafRule.armour_type) {
result = result.flatMap((r) => {
return leaf.leafRule.armour_type!.map((t) => {
let rule: FilterRuleRaw = { ...r }
switch (t) {
case 'Armour':
rule.base_armour = ['>', 0]
rule.base_evasion = ['<=', 0]
rule.base_energy_shield = ['<=', 0]
break
case 'Evasion':
rule.base_armour = ['<=', 0]
rule.base_evasion = ['>', 0]
rule.base_energy_shield = ['<=', 0]
break
case 'EnergyShield':
rule.base_armour = ['<=', 0]
rule.base_evasion = ['<=', 0]
rule.base_energy_shield = ['>', 0]
break
case 'Armour + Evasion':
rule.base_armour = ['>', 0]
rule.base_evasion = ['>', 0]
rule.base_energy_shield = ['<=', 0]
break
case 'Armour + EnergyShield':
rule.base_armour = ['>', 0]
rule.base_evasion = ['<=', 0]
rule.base_energy_shield = ['>', 0]
break
case 'Evasion + EnergyShield':
rule.base_armour = ['<=', 0]
rule.base_evasion = ['>', 0]
rule.base_energy_shield = ['>', 0]
break
}
return rule
})
})
}
return result
}

97
src/models/settings.ts Normal file
View File

@ -0,0 +1,97 @@
export const OPERATORS = ['=', '==', '!=', '<', '<=', '>', '>='] as const
export const COLORS = [
'Red',
'Green',
'Blue',
'Brown',
'White',
'Yellow',
'Cyan',
'Grey',
'Orange',
'Pink',
'Purple',
] as const
export const SHAPES = [
'Circle',
'Diamond',
'Hexagon',
'Square',
'Star',
'Triangle',
'Cross',
'Moon',
'Raindrop',
'Kite',
'Pentagon',
'UpsideDownHouse',
] as const
export const RARITIES = ['Normal', 'Magic', 'Rare', 'Unique'] as const
export const CLASSES = [
'Currency',
'Stackable Currency',
'Jewel',
'Abyss Jewel',
'Divination Card',
'Gem',
'Flask',
'Map',
'Map Fragment',
'Fishing Rods',
'Amulet',
'Ring',
'Claw',
'Dagger',
'Wand',
'One Hand Sword',
'One Hand Axe',
'One Hand Mace',
'Sceptre',
'Bow',
'Staff',
'Two Hand Sword',
'Two Hand Axe',
'Two Hand Mace',
'Body Armour',
'Boots',
'Gloves',
'Helmet',
'Shield',
'Quiver',
] as const
export const BASE_TYPES = [
'Exalted Orb',
'Mirror of Kalandra',
'Eternal Orb',
'Divine Orb',
'Orb of Annulment',
'Chaos Orb',
'Vaal Orb',
'Regal Orb',
'Orb of Alchemy',
'Orb of Fusing',
'Blessed Orb',
"Cartographer's Chisel",
'Orb of Scouring',
"Jeweller's Orb",
'Chromatic Orb',
'Orb of Chance',
'Orb of Alteration',
'Orb of Transmutation',
'Scroll of Wisdom',
'Portal Scroll',
] as const
export const ARMOUR_TYPES = [
'Armour',
'Evasion',
'EnergyShield',
'Armour + Evasion',
'Armour + EnergyShield',
'Evasion + EnergyShield',
] as const

View File

@ -3,16 +3,32 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
const host = process.env.TAURI_DEV_HOST
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
// prevent vite from obscuring rust errors
clearScreen: false,
server: {
// Tauri expects a fixed port, fail if that port is not available
strictPort: true,
// if the host Tauri is expecting is set, use it
host: host || false,
port: 5173,
},
// Env variables starting with the item of `envPrefix` will be exposed in tauri's source code through `import.meta.env`.
envPrefix: ['VITE_', 'TAURI_ENV_*'],
build: {
// Tauri uses Chromium on Windows and WebKit on macOS and Linux
target: process.env.TAURI_ENV_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
// don't minify for debug builds
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_ENV_DEBUG,
},
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})