mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-13 04:21:02 +00:00
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:
1 parent
b647e62ca6
commit
3987f1b2ff
8 files changed
+1088
-743
No files matched your search
Generated
+16
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+375
-662
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user