| Logo |
Mod Name (ID) |
Last Released |
Compatibility |
from js import document, console, fetch
import json
import asyncio
from pyodide.ffi import create_proxy
def update_mod_count(event):
all_files = document.getElementById("folderPicker").files
label = document.getElementById("fileCountLabel")
if len(all_files) > 0:
label.textContent = f"Log file '{all_files.item(0).name}' selected"
else:
label.textContent = "No file selected"
# api_mods will store: "modidstr": {full_mod_data_object}
api_mods = {}
async def load_api_data():
global api_mods
try:
response = await fetch("mods.json")
data = await response.text()
json_obj = json.loads(data)
# 1. Update App Version from the JSON itself
app_v = json_obj.get("app_version", "v0.0.0-dev")
document.getElementById("versionBadge").textContent = app_v
# Update Game Version UI
vs_version = json_obj.get("version", "Unknown")
document.getElementById("gameVersion").textContent = f"{vs_version}"
# Update Sync Date UI
sync_date = json_obj.get("last_updated", "Unknown")
document.getElementById("syncDate").textContent = sync_date
# Map mods into dictionary by their string ID for fast lookup
list_data = json_obj.get("mods", [])
for m in list_data:
# Get the first string ID from the list
mid_str = m['modidstrs'][0] if isinstance(m['modidstrs'], list) else m['modidstrs']
# Store the whole object using the string ID as the key
api_mods[mid_str] = m
console.log(f"API Data loaded. Version: {vs_version}")
except Exception as e:
console.log(f"Error loading mods.json: {e}")
async def scan_folder(event):
all_files = document.getElementById("folderPicker").files
if not all_files:
return
log_file = all_files.item(0)
found_ids = []
try:
# Read the single log file directly into memory
content = await log_file.text()
# Look for the specific line pattern
target_phrase = "Mods, sorted by dependency:"
for line in content.splitlines():
if target_phrase in line:
# Extract everything after the phrase
parts = line.split(target_phrase)
if len(parts) > 1:
# Split the mod names by comma and strip extra whitespaces
mods_list_str = parts[1].strip()
raw_mods = mods_list_str.split(",")
found_ids = [m.strip() for m in raw_mods if m.strip()]
break # Found the line, no need to keep reading
except Exception as e:
console.log(f"Error reading log file: {e}")
# Show the editor and populate it with the parsed mod IDs
editor = document.getElementById("modIdEditor")
editor.value = "\n".join(sorted(list(set(found_ids))))
document.getElementById("reviewSection").style.display = "block"
document.getElementById("folderPicker").value = "" # Clear file handles
document.getElementById("fileCountLabel").textContent = f"Scan complete. Found {len(found_ids)} mods."
async def run_final_check(event):
btn = document.getElementById("runCompatibilityCheck")
loading_area = document.getElementById("loadingArea")
progress_bar = document.getElementById("scanProgress")
label = document.getElementById("progressLabel")
# Get IDs from the text area
raw_text = document.getElementById("modIdEditor").value
mod_ids = [line.strip() for line in raw_text.split('\n') if line.strip()]
if not mod_ids: return
btn.disabled = True
loading_area.classList.add("show")
# Process the list
found_mods = {}
total = len(mod_ids)
for i, mid in enumerate(mod_ids):
progress_bar.value = int(((i + 1) / total) * 100)
label.textContent = f"Checking {mid}..."
# We don't have local names anymore since we are just using IDs,
# so we default the name to the ID. display_results will try to find better names.
found_mods[mid] = mid
await asyncio.sleep(0.01) # Small delay for visual feedback
display_results(found_mods)
loading_area.classList.remove("show")
btn.disabled = False
def display_results(found_mods):
body = document.getElementById("resultsBody")
body.innerHTML = ""
# --- SORTING LOGIC START ---
# Sort the items: (True/False, Name)
# This puts True (Compatible) first, then sorts alphabetically by name
sorted_items = sorted(
found_mods.items(),
key=lambda item: (item[0] not in api_mods, item[1].lower())
)
# --- SORTING LOGIC END ---
for mid, local_name in sorted_items:
semver = document.getElementById("gameVersion").textContent.split('.x')[0].strip()
row = document.createElement("tr")
mod_info = api_mods.get(mid)
is_compat = mod_info is not None
# 1. Logo Cell
logo_td = document.createElement("td")
img_src = mod_info.get("logo") if is_compat else None
if not img_src:
img_src = "https://mods.vintagestory.at/web/img/mod-default.png"
logo_html = f'
'
# Just run a search instead. URLs arent consisting enough to build.
logo_td.innerHTML = f'{logo_html}'
# 2. Name Cell
name_td = document.createElement("td")
display_name = mod_info.get("name", local_name) if is_compat else local_name
# If compat, we use the modid from API, otherwise use local ID
sub_name = mid
moddb_row = f'{display_name}
{sub_name}'
local_row = f'{display_name}
{sub_name}'
if is_compat:
name_td.innerHTML = moddb_row
else:
sub_name = local_name
name_td.innerHTML = local_row
# 3. Release Date Cell
date_td = document.createElement("td")
date_td.textContent = mod_info.get("lastreleased", "N/A") if is_compat else "Unknown"
# 4. Status Cell
status_td = document.createElement("td")
# Get the version string from the header we already set
vs_label = document.getElementById("gameVersion").textContent
status_class = 'yes' if is_compat else 'no'
status_text = f"✅ v{vs_label} Compatible" if is_compat else f"❌ v{vs_label} Not Found"
status_td.innerHTML = f'{status_text}'
for td in [logo_td, name_td, date_td, status_td]:
row.appendChild(td)
body.appendChild(row)
document.getElementById("resultsTable").classList.add("show")
from js import Tablesort
Tablesort.new(document.getElementById('resultsTable'))
async def setup():
await load_api_data()
picker = document.getElementById("folderPicker")
picker.onchange = create_proxy(update_mod_count)
# Step 1: Scan
document.getElementById("scanFolder").onclick = create_proxy(scan_folder)
# Step 2: Final Check
document.getElementById("runCompatibilityCheck").onclick = create_proxy(run_final_check)
asyncio.ensure_future(setup())
asyncio.ensure_future(setup())