From 077d8e4f401ad1806c6af0540f432366314a2300 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 18:25:00 -0400 Subject: remove some unused/redundant code --- src/SMAPI.Web/wwwroot/Content/css/file-upload.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/css/file-upload.css b/src/SMAPI.Web/wwwroot/Content/css/file-upload.css index ff170691..f29d46aa 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/file-upload.css +++ b/src/SMAPI.Web/wwwroot/Content/css/file-upload.css @@ -11,7 +11,7 @@ border-radius: 5px; border: 1px solid #000088; outline: none; - box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); + box-shadow: inset 0 0 1px 1px rgba(0, 0, 192, .2); } #submit { -- cgit From 0beff189d19416dfcbb64bce800af41de37ccd08 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Fri, 8 Apr 2022 14:59:52 -0400 Subject: Implement client-side log rendering, better filtering, and pagination to improve performance and enhance the user experience with using the log parser. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 143 +++-- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 109 +++- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 632 ++++++++++++++++++++++- 3 files changed, 827 insertions(+), 57 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index c26ec230..39a2da0f 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -20,6 +20,16 @@ .Cast() .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); + IDictionary logLevels = Enum + .GetValues(typeof(LogLevel)) + .Cast() + .ToDictionary(level => (int)level, level => level.ToString().ToLower()); + + IDictionary logSections = Enum + .GetValues(typeof(LogSection)) + .Cast() + .ToDictionary(section => (int)section, section => section.ToString()); + string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true); ISet screenIds = new HashSet(log?.Messages?.Select(p => p.ScreenId) ?? Array.Empty()); @@ -34,8 +44,15 @@ + @if (!Model.ShowRaw) + { + + + + + } - + @@ -275,29 +292,35 @@ else if (log?.IsValid == true) click any mod to filter show all hide all + toggle content packs } @foreach (var mod in log.Mods.Where(p => p.Loaded && !p.IsContentPack)) { + LogModInfo[]? contentPackList; + if (contentPacks == null || !contentPacks.TryGetValue(mod.Name, out contentPackList)) + contentPackList = null; + - - @mod.Name @mod.Version - @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) + + @mod.Name @mod.Version + @if (contentPackList != null) { -
+
@foreach (var contentPack in contentPackList) { + @contentPack.Name @contentPack.Version
}
+ (+ @contentPackList.Length Content Packs) } - + @mod.Author - @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList)) + @if (contentPackList != null) { -
+
@foreach (var contentPack in contentPackList) { + @contentPack.Author
@@ -323,57 +346,67 @@ else if (log?.IsValid == true) @if (!Model.ShowRaw) { +
- Filter messages: - TRACE | - DEBUG | - INFO | - ALERT | - WARN | - ERROR +
+
+ Filter messages: +
+
+ TRACE | + DEBUG | + INFO | + ALERT | + WARN | + ERROR +
+ + .* + aA + Ab + HL +
+ +
+
+
- - @foreach (var message in log.Messages) - { - string levelStr = message.Level.ToString().ToLower(); - string sectionStartClass = message.IsStartOfSection ? "section-start" : null; - string sectionFilter = message.Section != null && !message.IsStartOfSection ? $"&& sectionsAllow('{message.Section}')" : null; // filter the message by section if applicable + - v-on:click="toggleSection('@message.Section')" } - v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter"> - - @if (screenIds.Count > 1) - { - - } - - - - - if (message.Repeated > 0) - { - - - - - } - } -
@message.Timescreen_@message.ScreenId@message.Level.ToString().ToUpper()@message.Mod - @message.Text - @if (message.IsStartOfSection) - { - - - - - } -
repeats [@message.Repeated] times.
+ + + } else { diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 8c3acceb..94bc049b 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -148,6 +148,10 @@ table caption { font-style: italic; } +.content-packs--short { + opacity: 0.75; +} + #metadata td:first-child { padding-right: 5px; } @@ -158,8 +162,58 @@ table caption { #filters { margin: 1em 0 0 0; - padding: 0; + padding: 0 0 0.5em; + display: flex; + justify-content: space-between; + width: calc(100vw - 16em); +} + +#filters > div { + align-self: center; +} + +#filters .toggles { + display: flex; +} + +#filters .toggles > div:first-child { font-weight: bold; + padding: 0.2em 1em 0 0; +} + +#filters .filter-text { + margin-top: 0.5em; +} + +#filters .stats { + margin-top: 0.5em; + font-size: 0.75em; +} + +#filters.sticky { + position: fixed; + top: 0; + left: 0em; + background: #fff; + margin: 0; + padding: 0.5em; + width: calc(100% - 1em); +} + +@media (min-width: 1020px) and (max-width: 1199px) { + #filters:not(.sticky) { + width: calc(100vw - 13em); + } +} + +@media (max-width: 1019px) { + #filters:not(.sticky) { + width: calc(100vw - 3em); + } + + #filters { + display: block; + } } #filters span { @@ -173,6 +227,17 @@ table caption { color: #000; border-color: #880000; background-color: #fcc; + + user-select: none; +} + +#filters .filter-text span { + padding: 3px 0.5em; +} + +#filters .whole-word i { + padding: 0 1px; + border: 1px dashed; } #filters span:hover { @@ -188,11 +253,48 @@ table caption { background: #efe; } +#filters .pager { + margin-top: 0.5em; + text-align: right; +} + +#filters .pager div { + margin-top: 0.5em; +} + +#filters .pager div span { + padding: 0 0.5em; + margin: 0 1px; +} + +#filters .pager span { + background-color: #eee; + border-color: #888; +} + +#filters .pager span.active { + font-weight: bold; + border-color: transparent; + background: transparent; + cursor: unset; +} + +#filters .pager span.disabled { + opacity: 0.3; + cursor: unset; +} + +#filters .pager span:not(.disabled):hover { + background-color: #fff; +} + + /********* ** Log *********/ #log .mod-repeat { font-size: 0.85em; + font-style: italic; } #log .trace { @@ -237,6 +339,11 @@ table caption { white-space: pre-wrap; } +#log .log-message-text strong { + background-color: yellow; + font-weight: normal; +} + #log { border-spacing: 0; } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 90715375..c16b237a 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -2,12 +2,98 @@ var smapi = smapi || {}; var app; +var messages; + + +// Necessary helper method for updating our text filter in a performant way. +// Wouldn't want to update it for every individually typed character. +function debounce(fn, delay) { + var timeoutID = null + return function () { + clearTimeout(timeoutID) + var args = arguments + var that = this + timeoutID = setTimeout(function () { + fn.apply(that, args) + }, delay) + } +} + +// Case insensitive text searching and match word searching is best done in +// regex, so if the user isn't trying to use regex, escape their input. +function escapeRegex(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Use a scroll event to apply a sticky effect to the filters / pagination +// bar. We can't just use "position: sticky" due to how the page is structured +// but this works well enough. +$(function () { + let sticking = false; + + document.addEventListener('scroll', function (event) { + const filters = document.getElementById('filters'); + const holder = document.getElementById('filterHolder'); + if (!filters || !holder) + return; + + const offset = holder.offsetTop; + const should_stick = window.pageYOffset > offset; + if (should_stick === sticking) + return; + + sticking = should_stick; + if (sticking) { + holder.style.marginBottom = `calc(1em + ${filters.offsetHeight}px)`; + filters.classList.add('sticky'); + } else { + filters.classList.remove('sticky'); + holder.style.marginBottom = ''; + } + }); +}); + +// This method is called when we click a log line to toggle the visibility +// of a section. Binding methods is problematic with functional components +// so we just use the `data-section` parameter and our global reference +// to the app. +smapi.clickLogLine = function (event) { + app.toggleSection(event.currentTarget.dataset.section); + event.preventDefault(); + return false; +} + +// And these methods are called when doing pagination. Just makes things +// easier, so may as well use helpers. +smapi.prevPage = function () { + app.prevPage(); +} + +smapi.nextPage = function () { + app.nextPage(); +} + +smapi.changePage = function (event) { + if (typeof event === 'number') + app.changePage(event); + else if (event) { + const page = parseInt(event.currentTarget.dataset.page); + if (!isNaN(page) && isFinite(page)) + app.changePage(page); + } +} + + smapi.logParser = function (data, sectionUrl) { + if (!data) + data = {}; + // internal filter counts var stats = data.stats = { modsShown: 0, modsHidden: 0 }; + function updateModFilters() { // counts stats.modsShown = 0; @@ -22,10 +108,357 @@ smapi.logParser = function (data, sectionUrl) { } } + // load our data + + // Rather than pre-rendering the list elements into the document, we read + // a lot of JSON and use Vue to build the list. This is a lot more + // performant and easier on memory.Our JSON is stored in special script + // tags, that we later remove to let the browser clean up even more memory. + let nodeParsedMessages = document.querySelector('script#parsedMessages'); + if (nodeParsedMessages) { + messages = JSON.parse(nodeParsedMessages.textContent) || []; + const logLevels = JSON.parse(document.querySelector('script#logLevels').textContent) || {}; + const logSections = JSON.parse(document.querySelector('script#logSections').textContent) || {}; + const modSlugs = JSON.parse(document.querySelector('script#modSlugs').textContent) || {}; + + // Remove all references to the script tags and remove them from the + // DOM so that the browser can clean them up. + nodeParsedMessages.remove(); + document.querySelector('script#logLevels').remove(); + document.querySelector('script#logSections').remove(); + document.querySelector('script#modSlugs').remove(); + nodeParsedMessages = null; + + // Pre-process the messages since they aren't quite serialized in + // the format we want. We also want to freeze every last message + // so that Vue won't install its change listening behavior. + for (let i = 0, length = messages.length; i < length; i++) { + const msg = messages[i]; + msg.id = i; + msg.LevelName = logLevels && logLevels[msg.Level]; + msg.SectionName = logSections && logSections[msg.Section]; + msg.ModSlug = modSlugs && modSlugs[msg.Mod] || msg.Mod; + + // For repeated messages, since our component + // can't return two rows, just insert a second message + // which will display as the message repeated notice. + if (msg.Repeated > 0 && ! msg.isRepeated) { + const second = { + id: i + 1, + Level: msg.Level, + Section: msg.Section, + Mod: msg.Mod, + Repeated: msg.Repeated, + isRepeated: true, + }; + + messages.splice(i + 1, 0, second); + length++; + } + + Object.freeze(msg); + } + + Object.freeze(messages); + + } else + messages = []; + // set local time started - if (data) + if (data.logStarted) data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); + // Add some properties to the data we're passing to Vue. + data.totalMessages = messages.length; + + data.filterText = ''; + data.filterRegex = ''; + + data.showContentPacks = true; + data.useHighlight = true; + data.useRegex = false; + data.useInsensitive = true; + data.useWord = false; + + data.perPage = 1000; + data.page = 1; + + // Now load these values. + if (localStorage.settings) { + try { + const saved = JSON.parse(localStorage.settings); + if (saved.hasOwnProperty('showContentPacks')) + data.showContentPacks = saved.showContentPacks; + if (saved.hasOwnProperty('useHighlight')) + dat.useHighlight = saved.useHighlight; + if (saved.hasOwnProperty('useRegex')) + data.useRegex = saved.useRegex; + if (saved.hasOwnProperty('useInsensitive')) + data.useInsensitive = saved.useInsensitive; + if (saved.hasOwnProperty('useWord')) + data.useWord = saved.useWord; + } catch { /* ignore errors */ } + } + + // This would be easier if we could just use JSX but this project doesn't + // have a proper JavaScript build environment and I really don't feel + // like setting one up. + + // Add a number formatter so that our numbers look nicer. + const fmt = window.Intl && Intl.NumberFormat && new Intl.NumberFormat(); + function formatNumber(value) { + if (!fmt || !fmt.format) return `${value}`; + return fmt.format(value); + } + Vue.filter('number', formatNumber); + + // Strictly speaking, we don't need this. However, due to the way our + // Vue template is living in-page the browser is "helpful" and moves + // our s outside of a basic since obviously they + // aren't table rows and don't belong inside a table. By using another + // Vue component, we avoid that. + Vue.component('log-table', { + functional: true, + render: function (createElement, context) { + return createElement('table', { + attrs: { + id: 'log' + } + }, context.children); + } + }); + + // The component draws a nice message under the filters + // telling a user how many messages match their filters, and also expands + // on how many of them they're seeing because of pagination. + Vue.component('filter-stats', { + functional: true, + render: function (createElement, context) { + const props = context.props; + if (props.pages > 1) + return createElement('div', { + class: 'stats' + }, [ + 'showing ', + createElement('strong', formatNumber(props.start + 1)), + ' to ', + createElement('strong', formatNumber(props.end)), + ' of ', + createElement('strong', formatNumber(props.filtered)), + ' (total: ', + createElement('strong', formatNumber(props.total)), + ')' + ]); + + return createElement('div', { + class: 'stats' + }, [ + 'showing ', + createElement('strong', formatNumber(props.filtered)), + ' out of ', + createElement('strong', formatNumber(props.total)) + ]); + } + }); + + // Next up we have which renders the pagination list. This has a + // helper method to make building the list of links easier. + function addPageLink(page, links, visited, createElement, currentPage) { + if (visited.has(page)) + return; + + if (page > 1 && !visited.has(page - 1)) + links.push(' … '); + + visited.add(page); + links.push(createElement('span', { + class: page == currentPage ? 'active' : null, + attrs: { + 'data-page': page + }, + on: { + click: smapi.changePage + } + }, formatNumber(page))); + } + + Vue.component('pager', { + functional: true, + render: function (createElement, context) { + const props = context.props; + if (props.pages <= 1) + return null; + + const visited = new Set; + const pageLinks = []; + + for (let i = 1; i <= 2; i++) + addPageLink(i, pageLinks, visited, createElement, props.page); + + for (let i = props.page - 2; i <= props.page + 2; i++) { + if (i < 1 || i > props.pages) + continue; + + addPageLink(i, pageLinks, visited, createElement, props.page); + } + + for (let i = props.pages - 2; i <= props.pages; i++) { + if (i < 1) + continue; + + addPageLink(i, pageLinks, visited, createElement, props.page); + } + + return createElement('div', { + class: 'pager' + }, [ + createElement('span', { + class: props.page <= 1 ? 'disabled' : null, + on: { + click: smapi.prevPage + } + }, 'Prev'), + ' ', + 'Page ', + formatNumber(props.page), + ' of ', + formatNumber(props.pages), + ' ', + createElement('span', { + class: props.page >= props.pages ? 'disabled' : null, + on: { + click: smapi.nextPage + } + }, 'Next'), + createElement('div', {}, pageLinks) + ]); + } + }); + + // Our functional component draws each log line. + Vue.component('log-line', { + functional: true, + props: { + showScreenId: { + type: Boolean, + required: true + }, + message: { + type: Object, + required: true + }, + sectionExpanded: { + type: Boolean, + required: false + }, + highlight: { + type: Boolean, + required: false + } + }, + render: function (createElement, context) { + const msg = context.props.message; + const level = msg.LevelName; + + if (msg.isRepeated) + return createElement('tr', { + class: [ + "mod", + level, + "mod-repeat" + ] + }, [ + createElement('td', { + attrs: { + colspan: context.props.showScreenId ? 4 : 3 + } + }, ''), + createElement('td', `repeats ${msg.Repeated} times`) + ]); + + const events = {}; + let toggleMessage; + if (msg.IsStartOfSection) { + const visible = context.props.sectionExpanded; + events.click = smapi.clickLogLine; + toggleMessage = visible ? + 'This section is shown. Click here to hide it.' : + 'This section is hidden. Click here to show it.'; + } + + let text = msg.Text; + const filter = window.app && app.filterRegex; + if (text && filter && context.props.highlight) { + text = []; + let match, consumed = 0, idx = 0; + filter.lastIndex = -1; + + // Our logic to highlight the text is a bit funky because we + // want to group consecutive matches to avoid a situation + // where a ton of single characters are in their own elements + // if the user gives us bad input. + + while (match = filter.exec(msg.Text)) { + // Do we have an area of non-matching text? This + // happens if the new match's index is further + // along than the last index. + if (match.index > idx) { + // Alright, do we have a previous match? If + // we do, we need to consume some text. + if (consumed < idx) + text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + + text.push(msg.Text.slice(idx, match.index)); + consumed = match.index; + } + + idx = match.index + match[0].length; + } + + // Add any trailing text after the last match was found. + if (consumed < msg.Text.length) { + if (consumed < idx) + text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + + if (idx < msg.Text.length) + text.push(msg.Text.slice(idx)); + } + } + + return createElement('tr', { + class: [ + "mod", + level, + msg.IsStartOfSection ? "section-start" : null + ], + attrs: { + 'data-section': msg.SectionName + }, + on: events + }, [ + createElement('td', msg.Time), + context.props.showScreenId ? createElement('td', msg.ScreenId) : null, + createElement('td', level.toUpperCase()), + createElement('td', { + attrs: { + 'data-title': msg.Mod + } + }, msg.Mod), + createElement('td', [ + createElement('span', { + class: 'log-message-text' + }, text), + msg.IsStartOfSection ? createElement('span', { + class: 'section-toggle-message' + }, [ + ' ', + toggleMessage + ]) : null + ]) + ]); + } + }); + // init app app = new Vue({ el: '#output', @@ -36,9 +469,114 @@ smapi.logParser = function (data, sectionUrl) { }, anyModsShown: function () { return stats.modsShown > 0; + }, + showScreenId: function () { + return this.screenIds.length > 1; + }, + + // Maybe not strictly necessary, but the Vue template is being + // weird about accessing data entries on the app rather than + // computed properties. + visibleSections: function () { + const ret = []; + for (const [k, v] of Object.entries(this.showSections)) + if (v !== false) + ret.push(k); + return ret; + }, + hideContentPacks: function () { + return !data.showContentPacks; + }, + + // Filter messages for visibility. + filterUseRegex: function () { return data.useRegex; }, + filterInsensitive: function () { return data.useInsensitive; }, + filterUseWord: function () { return data.useWord; }, + shouldHighlight: function () { return data.useHighlight; }, + + filteredMessages: function () { + if (!messages) + return []; + + const start = performance.now(); + const ret = []; + + // This is slightly faster than messages.filter(), which is + // important when working with absolutely huge logs. + for (let i = 0, length = messages.length; i < length; i++) { + const msg = messages[i]; + if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName)) + continue; + + if (!this.filtersAllow(msg.ModSlug, msg.LevelName)) + continue; + + let text = msg.Text || (i > 0 ? messages[i - 1].Text : null); + + if (this.filterRegex) { + this.filterRegex.lastIndex = -1; + if (!text || !this.filterRegex.test(text)) + continue; + } else if (this.filterText && (!text || text.indexOf(this.filterText) == -1)) + continue; + + ret.push(msg); + } + + const end = performance.now(); + console.log(`filter took ${end - start}ms`); + + return ret; + }, + + // And the rest are about pagination. + start: function () { + return (this.page - 1) * data.perPage; + }, + end: function () { + return this.start + this.visibleMessages.length; + }, + totalPages: function () { + return Math.ceil(this.filteredMessages.length / data.perPage); + }, + // + visibleMessages: function () { + if (this.totalPages <= 1) + return this.filteredMessages; + + const start = this.start; + const end = start + data.perPage; + + return this.filteredMessages.slice(start, end); } }, + created: function () { + this.loadFromUrl = this.loadFromUrl.bind(this); + window.addEventListener('popstate', this.loadFromUrl); + 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')) + try { + const perPage = parseInt(params.get('PerPage')); + if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) + data.perPage = perPage; + } catch { /* ignore errors */ } + + if (params.has('Page')) + try { + const page = parseInt(params.get('Page')); + if (!isNaN(page) && isFinite(page) && page > 0) + this.page = page; + } catch { /* ignore errors */ } + }, + toggleLevel: function (id) { if (!data.enableFilters) return; @@ -46,6 +584,98 @@ smapi.logParser = function (data, sectionUrl) { this.showLevels[id] = !this.showLevels[id]; }, + toggleContentPacks: function () { + data.showContentPacks = !data.showContentPacks; + this.saveSettings(); + }, + + toggleFilterUseRegex: function () { + data.useRegex = !data.useRegex; + this.saveSettings(); + this.updateFilterText(); + }, + + toggleFilterInsensitive: function () { + data.useInsensitive = !data.useInsensitive; + this.saveSettings(); + this.updateFilterText(); + }, + + toggleFilterWord: function () { + data.useWord = !data.useWord; + this.saveSettings(); + this.updateFilterText(); + }, + + toggleHighlight: function () { + data.useHighlight = !data.useHighlight; + this.saveSettings(); + }, + + prevPage: function () { + if (this.page <= 1) + return; + this.page--; + this.updateUrl(); + }, + + nextPage: function () { + if (this.page >= this.totalPages) + return; + this.page++; + this.updateUrl(); + }, + + changePage: function (page) { + if (page < 1 || page > this.totalPages) + return; + this.page = page; + this.updateUrl(); + }, + + // Persist settings into localStorage for use the next time + // the user opens a log. + saveSettings: function () { + localStorage.settings = JSON.stringify({ + showContentPacks: data.showContentPacks, + useRegex: data.useRegex, + useInsensitive: data.useInsensitive, + useWord: data.useWord, + useHighlight: data.useHighlight + }); + }, + + // 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', data.page); + url.searchParams.set('PerPage', data.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: debounce(function () { + let text = this.filterText = document.querySelector('input[type=text]').value; + if (!text || !text.length) { + this.filterText = ''; + this.filterRegex = null; + } else { + if (!data.useRegex) + text = escapeRegex(text); + this.filterRegex = new RegExp( + data.useWord ? `\\b${text}\\b` : text, + data.useInsensitive ? 'ig' : 'g' + ); + } + }, 250), + toggleMod: function (id) { if (!data.enableFilters) return; -- cgit From 631d0375c3868cb68d1487662955db4ea1b7dd77 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Fri, 8 Apr 2022 15:26:35 -0400 Subject: Simplify visible section checking by abusing Vue behavior, since the proper way is being buggy. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 1 - src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 13 +------------ 2 files changed, 1 insertion(+), 13 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 39a2da0f..8f44b4a2 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -404,7 +404,6 @@ else if (log?.IsValid == true) v-bind:showScreenId="showScreenId" v-bind:message="msg" v-bind:highlight="shouldHighlight" - v-bind:sectionExpanded="msg.SectionName && visibleSections.includes(msg.SectionName)" /> } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index c16b237a..1984d58f 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -347,10 +347,6 @@ smapi.logParser = function (data, sectionUrl) { type: Object, required: true }, - sectionExpanded: { - type: Boolean, - required: false - }, highlight: { type: Boolean, required: false @@ -379,7 +375,7 @@ smapi.logParser = function (data, sectionUrl) { const events = {}; let toggleMessage; if (msg.IsStartOfSection) { - const visible = context.props.sectionExpanded; + const visible = msg.SectionName && window.app && app.sectionsAllow(msg.SectionName); events.click = smapi.clickLogLine; toggleMessage = visible ? 'This section is shown. Click here to hide it.' : @@ -477,13 +473,6 @@ smapi.logParser = function (data, sectionUrl) { // Maybe not strictly necessary, but the Vue template is being // weird about accessing data entries on the app rather than // computed properties. - visibleSections: function () { - const ret = []; - for (const [k, v] of Object.entries(this.showSections)) - if (v !== false) - ret.push(k); - return ret; - }, hideContentPacks: function () { return !data.showContentPacks; }, -- cgit From b3519f3cc161f460e56cfc0a0662ec3b5bfb841b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Apr 2022 12:59:21 -0400 Subject: rename 'data' to 'state' for upcoming changes --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 106 ++++++++++++------------- 1 file changed, 53 insertions(+), 53 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 1984d58f..51d6b53e 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -84,12 +84,12 @@ smapi.changePage = function (event) { } -smapi.logParser = function (data, sectionUrl) { - if (!data) - data = {}; +smapi.logParser = function (state, sectionUrl) { + if (!state) + state = {}; // internal filter counts - var stats = data.stats = { + var stats = state.stats = { modsShown: 0, modsHidden: 0 }; @@ -98,9 +98,9 @@ smapi.logParser = function (data, sectionUrl) { // counts stats.modsShown = 0; stats.modsHidden = 0; - for (var key in data.showMods) { - if (data.showMods.hasOwnProperty(key)) { - if (data.showMods[key]) + for (var key in state.showMods) { + if (state.showMods.hasOwnProperty(key)) { + if (state.showMods[key]) stats.modsShown++; else stats.modsHidden++; @@ -165,38 +165,38 @@ smapi.logParser = function (data, sectionUrl) { messages = []; // set local time started - if (data.logStarted) - data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); + if (state.logStarted) + state.localTimeStarted = ("0" + state.logStarted.getHours()).slice(-2) + ":" + ("0" + state.logStarted.getMinutes()).slice(-2); // Add some properties to the data we're passing to Vue. - data.totalMessages = messages.length; + state.totalMessages = messages.length; - data.filterText = ''; - data.filterRegex = ''; + state.filterText = ''; + state.filterRegex = ''; - data.showContentPacks = true; - data.useHighlight = true; - data.useRegex = false; - data.useInsensitive = true; - data.useWord = false; + state.showContentPacks = true; + state.useHighlight = true; + state.useRegex = false; + state.useInsensitive = true; + state.useWord = false; - data.perPage = 1000; - data.page = 1; + state.perPage = 1000; + state.page = 1; // Now load these values. if (localStorage.settings) { try { const saved = JSON.parse(localStorage.settings); if (saved.hasOwnProperty('showContentPacks')) - data.showContentPacks = saved.showContentPacks; + state.showContentPacks = saved.showContentPacks; if (saved.hasOwnProperty('useHighlight')) dat.useHighlight = saved.useHighlight; if (saved.hasOwnProperty('useRegex')) - data.useRegex = saved.useRegex; + state.useRegex = saved.useRegex; if (saved.hasOwnProperty('useInsensitive')) - data.useInsensitive = saved.useInsensitive; + state.useInsensitive = saved.useInsensitive; if (saved.hasOwnProperty('useWord')) - data.useWord = saved.useWord; + state.useWord = saved.useWord; } catch { /* ignore errors */ } } @@ -458,7 +458,7 @@ smapi.logParser = function (data, sectionUrl) { // init app app = new Vue({ el: '#output', - data: data, + data: state, computed: { anyModsHidden: function () { return stats.modsHidden > 0; @@ -474,14 +474,14 @@ smapi.logParser = function (data, sectionUrl) { // weird about accessing data entries on the app rather than // computed properties. hideContentPacks: function () { - return !data.showContentPacks; + return !state.showContentPacks; }, // Filter messages for visibility. - filterUseRegex: function () { return data.useRegex; }, - filterInsensitive: function () { return data.useInsensitive; }, - filterUseWord: function () { return data.useWord; }, - shouldHighlight: function () { return data.useHighlight; }, + filterUseRegex: function () { return state.useRegex; }, + filterInsensitive: function () { return state.useInsensitive; }, + filterUseWord: function () { return state.useWord; }, + shouldHighlight: function () { return state.useHighlight; }, filteredMessages: function () { if (!messages) @@ -520,13 +520,13 @@ smapi.logParser = function (data, sectionUrl) { // And the rest are about pagination. start: function () { - return (this.page - 1) * data.perPage; + return (this.page - 1) * state.perPage; }, end: function () { return this.start + this.visibleMessages.length; }, totalPages: function () { - return Math.ceil(this.filteredMessages.length / data.perPage); + return Math.ceil(this.filteredMessages.length / state.perPage); }, // visibleMessages: function () { @@ -534,7 +534,7 @@ smapi.logParser = function (data, sectionUrl) { return this.filteredMessages; const start = this.start; - const end = start + data.perPage; + const end = start + state.perPage; return this.filteredMessages.slice(start, end); } @@ -555,7 +555,7 @@ smapi.logParser = function (data, sectionUrl) { try { const perPage = parseInt(params.get('PerPage')); if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) - data.perPage = perPage; + state.perPage = perPage; } catch { /* ignore errors */ } if (params.has('Page')) @@ -567,37 +567,37 @@ smapi.logParser = function (data, sectionUrl) { }, toggleLevel: function (id) { - if (!data.enableFilters) + if (!state.enableFilters) return; this.showLevels[id] = !this.showLevels[id]; }, toggleContentPacks: function () { - data.showContentPacks = !data.showContentPacks; + state.showContentPacks = !state.showContentPacks; this.saveSettings(); }, toggleFilterUseRegex: function () { - data.useRegex = !data.useRegex; + state.useRegex = !state.useRegex; this.saveSettings(); this.updateFilterText(); }, toggleFilterInsensitive: function () { - data.useInsensitive = !data.useInsensitive; + state.useInsensitive = !state.useInsensitive; this.saveSettings(); this.updateFilterText(); }, toggleFilterWord: function () { - data.useWord = !data.useWord; + state.useWord = !state.useWord; this.saveSettings(); this.updateFilterText(); }, toggleHighlight: function () { - data.useHighlight = !data.useHighlight; + state.useHighlight = !state.useHighlight; this.saveSettings(); }, @@ -626,11 +626,11 @@ smapi.logParser = function (data, sectionUrl) { // the user opens a log. saveSettings: function () { localStorage.settings = JSON.stringify({ - showContentPacks: data.showContentPacks, - useRegex: data.useRegex, - useInsensitive: data.useInsensitive, - useWord: data.useWord, - useHighlight: data.useHighlight + showContentPacks: state.showContentPacks, + useRegex: state.useRegex, + useInsensitive: state.useInsensitive, + useWord: state.useWord, + useHighlight: state.useHighlight }); }, @@ -640,8 +640,8 @@ smapi.logParser = function (data, sectionUrl) { // really care about. updateUrl: function () { const url = new URL(location); - url.searchParams.set('Page', data.page); - url.searchParams.set('PerPage', data.perPage); + url.searchParams.set('Page', state.page); + url.searchParams.set('PerPage', state.perPage); window.history.replaceState(null, document.title, url.toString()); }, @@ -656,17 +656,17 @@ smapi.logParser = function (data, sectionUrl) { this.filterText = ''; this.filterRegex = null; } else { - if (!data.useRegex) + if (!state.useRegex) text = escapeRegex(text); this.filterRegex = new RegExp( - data.useWord ? `\\b${text}\\b` : text, - data.useInsensitive ? 'ig' : 'g' + state.useWord ? `\\b${text}\\b` : text, + state.useInsensitive ? 'ig' : 'g' ); } }, 250), toggleMod: function (id) { - if (!data.enableFilters) + if (!state.enableFilters) return; var curShown = this.showMods[id]; @@ -689,14 +689,14 @@ smapi.logParser = function (data, sectionUrl) { }, toggleSection: function (name) { - if (!data.enableFilters) + if (!state.enableFilters) return; this.showSections[name] = !this.showSections[name]; }, showAllMods: function () { - if (!data.enableFilters) + if (!state.enableFilters) return; for (var key in this.showMods) { @@ -708,7 +708,7 @@ smapi.logParser = function (data, sectionUrl) { }, hideAllMods: function () { - if (!data.enableFilters) + if (!state.enableFilters) return; for (var key in this.showMods) { -- cgit From 260dbbf205bfa86c76a999ccd00e01941e9ce469 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Apr 2022 13:02:25 -0400 Subject: standardize quote style --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 192 ++++++++++++------------- 1 file changed, 96 insertions(+), 96 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 51d6b53e..9684834c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -22,7 +22,7 @@ function debounce(fn, delay) { // Case insensitive text searching and match word searching is best done in // regex, so if the user isn't trying to use regex, escape their input. function escapeRegex(text) { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // Use a scroll event to apply a sticky effect to the filters / pagination @@ -31,9 +31,9 @@ function escapeRegex(text) { $(function () { let sticking = false; - document.addEventListener('scroll', function (event) { - const filters = document.getElementById('filters'); - const holder = document.getElementById('filterHolder'); + document.addEventListener("scroll", function (event) { + const filters = document.getElementById("filters"); + const holder = document.getElementById("filterHolder"); if (!filters || !holder) return; @@ -45,10 +45,10 @@ $(function () { sticking = should_stick; if (sticking) { holder.style.marginBottom = `calc(1em + ${filters.offsetHeight}px)`; - filters.classList.add('sticky'); + filters.classList.add("sticky"); } else { - filters.classList.remove('sticky'); - holder.style.marginBottom = ''; + filters.classList.remove("sticky"); + holder.style.marginBottom = ""; } }); }); @@ -74,7 +74,7 @@ smapi.nextPage = function () { } smapi.changePage = function (event) { - if (typeof event === 'number') + if (typeof event === "number") app.changePage(event); else if (event) { const page = parseInt(event.currentTarget.dataset.page); @@ -114,19 +114,19 @@ smapi.logParser = function (state, sectionUrl) { // a lot of JSON and use Vue to build the list. This is a lot more // performant and easier on memory.Our JSON is stored in special script // tags, that we later remove to let the browser clean up even more memory. - let nodeParsedMessages = document.querySelector('script#parsedMessages'); + let nodeParsedMessages = document.querySelector("script#parsedMessages"); if (nodeParsedMessages) { messages = JSON.parse(nodeParsedMessages.textContent) || []; - const logLevels = JSON.parse(document.querySelector('script#logLevels').textContent) || {}; - const logSections = JSON.parse(document.querySelector('script#logSections').textContent) || {}; - const modSlugs = JSON.parse(document.querySelector('script#modSlugs').textContent) || {}; + const logLevels = JSON.parse(document.querySelector("script#logLevels").textContent) || {}; + const logSections = JSON.parse(document.querySelector("script#logSections").textContent) || {}; + const modSlugs = JSON.parse(document.querySelector("script#modSlugs").textContent) || {}; // Remove all references to the script tags and remove them from the // DOM so that the browser can clean them up. nodeParsedMessages.remove(); - document.querySelector('script#logLevels').remove(); - document.querySelector('script#logSections').remove(); - document.querySelector('script#modSlugs').remove(); + document.querySelector("script#logLevels").remove(); + document.querySelector("script#logSections").remove(); + document.querySelector("script#modSlugs").remove(); nodeParsedMessages = null; // Pre-process the messages since they aren't quite serialized in @@ -171,8 +171,8 @@ smapi.logParser = function (state, sectionUrl) { // Add some properties to the data we're passing to Vue. state.totalMessages = messages.length; - state.filterText = ''; - state.filterRegex = ''; + state.filterText = ""; + state.filterRegex = ""; state.showContentPacks = true; state.useHighlight = true; @@ -187,15 +187,15 @@ smapi.logParser = function (state, sectionUrl) { if (localStorage.settings) { try { const saved = JSON.parse(localStorage.settings); - if (saved.hasOwnProperty('showContentPacks')) + if (saved.hasOwnProperty("showContentPacks")) state.showContentPacks = saved.showContentPacks; - if (saved.hasOwnProperty('useHighlight')) + if (saved.hasOwnProperty("useHighlight")) dat.useHighlight = saved.useHighlight; - if (saved.hasOwnProperty('useRegex')) + if (saved.hasOwnProperty("useRegex")) state.useRegex = saved.useRegex; - if (saved.hasOwnProperty('useInsensitive')) + if (saved.hasOwnProperty("useInsensitive")) state.useInsensitive = saved.useInsensitive; - if (saved.hasOwnProperty('useWord')) + if (saved.hasOwnProperty("useWord")) state.useWord = saved.useWord; } catch { /* ignore errors */ } } @@ -210,19 +210,19 @@ smapi.logParser = function (state, sectionUrl) { if (!fmt || !fmt.format) return `${value}`; return fmt.format(value); } - Vue.filter('number', formatNumber); + Vue.filter("number", formatNumber); // Strictly speaking, we don't need this. However, due to the way our // Vue template is living in-page the browser is "helpful" and moves // our s outside of a basic
since obviously they // aren't table rows and don't belong inside a table. By using another // Vue component, we avoid that. - Vue.component('log-table', { + Vue.component("log-table", { functional: true, render: function (createElement, context) { - return createElement('table', { + return createElement("table", { attrs: { - id: 'log' + id: "log" } }, context.children); } @@ -231,32 +231,32 @@ smapi.logParser = function (state, sectionUrl) { // The component draws a nice message under the filters // telling a user how many messages match their filters, and also expands // on how many of them they're seeing because of pagination. - Vue.component('filter-stats', { + Vue.component("filter-stats", { functional: true, render: function (createElement, context) { const props = context.props; if (props.pages > 1) - return createElement('div', { - class: 'stats' + return createElement("div", { + class: "stats" }, [ - 'showing ', - createElement('strong', formatNumber(props.start + 1)), - ' to ', - createElement('strong', formatNumber(props.end)), - ' of ', - createElement('strong', formatNumber(props.filtered)), - ' (total: ', - createElement('strong', formatNumber(props.total)), - ')' + "showing ", + createElement("strong", formatNumber(props.start + 1)), + " to ", + createElement("strong", formatNumber(props.end)), + " of ", + createElement("strong", formatNumber(props.filtered)), + " (total: ", + createElement("strong", formatNumber(props.total)), + ")" ]); - return createElement('div', { - class: 'stats' + return createElement("div", { + class: "stats" }, [ - 'showing ', - createElement('strong', formatNumber(props.filtered)), - ' out of ', - createElement('strong', formatNumber(props.total)) + "showing ", + createElement("strong", formatNumber(props.filtered)), + " out of ", + createElement("strong", formatNumber(props.total)) ]); } }); @@ -268,13 +268,13 @@ smapi.logParser = function (state, sectionUrl) { return; if (page > 1 && !visited.has(page - 1)) - links.push(' … '); + links.push(" … "); visited.add(page); - links.push(createElement('span', { - class: page == currentPage ? 'active' : null, + links.push(createElement("span", { + class: page == currentPage ? "active" : null, attrs: { - 'data-page': page + "data-page": page }, on: { click: smapi.changePage @@ -282,7 +282,7 @@ smapi.logParser = function (state, sectionUrl) { }, formatNumber(page))); } - Vue.component('pager', { + Vue.component("pager", { functional: true, render: function (createElement, context) { const props = context.props; @@ -309,34 +309,34 @@ smapi.logParser = function (state, sectionUrl) { addPageLink(i, pageLinks, visited, createElement, props.page); } - return createElement('div', { - class: 'pager' + return createElement("div", { + class: "pager" }, [ - createElement('span', { - class: props.page <= 1 ? 'disabled' : null, + createElement("span", { + class: props.page <= 1 ? "disabled" : null, on: { click: smapi.prevPage } - }, 'Prev'), - ' ', - 'Page ', + }, "Prev"), + " ", + "Page ", formatNumber(props.page), - ' of ', + " of ", formatNumber(props.pages), - ' ', - createElement('span', { - class: props.page >= props.pages ? 'disabled' : null, + " ", + createElement("span", { + class: props.page >= props.pages ? "disabled" : null, on: { click: smapi.nextPage } - }, 'Next'), - createElement('div', {}, pageLinks) + }, "Next"), + createElement("div", {}, pageLinks) ]); } }); // Our functional component draws each log line. - Vue.component('log-line', { + Vue.component("log-line", { functional: true, props: { showScreenId: { @@ -357,19 +357,19 @@ smapi.logParser = function (state, sectionUrl) { const level = msg.LevelName; if (msg.isRepeated) - return createElement('tr', { + return createElement("tr", { class: [ "mod", level, "mod-repeat" ] }, [ - createElement('td', { + createElement("td", { attrs: { colspan: context.props.showScreenId ? 4 : 3 } - }, ''), - createElement('td', `repeats ${msg.Repeated} times`) + }, ""), + createElement("td", `repeats ${msg.Repeated} times`) ]); const events = {}; @@ -377,9 +377,9 @@ smapi.logParser = function (state, sectionUrl) { if (msg.IsStartOfSection) { const visible = msg.SectionName && window.app && app.sectionsAllow(msg.SectionName); events.click = smapi.clickLogLine; - toggleMessage = visible ? - 'This section is shown. Click here to hide it.' : - 'This section is hidden. Click here to show it.'; + toggleMessage = visible + ? "This section is shown. Click here to hide it." + : "This section is hidden. Click here to show it."; } let text = msg.Text; @@ -402,7 +402,7 @@ smapi.logParser = function (state, sectionUrl) { // Alright, do we have a previous match? If // we do, we need to consume some text. if (consumed < idx) - text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + text.push(createElement("strong", {}, msg.Text.slice(consumed, idx))); text.push(msg.Text.slice(idx, match.index)); consumed = match.index; @@ -414,40 +414,40 @@ smapi.logParser = function (state, sectionUrl) { // Add any trailing text after the last match was found. if (consumed < msg.Text.length) { if (consumed < idx) - text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + text.push(createElement("strong", {}, msg.Text.slice(consumed, idx))); if (idx < msg.Text.length) text.push(msg.Text.slice(idx)); } } - return createElement('tr', { + return createElement("tr", { class: [ "mod", level, msg.IsStartOfSection ? "section-start" : null ], attrs: { - 'data-section': msg.SectionName + "data-section": msg.SectionName }, on: events }, [ - createElement('td', msg.Time), - context.props.showScreenId ? createElement('td', msg.ScreenId) : null, - createElement('td', level.toUpperCase()), - createElement('td', { + createElement("td", msg.Time), + context.props.showScreenId ? createElement("td", msg.ScreenId) : null, + createElement("td", level.toUpperCase()), + createElement("td", { attrs: { - 'data-title': msg.Mod + "data-title": msg.Mod } }, msg.Mod), - createElement('td', [ - createElement('span', { - class: 'log-message-text' + createElement("td", [ + createElement("span", { + class: "log-message-text" }, text), - msg.IsStartOfSection ? createElement('span', { - class: 'section-toggle-message' + msg.IsStartOfSection ? createElement("span", { + class: "section-toggle-message" }, [ - ' ', + " ", toggleMessage ]) : null ]) @@ -457,7 +457,7 @@ smapi.logParser = function (state, sectionUrl) { // init app app = new Vue({ - el: '#output', + el: "#output", data: state, computed: { anyModsHidden: function () { @@ -541,7 +541,7 @@ smapi.logParser = function (state, sectionUrl) { }, created: function () { this.loadFromUrl = this.loadFromUrl.bind(this); - window.addEventListener('popstate', this.loadFromUrl); + window.addEventListener("popstate", this.loadFromUrl); this.loadFromUrl(); }, methods: { @@ -551,16 +551,16 @@ smapi.logParser = function (state, sectionUrl) { // user can link to their exact page state for someone else? loadFromUrl: function () { const params = new URL(location).searchParams; - if (params.has('PerPage')) + if (params.has("PerPage")) try { - const perPage = parseInt(params.get('PerPage')); + const perPage = parseInt(params.get("PerPage")); if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) state.perPage = perPage; } catch { /* ignore errors */ } - if (params.has('Page')) + if (params.has("Page")) try { - const page = parseInt(params.get('Page')); + const page = parseInt(params.get("Page")); if (!isNaN(page) && isFinite(page) && page > 0) this.page = page; } catch { /* ignore errors */ } @@ -640,8 +640,8 @@ smapi.logParser = function (state, sectionUrl) { // 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("Page", state.page); + url.searchParams.set("PerPage", state.perPage); window.history.replaceState(null, document.title, url.toString()); }, @@ -651,16 +651,16 @@ smapi.logParser = function (state, sectionUrl) { // since we use it for highlighting, and it also make case insensitivity // much easier. updateFilterText: debounce(function () { - let text = this.filterText = document.querySelector('input[type=text]').value; + let text = this.filterText = document.querySelector("input[type=text]").value; if (!text || !text.length) { - this.filterText = ''; + this.filterText = ""; this.filterRegex = null; } else { if (!state.useRegex) text = escapeRegex(text); this.filterRegex = new RegExp( state.useWord ? `\\b${text}\\b` : text, - state.useInsensitive ? 'ig' : 'g' + state.useInsensitive ? "ig" : "g" ); } }, 250), -- cgit From ccf760452d64e1965c92c5cf8af399a5e80d5a3a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Apr 2022 13:08:38 -0400 Subject: pass data directly to script instead of serializing & deserializing it --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 43 ++++++---- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 113 +++++++++++-------------- 2 files changed, 76 insertions(+), 80 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 8f44b4a2..ff8aa003 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -44,13 +44,6 @@ - @if (!Model.ShowRaw) - { - - - - - } @@ -58,15 +51,33 @@ + + + } @@ -378,6 +383,7 @@ else if (log?.IsValid == true) @@ -390,7 +396,7 @@ else if (log?.IsValid == true) v-bind:start="start" v-bind:end="end" v-bind:pages="totalPages" - v-bind:filtered="filteredMessages.length" + v-bind:filtered="filteredMessages.total" v-bind:total="totalMessages" /> 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') 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') 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 0b9227564979b3e6e71dbd48ced2a9b7407fd640 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Tue, 12 Apr 2022 02:18:51 -0400 Subject: Make horizontal scrolling with the quick navigation links less bad. Probably need to move them into the actual sidebar element though for proper sorting. --- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 41b54e11..e47a938d 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -13,6 +13,9 @@ caption { #output { padding: 10px; overflow: auto; + z-index: 1; + background: #fff; + position: relative; } #output h2 { -- cgit From 4f54f517ce3d6fd6e87cfee6b0ce61346d62c3e3 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Tue, 12 Apr 2022 13:50:51 -0400 Subject: Use an optional section for rendering quick navigation links on the mod viewer, containing them within the #sidebar element. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 22 ++++++++++++---------- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 2 ++ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 3 --- src/SMAPI.Web/wwwroot/Content/css/main.css | 6 ------ 4 files changed, 14 insertions(+), 19 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d95499b7..d55bfd4d 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -95,16 +95,18 @@ } @* quick navigation links *@ -@if (log != null) -{ - +@section SidebarExtra { + @if (log != null) + { + + } } @* upload result banner *@ diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 7c86a68c..248cc7ef 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -30,6 +30,8 @@
  • Log parser
  • JSON validator
  • + + @RenderSection("SidebarExtra", required: false)
    diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index e47a938d..41b54e11 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -13,9 +13,6 @@ caption { #output { padding: 10px; overflow: auto; - z-index: 1; - background: #fff; - position: relative; } #output h2 { diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index 52b304d0..a0a407d8 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -72,7 +72,6 @@ a { color: #666; } -#quickNav h4, #sidebar h4 { margin: 1.5em 0 0.2em 0; width: 10em; @@ -81,13 +80,11 @@ a { font-weight: normal; } -#quickNav a, #sidebar a { color: #77B; border: 0; } -#quickNav ul, #quickNav li, #sidebar ul, #sidebar li { margin: 0; padding: 0; @@ -96,7 +93,6 @@ a { color: #888; } -#quickNav li, #sidebar li { margin-left: 1em; } @@ -105,10 +101,8 @@ a { #quickNav { position: fixed; - left: 8px; bottom: 3em; width: 12em; - color: #666; } @media (max-height: 400px) { -- 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') 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 From 446205c7bd342422f4b4f14d6c8f36dc0b6cf468 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Sat, 16 Apr 2022 13:55:54 -0400 Subject: Add regex error checking, and display a message to the user when their regular expression has a syntax error. Additionally, use a non-capturing group to surround the user input when `Match whole word` is enabled in case alternates are being used. Finally, add a safety check to highlighting to avoid an infinite loop when zero-length matches happen. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 +++++ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 7 ++++- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 33 ++++++++++++++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index dfb603f2..5e55906d 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -406,6 +406,12 @@ else if (log?.IsValid == true) aA “ ” HL +
    + {{ filterError }} +
    Date: Sat, 16 Apr 2022 21:07:34 -0400 Subject: tweak code style --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 27 +++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index e19e3301..8886715e 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -860,34 +860,29 @@ smapi.logParser = function (state) { // much easier. updateFilterText: helpers.getDebouncedHandler( function () { + // reset + this.filterError = null; + this.filterRegex = null; + + // apply search let text = state.filterText; - if (!text || !text.length) { + if (!text) this.filterText = ""; - this.filterRegex = null; - this.filterError = null; - } else { if (!state.useRegex) text = helpers.escapeRegex(text); const flags = state.useInsensitive ? "ig" : "g"; - this.filterError = null; - let regex; - try { - regex = new RegExp(text, flags); - } catch (err) { - regex = null; + this.filterRegex = new RegExp(text, flags); + } + catch (err) { this.filterError = err.message; } - if (regex) - this.filterRegex = state.useWord ? - new RegExp(`\\b(?:${text})\\b`, flags) : - regex; - else - this.filterRegex = null; + if (this.filterRegex && state.useWord) + this.filterRegex = new RegExp(`\\b(?:${text})\\b`, flags); } this.updateUrl(); -- cgit From d4d378bff3a8079e6d665b8c99d9a80937f588cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Apr 2022 19:25:10 -0400 Subject: don't add default pagination values to log URL --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 8886715e..3fb5fd6c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -245,6 +245,7 @@ smapi.logParser = function (state) { state.localTimeStarted = ("0" + state.logStarted.getHours()).slice(-2) + ":" + ("0" + state.logStarted.getMinutes()).slice(-2); // add the properties we're passing to Vue + const defaultPerPage = 1000; state.totalMessages = state.messages.length; state.filterText = ""; state.filterRegex = null; @@ -254,7 +255,7 @@ smapi.logParser = function (state) { state.useRegex = false; state.useInsensitive = true; state.useWord = false; - state.perPage = 1000; + state.perPage = defaultPerPage; state.page = 1; state.defaultMods = { ...state.showMods }; @@ -682,7 +683,7 @@ smapi.logParser = function (state) { loadFromUrl: function () { const params = new URL(location).searchParams; - state.perPage = helpers.tryParseNumber(params.get("PerPage"), 1000, n => n > 0); + state.perPage = helpers.tryParseNumber(params.get("PerPage"), defaultPerPage, n => n > 0); this.page = helpers.tryParseNumber(params.get("Page"), 1, n => n > 0); state.filterText = params.get("Filter") || ""; @@ -741,8 +742,12 @@ smapi.logParser = function (state) { */ updateUrl: function () { const url = new URL(location); - url.searchParams.set("Page", state.page); - url.searchParams.set("PerPage", state.perPage); + + if (state.page != 1) + url.searchParams.set("Page", state.page); + + if (state.perPage != defaultPerPage) + url.searchParams.set("PerPage", state.perPage); if (!helpers.shallowEquals(this.showMods, state.defaultMods)) url.searchParams.set("Mods", Object.entries(this.showMods).filter(p => p[1]).map(p => p[0]).join("~")); -- cgit From 4fa414c2bd5ddb452c0560d00e0d4f1d383c1d8b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Apr 2022 20:59:35 -0400 Subject: set page/perPage URL args together Since there's no UI to set the page size, this makes the argument more discoverable. --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Web/wwwroot/Content') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 3fb5fd6c..fccd00be 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -743,11 +743,14 @@ smapi.logParser = function (state) { updateUrl: function () { const url = new URL(location); - if (state.page != 1) + if (state.page != 1 || state.perPage != defaultPerPage) { url.searchParams.set("Page", state.page); - - if (state.perPage != defaultPerPage) url.searchParams.set("PerPage", state.perPage); + } + else { + url.searchParams.delete("Page"); + url.searchParams.delete("PerPage"); + } if (!helpers.shallowEquals(this.showMods, state.defaultMods)) url.searchParams.set("Mods", Object.entries(this.showMods).filter(p => p[1]).map(p => p[0]).join("~")); -- cgit