Compare commits

...

3 Commits

Author SHA1 Message Date
31e57da117 Move currency out of the storage page in the web UI
This brings the currency page in line with the grail page.
2025-10-02 22:40:18 +02:00
2baa43db20 Add rudimentary grail tracker for sets and uniques 2025-10-02 22:40:04 +02:00
3e2c481f6f Add basic menu to the web UI 2025-10-02 19:19:05 +02:00
8 changed files with 196 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ from flask import Flask, redirect, abort, render_template, request
from pathlib import Path from pathlib import Path
import shutil import shutil
from datetime import datetime from datetime import datetime
import json
import psutil import psutil
from d2warehouse.item import Item, Quality, lookup_basetype from d2warehouse.item import Item, Quality, lookup_basetype
@@ -14,6 +15,7 @@ import re
from d2warehouse.stash import StashFullError from d2warehouse.stash import StashFullError
STASH_FILES = { STASH_FILES = {
"softcore": "SharedStashSoftCoreV2.d2i", "softcore": "SharedStashSoftCoreV2.d2i",
"hardcore": "SharedStashHardCoreV2.d2i", "hardcore": "SharedStashHardCoreV2.d2i",
@@ -271,6 +273,7 @@ def list_storage_category(stash_name: str, category: str):
"list_storage.html", "list_storage.html",
stash_name=stash_name, stash_name=stash_name,
storage_items=items, storage_items=items,
category=category,
storage_count=lambda x: storage_count(x, stash_name), storage_count=lambda x: storage_count(x, stash_name),
) )
@@ -345,7 +348,7 @@ def storage_currency_counts(item_codes: list[str], stash_name: str) -> dict:
return currencies return currencies
@app.route("/storage/<stash_name>/currency") @app.route("/currency/<stash_name>")
def storage_currency(stash_name: str): def storage_currency(stash_name: str):
if stash_name not in DB_FILES: if stash_name not in DB_FILES:
abort(404) abort(404)
@@ -356,3 +359,86 @@ def storage_currency(stash_name: str):
return render_template( return render_template(
"currency.html", runes=runes, gems=gems, keys=keys, essences=essences "currency.html", runes=runes, gems=gems, keys=keys, essences=essences
) )
def load_uniques_data():
"""Return a sorted dictionary of unique item ids indexed by names."""
uniques_path = Path(__file__).resolve().parent.parent / "data/uniques.json"
with uniques_path.open() as f:
data = json.load(f)
name_to_id = {v["name"]: int(k) for k, v in data.items()}
del name_to_id["Amulet of the Viper"]
del name_to_id["Staff of Kings"]
del name_to_id["Horadric Staff"]
del name_to_id["Khalim's Flail"]
del name_to_id["Khalim's Will"]
del name_to_id["Hell Forge Hammer"]
return {name: name_to_id[name] for name in sorted(name_to_id.keys())}
def load_sets_data():
"""Return a sorted dictionary of unique item ids indexed by names."""
uniques_path = Path(__file__).resolve().parent.parent / "data/sets.json"
with uniques_path.open() as f:
data = json.load(f)
name_to_id = {v["name"]: int(k) for k, v in data.items()}
return {name: name_to_id[name] for name in sorted(name_to_id.keys())}
all_uniques = load_uniques_data()
all_sets = load_sets_data()
def get_found_uniques(db):
rows = db.execute(
"SELECT DISTINCT unique_id "
"FROM item INNER JOIN item_extra ON id = item_id "
"WHERE quality = 7 AND deleted IS NULL AND socketed_into IS NULL",
).fetchall()
return {row["unique_id"]: True for row in rows}
def get_found_sets(db):
rows = db.execute(
"SELECT DISTINCT set_id "
"FROM item INNER JOIN item_extra ON id = item_id "
"WHERE quality = 5 AND deleted IS NULL AND socketed_into IS NULL",
).fetchall()
return {row["set_id"]: True for row in rows}
@app.route("/grail/<stash_name>")
def storage_grail(stash_name: str):
if stash_name not in DB_FILES:
abort(404)
db = get_stash_db(stash_name)
# Unique progress
found = get_found_uniques(db)
items = {name: found.get(id, False) for name, id in all_uniques.items()}
count = len(found)
total = len(all_uniques)
uniques = {
"list": items,
"count": count,
"total": total,
"progress": count / total * 100,
}
# Set progress
found = get_found_sets(db)
items = {name: found.get(id, False) for name, id in all_sets.items()}
count = len(found)
total = len(all_sets)
sets = {
"list": items,
"count": count,
"total": total,
"progress": count / total * 100,
}
return render_template(
"grail.html",
uniques=uniques,
sets=sets,
)

