From 0babc3e4461db10b05a60c138de017ff032dc884 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Mon, 11 Apr 2022 14:29:33 -0400 Subject: Cleanup log filtering a bit, with a clearer string for the number of total messages. Additionally, save and restore filter state from the URL for better linking. --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 233 ++++++++++++++++++++----- 1 file changed, 187 insertions(+), 46 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content/js') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 59c6026c..69f0a46d 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -94,7 +94,93 @@ smapi.logParser = function (state) { return formatter && formatter.format ? formatter.format(value) : `${value}`; + }, + + /** + * Convert an array of boolean values into a bitmap. + * @param {Boolean[]} value An array of boolean values + * @returns {BigInt} + */ + toBitmap(value) { + let result = BigInt(0); + if (!Array.isArray(value)) + return value ? BigInt(1) : BigInt(0); + + for (let i = 0; i < value.length; i++) { + if (value[i]) + result += BigInt(2) ** BigInt(value.length - i - 1); + } + + return result; + }, + + /** + * Convert a bitmap into an array of boolean values. + * @param {BigInt} value The bitmap + * @param {Number} length The expected length of the result + * @returns {Boolean[]} + */ + fromBitmap(value, length = -1) { + if (typeof value != "bigint") + value = ""; + else + value = value.toString(2); + + const result = []; + while (length > value.length) { + result.push(false); + length--; + } + + for (let i = 0; i < value.length; i++) { + result.push(value[i] === "1" ? true : false); + } + + return result; + }, + + b64ToBigInt(value) { + const bin = atob(value); + const hex = []; + + for (let i = 0; i < bin.length; i++) { + let h = bin.charCodeAt(i).toString(16); + if (h.length % 2) h = `0${h}`; + hex.push(h); + } + + return BigInt(`0x${hex.join('')}`); + }, + + bigIntTo64(value) { + let hex = value.toString(16); + if (hex.length % 2) hex = `0${hex}`; + + const result = []; + for (let i = 0; i < hex.length; i += 2) { + const val = parseInt(hex.slice(i, i + 2), 16); + result.push(String.fromCharCode(val)); + } + + return btoa(result.join('')); + }, + + b64ToUrl(value) { + return value.replace(/\//g, '_').replace(/=/g, '-').replace(/\+/g, '.'); + }, + + urlTob64(value) { + return value.replace(/_/g, '/').replace(/-/g, '=').replace(/\./g, '+'); + }, + + toUrlBitmap(value) { + return helpers.b64ToUrl(helpers.bigIntTo64(helpers.toBitmap(value))); + }, + + fromUrlBitmap(value, length = -1) { + return helpers.fromBitmap(helpers.b64ToBigInt(helpers.urlTob64(value)), length); } + }; // internal event handlers @@ -272,32 +358,19 @@ smapi.logParser = function (state) { functional: true, render: function (createElement, context) { const props = context.props; - if (props.pages > 1) { - return createElement( - "div", - { class: "stats" }, - [ - "showing ", - createElement("strong", helpers.formatNumber(props.start + 1)), - " to ", - createElement("strong", helpers.formatNumber(props.end)), - " of ", - createElement("strong", helpers.formatNumber(props.filtered)), - " (total: ", - createElement("strong", helpers.formatNumber(props.total)), - ")" - ] - ); - } - return createElement( "div", { class: "stats" }, [ "showing ", + createElement("strong", helpers.formatNumber(props.start + 1)), + " to ", + createElement("strong", helpers.formatNumber(props.end)), + " of ", createElement("strong", helpers.formatNumber(props.filtered)), - " out of ", - createElement("strong", helpers.formatNumber(props.total)) + " (total: ", + createElement("strong", helpers.formatNumber(props.total)), + ")" ] ); } @@ -578,34 +651,39 @@ smapi.logParser = function (state) { if (!state.messages) return []; - const start = performance.now(); + //const start = performance.now(); const filtered = []; + let total = 0; + // This is slightly faster than messages.filter(), which is // important when working with absolutely huge logs. for (let i = 0, length = state.messages.length; i < length; i++) { const msg = state.messages[i]; - if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName)) - continue; - if (!this.filtersAllow(msg.ModSlug, msg.LevelName)) continue; - const text = msg.Text || (i > 0 ? state.messages[i - 1].Text : null); - if (this.filterRegex) { + const text = msg.Text || (i > 0 ? state.messages[i - 1].Text : null); this.filterRegex.lastIndex = -1; if (!text || !this.filterRegex.test(text)) continue; } - else if (this.filterText && (!text || text.indexOf(this.filterText) === -1)) + + total++; + + if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName)) continue; filtered.push(msg); } - const end = performance.now(); - //console.log(`applied ${(this.filterRegex ? "regex" : "text")} filter '${this.filterRegex || this.filterText}' in ${end - start}ms`); + filtered.total = total; + + Object.freeze(filtered); + + //const end = performance.now(); + //console.log(`applied ${(this.useRegex ? "regex" : "text")} filter '${this.filterRegex}' in ${end - start}ms`); return filtered; }, @@ -636,10 +714,6 @@ smapi.logParser = function (state) { this.loadFromUrl(); }, methods: { - // Mostly I wanted people to know they can override the PerPage - // message count with a URL parameter, but we can read Page too. - // In the future maybe we should read *all* filter state so a - // user can link to their exact page state for someone else? loadFromUrl: function () { const params = new URL(location).searchParams; if (params.has("PerPage")) { @@ -653,6 +727,78 @@ smapi.logParser = function (state) { if (!isNaN(page) && isFinite(page) && page > 0) this.page = page; } + + let updateFilter = false; + + if (params.has("Filter")) { + state.filterText = params.get("Filter"); + updateFilter = true; + } + + if (params.has("FilterMode")) { + const values = helpers.fromUrlBitmap(params.get("FilterMode"), 3); + state.useRegex = values[0]; + state.useInsensitive = values[1]; + state.useWord = values[2]; + updateFilter = true; + } + + if (params.has("Mods")) { + const keys = Object.keys(this.showMods); + const value = params.get("Mods"); + const values = value === "all" ? true : value === "none" ? false : helpers.fromUrlBitmap(value, keys.length); + + for (let i = 0; i < keys.length; i++) { + this.showMods[keys[i]] = Array.isArray(values) ? values[i] : values; + } + + updateModFilters(); + } + + if (params.has("Levels")) { + const keys = Object.keys(this.showLevels); + const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + + for (let i = 0; i < keys.length; i++) { + this.showLevels[keys[i]] = values[i]; + } + } + + if (params.has("Sections")) { + const keys = Object.keys(this.showSections); + const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + + for (let i = 0; i < keys.length; i++) { + this.showSections[keys[i]] = values[i]; + } + } + + if (updateFilter) + this.updateFilterText(); + }, + + // Whenever the page state changed, replace the current page URL. Using + // replaceState rather than pushState to avoid filling the tab history + // with tons of useless history steps the user probably doesn't + // really care about. + updateUrl: function () { + const url = new URL(location); + url.searchParams.set("Page", state.page); + url.searchParams.set("PerPage", state.perPage); + + url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); + url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); + url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + + if (state.filterText && state.filterText.length) { + url.searchParams.set("Filter", state.filterText); + url.searchParams.set("FilterMode", helpers.toUrlBitmap([state.useRegex, state.useInsensitive, state.useWord])); + } else { + url.searchParams.delete("Filter"); + url.searchParams.delete("FilterMode"); + } + + window.history.replaceState(null, document.title, url.toString()); }, toggleLevel: function (id) { @@ -660,6 +806,7 @@ smapi.logParser = function (state) { return; this.showLevels[id] = !this.showLevels[id]; + this.updateUrl(); }, toggleContentPacks: function () { @@ -723,25 +870,13 @@ smapi.logParser = function (state) { }); }, - // Whenever the page is changed, replace the current page URL. Using - // replaceState rather than pushState to avoid filling the tab history - // with tons of useless history steps the user probably doesn't - // really care about. - updateUrl: function () { - const url = new URL(location); - url.searchParams.set("Page", state.page); - url.searchParams.set("PerPage", state.perPage); - - window.history.replaceState(null, document.title, url.toString()); - }, - // We don't want to update the filter text often, so use a debounce with // a quarter second delay. We basically always build a regular expression // since we use it for highlighting, and it also make case insensitivity // much easier. updateFilterText: helpers.getDebouncedHandler( function () { - let text = this.filterText = document.querySelector("input[type=text]").value; + let text = state.filterText; if (!text || !text.length) { this.filterText = ""; this.filterRegex = null; @@ -754,6 +889,8 @@ smapi.logParser = function (state) { state.useInsensitive ? "ig" : "g" ); } + + this.updateUrl(); }, 250 ), @@ -779,6 +916,7 @@ smapi.logParser = function (state) { this.showMods[id] = !this.showMods[id]; updateModFilters(); + this.updateUrl(); }, toggleSection: function (name) { @@ -786,6 +924,7 @@ smapi.logParser = function (state) { return; this.showSections[name] = !this.showSections[name]; + this.updateUrl(); }, showAllMods: function () { @@ -798,6 +937,7 @@ smapi.logParser = function (state) { } } updateModFilters(); + this.updateUrl(); }, hideAllMods: function () { @@ -810,6 +950,7 @@ smapi.logParser = function (state) { } } updateModFilters(); + this.updateUrl(); }, filtersAllow: function (modId, level) { -- cgit From 94b8507a4763020c578e98ecf5af645fe6583cee Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Mon, 11 Apr 2022 15:01:59 -0400 Subject: Add more documentation strings. Use shallow equality checking to decide whether to include a filter in the URL or not to avoid unnecessarily large URLs. --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 90 ++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content/js') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 69f0a46d..8538423f 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -139,6 +139,11 @@ smapi.logParser = function (state) { return result; }, + /** + * Convert a base-64 string to a BigInt. + * @param {string} value + * @returns {BigInt} + */ b64ToBigInt(value) { const bin = atob(value); const hex = []; @@ -152,6 +157,11 @@ smapi.logParser = function (state) { return BigInt(`0x${hex.join('')}`); }, + /** + * Convert a BigInt to a base-64 string. + * @param {BigInt} value + * @returns {string} + */ bigIntTo64(value) { let hex = value.toString(16); if (hex.length % 2) hex = `0${hex}`; @@ -165,22 +175,79 @@ smapi.logParser = function (state) { return btoa(result.join('')); }, + /** + * Make a base-64 string URL safe. + * @param {string} value + * @returns {string} + */ b64ToUrl(value) { return value.replace(/\//g, '_').replace(/=/g, '-').replace(/\+/g, '.'); }, + /** + * Convert a URL safe base-64 string back to normal. + * @param {string} value + * @returns {string} + */ urlTob64(value) { return value.replace(/_/g, '/').replace(/-/g, '=').replace(/\./g, '+'); }, + /** + * Convert an array of booleans to a BigInt bitmap, then convert that + * to a base-64 string, then make it URL safe. + * @param {Boolean[]} value + * @returns {string} + */ toUrlBitmap(value) { return helpers.b64ToUrl(helpers.bigIntTo64(helpers.toBitmap(value))); }, + /** + * Convert a URL safe base-64 string to a normal base-64 string, convert + * that to a BigInt, and then parse a bitmap from the BigInt. + * @param {string} value + * @param {Number} length The expected length of the bitmap. + */ fromUrlBitmap(value, length = -1) { return helpers.fromBitmap(helpers.b64ToBigInt(helpers.urlTob64(value)), length); - } + }, + /** + * Check the shallow equality of two objects. + * @param {Array} first + * @param {Array} second + * @returns {Boolean} + */ + shallowEquals(first, second) { + if (typeof first !== "object" || typeof second !== "object") + return first === second; + + if (first == null || second == null) + return first == null && second == null; + + const f_array = Array.isArray(first); + const s_array = Array.isArray(second); + + if (f_array !== s_array) + return false; + + const f_keys = Object.keys(first); + const s_keys = Object.keys(second); + + if (f_keys.length != s_keys.length) + return false; + + for (const key of f_keys) { + if (!s_keys.includes(key)) + return false; + + if (first[key] !== second[key]) + return false; + } + + return true; + } }; // internal event handlers @@ -312,6 +379,10 @@ smapi.logParser = function (state) { state.perPage = 1000; state.page = 1; + state.defaultMods = JSON.parse(JSON.stringify(state.showMods)); + state.defaultSections = JSON.parse(JSON.stringify(state.showSections)); + state.defaultLevels = JSON.parse(JSON.stringify(state.showLevels)); + // load saved values, if any if (localStorage.settings) { try { @@ -786,9 +857,20 @@ smapi.logParser = function (state) { url.searchParams.set("Page", state.page); url.searchParams.set("PerPage", state.perPage); - url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); - url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); - url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + if (!helpers.shallowEquals(this.showMods, state.defaultMods)) + url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); + else + url.searchParams.delete("Mods"); + + if (!helpers.shallowEquals(this.showLevels, state.defaultLevels)) + url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); + else + url.searchParams.delete("Levels"); + + if (!helpers.shallowEquals(this.showSections, state.defaultSections)) + url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + else + url.searchParams.delete("Sections"); if (state.filterText && state.filterText.length) { url.searchParams.set("Filter", state.filterText); -- cgit From a21d24f4b7d14701205a6805422de31da84da6ca Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Tue, 12 Apr 2022 02:07:21 -0400 Subject: Replace bitfields for state and just use comma-separated strings. Add a note that numbers may be inaccurate if filtering is used when sections are collapsed. Add quick navigation links. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 13 ++ src/SMAPI.Web/wwwroot/Content/css/main.css | 32 ++++ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 235 +++++++++---------------- 3 files changed, 124 insertions(+), 156 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content/js') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 7aa0fd6b..d95499b7 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -94,6 +94,19 @@ } +@* quick navigation links *@ +@if (log != null) +{ + +} + @* upload result banner *@ @if (Model.UploadError != null) { diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index dcc7a798..52b304d0 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -72,6 +72,7 @@ a { color: #666; } +#quickNav h4, #sidebar h4 { margin: 1.5em 0 0.2em 0; width: 10em; @@ -80,11 +81,13 @@ a { font-weight: normal; } +#quickNav a, #sidebar a { color: #77B; border: 0; } +#quickNav ul, #quickNav li, #sidebar ul, #sidebar li { margin: 0; padding: 0; @@ -93,10 +96,29 @@ a { color: #888; } +#quickNav li, #sidebar li { margin-left: 1em; } +/* quick navigation */ + +#quickNav { + position: fixed; + left: 8px; + bottom: 3em; + width: 12em; + color: #666; +} + +@media (max-height: 400px) { + #quickNav { + position: unset; + width: auto; + } +} + + /* footer */ #footer { margin: 1em; @@ -111,11 +133,16 @@ a { /* mobile fixes */ @media (min-width: 1020px) and (max-width: 1199px) { + #quickNav, #sidebar { width: 7em; background: none; } + #quickNav h4 { + width: unset; + } + #content-column { left: 7em; } @@ -138,4 +165,9 @@ a { top: inherit; left: inherit; } + + #quickNav { + position: unset; + width: auto; + } } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 8538423f..37c57082 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -97,120 +97,23 @@ smapi.logParser = function (state) { }, /** - * Convert an array of boolean values into a bitmap. - * @param {Boolean[]} value An array of boolean values - * @returns {BigInt} + * Try parsing the value as an integer, in base 10. Return the number + * if it's valid, or return the default value otherwise. + * @param {String} value The value to parse. + * @param {Number} defaultValue The value to return if parsing fails. + * @param {Function} critera An optional criteria to check the number with. */ - toBitmap(value) { - let result = BigInt(0); - if (!Array.isArray(value)) - return value ? BigInt(1) : BigInt(0); - - for (let i = 0; i < value.length; i++) { - if (value[i]) - result += BigInt(2) ** BigInt(value.length - i - 1); + tryNumber(value, defaultValue, critera = null) { + try { + value = parseInt(value, 10); + } catch { + return defaultValue; } - return result; - }, - - /** - * Convert a bitmap into an array of boolean values. - * @param {BigInt} value The bitmap - * @param {Number} length The expected length of the result - * @returns {Boolean[]} - */ - fromBitmap(value, length = -1) { - if (typeof value != "bigint") - value = ""; - else - value = value.toString(2); - - const result = []; - while (length > value.length) { - result.push(false); - length--; - } - - for (let i = 0; i < value.length; i++) { - result.push(value[i] === "1" ? true : false); - } - - return result; - }, - - /** - * Convert a base-64 string to a BigInt. - * @param {string} value - * @returns {BigInt} - */ - b64ToBigInt(value) { - const bin = atob(value); - const hex = []; - - for (let i = 0; i < bin.length; i++) { - let h = bin.charCodeAt(i).toString(16); - if (h.length % 2) h = `0${h}`; - hex.push(h); - } - - return BigInt(`0x${hex.join('')}`); - }, - - /** - * Convert a BigInt to a base-64 string. - * @param {BigInt} value - * @returns {string} - */ - bigIntTo64(value) { - let hex = value.toString(16); - if (hex.length % 2) hex = `0${hex}`; - - const result = []; - for (let i = 0; i < hex.length; i += 2) { - const val = parseInt(hex.slice(i, i + 2), 16); - result.push(String.fromCharCode(val)); - } - - return btoa(result.join('')); - }, - - /** - * Make a base-64 string URL safe. - * @param {string} value - * @returns {string} - */ - b64ToUrl(value) { - return value.replace(/\//g, '_').replace(/=/g, '-').replace(/\+/g, '.'); - }, - - /** - * Convert a URL safe base-64 string back to normal. - * @param {string} value - * @returns {string} - */ - urlTob64(value) { - return value.replace(/_/g, '/').replace(/-/g, '=').replace(/\./g, '+'); - }, + if (isNaN(value) || !isFinite(value) || (critera && !critera(value))) + return defaultValue; - /** - * Convert an array of booleans to a BigInt bitmap, then convert that - * to a base-64 string, then make it URL safe. - * @param {Boolean[]} value - * @returns {string} - */ - toUrlBitmap(value) { - return helpers.b64ToUrl(helpers.bigIntTo64(helpers.toBitmap(value))); - }, - - /** - * Convert a URL safe base-64 string to a normal base-64 string, convert - * that to a BigInt, and then parse a bitmap from the BigInt. - * @param {string} value - * @param {Number} length The expected length of the bitmap. - */ - fromUrlBitmap(value, length = -1) { - return helpers.fromBitmap(helpers.b64ToBigInt(helpers.urlTob64(value)), length); + return value; }, /** @@ -433,12 +336,18 @@ smapi.logParser = function (state) { "div", { class: "stats" }, [ - "showing ", - createElement("strong", helpers.formatNumber(props.start + 1)), - " to ", - createElement("strong", helpers.formatNumber(props.end)), - " of ", - createElement("strong", helpers.formatNumber(props.filtered)), + createElement('abbr', { + attrs: { + title: "These numbers may be inaccurate when using filtering with sections collapsed." + } + }, [ + "showing ", + createElement("strong", helpers.formatNumber(props.start + 1)), + " to ", + createElement("strong", helpers.formatNumber(props.end)), + " of ", + createElement("strong", helpers.formatNumber(props.filtered)) + ]), " (total: ", createElement("strong", helpers.formatNumber(props.total)), ")" @@ -788,64 +697,66 @@ smapi.logParser = function (state) { loadFromUrl: function () { const params = new URL(location).searchParams; if (params.has("PerPage")) { - const perPage = parseInt(params.get("PerPage")); - if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) - state.perPage = perPage; + state.perPage = helpers.tryNumber(params.get("PerPage"), 1000, n => n > 0); + } else { + state.perPage = 1000; } if (params.has("Page")) { - const page = parseInt(params.get("Page")); - if (!isNaN(page) && isFinite(page) && page > 0) - this.page = page; + this.page = helpers.tryNumber(params.get("Page"), 1, n => n > 0); + } else { + this.page = 1; } - let updateFilter = false; - - if (params.has("Filter")) { + if (params.has("Filter")) state.filterText = params.get("Filter"); - updateFilter = true; - } + else + state.filterText = ""; if (params.has("FilterMode")) { - const values = helpers.fromUrlBitmap(params.get("FilterMode"), 3); - state.useRegex = values[0]; - state.useInsensitive = values[1]; - state.useWord = values[2]; - updateFilter = true; + const values = params.get("FilterMode").split("~"); + state.useRegex = values.includes('Regex'); + state.useInsensitive = !values.includes('Sensitive'); + state.useWord = values.includes('Word'); + } else { + state.useRegex = false; + state.useInsensitive = true; + state.useWord = false; } if (params.has("Mods")) { - const keys = Object.keys(this.showMods); - const value = params.get("Mods"); - const values = value === "all" ? true : value === "none" ? false : helpers.fromUrlBitmap(value, keys.length); - - for (let i = 0; i < keys.length; i++) { - this.showMods[keys[i]] = Array.isArray(values) ? values[i] : values; - } + const value = params.get("Mods").split("~"); + for (const key of Object.keys(this.showMods)) + this.showMods[key] = value.includes(key); - updateModFilters(); + } else { + for (const key of Object.keys(this.showMods)) + this.showMods[key] = state.defaultMods[key]; } if (params.has("Levels")) { - const keys = Object.keys(this.showLevels); - const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + const values = params.get("Levels").split("~"); + for (const key of Object.keys(this.showLevels)) + this.showLevels[key] = values.includes(key); - for (let i = 0; i < keys.length; i++) { - this.showLevels[keys[i]] = values[i]; - } + } else { + const keys = Object.keys(this.showLevels); + for (const key of Object.keys(this.showLevels)) + this.showLevels[key] = state.defaultLevels[key]; } if (params.has("Sections")) { - const keys = Object.keys(this.showSections); - const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + const values = params.get("Sections").split("~"); + for (const key of Object.keys(this.showSections)) + this.showSections[key] = values.includes(key); - for (let i = 0; i < keys.length; i++) { - this.showSections[keys[i]] = values[i]; - } + } else { + for (const key of Object.keys(this.showSections)) + this.showSections[key] = state.defaultSections[key]; } - if (updateFilter) - this.updateFilterText(); + updateModFilters(); + this.updateFilterText(); }, // Whenever the page state changed, replace the current page URL. Using @@ -858,23 +769,35 @@ smapi.logParser = function (state) { url.searchParams.set("PerPage", state.perPage); if (!helpers.shallowEquals(this.showMods, state.defaultMods)) - url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); + url.searchParams.set("Mods", Object.entries(this.showMods).filter(x => x[1]).map(x => x[0]).join("~")); else url.searchParams.delete("Mods"); if (!helpers.shallowEquals(this.showLevels, state.defaultLevels)) - url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); + url.searchParams.set("Levels", Object.entries(this.showLevels).filter(x => x[1]).map(x => x[0]).join("~")); else url.searchParams.delete("Levels"); if (!helpers.shallowEquals(this.showSections, state.defaultSections)) - url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + url.searchParams.set("Sections", Object.entries(this.showSections).filter(x => x[1]).map(x => x[0]).join("~")); else url.searchParams.delete("Sections"); if (state.filterText && state.filterText.length) { url.searchParams.set("Filter", state.filterText); - url.searchParams.set("FilterMode", helpers.toUrlBitmap([state.useRegex, state.useInsensitive, state.useWord])); + const modes = []; + if (state.useRegex) + modes.push("Regex"); + if (!state.useInsensitive) + modes.push("Sensitive"); + if (state.useWord) + modes.push("Word"); + + if (modes.length) + url.searchParams.set("FilterMode", modes.join("~")); + else + url.searchParams.delete("FilterMode"); + } else { url.searchParams.delete("Filter"); url.searchParams.delete("FilterMode"); -- cgit From 64b9da560fcde5696b27126adbc45e8331f1cc70 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Apr 2022 12:56:38 -0400 Subject: minor refactoring & code style --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 8 +- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 192 ++++++++++++------------- 2 files changed, 93 insertions(+), 107 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content/js') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d55bfd4d..dbaa14e0 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -98,14 +98,14 @@ @section SidebarExtra { @if (log != null) { - + } } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 37c57082..c730309b 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -97,55 +97,43 @@ smapi.logParser = function (state) { }, /** - * Try parsing the value as an integer, in base 10. Return the number - * if it's valid, or return the default value otherwise. - * @param {String} value The value to parse. - * @param {Number} defaultValue The value to return if parsing fails. - * @param {Function} critera An optional criteria to check the number with. + * Try parsing the value as a base-10 integer. + * @param {string} value The value to parse. + * @param {number} defaultValue The value to return if parsing fails. + * @param {() => boolean} criteria An optional callback to check whether a parsed number is valid. + * @returns {number} The parsed number if it's valid, else the default value. */ - tryNumber(value, defaultValue, critera = null) { - try { - value = parseInt(value, 10); - } catch { - return defaultValue; - } - - if (isNaN(value) || !isFinite(value) || (critera && !critera(value))) - return defaultValue; - - return value; + tryParseNumber(value, defaultValue, criteria = null) { + value = parseInt(value, 10); + return !isNaN(value) && isFinite(value) && (!criteria || criteria(value)) + ? value + : defaultValue; }, /** - * Check the shallow equality of two objects. - * @param {Array} first - * @param {Array} second + * Get whether two objects are equivalent based on their top-level properties. + * @param {Object} left The first value to compare. + * @param {Object} right The second value to compare. * @returns {Boolean} */ - shallowEquals(first, second) { - if (typeof first !== "object" || typeof second !== "object") - return first === second; + shallowEquals(left, right) { + if (typeof left !== "object" || typeof right !== "object") + return left === right; - if (first == null || second == null) - return first == null && second == null; + if (left == null || right == null) + return left == null && right == null; - const f_array = Array.isArray(first); - const s_array = Array.isArray(second); - - if (f_array !== s_array) + if (Array.isArray(left) !== Array.isArray(right)) return false; - const f_keys = Object.keys(first); - const s_keys = Object.keys(second); + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); - if (f_keys.length != s_keys.length) + if (leftKeys.length != rightKeys.length) return false; - for (const key of f_keys) { - if (!s_keys.includes(key)) - return false; - - if (first[key] !== second[key]) + for (const key of leftKeys) { + if (!rightKeys.includes(key) || left[key] !== right[key]) return false; } @@ -204,20 +192,6 @@ smapi.logParser = function (state) { modsHidden: 0 }; - function updateModFilters() { - // counts - stats.modsShown = 0; - stats.modsHidden = 0; - for (let key in state.showMods) { - if (state.showMods.hasOwnProperty(key)) { - if (state.showMods[key]) - stats.modsShown++; - else - stats.modsHidden++; - } - } - } - // load raw log data { const dataElement = document.querySelector(state.dataElement); @@ -282,9 +256,9 @@ smapi.logParser = function (state) { state.perPage = 1000; state.page = 1; - state.defaultMods = JSON.parse(JSON.stringify(state.showMods)); - state.defaultSections = JSON.parse(JSON.stringify(state.showSections)); - state.defaultLevels = JSON.parse(JSON.stringify(state.showLevels)); + state.defaultMods = { ...state.showMods }; + state.defaultSections = { ...state.showSections }; + state.defaultLevels = { ...state.showLevels }; // load saved values, if any if (localStorage.settings) { @@ -336,18 +310,22 @@ smapi.logParser = function (state) { "div", { class: "stats" }, [ - createElement('abbr', { - attrs: { - title: "These numbers may be inaccurate when using filtering with sections collapsed." - } - }, [ - "showing ", - createElement("strong", helpers.formatNumber(props.start + 1)), - " to ", - createElement("strong", helpers.formatNumber(props.end)), - " of ", - createElement("strong", helpers.formatNumber(props.filtered)) - ]), + createElement( + "abbr", + { + attrs: { + title: "These numbers may be inaccurate when using filtering with sections collapsed." + } + }, + [ + "showing ", + createElement("strong", helpers.formatNumber(props.start + 1)), + " to ", + createElement("strong", helpers.formatNumber(props.end)), + " of ", + createElement("strong", helpers.formatNumber(props.filtered)) + ] + ), " (total: ", createElement("strong", helpers.formatNumber(props.total)), ")" @@ -696,29 +674,18 @@ smapi.logParser = function (state) { methods: { loadFromUrl: function () { const params = new URL(location).searchParams; - if (params.has("PerPage")) { - state.perPage = helpers.tryNumber(params.get("PerPage"), 1000, n => n > 0); - } else { - state.perPage = 1000; - } - - if (params.has("Page")) { - this.page = helpers.tryNumber(params.get("Page"), 1, n => n > 0); - } else { - this.page = 1; - } - if (params.has("Filter")) - state.filterText = params.get("Filter"); - else - state.filterText = ""; + state.perPage = helpers.tryParseNumber(params.get("PerPage"), 1000, n => n > 0); + this.page = helpers.tryParseNumber(params.get("Page"), 1, n => n > 0); + state.filterText = params.get("Filter") || ""; if (params.has("FilterMode")) { const values = params.get("FilterMode").split("~"); - state.useRegex = values.includes('Regex'); - state.useInsensitive = !values.includes('Sensitive'); - state.useWord = values.includes('Word'); - } else { + state.useRegex = values.includes("Regex"); + state.useInsensitive = !values.includes("Sensitive"); + state.useWord = values.includes("Word"); + } + else { state.useRegex = false; state.useInsensitive = true; state.useWord = false; @@ -729,7 +696,8 @@ smapi.logParser = function (state) { for (const key of Object.keys(this.showMods)) this.showMods[key] = value.includes(key); - } else { + } + else { for (const key of Object.keys(this.showMods)) this.showMods[key] = state.defaultMods[key]; } @@ -739,7 +707,8 @@ smapi.logParser = function (state) { for (const key of Object.keys(this.showLevels)) this.showLevels[key] = values.includes(key); - } else { + } + else { const keys = Object.keys(this.showLevels); for (const key of Object.keys(this.showLevels)) this.showLevels[key] = state.defaultLevels[key]; @@ -750,41 +719,42 @@ smapi.logParser = function (state) { for (const key of Object.keys(this.showSections)) this.showSections[key] = values.includes(key); - } else { + } + else { for (const key of Object.keys(this.showSections)) this.showSections[key] = state.defaultSections[key]; } - updateModFilters(); + this.updateModFilters(); this.updateFilterText(); }, - // Whenever the page state changed, replace the current page URL. Using - // replaceState rather than pushState to avoid filling the tab history - // with tons of useless history steps the user probably doesn't - // really care about. + /** + * Update the page URL to track non-default filter values. + */ updateUrl: function () { const url = new URL(location); url.searchParams.set("Page", state.page); url.searchParams.set("PerPage", state.perPage); if (!helpers.shallowEquals(this.showMods, state.defaultMods)) - url.searchParams.set("Mods", Object.entries(this.showMods).filter(x => x[1]).map(x => x[0]).join("~")); + url.searchParams.set("Mods", Object.entries(this.showMods).filter(p => p[1]).map(p => p[0]).join("~")); else url.searchParams.delete("Mods"); if (!helpers.shallowEquals(this.showLevels, state.defaultLevels)) - url.searchParams.set("Levels", Object.entries(this.showLevels).filter(x => x[1]).map(x => x[0]).join("~")); + url.searchParams.set("Levels", Object.entries(this.showLevels).filter(p => p[1]).map(p => p[0]).join("~")); else url.searchParams.delete("Levels"); if (!helpers.shallowEquals(this.showSections, state.defaultSections)) - url.searchParams.set("Sections", Object.entries(this.showSections).filter(x => x[1]).map(x => x[0]).join("~")); + url.searchParams.set("Sections", Object.entries(this.showSections).filter(p => p[1]).map(p => p[0]).join("~")); else url.searchParams.delete("Sections"); - if (state.filterText && state.filterText.length) { + if (state.filterText?.length) { url.searchParams.set("Filter", state.filterText); + const modes = []; if (state.useRegex) modes.push("Regex"); @@ -798,12 +768,13 @@ smapi.logParser = function (state) { else url.searchParams.delete("FilterMode"); - } else { + } + else { url.searchParams.delete("Filter"); url.searchParams.delete("FilterMode"); } - window.history.replaceState(null, document.title, url.toString()); + window.history.replaceState(null, document.title, url.toString()); // use replaceState instead of pushState to avoid filling the tab history with history steps the user probably doesn't care about }, toggleLevel: function (id) { @@ -863,8 +834,9 @@ smapi.logParser = function (state) { this.updateUrl(); }, - // Persist settings into localStorage for use the next time - // the user opens a log. + /** + * Persist settings into localStorage for use the next time the user opens a log. + */ saveSettings: function () { localStorage.settings = JSON.stringify({ showContentPacks: state.showContentPacks, @@ -900,6 +872,20 @@ smapi.logParser = function (state) { 250 ), + updateModFilters: function () { + // counts + stats.modsShown = 0; + stats.modsHidden = 0; + for (let key in state.showMods) { + if (state.showMods.hasOwnProperty(key)) { + if (state.showMods[key]) + stats.modsShown++; + else + stats.modsHidden++; + } + } + }, + toggleMod: function (id) { if (!state.enableFilters) return; @@ -920,7 +906,7 @@ smapi.logParser = function (state) { else this.showMods[id] = !this.showMods[id]; - updateModFilters(); + this.updateModFilters(); this.updateUrl(); }, @@ -941,7 +927,7 @@ smapi.logParser = function (state) { this.showMods[key] = true; } } - updateModFilters(); + this.updateModFilters(); this.updateUrl(); }, @@ -954,7 +940,7 @@ smapi.logParser = function (state) { this.showMods[key] = false; } } - updateModFilters(); + this.updateModFilters(); this.updateUrl(); }, -- cgit