var smapi = smapi || {}; var app; smapi.modList = function (mods, enableBeta) { // init data var defaultStats = { total: 0, compatible: 0, workaround: 0, soon: 0, broken: 0, abandoned: 0, invalid: 0, percentCompatible: 0, percentBroken: 0, percentObsolete: 0 }; var data = { mods: mods, showAdvanced: false, visibleMainStats: $.extend({}, defaultStats), visibleBetaStats: $.extend({}, defaultStats), filters: { source: { value: { open: { value: true }, closed: { value: true } } }, status: { label: enableBeta ? "main status" : "status", value: { // note: keys must match status returned by the API ok: { value: true }, optional: { value: true }, unofficial: { value: true }, workaround: { value: true }, broken: { value: true }, abandoned: { value: true }, obsolete: { value: true } } }, betaStatus: { label: "beta status", value: {} // cloned from status field if needed }, download: { value: { chucklefish: { value: true, label: "Chucklefish" }, curseforge: { value: true, label: "CurseForge" }, moddrop: { value: true, label: "ModDrop" }, nexus: { value: true, label: "Nexus" }, custom: { value: true } } } }, search: "" }; // init filters Object.entries(data.filters).forEach(([groupKey, filterGroup]) => { filterGroup.label = filterGroup.label || groupKey; Object.entries(filterGroup.value).forEach(([filterKey, filter]) => { filter.id = ("filter_" + groupKey + "_" + filterKey).replace(/[^a-zA-Z0-9]/g, "_"); filter.label = filter.label || filterKey; }); }); // init beta filters if (enableBeta) { var filterGroup = data.filters.betaStatus; $.extend(true, filterGroup.value, data.filters.status.value); Object.entries(filterGroup.value).forEach(([filterKey, filter]) => { filter.id = "beta_" + filter.id; }); } else delete data.filters.betaStatus; // init mods for (var i = 0; i < data.mods.length; i++) { var mod = mods[i]; // set initial visibility mod.Visible = true; // set overall compatibility mod.LatestCompatibility = mod.BetaCompatibility || mod.Compatibility; // concatenate searchable text mod.SearchableText = [mod.Name, mod.AlternateNames, mod.Author, mod.AlternateAuthors, mod.Compatibility.Summary, mod.BrokeIn]; if (mod.Compatibility.UnofficialVersion) mod.SearchableText.push(mod.Compatibility.UnofficialVersion); if (mod.BetaCompatibility) { mod.SearchableText.push(mod.BetaCompatibility.Summary); if (mod.BetaCompatibility.UnofficialVersion) mod.SearchableText.push(mod.BetaCompatibility.UnofficialVersion); } for (var p = 0; p < mod.ModPages; p++) mod.SearchableField.push(mod.ModPages[p].Text); mod.SearchableText = mod.SearchableText.join(" ").toLowerCase(); } // init app app = new Vue({ el: "#app", data: data, mounted: function () { // enable table sorting $("#mod-list").tablesorter({ cssHeader: "header", cssAsc: "headerSortUp", cssDesc: "headerSortDown" }); // put focus in textbox for quick search if (!location.hash) $("#search-box").focus(); // jump to anchor (since table is added after page load) this.fixHashPosition(); }, methods: { /** * Update the visibility of all mods based on the current search text and filters. */ applyFilters: function () { // get search terms var words = data.search.toLowerCase().split(" "); // apply criteria var mainStats = data.visibleMainStats = $.extend({}, defaultStats); var betaStats = data.visibleBetaStats = $.extend({}, defaultStats); for (var i = 0; i < data.mods.length; i++) { var mod = data.mods[i]; mod.Visible = true; // check filters mod.Visible = this.matchesFilters(mod, words); if (mod.Visible) { mainStats.total++; betaStats.total++; mainStats[this.getCompatibilityGroup(mod.Compatibility.Status)]++; betaStats[this.getCompatibilityGroup(mod.LatestCompatibility.Status)]++; } } // add aggregate stats for (let stats of [mainStats, betaStats]) { stats.percentCompatible = Math.round((stats.compatible + stats.workaround) / stats.total * 100); stats.percentBroken = Math.round((stats.soon + stats.broken) / stats.total * 100); stats.percentObsolete = Math.round(stats.abandoned / stats.total * 100); } }, /** * Fix the window position for the current hash. */ fixHashPosition: function () { if (!location.hash) return; var row = $(location.hash); var target = row.prev().get(0) || row.get(0); if (target) target.scrollIntoView(); }, /** * Get whether a mod matches the current filters. * @param {object} mod The mod to check. * @param {string[]} searchWords The search words to match. * @returns {bool} Whether the mod matches the filters. */ matchesFilters: function (mod, searchWords) { var filters = data.filters; // check hash if (location.hash === "#" + mod.Slug) return true; // check source if (!filters.source.value.open.value && mod.SourceUrl) return false; if (!filters.source.value.closed.value && !mod.SourceUrl) return false; // check status var mainStatus = mod.Compatibility.Status; if (filters.status.value[mainStatus] && !filters.status.value[mainStatus].value) return false; // check beta status if (enableBeta) { var betaStatus = mod.LatestCompatibility.Status; if (filters.betaStatus.value[betaStatus] && !filters.betaStatus.value[betaStatus].value) return false; } // check download sites var ignoreSites = []; if (!filters.download.value.chucklefish.value) ignoreSites.push("Chucklefish"); if (!filters.download.value.curseforge.value) ignoreSites.push("CurseForge"); if (!filters.download.value.moddrop.value) ignoreSites.push("ModDrop"); if (!filters.download.value.nexus.value) ignoreSites.push("Nexus"); if (!filters.download.value.custom.value) ignoreSites.push("custom"); if (ignoreSites.length) { var anyLeft = false; for (var i = 0; i < mod.ModPageSites.length; i++) { if (ignoreSites.indexOf(mod.ModPageSites[i]) === -1) { anyLeft = true; break; } } if (!anyLeft) return false; } // check search terms for (var w = 0; w < searchWords.length; w++) { if (mod.SearchableText.indexOf(searchWords[w]) === -1) return false; } return true; }, /** * Get a mod's compatibility group for mod metrics. * @param {string} mod The mod status for which to get the group. * @returns {string} The compatibility group (one of 'compatible', 'workaround', 'soon', 'broken', 'abandoned', or 'invalid'). */ getCompatibilityGroup: function (status) { switch (status) { // obsolete case "abandoned": case "obsolete": return "abandoned"; // compatible case "ok": case "optional": return "compatible"; // workaround case "workaround": case "unofficial": return "workaround"; // soon/broken case "broken": if (mod.SourceUrl) return "soon"; else return "broken"; default: return "invalid"; } } } }); app.applyFilters(); app.fixHashPosition(); window.addEventListener("hashchange", function () { app.applyFilters(); app.fixHashPosition(); }); };