View File

@@ -11,3 +11,10 @@ function toggleSelectAll(tabIndex) {
cb.checked = !allSelected; cb.checked = !allSelected;
}); });
} }
function toggleCollected() {
const collected = document.querySelectorAll('.collected');
collected.forEach(item => {
item.classList.toggle('hidden');
});
}

View File

@@ -5,6 +5,25 @@ body {
color: rgb(240, 240, 240); color: rgb(240, 240, 240);
} }
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
background-color: #444;
}
nav a {
display: block;
padding: 1rem 1.5rem;
text-decoration: none;
color: rgb(240, 240, 240);
}
nav a:hover {
background-color: #333;
}
.stash-tab { .stash-tab {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
@@ -104,3 +123,28 @@ input[type="checkbox"]:checked + label {
background-color: #444; background-color: #444;
color: rgb(240, 240, 240); color: rgb(240, 240, 240);
} }
ul.grail {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 2rem;
}
ul.grail li {
flex: 0 0 15rem;
padding: 0.5rem;
width: 15rem;
display: flex;
}
.hidden {
display: none !important;
}
.collected {
background-color: #343;
}
.uncollected {
background-color: #433;
}

View File

@@ -2,10 +2,19 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Shared Stash</title> <title>Currency</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<head> <head>
<body> <body>
{% include "menu.html" %}
<nav>
<ul>
<li><a href="/storage/{{stash_name or 'softcore'}}">All</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/uniques">Uniques</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/sets">Sets</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/currency">Currency</a></li>
</ul>
</nav>
<div class="currencies"> <div class="currencies">
<div> <div>
<table> <table>

View File

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Grail</title>
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/helpers.js"></script>
<head>
<body>
{% include "menu.html" %}
<button type="button" onclick="toggleCollected()">Toggle collected</button>
<h1>Unique Items</h1>
Progress: {{uniques.count}}/{{uniques.total}} ({{uniques.progress | round(1)}}%)
<ul class="grail">
{% for name,collected in uniques.list.items() %}
<li class="{{ 'collected' if collected else 'uncollected' }}">{{name}}</li>
{% endfor %}
</ul>
<h1>Set Items</h1>
Progress: {{sets.count}}/{{sets.total}} ({{sets.progress | round(1)}}%)
<ul class="grail">
{% for name,collected in sets.list.items() %}
<li class="{{ 'collected' if collected else 'uncollected' }}">{{name}}</li>
{% endfor %}
</ul>
</body>
</html>

View File

@@ -7,6 +7,7 @@
<script src="/static/helpers.js"></script> <script src="/static/helpers.js"></script>
<head> <head>
<body> <body>
{% include "menu.html" %}
<form action="/stash/{{stash_name}}/store" method="POST"> <form action="/stash/{{stash_name}}/store" method="POST">
{% for tab in stash.tabs %} {% for tab in stash.tabs %}
{% set tabloop = loop %} {% set tabloop = loop %}

View File

@@ -2,13 +2,23 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Storage</title> <title>Storage - {{category or 'all'}}</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<head> <head>
<body> <body>
{% include "menu.html" %}
<nav>
<ul>
<li><a href="/storage/{{stash_name or 'softcore'}}">All</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/uniques">Uniques</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/sets">Sets</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/misc">Misc</a></li>
</ul>
</nav>
<form action="/storage/{{stash_name}}/take" method="POST"> <form action="/storage/{{stash_name}}/take" method="POST">
<div> <div>
<!-- TODO: Include item.html --> <!-- TODO: Include item.html -->
There are {{ storage_items | length }} items.
{% for db_id, item in storage_items.items() %} {% for db_id, item in storage_items.items() %}
<div class="storage-item-entry"> <div class="storage-item-entry">
<input type="checkbox" name="item_{{db_id}}" id="item_{{db_id}}" value="take" /> <input type="checkbox" name="item_{{db_id}}" id="item_{{db_id}}" value="take" />

View File

@@ -0,0 +1,8 @@
<nav>
<ul>
<li><a href="/stash/{{stash_name or 'softcore'}}">Stash</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}">Storage</a></li>
<li><a href="/grail/{{stash_name or 'softcore'}}">Grail</a></li>
<li><a href="/currency/{{stash_name or 'softcore'}}">Currency</a></li>
</ul>
</nav>