diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index 9d39705..f0f7a8a 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -221,6 +221,33 @@ class Scoresheet { if (this.leaderboardSubmitted) { return } + + // Fallback: This is CRITICAL for cases where saveScore() failed or was skipped + if (!this.leaderboardData) { + console.warn("Leaderboard data missing, attempting to reconstruct...") + var song = this.controller.selectedSong + var results = this.resultsObj + + if (song && results) { + var songId = song.id || song.hash + + if (songId) { + this.leaderboardData = { + songId: songId, + difficulty: results.difficulty, + scoreObj: Object.assign({}, results) + } + // Clean up scoreObj + if (this.leaderboardData.scoreObj) { + delete this.leaderboardData.scoreObj.title + delete this.leaderboardData.scoreObj.difficulty + delete this.leaderboardData.scoreObj.gauge + } + console.log("Leaderboard data reconstructed:", this.leaderboardData) + } + } + } + if (!this.leaderboardData || !this.leaderboardData.songId) { this.showLeaderboardNotification("no_song_id") return diff --git a/public/src/js/search.js b/public/src/js/search.js index 939b9e9..81e38e7 100644 --- a/public/src/js/search.js +++ b/public/src/js/search.js @@ -1,19 +1,19 @@ -class Search{ - constructor(...args){ +class Search { + constructor(...args) { this.init(...args) } - init(songSelect){ + init(songSelect) { this.songSelect = songSelect this.opened = false this.enabled = true - + this.style = document.createElement("style") var css = [] - for(var i in this.songSelect.songSkin){ + for (var i in this.songSelect.songSkin) { var skin = this.songSelect.songSkin[i] - if("id" in skin || i === "default"){ + if ("id" in skin || i === "default") { var id = "id" in skin ? ("cat" + skin.id) : i - + css.push(loader.cssRuleset({ [".song-search-" + id]: { "background-color": skin.background @@ -33,7 +33,7 @@ class Search{ loader.screen.appendChild(this.style) } - normalizeString(string){ + normalizeString(string) { string = string .replace('’', '\'').replace('“', '"').replace('”', '"') .replace('。', '.').replace(',', ',').replace('、', ',') @@ -44,29 +44,29 @@ class Search{ return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "") } - - perform(query){ + + perform(query) { var results = [] var filters = {} - + var querySplit = query.split(" ").filter(word => { - if(word.length > 0){ + if (word.length > 0) { var parts = word.toLowerCase().split(":") - if(parts.length > 1){ - switch(parts[0]){ + if (parts.length > 1) { + switch (parts[0]) { case "easy": case "normal": case "hard": case "oni": case "ura": var range = this.parseRange(parts[1]) - if(range){ + if (range) { filters[parts[0]] = range } break case "extreme": var range = this.parseRange(parts[1]) - if(range){ + if (range) { filters.oni = this.parseRange(parts[1]) } break @@ -91,175 +91,175 @@ class Search{ } return true }) - + query = this.normalizeString(querySplit.join(" ").trim()) - + var totalFilters = Object.keys(filters).length var random = false var allResults = false - for(var i = 0; i < assets.songs.length; i++){ + for (var i = 0; i < assets.songs.length; i++) { var song = assets.songs[i] var passedFilters = 0 - + Object.keys(filters).forEach(filter => { var value = filters[filter] - switch(filter){ + switch (filter) { case "easy": case "normal": case "hard": case "oni": case "ura": - if(song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max){ + if (song.courses[filter] && song.courses[filter].stars >= value.min && song.courses[filter].stars <= value.max) { passedFilters++ } break case "clear": case "silver": case "gold": - if(value === "any"){ + if (value === "any") { var score = scoreStorage.scores[song.hash] scoreStorage.difficulty.forEach(difficulty => { - if(score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)){ + if (score && score[difficulty] && score[difficulty].crown && (filter === "clear" || score[difficulty].crown === filter)) { passedFilters++ } }) } else { var score = scoreStorage.scores[song.hash] - if(score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)){ + if (score && score[value] && score[value].crown && (filter === "clear" || score[value].crown === filter)) { passedFilters++ } } break case "played": var score = scoreStorage.scores[song.hash] - if((value === "yes" && score) || (value === "no" && !score)){ + if ((value === "yes" && score) || (value === "no" && !score)) { passedFilters++ } break case "lyrics": - if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){ + if ((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)) { passedFilters++ } break case "creative": - if((value === "yes" && song.maker) || (value === "no" && !song.maker)){ + if ((value === "yes" && song.maker) || (value === "no" && !song.maker)) { passedFilters++ } break case "maker": - if(song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())){ + if (song.maker && song.maker.name.toLowerCase().includes(value.toLowerCase())) { passedFilters++ } break case "genre": var cat = assets.categories.find(cat => cat.id === song.category_id) var aliases = cat.aliases ? cat.aliases.concat([cat.title]) : [cat.title] - - if(aliases.find(alias => alias.toLowerCase() === value.toLowerCase())){ + + if (aliases.find(alias => alias.toLowerCase() === value.toLowerCase())) { passedFilters++ } break case "diverge": var branch = Object.values(song.courses).find(course => course && course.branch) - if((value === "yes" && branch) || (value === "no" && !branch)){ + if ((value === "yes" && branch) || (value === "no" && !branch)) { passedFilters++ } break case "random": - if(value === "yes" || value === "no"){ + if (value === "yes" || value === "no") { random = value === "yes" passedFilters++ } break case "all": - if(value === "yes" || value === "no"){ + if (value === "yes" || value === "no") { allResults = value === "yes" passedFilters++ } break } }) - - if(passedFilters === totalFilters){ + + if (passedFilters === totalFilters) { results.push(song) } } - + var maxResults = allResults ? Infinity : (totalFilters > 0 && !query ? 100 : 50) - - if(query){ + + if (query) { results = fuzzysort.go(query, results, { keys: ["titlePrepared", "subtitlePrepared"], allowTypo: true, limit: maxResults, scoreFn: a => { - if(a[0]){ + if (a[0]) { var score0 = a[0].score a[0].ranges = this.indexesToRanges(a[0].indexes) - if(a[0].indexes.length > 1){ + if (a[0].indexes.length > 1) { var rangeAmount = a[0].ranges.length var lastIdx = -3 a[0].ranges.forEach(range => { - if(range[0] - lastIdx <= 2){ + if (range[0] - lastIdx <= 2) { rangeAmount-- score0 -= 1000 } lastIdx = range[1] }) var index = a[0].target.toLowerCase().indexOf(query) - if(index !== -1){ + if (index !== -1) { a[0].ranges = [[index, index + query.length - 1]] - }else if(rangeAmount > a[0].indexes.length / 2){ + } else if (rangeAmount > a[0].indexes.length / 2) { score0 = -Infinity a[0].ranges = null - }else if(rangeAmount !== 1){ + } else if (rangeAmount !== 1) { score0 -= 9000 } } } - if(a[1]){ + if (a[1]) { var score1 = a[1].score - 1000 a[1].ranges = this.indexesToRanges(a[1].indexes) - if(a[1].indexes.length > 1){ + if (a[1].indexes.length > 1) { var rangeAmount = a[1].ranges.length var lastIdx = -3 a[1].ranges.forEach(range => { - if(range[0] - lastIdx <= 2){ + if (range[0] - lastIdx <= 2) { rangeAmount-- score1 -= 1000 } lastIdx = range[1] }) var index = a[1].target.indexOf(query) - if(index !== -1){ + if (index !== -1) { a[1].ranges = [[index, index + query.length - 1]] - }else if(rangeAmount > a[1].indexes.length / 2){ + } else if (rangeAmount > a[1].indexes.length / 2) { score1 = -Infinity a[1].ranges = null - }else if(rangeAmount !== 1){ + } else if (rangeAmount !== 1) { score1 -= 9000 } } } - if(random){ + if (random) { var rand = Math.random() * -9000 - if(score0 !== -Infinity){ + if (score0 !== -Infinity) { score0 = rand } - if(score1 !== -Infinity){ + if (score1 !== -Infinity) { score1 = rand } } - if(a[0]){ + if (a[0]) { return a[1] ? Math.max(score0, score1) : score0 - }else{ + } else { return a[1] ? score1 : -Infinity } } }) - }else{ - if(random){ - for(var i = results.length - 1; i > 0; i--){ + } else { + if (random) { + for (var i = results.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)) var temp = results[i] results[i] = results[j] @@ -267,53 +267,53 @@ class Search{ } } results = results.slice(0, maxResults).map(result => { - return {obj: result} + return { obj: result } }) } - + return results } - - createResult(result, resultWidth, fontSize){ + + createResult(result, resultWidth, fontSize) { var song = result.obj var title = this.songSelect.getLocalTitle(song.title, song.title_lang) var subtitle = this.songSelect.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) - + var id = "default" - if(song.category_id){ + if (song.category_id) { var cat = assets.categories.find(cat => cat.id === song.category_id) - if(cat && "id" in cat){ + if (cat && "id" in cat) { id = "cat" + cat.id } } - + var resultDiv = document.createElement("div") resultDiv.classList.add("song-search-result", "song-search-" + id) resultDiv.dataset.songId = song.id - + var resultInfoDiv = document.createElement("div") resultInfoDiv.classList.add("song-search-result-info") var resultInfoTitle = document.createElement("span") resultInfoTitle.classList.add("song-search-result-title") - + resultInfoTitle.appendChild(this.highlightResult(title, result[0])) resultInfoTitle.setAttribute("alt", title) - + resultInfoDiv.appendChild(resultInfoTitle) - - if(subtitle){ + + if (subtitle) { resultInfoDiv.appendChild(document.createElement("br")) var resultInfoSubtitle = document.createElement("span") resultInfoSubtitle.classList.add("song-search-result-subtitle") - + resultInfoSubtitle.appendChild(this.highlightResult(subtitle, result[1])) resultInfoSubtitle.setAttribute("alt", subtitle) - + resultInfoDiv.appendChild(resultInfoSubtitle) } - + resultDiv.appendChild(resultInfoDiv) - + var courses = ["easy", "normal", "hard", "oni", "ura"] courses.forEach(course => { var courseDiv = document.createElement("div") @@ -330,40 +330,41 @@ class Search{ var courseStars = document.createElement("div") courseStars.classList.add("song-search-result-stars") courseStars.innerText = song.courses[course].stars + "\u2605" - + courseDiv.appendChild(courseCrown) courseDiv.appendChild(courseStars) } else { courseDiv.classList.add("song-search-result-hidden") } - + resultDiv.appendChild(courseDiv) }) - + this.songSelect.ctx.font = (1.2 * fontSize) + "px " + strings.font var titleWidth = this.songSelect.ctx.measureText(title).width var titleRatio = resultWidth / titleWidth - if(titleRatio < 1){ + if (titleRatio < 1) { resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)" } - if(subtitle){ - this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font + if (subtitle) { + this.songSelect.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font var subtitleWidth = this.songSelect.ctx.measureText(subtitle).width var subtitleRatio = resultWidth / subtitleWidth - if(subtitleRatio < 1){ + if (subtitleRatio < 1) { resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)" } } - + return resultDiv } - - highlightResult(text, result){ + + highlightResult(text, result) { + if (text === null || text === undefined) return document.createDocumentFragment(); var fragment = document.createDocumentFragment() var ranges = (result ? result.ranges : null) || [] var lastIdx = 0 ranges.forEach(range => { - if(lastIdx !== range[0]){ + if (lastIdx !== range[0]) { fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0]))) } var span = document.createElement("span") @@ -372,91 +373,91 @@ class Search{ fragment.appendChild(span) lastIdx = range[1] + 1 }) - if(text.length !== lastIdx){ + if (text.length !== lastIdx) { fragment.appendChild(document.createTextNode(text.slice(lastIdx))) } return fragment } - - setActive(idx){ + + setActive(idx) { this.songSelect.playSound("se_ka") var active = this.div.querySelector(":scope .song-search-result-active") - if(active){ + if (active) { active.classList.remove("song-search-result-active") } - - if(idx === null){ + + if (idx === null) { this.active = null return } - + var el = this.results[idx] this.input.blur() el.classList.add("song-search-result-active") this.scrollTo(el) - + this.active = idx } - - display(fromButton=false){ - if(!this.enabled){ + + display(fromButton = false) { + if (!this.enabled) { return } - if(this.opened){ + if (this.opened) { return this.remove(true) } - + this.opened = true this.results = [] this.div = document.createElement("div") this.div.innerHTML = assets.pages["search"] - + this.container = this.div.querySelector(":scope #song-search-container") - if(this.touchEnabled){ + if (this.touchEnabled) { this.container.classList.add("touch-enabled") } pageEvents.add(this.container, ["mousedown", "touchstart"], this.onClick.bind(this)) - + this.input = this.div.querySelector(":scope #song-search-input") this.input.setAttribute("placeholder", strings.search.searchInput) pageEvents.add(this.input, ["input"], () => this.onInput()) - + this.songSelect.playSound("se_pause") loader.screen.appendChild(this.div) this.setTip() cancelTouch = false noResizeRoot = true - if(this.songSelect.songs[this.songSelect.selectedSong].courses){ + if (this.songSelect.songs[this.songSelect.selectedSong].courses) { snd.previewGain.setVolumeMul(0.5) - }else if(this.songSelect.bgmEnabled){ + } else if (this.songSelect.bgmEnabled) { snd.musicGain.setVolumeMul(0.5) } - + setTimeout(() => { this.input.focus() this.input.setSelectionRange(0, this.input.value.length) }, 10) - + var lastQuery = localStorage.getItem("lastSearchQuery") - if(lastQuery){ + if (lastQuery) { this.input.value = lastQuery this.input.dispatchEvent(new Event("input", { value: lastQuery })) } } - - remove(byUser=false){ - if(this.opened){ + + remove(byUser = false) { + if (this.opened) { this.opened = false - if(byUser){ + if (byUser) { this.songSelect.playSound("se_cancel") } - + pageEvents.remove(this.div.querySelector(":scope #song-search-container"), - ["mousedown", "touchstart"]) + ["mousedown", "touchstart"]) pageEvents.remove(this.input, ["input"]) - + this.div.remove() delete this.results delete this.div @@ -465,39 +466,39 @@ class Search{ delete this.active cancelTouch = true noResizeRoot = false - if(this.songSelect.songs[this.songSelect.selectedSong].courses){ + if (this.songSelect.songs[this.songSelect.selectedSong].courses) { snd.previewGain.setVolumeMul(1) - }else if(this.songSelect.bgmEnabled){ + } else if (this.songSelect.bgmEnabled) { snd.musicGain.setVolumeMul(1) } } } - - setTip(tip, error=false){ - if(this.tip){ + + setTip(tip, error = false) { + if (this.tip) { this.tip.remove() delete this.tip } - - if(!tip){ + + if (!tip) { tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)] } - + var resultsDiv = this.div.querySelector(":scope #song-search-results") resultsDiv.innerHTML = "" this.results = [] - + this.tip = document.createElement("div") this.tip.id = "song-search-tip" this.tip.innerText = tip this.div.querySelector(":scope #song-search").appendChild(this.tip) - - if(error){ + + if (error) { this.tip.classList.add("song-search-tip-error") } } - - proceed(songId){ + + proceed(songId) { if (/^-?\d+$/.test(songId)) { songId = parseInt(songId) } @@ -505,91 +506,91 @@ class Search{ var song = this.songSelect.songs.find(song => song.id === songId) this.remove() this.songSelect.playBgm(false) - if(this.songSelect.previewing === "muted"){ + if (this.songSelect.previewing === "muted") { this.songSelect.previewing = null } - + var songIndex = this.songSelect.songs.findIndex(song => song.id === songId) this.songSelect.setSelectedSong(songIndex) this.songSelect.toSelectDifficulty() } - - scrollTo(element){ + + scrollTo(element) { var parentNode = element.parentNode var selected = element.getBoundingClientRect() var parent = parentNode.getBoundingClientRect() var scrollY = parentNode.scrollTop var selectedPosTop = selected.top - selected.height / 2 - if(Math.floor(selectedPosTop) < Math.floor(parent.top)){ + if (Math.floor(selectedPosTop) < Math.floor(parent.top)) { parentNode.scrollTop += selectedPosTop - parent.top - }else{ + } else { var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top - if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){ + if (Math.floor(selectedPosBottom) > Math.floor(parent.height)) { parentNode.scrollTop += selectedPosBottom - parent.height } } } - - parseRange(string){ + + parseRange(string) { var range = string.split("-") - if(range.length == 1){ + if (range.length == 1) { var min = parseInt(range[0]) || 0 - return min > 0 ? {min: min, max: min} : false - } else if(range.length == 2){ + return min > 0 ? { min: min, max: min } : false + } else if (range.length == 2) { var min = parseInt(range[0]) || 0 var max = parseInt(range[1]) || 0 - return min > 0 && max > 0 ? {min: min, max: max} : false + return min > 0 && max > 0 ? { min: min, max: max } : false } } - - indexesToRanges(indexes){ + + indexesToRanges(indexes) { var ranges = [] var range indexes.forEach(idx => { - if(range && range[1] === idx - 1){ + if (range && range[1] === idx - 1) { range[1] = idx - }else{ + } else { range = [idx, idx] ranges.push(range) } }) return ranges } - - onInput(resize){ + + onInput(resize) { var text = this.input.value localStorage.setItem("lastSearchQuery", text) text = text.toLowerCase() - - if(text.length === 0){ - if(!resize){ + + if (text.length === 0) { + if (!resize) { this.setTip() } return } - + var new_results = this.perform(text) - - if(new_results.length === 0){ + + if (new_results.length === 0) { this.setTip(strings.search.noResults, true) return - }else if(this.tip){ + } else if (this.tip) { this.tip.remove() delete this.tip } - + var resultsDiv = this.div.querySelector(":scope #song-search-results") resultsDiv.innerHTML = "" this.results = [] - + var fontSize = parseFloat(getComputedStyle(this.div.querySelector(":scope #song-search")).fontSize.slice(0, -2)) var resultsWidth = parseFloat(getComputedStyle(resultsDiv).width.slice(0, -2)) var vmin = Math.min(innerWidth, lastHeight) / 100 var courseWidth = Math.min(3 * fontSize * 1.2, 7 * vmin) var resultWidth = resultsWidth - 1.8 * fontSize - 0.8 * fontSize - (courseWidth + 0.4 * fontSize * 1.2) * 5 - 0.6 * fontSize - + this.songSelect.ctx.save() - + var fragment = document.createDocumentFragment() new_results.forEach(result => { var result = this.createResult(result, resultWidth, fontSize) @@ -597,78 +598,78 @@ class Search{ this.results.push(result) }) resultsDiv.appendChild(fragment) - + this.songSelect.ctx.restore() } - - onClick(e){ - if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){ + + onClick(e) { + if ((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1) { this.remove(true) - }else if(e.which === 1){ + } else if (e.which === 1) { var songEl = e.target.closest(".song-search-result") - if(songEl){ + if (songEl) { var songId = songEl.dataset.songId this.proceed(songId) } } } - - keyPress(pressed, name, event, repeat, ctrl){ - if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { + + keyPress(pressed, name, event, repeat, ctrl) { + if (name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { this.remove(true) - if(event){ + if (event) { event.preventDefault() } - }else if(name === "down" && this.results.length){ - if(this.input == document.activeElement && this.results){ + } else if (name === "down" && this.results.length) { + if (this.input == document.activeElement && this.results) { this.setActive(0) - }else if(this.active === this.results.length - 1){ + } else if (this.active === this.results.length - 1) { this.setActive(null) this.input.focus() - }else if(Number.isInteger(this.active)){ + } else if (Number.isInteger(this.active)) { this.setActive(this.active + 1) - }else{ + } else { this.setActive(0) } - }else if(name === "up" && this.results.length){ - if(this.input == document.activeElement && this.results){ + } else if (name === "up" && this.results.length) { + if (this.input == document.activeElement && this.results) { this.setActive(this.results.length - 1) - }else if(this.active === 0){ + } else if (this.active === 0) { this.setActive(null) this.input.focus() setTimeout(() => { this.input.setSelectionRange(this.input.value.length, this.input.value.length) }, 0) - }else if(Number.isInteger(this.active)){ + } else if (Number.isInteger(this.active)) { this.setActive(this.active - 1) - }else{ + } else { this.setActive(this.results.length - 1) - } - }else if(name === "confirm"){ - if(Number.isInteger(this.active)){ + } + } else if (name === "confirm") { + if (Number.isInteger(this.active)) { this.proceed(this.results[this.active].dataset.songId) - }else{ + } else { this.onInput() - if(event.keyCode === 13 && this.songSelect.touchEnabled){ + if (event.keyCode === 13 && this.songSelect.touchEnabled) { this.input.blur() } } } } - - redraw(){ - if(this.opened && this.container){ + + redraw() { + if (this.opened && this.container) { var vmin = Math.min(innerWidth, lastHeight) / 100 - if(this.vmin !== vmin){ + if (this.vmin !== vmin) { this.container.style.setProperty("--vmin", vmin + "px") this.vmin = vmin } - }else{ + } else { this.vmin = null } } - - clean(){ + + clean() { loader.screen.removeChild(this.style) fuzzysort.cleanup() delete this.container