preview: simpler Modules screen, catalogue as a compact list with categories

- Header is just "Modules" with "Add from a link" (the GitHub URL bar is folded
  away until asked for) and a menu for builds/logs, build tools and the
  diagnostics export. The build-tools bar shows only when something is missing.
- Two tabs (Installed, Catalog) styled like the settings page.
- Catalogue: one list of rows (name, owner, category, stars, one action); CoA's
  own modules first. Compatibility and sort are drop-downs; 20 functional
  categories as chips with counts. "Checked by CoA" is the curated list only.
- Categories: owner's reviewed list (181 modules, high/medium confidence) plus
  keyword rules for every other module (coa-module-categories.json).
- Catalogue statuses come from the current overlay on every read (a cached copy
  kept the old Verified marks), and no more made-up entries for overlay rules
  the public catalogue does not list (fake AGPL-3.0, 0 stars, duplicate mod-cfbg).
- Window size, position and maximized state persist (tauri-plugin-window-state,
  visibility left to the app).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ДмитрийandClaude Opus 5.5 committed 2026-11-07 10:05:58 +05:00
1 parent b647e62ca6
commit 3987f1b2ff
8 files changed
+1088 -743

No files matched your search

Generated
+16
View File
@@ -786,6 +786,7 @@ dependencies = [
"tauri-plugin-process",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tracing",
]
@@ -4656,6 +4657,21 @@ dependencies = [
"zip 4.6.1",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6815cab7b212f9cfe16a9d79d8d22f9e14159e4a9460f7dec1467167a008e3"
dependencies = [
"bitflags 2.13.2",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.21",
]
[[package]]
name = "tauri-runtime"
version = "2.12.0"
@@ -0,0 +1,23 @@
//! How the module categories fall on a real catalogue: counts per category and a few modules of each.
//! usage: catalogue_categories <catalogue.json downloaded from azerothcore.org>
fn main() {
let path = std::env::args().nth(1).expect("usage: catalogue_categories <catalogue.json>");
let raw: coa_core::catalogue::OfficialCatalogueRaw =
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
let overlay = coa_core::catalogue::load_coa_overlay(None);
let items = coa_core::catalogue::normalize_catalogue(&raw, &overlay);
let mut by: std::collections::BTreeMap<String, Vec<(u32, String, String)>> = Default::default();
for i in &items {
let c = coa_core::catalogue::categorize(&i.full_name, &i.name, &i.description, &i.topics);
by.entry(c).or_default().push((i.stars, i.full_name.clone(), i.description.chars().take(70).collect()));
}
println!("modules: {}", items.len());
for c in coa_core::catalogue::categories() {
let mut list = by.remove(&c.code).unwrap_or_default();
list.sort_by(|a, b| b.0.cmp(&a.0));
println!("\n== {} ({}) {}", c.code, c.ru, list.len());
for (s, n, d) in list.iter().take(6) {
println!(" {s:>5} {n:<45} {d}");
}
}
}
+181 -81
View File
@@ -170,6 +170,124 @@ pub struct PublicCatalogueItem {
pub coa_status_source: String, // "Overlay", "VerifiedCatalog", "AutoPublic"
pub conflict_reason: Option<String>,
pub is_coa_recommended: bool,
/// Functional category code (see `coa-module-categories.json`); "other" when nothing fits.
#[serde(default)]
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryInfo {
pub code: String,
pub ru: String,
pub en: String,
#[serde(default)]
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct CategoryDocument {
categories: Vec<CategoryInfo>,
/// Owner-reviewed assignments (lowercase `owner/repo` -> category code).
repos: BTreeMap<String, String>,
}
static EMBEDDED_CATEGORIES_JSON: &str = include_str!("coa-module-categories.json");
fn category_document() -> &'static CategoryDocument {
static DOC: std::sync::OnceLock<CategoryDocument> = std::sync::OnceLock::new();
DOC.get_or_init(|| {
serde_json::from_str(EMBEDDED_CATEGORIES_JSON).unwrap_or(CategoryDocument { categories: Vec::new(), repos: BTreeMap::new() })
})
}
pub fn categories() -> Vec<CategoryInfo> {
category_document().categories.clone()
}
/// Lowercase words of a text: letters and digits, so `mod-pvp-titles` gives `mod`, `pvp`, `titles`.
fn words(text: &str) -> Vec<String> {
text.to_lowercase()
.split(|c: char| !c.is_alphanumeric() && c != '+')
.filter(|w| !w.is_empty())
.map(str::to_string)
.collect()
}
fn keyword_hits(keyword: &str, text_words: &[String]) -> usize {
let kw = words(keyword);
match kw.as_slice() {
[] => 0,
// one word: whole-word match; words of 4+ letters also match their plural or longer forms
[k] => text_words.iter().filter(|w| *w == k || (k.chars().count() >= 4 && w.starts_with(k.as_str()))).count(),
// a phrase: its words in a row
many => text_words.windows(many.len()).filter(|win| win.iter().zip(many).all(|(a, b)| a == b)).count(),
}
}
/// The category of a module: the owner-reviewed list first, then the keywords of each category against the
/// repository's name (counted twice), description and topics; "other" when no keyword matches.
pub fn categorize(slug: &str, name: &str, description: &str, topics: &[String]) -> String {
let doc = category_document();
if let Some(code) = doc.repos.get(&normalize_repo_slug(slug)) {
return code.clone();
}
let name_words = words(name);
let mut body = words(description);
for t in topics.iter().filter(|t| !t.starts_with("azerothcore") && !matches!(t.as_str(), "wow" | "wotlk" | "world-of-warcraft")) {
body.extend(words(t));
}
let mut best: Option<(&str, usize)> = None;
for c in &doc.categories {
let score: usize = c.keywords.iter().map(|k| 2 * keyword_hits(k, &name_words) + keyword_hits(k, &body)).sum();
if score > 0 && best.is_none_or(|(_, s)| score > s) {
best = Some((&c.code, score));
}
}
best.map(|(c, _)| c.to_string()).unwrap_or_else(|| "other".into())
}
/// Statuses and categories come from the current overlay and category list on every read, never from what a
/// cache stored: a corrected overlay must show at once. Entries an earlier version made up for overlay rules the
/// public catalogue does not contain (marked with the `coa-verified` topic) are dropped.
fn finalize(items: Vec<PublicCatalogueItem>, overlay: &CoaOverlayDocument) -> Vec<PublicCatalogueItem> {
let rules: BTreeMap<String, &CoaOverlayRule> =
overlay.rules.iter().map(|r| (normalize_repo_slug(&r.repo_slug), r)).collect();
items
.into_iter()
.filter(|i| !i.topics.iter().any(|t| t == "coa-verified"))
.map(|mut i| {
let slug = normalize_repo_slug(&i.full_name);
match rules.get(&slug) {
Some(rule) => {
i.coa_status = rule.status;
i.coa_status_source = "Overlay".into();
i.conflict_reason = rule.reason.clone();
i.is_coa_recommended = rule.is_recommended || rule.status == CoaCompatibilityStatus::Recommended;
}
None => {
i.coa_status = CoaCompatibilityStatus::Unverified;
i.coa_status_source = "AutoPublic".into();
i.conflict_reason = None;
i.is_coa_recommended = false;
}
}
i.category = categorize(&slug, &i.name, &i.description, &i.topics);
i
})
.collect()
}
fn respond(items: Vec<PublicCatalogueItem>, overlay: &CoaOverlayDocument, cached_at: String, is_offline: bool, etag: Option<String>) -> PublicCatalogueResponse {
let items = finalize(items, overlay);
PublicCatalogueResponse {
total_count: items.len(),
recommended_count: items.iter().filter(|i| i.is_coa_recommended).count(),
cached_at,
is_offline,
etag,
items,
categories: categories().into_iter().map(|c| CategoryInfo { keywords: Vec::new(), ..c }).collect(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -191,6 +309,8 @@ pub struct PublicCatalogueResponse {
pub etag: Option<String>,
pub total_count: usize,
pub recommended_count: usize,
#[serde(default)]
pub categories: Vec<CategoryInfo>,
}
// ---------------------------------------------------------------------------
@@ -368,44 +488,14 @@ pub fn normalize_catalogue(
coa_status_source: coa_source,
conflict_reason: reason,
is_coa_recommended: is_rec,
category: String::new(),
});
}
// 4. Also ensure any Recommended overlay rules that were not present in the public raw catalog
// are included so they are never lost
for rule in &overlay.rules {
let slug = normalize_repo_slug(&rule.repo_slug);
if seen_slugs.insert(slug.clone()) {
let parts: Vec<&str> = slug.split('/').collect();
let (owner, name) = if parts.len() >= 2 {
(parts[0].to_string(), parts[1].to_string())
} else {
("corfirean".to_string(), slug.clone())
};
// Overlay rules for repositories the public catalogue does not list are not turned into entries: the
// catalogue shows only what azerothcore.org lists (CoA's own modules are in the Manager's curated list).
normalized.push(PublicCatalogueItem {
id: rule.repo_id.unwrap_or(0),
name: name.clone(),
full_name: slug.clone(),
owner,
html_url: format!("https://github.com/{}", slug),
description: rule.reason.clone().unwrap_or_default(),
stars: 0,
forks: 0,
default_branch: "master".to_string(),
pushed_at: None,
updated_at: None,
license: Some("AGPL-3.0".to_string()),
topics: vec!["azerothcore-module".to_string(), "coa-verified".to_string()],
coa_status: rule.status,
coa_status_source: "Overlay".to_string(),
conflict_reason: rule.reason.clone(),
is_coa_recommended: rule.is_recommended || rule.status == CoaCompatibilityStatus::Recommended,
});
}
}
// 5. Sort:
// 4. Sort:
// Group A (Recommended/Verified) first, then by Stars desc, then by pushed_at desc
normalized.sort_by(|a, b| {
// Priority 1: Recommended modules first
@@ -461,15 +551,7 @@ pub fn fetch_or_load_catalogue(
if let Ok(cached) = serde_json::from_str::<CachedDocument>(&content) {
let age = now_ts - cached.timestamp;
if age < DEFAULT_CACHE_TTL_SECS && !cached.items.is_empty() {
let rec_count = cached.items.iter().filter(|i| i.is_coa_recommended).count();
return Ok(PublicCatalogueResponse {
total_count: cached.items.len(),
recommended_count: rec_count,
cached_at: cached.cached_at,
is_offline: false,
etag: cached.etag,
items: cached.items,
});
return Ok(respond(cached.items, &overlay, cached.cached_at, false, cached.etag));
}
}
}
@@ -507,15 +589,7 @@ pub fn fetch_or_load_catalogue(
cached.timestamp = now_ts;
cached.cached_at = Utc::now().to_rfc3339();
let _ = fsx::atomic_write_json(&cache_file, &cached);
let rec_count = cached.items.iter().filter(|i| i.is_coa_recommended).count();
return Ok(PublicCatalogueResponse {
total_count: cached.items.len(),
recommended_count: rec_count,
cached_at: cached.cached_at,
is_offline: false,
etag: cached.etag,
items: cached.items,
});
return Ok(respond(cached.items, &overlay, cached.cached_at, false, cached.etag));
}
}
}
@@ -540,7 +614,6 @@ pub fn fetch_or_load_catalogue(
})?;
let items = normalize_catalogue(&raw, &overlay);
let rec_count = items.iter().filter(|i| i.is_coa_recommended).count();
let now_str = Utc::now().to_rfc3339();
let cached_doc = CachedDocument {
@@ -557,14 +630,7 @@ pub fn fetch_or_load_catalogue(
let _ = fs::write(&etag_file, tag);
}
return Ok(PublicCatalogueResponse {
total_count: items.len(),
recommended_count: rec_count,
cached_at: now_str,
is_offline: false,
etag: new_etag,
items,
});
return Ok(respond(items, &overlay, now_str, false, new_etag));
}
// If non-200 and non-304 HTTP status, fallback to existing cache if available
@@ -585,15 +651,7 @@ fn try_offline_fallback(
if cache_file.is_file() {
if let Ok(content) = fs::read_to_string(cache_file) {
if let Ok(cached) = serde_json::from_str::<CachedDocument>(&content) {
let rec_count = cached.items.iter().filter(|i| i.is_coa_recommended).count();
return Ok(PublicCatalogueResponse {
total_count: cached.items.len(),
recommended_count: rec_count,
cached_at: cached.cached_at,
is_offline: true,
etag: cached.etag.or(stored_etag),
items: cached.items,
});
return Ok(respond(cached.items, overlay, cached.cached_at, true, cached.etag.or(stored_etag)));
}
}
}
@@ -604,16 +662,7 @@ fn try_offline_fallback(
organizations: BTreeMap::new(),
};
let items = normalize_catalogue(&empty_raw, overlay);
let rec_count = items.iter().filter(|i| i.is_coa_recommended).count();
Ok(PublicCatalogueResponse {
total_count: items.len(),
recommended_count: rec_count,
cached_at: Utc::now().to_rfc3339(),
is_offline: true,
etag: None,
items,
})
Ok(respond(items, overlay, Utc::now().to_rfc3339(), true, None))
}
// ---------------------------------------------------------------------------
@@ -808,6 +857,7 @@ mod tests {
coa_status_source: "CacheTest".into(),
conflict_reason: None,
is_coa_recommended: false,
category: String::new(),
}
];
@@ -833,6 +883,56 @@ mod tests {
let overlay = load_coa_overlay(None);
let fallback_resp = try_offline_fallback(&cache_file, None, &overlay).expect("fallback must succeed");
assert!(fallback_resp.is_offline);
assert!(!fallback_resp.items.is_empty(), "Fallback must supply overlay items even on corrupt cache");
// nothing is made up from overlay rules when there is no catalogue to show
assert!(fallback_resp.items.is_empty());
}
fn item(full_name: &str, name: &str, description: &str, topics: &[&str]) -> PublicCatalogueItem {
PublicCatalogueItem {
id: 1,
name: name.into(),
full_name: full_name.into(),
owner: full_name.split('/').next().unwrap().into(),
html_url: format!("https://github.com/{full_name}"),
description: description.into(),
stars: 0,
forks: 0,
default_branch: "master".into(),
pushed_at: None,
updated_at: None,
license: None,
topics: topics.iter().map(|t| t.to_string()).collect(),
coa_status: CoaCompatibilityStatus::Verified,
coa_status_source: "Overlay".into(),
conflict_reason: Some("stale".into()),
is_coa_recommended: true,
category: String::new(),
}
}
#[test]
fn statuses_come_from_the_current_overlay_not_the_cache_and_made_up_entries_are_dropped() {
let overlay = load_coa_overlay(None);
let cached = vec![
item("azerothcore/mod-duel-reset", "mod-duel-reset", "Duel reset module", &["azerothcore-module"]),
item("azerothcore/mod-cfbg", "mod-cfbg", "Cross-faction battlegrounds", &["azerothcore-module", "coa-verified"]),
];
let out = finalize(cached, &overlay);
assert_eq!(out.len(), 1, "the made-up mod-cfbg entry is dropped");
assert_eq!(out[0].coa_status, CoaCompatibilityStatus::Unverified);
assert!(out[0].conflict_reason.is_none() && !out[0].is_coa_recommended);
assert_eq!(out[0].category, "pvp");
}
#[test]
fn modules_get_a_category_from_the_reviewed_list_or_keywords() {
// reviewed list (the owner's file)
assert_eq!(categorize("DustinHendrickson/mod-ollama-chat", "mod-ollama-chat", "", &[]), "bots");
// keywords: whole words, not parts of other words
assert_eq!(categorize("x/mod-arena-points", "mod-arena-points", "Gives arena points", &[]), "pvp");
assert_eq!(categorize("x/mod-guild-chat", "mod-guild-chat", "", &[]), "social");
assert_eq!(categorize("x/mod-maintain", "mod-maintain", "Maintains things", &[]), "other");
assert_eq!(categorize("x/mod-nothing", "mod-nothing", "", &["azerothcore-module".to_string(), "wotlk".to_string()]), "other");
assert!(categories().iter().any(|c| c.code == "other"));
}
}
@@ -0,0 +1,473 @@
{
"version": 1,
"source": "azerothcore_catalogue_categorized_2026-11-07.xlsx (owner), modules with high or medium confidence",
"categories": [
{
"code": "bots",
"ru": "Боты и искусственный интеллект",
"en": "Bots and AI",
"keywords": [
"playerbots",
"bots",
"npcbot",
"llm",
"ollama",
"ai",
"автономные спутники"
]
},
{
"code": "pve",
"ru": "PvE, подземелья и рейды",
"en": "PvE, dungeons and raids",
"keywords": [
"pve",
"dungeon",
"raid",
"boss",
"mythic+",
"scaling",
"rdf",
"инстансы"
]
},
{
"code": "pvp",
"ru": "PvP, арены и поля боя",
"en": "PvP, arenas and battlegrounds",
"keywords": [
"pvp",
"arena",
"battleground",
"bg",
"duel",
"ffa",
"фракции"
]
},
{
"code": "hardcore",
"ru": "Hardcore и режимы сложности",
"en": "Hardcore and difficulty",
"keywords": [
"hardcore",
"ironman",
"permadeath",
"high risk",
"difficulty"
]
},
{
"code": "progression",
"ru": "Прокачка и прогрессия",
"en": "Leveling and progression",
"keywords": [
"xp",
"level",
"rates",
"leveling",
"rewards",
"prestige",
"progression",
"experience",
"reputation",
"rested"
]
},
{
"code": "classes",
"ru": "Классы, расы и способности",
"en": "Classes, races and abilities",
"keywords": [
"race",
"class",
"talent",
"spells",
"specialization",
"pets"
]
},
{
"code": "cosmetics",
"ru": "Косметика и трансмог",
"en": "Cosmetics and transmog",
"keywords": [
"transmog",
"appearance",
"morph",
"visual",
"vanity",
"cosmetics"
]
},
{
"code": "items",
"ru": "Лут, предметы и экипировка",
"en": "Loot, items and gear",
"keywords": [
"loot",
"item",
"gear",
"enchant",
"reforging",
"bank",
"inventory",
"items",
"reagent"
]
},
{
"code": "economy",
"ru": "Торговля, экономика и профессии",
"en": "Trade, economy and professions",
"keywords": [
"auction",
"market",
"vendor",
"trade",
"gold",
"shop",
"profession",
"ahbot",
"auction house",
"gather",
"herbs",
"mining",
"fishing",
"crafting"
]
},
{
"code": "account",
"ru": "Аккаунты и персонажи",
"en": "Accounts and characters",
"keywords": [
"account",
"character",
"mounts",
"collection",
"migration",
"recruit",
"login",
"alts"
]
},
{
"code": "social",
"ru": "Гильдии, чат и социальные",
"en": "Guilds, chat and social",
"keywords": [
"guild",
"chat",
"messages",
"announcements",
"friends",
"mail"
]
},
{
"code": "npc",
"ru": "NPC и игровые сервисы",
"en": "NPCs and services",
"keywords": [
"npc",
"buffer",
"trainer",
"enchanter",
"services",
"assistant",
"vendor npc",
"gossip"
]
},
{
"code": "world",
"ru": "Мир, зоны и перемещение",
"en": "World, zones and travel",
"keywords": [
"world",
"zone",
"spawn",
"teleport",
"portal",
"travel",
"respawn"
]
},
{
"code": "events",
"ru": "События и мини-игры",
"en": "Events and mini-games",
"keywords": [
"events",
"holidays",
"quests",
"trivia",
"minigames",
"treasure"
]
},
{
"code": "server",
"ru": "Сервер, хостинг и автоматизация",
"en": "Server and automation",
"keywords": [
"server",
"deployment",
"backup",
"cache",
"relay",
"restart",
"monitoring"
]
},
{
"code": "admin",
"ru": "Администрирование и безопасность",
"en": "Administration and security",
"keywords": [
"gm",
"admin",
"moderation",
"logs",
"security",
"bans",
"abuse"
]
},
{
"code": "web",
"ru": "Сайты, API и интеграции",
"en": "Websites, API and integrations",
"keywords": [
"api",
"cms",
"website",
"discord",
"telegram",
"web",
"metrics"
]
},
{
"code": "dev",
"ru": "Разработка, SQL и инструменты",
"en": "Development, SQL and tools",
"keywords": [
"editor",
"db",
"sql",
"lua",
"eluna",
"ide",
"tooling",
"sdk"
]
},
{
"code": "client",
"ru": "Клиент, патчи и интерфейс",
"en": "Client, patches and UI",
"keywords": [
"client",
"patch",
"addon",
"ui",
"launcher",
"controller",
"mpq"
]
},
{
"code": "other",
"ru": "Прочее",
"en": "Other",
"keywords": [
]
}
],
"repos": {
"liyunfan1223/mod-playerbots": "bots",
"dustinhendrickson/mod-ollama-chat": "bots",
"dustinhendrickson/mod-player-bot-level-brackets": "bots",
"dustinhendrickson/mod-ollama-bot-buddy": "bots",
"azerothcore/mod-autobalance": "pve",
"zhengpeiru21/mod-challenge-modes": "pve",
"anchydev/dungeonrespawn": "pve",
"azerothcore/mod-rdf-expansion": "pve",
"silviu20092/mod-mythic-plus": "pve",
"avarishd/mod-brawlers-guild": "pve",
"sogladev/mod-vanilla-naxxramas": "pve",
"pangolp/mod-cfpve": "pve",
"bewarellamas/mod-dungeonmaster": "pve",
"araxiaonline/mod-mythic-plus": "pve",
"sogladev/mod-reset-raid-cooldowns": "pve",
"hallgaeuer/mod-quickbalance": "pve",
"valsan-azerty-boi/mod-ony-naxx-logout-teleport": "pve",
"stealthyape/raidteleporter": "pve",
"zesman21/mod-nordf": "pve",
"helias/mod-bg-slaveryvalley": "pvp",
"azerothcore/mod-duel-reset": "pvp",
"gitdalisar/mod-faction-free": "pvp",
"azerothcore/mod-pvp-titles": "pvp",
"helias/mod-bg-twinpeaks": "pvp",
"helias/mod-bg-battle-for-gilneas": "pvp",
"talamortis/mod-pvpscript": "pvp",
"gozzim/mod-npc-spectator": "pvp",
"azerothcore/mod-bg-reward": "pvp",
"helias/mod-arena-tigerspeak": "pvp",
"helias/mod-arena-tolviron": "pvp",
"tbcstar/mod-high-risk-system": "pvp",
"azerothcore/mod-pvp-zones": "pvp",
"azerothcore/mod-bg-item-reward": "pvp",
"azerothcore/mod-desertion-warnings": "pvp",
"azerothcore/mod-low-level-rbg": "pvp",
"youpeoples/ultimate-full-loot-pvp": "pvp",
"azerothcore/mod-pvpstats-announcer": "pvp",
"anchydev/ffafix": "pvp",
"azerothcore/mod-low-level-arena": "pvp",
"trembon/mod-peacekeeper": "pvp",
"anchydev/bgqueuechecker": "pvp",
"anchydev/hardmode": "hardcore",
"kjack9/mod-dead-means-dead": "hardcore",
"conan1996-2/mod-hard-modes": "hardcore",
"jorge990125/mod-hardcore-makgora": "hardcore",
"stuntmonkey4u/mod-trial-of-finality": "hardcore",
"zindokar/mod-hardcore": "hardcore",
"zhengpeiru21/mod-individual-progression": "progression",
"azerothcore/mod-congrats-on-level": "progression",
"noisiver/mod-weekendbonus": "progression",
"youpeoples/prestige-and-draft-mode": "progression",
"anchydev/attriboost": "progression",
"azerothcore/mod-fireworks-on-level": "progression",
"anchydev/extendedxp": "progression",
"anchydev/prestigious": "progression",
"noisiver/mod-appreciation": "progression",
"betayi/mod_spec_reward": "progression",
"brian8544/setxpbar": "progression",
"nhobodyimportant/prestige": "progression",
"azerothcore/mod-alpha-rewards": "progression",
"michaeldelago/mod-quest-count-level": "progression",
"regrad/update-mob-level-to-player-and-random-item-stats": "progression",
"azerothcore/mod-chromie-xp": "progression",
"kadan-gx/mod-login-rewards": "progression",
"talamortis/mod-individual-xp": "progression",
"talamortis/reward-system": "progression",
"brian8544/mod-setxpbar": "progression",
"fischerlol/mod-level-15-boost": "progression",
"notepadguyofficial/mod-level-rewards": "progression",
"thearktect/mod-weekend-xp": "progression",
"heyitsbench/mod-arac": "classes",
"heyitsbench/mod-worgoblin": "classes",
"azerothcore/mod-npc-beastmaster": "classes",
"zhengpeiru21/mod-leech": "classes",
"azerothcore/mod-racial-trait-swap": "classes",
"noisiver/mod-learnspells": "classes",
"silviu20092/mod-warlock-pet-rename": "classes",
"vhiperdev/mod-spellregulator": "classes",
"xiii-hearts/mod-npc-subclass": "classes",
"gozzim/mod-starting-pet": "classes",
"bewarellamas/mod-profspecs": "classes",
"sogladev/mod-demonic-pact-classic": "classes",
"brian8544/mod-talentbutton": "classes",
"lenzork/mod-petequip": "classes",
"azerothcore/mod-transmog": "cosmetics",
"azerothcore/mod-weapon-visual": "cosmetics",
"azerothcore/mod-morphsummon": "cosmetics",
"azerothcore/mod-costumes": "cosmetics",
"azerothcore/mod-npc-morph": "cosmetics",
"azerothcore/mod-aoe-loot": "items",
"azerothcore/mod-npc-enchanter": "items",
"anchydev/statbooster": "items",
"zhengpeiru21/mod-reagent-bank": "items",
"silviu20092/mod-item-upgrade": "items",
"silviu20092/mod-reforging": "items",
"terrabyte-tbwps/mod-aoe-loot": "items",
"silviu20092/mod-improved-bank": "items",
"pangolp/mod-quest-loot-party": "items",
"anchydev/recycleditems": "items",
"anchydev/statboosterreroller": "items",
"hallgaeuer/mod-dynamic-loot-rates": "items",
"seaferer/aoe-loot---merge": "items",
"protonull/mod-starter-wands": "items",
"talamortis/mod-itemlevel": "items",
"noisiver/mod-junk-to-gold": "economy",
"youpeoples/black-market-auction-house": "economy",
"wowhellgarve-holydeew/mod-multi-vendor": "economy",
"azerothcore/mod-money-for-kills": "economy",
"azerothcore/mod-npc-gambler": "economy",
"elmsroth/mod-trade-items-filter": "economy",
"talamortis/mod_reward_shop": "economy",
"silviu20092/mod-sell-items": "economy",
"tereneckla/mod-profession-experience": "economy",
"jorge990125/mod-black-market": "economy",
"heyitsbench/mod-ptr-template": "account",
"pangolp/mod-recruit-friend": "account",
"azerothcore/mod-npc-all-mounts": "account",
"gozzim/mod-firstlogin-aio": "account",
"talamortis/mod-premium": "account",
"zerkenn/mod-character-services": "account",
"lucafolloni/mod-account-vanity-pets": "account",
"noisiver/mod-accountbound": "account",
"pangolp/mod-mounts-on-account": "account",
"microbica/mod-share-mounts": "account",
"gozzim/mod-globalchat": "social",
"azerothcore/mod-guild-zone-system": "social",
"azerothcore/mod-starter-guild": "social",
"anchydev/itembroadcastguildchat": "social",
"azerothcore/mod-chat-transmitter": "social",
"azerothcore/mod-global-chat": "social",
"noisiver/mod-guildfunds": "social",
"jorge990125/mod-streams": "social",
"noisiver/mod-assistant": "npc",
"azerothcore/mod-npc-services": "npc",
"azerothcore/mod-npc-buffer": "npc",
"pangolp/mod-npc-promotion": "npc",
"azerothcore/mod-npc-codebox": "npc",
"azerothcore/mod-buff-command": "npc",
"jorge990125/mod-bienvenida": "npc",
"dunjeon/mod-timeistime": "world",
"bytesgalore/mod-no-hearthstone-cooldown": "world",
"justin-kaufmann/mod-changeablespawnrates": "world",
"hallgaeuer/mod-quick-respawn": "world",
"justin-kaufmann/mod-noclip": "world",
"eldoba/mod-dynamic-resurrections-": "world",
"elmegaard/mod-auto-resurrect": "world",
"noisiver/mod-spawnpoints": "world",
"tralenor/mod-objscale": "world",
"silviu20092/mod-flightmaster-whistle": "world",
"sogladev/mod-ghost-speed": "world",
"talamortis/playerteleport": "world",
"vhiperdev/mod-no-farming": "world",
"noisiver/mod-activatezones": "world",
"zyggy123/treasure-chest-system": "events",
"azerothcore/mod-tic-tac-toe": "events",
"pangolp/mod-quest-status": "events",
"azerothcore/mod-war-effort": "events",
"youpeoples/quizmaster": "events",
"azerothcore/mod-cta-switch": "events",
"azerothcore/mod-dmf-switch": "events",
"azerothcore/mod-server-auto-shutdown": "server",
"anchydev/recache": "server",
"azerothcore/mod-queue-list-cache": "server",
"maluramichael/mod-influxdb": "server",
"sogladev/mod-increment-cache-version": "server",
"anchydev/aci": "server",
"azerothcore/mod-ip-tracker": "admin",
"tralenor/mod-gomove": "admin",
"talamortis/mod-whologged": "admin",
"talamortis/mod_character_tools": "admin",
"azerothcore/mod-notify-muted": "admin",
"mellianstudios/mod-encounter-logs": "admin",
"miithrandiir/mod_sponsorship": "web",
"exitare/mod-discord-webhook": "web",
"abutbul/mod-game-state-api": "web",
"thearktect/mod-discord-announce": "web",
"azerothcore/skeleton-module": "dev",
"azerothcore/eluna-ts": "dev",
"azerothcore/mod-premium-lib": "dev",
"anchydev/breakingnewsoverride": "client",
"gitdalisar/addon-faction-free-unit-popup": "client"
}
}
+1
View File
@@ -18,6 +18,7 @@ tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-opener = "2"
tauri-plugin-window-state = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
coa-core = { path = "../crates/coa-core" }
+10
View File
@@ -3509,6 +3509,16 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
// Size, position and maximized state come back after a restart. Visibility is left to the app (tray, start
// options), so a window last hidden in the tray still opens normally.
.plugin(
tauri_plugin_window_state::Builder::default()
.with_state_flags(
tauri_plugin_window_state::StateFlags::all()
- tauri_plugin_window_state::StateFlags::VISIBLE,
)
.build(),
)
.invoke_handler(tauri::generate_handler![
server_logging_preset,
release_history,
+9
View File
@@ -299,6 +299,14 @@ export interface PublicCatalogueItem {
coa_status_source: string;
conflict_reason?: string | null;
is_coa_recommended: boolean;
/** Functional category code; "other" when nothing fits. */
category?: string;
}
export interface CatalogueCategory {
code: string;
ru: string;
en: string;
}
export interface PublicCatalogueResponse {
@@ -308,6 +316,7 @@ export interface PublicCatalogueResponse {
etag?: string | null;
total_count: number;
recommended_count: number;
categories?: CatalogueCategory[];
}
export interface CompatibilityReport {
File diff suppressed because it is too large. Load diff