From e231ad1fcf3262a3a2283d66f20bed532d6c5d52 Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Wed, 16 Mar 2022 09:55:25 +0300 Subject: [PATCH] Fixes - Add a "Browse..." button to the plugin menu - Remove the "Unload All" button from the plugin menu if there are no imported plugins to unload - Add a new search filter: random:yes - Resolution settings now affects the results screen assets - Pixelate more assets with lowest resolution setting - Fix loading error message not appearing sometimes - Remove img.css from img assets, the background selectors have been moved to assets.js - Separate the search logic from SongSelect to its own js file - Load all image assets with crossorigin=anonymous, this could allow making assets low resolution or programatically taking screenshots at a later time - If EditFunction in a plugin tries to edit something that is not a function, it will give a better error message - Disallow search engine bots from indexing images and adding a translate link, which cannot load the game --- public/assets/img/img.css | 39 --- public/src/css/game.css | 12 + public/src/css/search.css | 4 + public/src/js/abstractfile.js | 10 +- public/src/js/assets.js | 38 +- public/src/js/customsongs.js | 33 +- public/src/js/gpicker.js | 10 +- public/src/js/loader.js | 80 ++++- public/src/js/loadsong.js | 76 ++-- public/src/js/plugins.js | 15 +- public/src/js/search.js | 637 ++++++++++++++++++++++++++++++++++ public/src/js/settings.js | 126 ++++++- public/src/js/songselect.js | 636 ++------------------------------- public/src/js/strings.js | 11 + public/src/js/titlescreen.js | 11 +- public/src/js/view.js | 6 +- templates/index.html | 4 +- 17 files changed, 993 insertions(+), 755 deletions(-) delete mode 100644 public/assets/img/img.css create mode 100644 public/src/js/search.js diff --git a/public/assets/img/img.css b/public/assets/img/img.css deleted file mode 100644 index 140d755..0000000 --- a/public/assets/img/img.css +++ /dev/null @@ -1,39 +0,0 @@ -.pattern-bg{ - background-image: url("bg-pattern-1.png"); -} -#song-select{ - background-image: url("bg_genre_0.png"); -} -#title-screen{ - background-image: url("title-screen.png"); -} -#loading-don{ - background-image: url("dancing-don.gif"); -} -#touch-full-btn{ - background-image: url("touch_fullscreen.png"); -} -#touch-pause-btn{ - background-image: url("touch_pause.png"); -} -.settings-outer{ - background-image: url("bg_settings.png"); -} -#gamepad-bg, -#gamepad-buttons{ - background-image: url("settings_gamepad.png"); -} -#song-search{ - background: linear-gradient(to top, rgb(245 246 252 / 8%), #ff5963), url("bg_search.png"); - background-size: auto, 3.12em; - background-position: -1.2em; -} -.song-search-result-course::before{ - background-image: url("difficulty.png"); -} -.song-search-result-crown{ - background-image: url("crown.png"); -} -.song-search-tip-error{ - background-image: url("miss.png"); -} diff --git a/public/src/css/game.css b/public/src/css/game.css index 8168f2e..faa56b5 100644 --- a/public/src/css/game.css +++ b/public/src/css/game.css @@ -127,3 +127,15 @@ #song-lyrics rt{ line-height: 1; } +.pixelated #canvas, +.pixelated .donbg>div, +.pixelated #songbg>div, +.pixelated #song-stage, +.pixelated #touch-drum-img, +.pixelated #flowers1-in, +.pixelated #flowers2-in, +.pixelated #mikoshi-in, +.pixelated #tetsuo-in, +.pixelated #hana-in{ + image-rendering: pixelated; +} diff --git a/public/src/css/search.css b/public/src/css/search.css index d93649f..eb859dd 100644 --- a/public/src/css/search.css +++ b/public/src/css/search.css @@ -26,6 +26,8 @@ padding: 1em 1em 0 1em; z-index: 1; box-sizing: border-box; + background-size: auto, 3.12em; + background-position: 0%, -2%; } #song-search-container.touch-enabled{ @@ -96,6 +98,7 @@ box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; + border: 0.4em solid; } .song-search-result:last-of-type { @@ -133,6 +136,7 @@ content: attr(alt); position: absolute; z-index: -1; + -webkit-text-stroke-width: 0.4em; } .song-search-result-course { diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index f3c49f6..62694f5 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -59,7 +59,9 @@ class RemoteFile{ } } blob(){ - return this.arrayBuffer().then(response => new Blob([response])) + return loader.ajax(this.url, request => { + request.responseType = "blob" + }) } } class LocalFile{ @@ -113,7 +115,7 @@ class GdriveFile{ this.url = gpicker.filesUrl + this.id + "?alt=media" } arrayBuffer(){ - return gpicker.downloadFile(this.id, true) + return gpicker.downloadFile(this.id, "arraybuffer") } read(encoding){ if(encoding){ @@ -123,7 +125,7 @@ class GdriveFile{ } } blob(){ - return this.arrayBuffer().then(response => new Blob([response])) + return gpicker.downloadFile(this.id, "blob") } } class CachedFile{ @@ -144,6 +146,6 @@ class CachedFile{ return this.arrayBuffer() } blob(){ - return this.arrayBuffer().then(response => new Blob([response])) + return this.arrayBuffer() } } diff --git a/public/src/js/assets.js b/public/src/js/assets.js index bf63f9c..24d4232 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -38,7 +38,8 @@ var assets = { "customsongs.js", "abstractfile.js", "idb.js", - "plugins.js" + "plugins.js", + "search.js" ], "css": [ "main.css", @@ -50,20 +51,13 @@ var assets = { "view.css", "search.css" ], - "assetsCss": [ - "img/img.css" - ], "img": [ - "title-screen.png", "notes.png", "notes_drumroll.png", "notes_hit.png", "notes_explosion.png", "balloon.png", "taiko.png", - "dancing-don.gif", - "bg-pattern-1.png", - "difficulty.png", "don_anim_normal_a.png", "don_anim_normal_b1.png", "don_anim_normal_b2.png", @@ -81,24 +75,26 @@ var assets = { "don_anim_clear_b2.png", "fire_anim.png", "fireworks_anim.png", - "bg_genre_def.png", "bg_score_p1.png", "bg_score_p2.png", - "bg_settings.png", "bg_pause.png", "badge_auto.png", - "touch_pause.png", - "touch_fullscreen.png", - "mimizu.png", - "results_flowers.png", - "results_mikoshi.png", - "results_tetsuohana.png", - "results_tetsuohana2.png", - "settings_gamepad.png", - "crown.png", - "miss.png", - "bg_search.png" + "mimizu.png" ], + "cssBackground": { + "#title-screen": "title-screen.png", + "#loading-don": "dancing-don.gif", + ".pattern-bg": "bg-pattern-1.png", + ".song-search-result-course::before": "difficulty.png", + "#song-select": "bg_genre_def.png", + ".settings-outer": "bg_settings.png", + "#touch-pause-btn": "touch_pause.png", + "#touch-full-btn": "touch_fullscreen.png", + "#gamepad-bg, #gamepad-buttons": "settings_gamepad.png", + ".song-search-result-crown": "crown.png", + ".song-search-tip-error": "miss.png", + "#song-search": "bg_search.png" + }, "audioSfx": [ "se_pause.ogg", "se_calibration.ogg", diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index e9f2d86..bcf7640 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -2,7 +2,7 @@ class CustomSongs{ constructor(...args){ this.init(...args) } - init(touchEnabled, noPage){ + init(touchEnabled, noPage, noLoading){ this.loaderDiv = document.createElement("div") this.loaderDiv.innerHTML = assets.pages["loadsong"] var loadingText = this.loaderDiv.querySelector("#loading-text") @@ -13,6 +13,7 @@ class CustomSongs{ if(noPage){ this.noPage = true + this.noLoading = noLoading return } @@ -262,11 +263,13 @@ class CustomSongs{ var importSongs = new ImportSongs() return importSongs.load(files).then(this.songsLoaded.bind(this), e => { - this.browse.parentNode.reset() + if(!this.noPage){ + this.browse.form.reset() + } this.locked = false this.loading(false) if(e === "nosongs"){ - this.showError(strings.customSongs.noSongs) + this.showError(strings.customSongs.noSongs, "nosongs") }else if(e !== "cancel"){ return Promise.reject(e) } @@ -308,7 +311,7 @@ class CustomSongs{ this.locked = false this.loading(false) if(e === "nosongs"){ - this.showError(strings.customSongs.noSongs) + this.showError(strings.customSongs.noSongs, "nosongs") }else if(e !== "cancel"){ return Promise.reject(e) } @@ -371,7 +374,7 @@ class CustomSongs{ open("privacy") } loading(show){ - if(this.noPage){ + if(this.noLoading){ return } if(show){ @@ -387,14 +390,16 @@ class CustomSongs{ assets.customSongs = true assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0 } - if(!this.noPage){ + if(this.noPage){ + pageEvents.send("import-songs", length) + }else{ assets.sounds["se_don"].play() + setTimeout(() => { + new SongSelect("customSongs", false, this.touchEnabled) + pageEvents.send("import-songs", length) + }, 500) } this.clean() - setTimeout(() => { - new SongSelect("customSongs", false, this.touchEnabled) - pageEvents.send("import-songs", length) - }, 500) return songs && songs.length } keyPressed(pressed, name){ @@ -474,10 +479,14 @@ class CustomSongs{ resolve() }, 500)) } - showError(text){ + showError(text, errorName){ this.locked = false this.loading(false) - if(this.noPage || this.mode === "error"){ + if(this.noPage){ + var error = new Error(text) + error.name = errorName + throw error + }else if(this.mode === "error"){ return } this.mode = "error" diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js index 5f6eaa0..f46f457 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -159,7 +159,7 @@ class Gpicker{ } clientCallback(tokenResponse){ this.tokenResponse = tokenResponse - this.oauthToken = tokenResponse.access_token + this.oauthToken = tokenResponse && tokenResponse.access_token if(this.oauthToken && this.tokenResolve){ this.tokenResolve() } @@ -220,12 +220,12 @@ class Gpicker{ .build() .setVisible(true) } - downloadFile(id, arrayBuffer, retry){ + downloadFile(id, responseType, retry){ var url = this.filesUrl + id + "?alt=media" return this.queue().then(this.getToken.bind(this)).then(() => loader.ajax(url, request => { - if(arrayBuffer){ - request.responseType = "arraybuffer" + if(responseType){ + request.responseType = responseType } request.setRequestHeader("Authorization", "Bearer " + this.oauthToken) }, true).then(event => { @@ -238,7 +238,7 @@ class Gpicker{ var e = response.error if(e && e.errors[0].reason === "authError"){ delete this.oauthToken - return this.downloadFile(id, arrayBuffer, true) + return this.downloadFile(id, responseType, true) }else{ return reject() } diff --git a/public/src/js/loader.js b/public/src/js/loader.js index bf52532..2caeb8e 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -61,12 +61,6 @@ class Loader{ stylesheet.href = "/src/css/" + name + this.queryString document.head.appendChild(stylesheet) }) - assets.assetsCss.forEach(name => { - var stylesheet = document.createElement("link") - stylesheet.rel = "stylesheet" - stylesheet.href = gameConfig.assets_baseurl + name + this.queryString - document.head.appendChild(stylesheet) - }) var checkStyles = () => { if(document.styleSheets.length >= cssCount){ resolve() @@ -84,9 +78,10 @@ class Loader{ }), url) } - assets.img.forEach(name=>{ + assets.img.forEach(name => { var id = this.getFilename(name) var image = document.createElement("img") + image.crossOrigin = "anonymous" var url = gameConfig.assets_baseurl + "img/" + name this.addPromise(pageEvents.load(image), url) image.id = name @@ -95,6 +90,37 @@ class Loader{ assets.image[id] = image }) + var css = [] + for(let selector in assets.cssBackground){ + let name = assets.cssBackground[selector] + var url = gameConfig.assets_baseurl + "img/" + name + this.addPromise(loader.ajax(url, request => { + request.responseType = "blob" + }).then(blob => { + var id = this.getFilename(name) + var image = document.createElement("img") + let blobUrl = URL.createObjectURL(blob) + var promise = pageEvents.load(image).then(() => { + var gradient = "" + if(selector === ".pattern-bg"){ + loader.screen.style.backgroundImage = "url(\"" + blobUrl + "\")" + }else if(selector === "#song-search"){ + gradient = "linear-gradient(to top, rgba(245, 246, 252, 0.08), #ff5963), " + } + css.push(this.cssRuleset({ + [selector]: { + "background-image": gradient + "url(\"" + blobUrl + "\")" + } + })) + }) + image.id = name + image.src = blobUrl + this.assetsDiv.appendChild(image) + assets.image[id] = image + return promise + }), url) + } + assets.views.forEach(name => { var id = this.getFilename(name) var url = "/src/views/" + name + this.queryString @@ -147,6 +173,10 @@ class Loader{ return } + var style = document.createElement("style") + style.appendChild(document.createTextNode(css.join("\n"))) + document.head.appendChild(style) + this.addPromise(this.ajax("/api/songs").then(songs => { songs = JSON.parse(songs) songs.forEach(song => { @@ -179,16 +209,22 @@ class Loader{ .filter(cat => cat.songSkin && cat.songSkin.bg_img) .forEach(cat => { let name = cat.songSkin.bg_img - var id = this.getFilename(name) - var image = document.createElement("img") var url = gameConfig.assets_baseurl + "img/" + name - categoryPromises.push(pageEvents.load(image).catch(response => { + categoryPromises.push(loader.ajax(url, request => { + request.responseType = "blob" + }).then(blob => { + var id = this.getFilename(name) + var image = document.createElement("img") + let blobUrl = URL.createObjectURL(blob) + var promise = pageEvents.load(image) + image.id = name + image.src = blobUrl + this.assetsDiv.appendChild(image) + assets.image[id] = image + return promise + }).catch(response => { return this.errorMsg(response, url) })) - image.id = name - image.src = url - this.assetsDiv.appendChild(image) - assets.image[id] = image }) this.addPromise(Promise.all(categoryPromises)) @@ -356,6 +392,7 @@ class Loader{ this.canvasTest.clean() this.clean() this.callback(songId) + this.ready = true pageEvents.send("ready", readyEvent) }, () => this.errorMsg()) }, () => this.errorMsg()) @@ -407,7 +444,7 @@ class Loader{ if(!lang){ lang = "en" } - loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang].errorOccured + loader.screen.getElementsByClassName("view-content")[0].innerText = allStrings[lang] && allStrings[lang].errorOccured || allStrings.en.errorOccured } var loaderError = loader.screen.getElementsByClassName("loader-error-div")[0] loaderError.style.display = "flex" @@ -472,6 +509,19 @@ class Loader{ this.screen.innerHTML = assets.pages[name] this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg") } + cssRuleset(rulesets){ + var css = [] + for(var selector in rulesets){ + var declarationsObj = rulesets[selector] + var declarations = [] + for(var property in declarationsObj){ + var value = declarationsObj[property] + declarations.push("\t" + property + ": " + value + ";") + } + css.push(selector + "{\n" + declarations.join("\n") + "\n}") + } + return css.join("\n") + } ajax(url, customRequest, customResponse){ var request = new XMLHttpRequest() request.open("GET", url) diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index 685c30e..2539648 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -103,8 +103,8 @@ class LoadSong{ } let img = document.createElement("img") let force = imgLoad[i].type === "song" && this.touchEnabled - if(!songObj.custom && (this.imgScale !== 1 || force)){ - img.crossOrigin = "Anonymous" + if(!songObj.custom){ + img.crossOrigin = "anonymous" } let promise = pageEvents.load(img) this.addPromise(promise.then(() => { @@ -147,15 +147,30 @@ class LoadSong{ } if(this.touchEnabled && !assets.image["touch_drum"]){ let img = document.createElement("img") - if(this.imgScale !== 1){ - img.crossOrigin = "Anonymous" - } + img.crossOrigin = "anonymous" var url = gameConfig.assets_baseurl + "img/touch_drum.png" this.addPromise(pageEvents.load(img).then(() => { return this.scaleImg(img, "touch_drum", "") }), url) img.src = url } + var resultsImg = [ + "results_flowers", + "results_mikoshi", + "results_tetsuohana", + "results_tetsuohana2" + ] + resultsImg.forEach(id => { + if(!assets.image[id]){ + var img = document.createElement("img") + img.crossOrigin = "anonymous" + var url = gameConfig.assets_baseurl + "img/" + id + ".png" + this.addPromise(pageEvents.load(img).then(() => { + return this.scaleImg(img, id, "") + }), url) + img.src = url + } + }) if(songObj.volume && songObj.volume !== 1){ this.promises.push(new Promise(resolve => setTimeout(resolve, 500))) } @@ -217,9 +232,7 @@ class LoadSong{ if(!(filenameAb in assets.image)){ let img = document.createElement("img") let force = filenameAb.startsWith("bg_song_") && this.touchEnabled - if(this.imgScale !== 1 || force){ - img.crossOrigin = "Anonymous" - } + img.crossOrigin = "anonymous" var url = gameConfig.assets_baseurl + "img/" + filenameAb + ".png" this.addPromise(pageEvents.load(img).then(() => { return this.scaleImg(img, filenameAb, "", force) @@ -235,32 +248,29 @@ class LoadSong{ if(force && scale > 0.5){ scale = 0.5 } - if(scale !== 1){ - var canvas = document.createElement("canvas") - var w = Math.floor(img.width * scale) - var h = Math.floor(img.height * scale) - canvas.width = Math.max(1, w) - canvas.height = Math.max(1, h) - var ctx = canvas.getContext("2d") - ctx.drawImage(img, 0, 0, w, h) - var saveScaled = url => { - let img2 = document.createElement("img") - pageEvents.load(img2).then(() => { - assets.image[prefix + filename] = img2 - resolve() - }, reject) - img2.src = url - } - if("toBlob" in canvas){ - canvas.toBlob(blob => { - saveScaled(URL.createObjectURL(blob)) - }) - }else{ - saveScaled(canvas.toDataURL()) - } + var canvas = document.createElement("canvas") + var w = Math.floor(img.width * scale) + var h = Math.floor(img.height * scale) + canvas.width = Math.max(1, w) + canvas.height = Math.max(1, h) + var ctx = canvas.getContext("2d") + ctx.drawImage(img, 0, 0, w, h) + var saveScaled = url => { + let img2 = document.createElement("img") + pageEvents.load(img2).then(() => { + assets.image[prefix + filename] = img2 + loader.assetsDiv.appendChild(img2) + resolve() + }, reject) + img2.id = prefix + filename + img2.src = url + } + if("toBlob" in canvas){ + canvas.toBlob(blob => { + saveScaled(URL.createObjectURL(blob)) + }) }else{ - assets.image[prefix + filename] = img - resolve() + saveScaled(canvas.toDataURL()) } }) } diff --git a/public/src/js/plugins.js b/public/src/js/plugins.js index 0e996d9..c9ca1e5 100644 --- a/public/src/js/plugins.js +++ b/public/src/js/plugins.js @@ -582,6 +582,12 @@ class EditFunction extends EditValue{ if(this.name){ this.original = this.name[0][this.name[1]] } + if(typeof this.original !== "function"){ + console.error(this.loadCallback) + var error = new Error() + error.stack = "Error editing the function value of " + this.getName() + ": Original value is not a function" + throw error + } var args = plugins.argsFromFunc(this.original) try{ var output = this.loadCallback(plugins.strFromFunc(this.original), args) @@ -618,8 +624,13 @@ class EditFunction extends EditValue{ } class Patch{ - edits = [] - addedLanguages = [] + constructor(...args){ + this.init(...args) + } + init(){ + this.edits = [] + this.addedLanguages = [] + } addEdits(...args){ args.forEach(arg => this.edits.push(arg)) } diff --git a/public/src/js/search.js b/public/src/js/search.js new file mode 100644 index 0000000..6a04eaa --- /dev/null +++ b/public/src/js/search.js @@ -0,0 +1,637 @@ +class Search{ + constructor(...args){ + this.init(...args) + } + 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){ + var skin = this.songSelect.songSkin[i] + 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 + }, + [".song-search-" + id + "::before"]: { + "border-color": skin.border[0], + "border-bottom-color": skin.border[1], + "border-right-color": skin.border[1] + }, + [".song-search-" + id + " .song-search-result-title::before, .song-search-" + id + " .song-search-result-subtitle::before"]: { + "-webkit-text-stroke-color": skin.outline + } + })) + } + } + this.style.appendChild(document.createTextNode(css.join("\n"))) + loader.screen.appendChild(this.style) + } + + perform(query){ + var results = [] + var filters = {} + + var querySplit = query.split(" ") + var editedSplit = query.split(" ") + querySplit.forEach(word => { + if(word.length > 0){ + var parts = word.toLowerCase().split(":") + 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){ + filters[parts[0]] = range + } + break + case "extreme": + var range = this.parseRange(parts[1]) + if(range){ + filters.oni = this.parseRange(parts[1]) + } + break + case "clear": + case "silver": + case "gold": + case "genre": + case "lyrics": + case "creative": + case "played": + case "maker": + case "diverge": + case "random": + filters[parts[0]] = parts[1] + break + } + + editedSplit.splice(editedSplit.indexOf(word), 1) + } + } + }) + + query = editedSplit.join(" ").trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "") + + var totalFilters = Object.keys(filters).length + var random = false + 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){ + 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){ + passedFilters++ + } + break + case "clear": + case "silver": + case "gold": + 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)){ + passedFilters++ + } + }) + } else { + var score = scoreStorage.scores[song.hash] + 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)){ + passedFilters++ + } + break + case "lyrics": + if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){ + passedFilters++ + } + break + case "creative": + if((value === "yes" && song.maker) || (value === "no" && !song.maker)){ + passedFilters++ + } + break + case "maker": + 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())){ + passedFilters++ + } + break + case "diverge": + var branch = Object.values(song.courses).find(course => course && course.branch) + if((value === "yes" && branch) || (value === "no" && !branch)){ + passedFilters++ + } + break + case "random": + if(value === "yes" || value === "no"){ + random = value === "yes" + passedFilters++ + } + break + } + }) + + if(passedFilters === totalFilters){ + results.push(song) + } + } + + var maxResults = totalFilters > 0 && !query ? 100 : 50 + + if(query){ + results = fuzzysort.go(query, results, { + keys: ["titlePrepared", "subtitlePrepared"], + allowTypo: true, + limit: maxResults, + scoreFn: a => { + if(a[0]){ + var score0 = a[0].score + a[0].ranges = this.indexesToRanges(a[0].indexes) + 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){ + rangeAmount-- + score0 -= 1000 + } + lastIdx = range[1] + }) + var index = a[0].target.toLowerCase().indexOf(query) + if(index !== -1){ + a[0].ranges = [[index, index + query.length - 1]] + }else if(rangeAmount > a[0].indexes.length / 2){ + score0 = -Infinity + a[0].ranges = null + }else if(rangeAmount !== 1){ + score0 -= 9000 + } + } + } + if(a[1]){ + var score1 = a[1].score - 1000 + a[1].ranges = this.indexesToRanges(a[1].indexes) + 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){ + rangeAmount-- + score1 -= 1000 + } + lastIdx = range[1] + }) + var index = a[1].target.indexOf(query) + if(index !== -1){ + a[1].ranges = [[index, index + query.length - 1]] + }else if(rangeAmount > a[1].indexes.length / 2){ + score1 = -Infinity + a[1].ranges = null + }else if(rangeAmount !== 1){ + score1 -= 9000 + } + } + } + if(a[0]){ + return a[1] ? Math.max(score0, score1) : score0 + }else{ + return a[1] ? score1 : -Infinity + } + } + }) + }else{ + results = results.slice(0, maxResults).map(result => { + return {obj: result} + }) + } + 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] + results[j] = temp + } + } + + return results + } + + 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){ + var cat = assets.categories.find(cat => cat.id === song.category_id) + 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){ + 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") + courseDiv.classList.add("song-search-result-course", "song-search-result-" + course) + if (song.courses[course]) { + var crown = "noclear" + if (scoreStorage.scores[song.hash]) { + if (scoreStorage.scores[song.hash][course]) { + crown = scoreStorage.scores[song.hash][course].crown || "noclear" + } + } + var courseCrown = document.createElement("div") + courseCrown.classList.add("song-search-result-crown", "song-search-result-" + crown) + 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){ + resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)" + } + 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){ + resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)" + } + } + + return resultDiv + } + + highlightResult(text, result){ + var fragment = document.createDocumentFragment() + var ranges = (result ? result.ranges : null) || [] + var lastIdx = 0 + ranges.forEach(range => { + if(lastIdx !== range[0]){ + fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0]))) + } + var span = document.createElement("span") + span.classList.add("highlighted-text") + span.innerText = text.slice(range[0], range[1] + 1) + fragment.appendChild(span) + lastIdx = range[1] + 1 + }) + if(text.length !== lastIdx){ + fragment.appendChild(document.createTextNode(text.slice(lastIdx))) + } + return fragment + } + + setActive(idx){ + this.songSelect.playSound("se_ka") + var active = this.div.querySelector(":scope .song-search-result-active") + if(active){ + active.classList.remove("song-search-result-active") + } + + 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){ + return + } + 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){ + 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.bind(this)) + + this.songSelect.playSound("se_pause") + loader.screen.appendChild(this.div) + this.setTip() + cancelTouch = false + noResizeRoot = true + if(this.songSelect.songs[this.songSelect.selectedSong].courses){ + snd.previewGain.setVolumeMul(0.5) + }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){ + this.input.value = lastQuery + this.input.dispatchEvent(new Event("input", { + value: lastQuery + })) + } + } + + remove(byUser=false){ + if(this.opened){ + this.opened = false + if(byUser){ + this.songSelect.playSound("se_cancel") + } + + pageEvents.remove(this.div.querySelector(":scope #song-search-container"), + ["mousedown", "touchstart"]) + pageEvents.remove(this.input, ["input"]) + + this.div.remove() + delete this.results + delete this.div + delete this.input + delete this.tip + delete this.active + cancelTouch = true + noResizeRoot = false + if(this.songSelect.songs[this.songSelect.selectedSong].courses){ + snd.previewGain.setVolumeMul(1) + }else if(this.songSelect.bgmEnabled){ + snd.musicGain.setVolumeMul(1) + } + } + } + + setTip(tip, error=false){ + if(this.tip){ + this.tip.remove() + delete this.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){ + this.tip.classList.add("song-search-tip-error") + } + } + + proceed(songId){ + var song = this.songSelect.songs.find(song => song.id === songId) + this.remove() + this.songSelect.playBgm(false) + + var songIndex = this.songSelect.songs.findIndex(song => song.id === songId) + this.songSelect.setSelectedSong(songIndex) + this.songSelect.toSelectDifficulty() + } + + 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)){ + parentNode.scrollTop += selectedPosTop - parent.top + }else{ + var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top + if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){ + parentNode.scrollTop += selectedPosBottom - parent.height + } + } + } + + parseRange(string){ + var range = string.split("-") + if(range.length == 1){ + var min = parseInt(range[0]) || 0 + 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 + } + } + + indexesToRanges(indexes){ + var ranges = [] + var range + indexes.forEach(idx => { + if(range && range[1] === idx - 1){ + range[1] = idx + }else{ + range = [idx, idx] + ranges.push(range) + } + }) + return ranges + } + + onInput(){ + var text = this.input.value + localStorage.setItem("lastSearchQuery", text) + text = text.toLowerCase() + + if(text.length === 0){ + this.setTip() + return + } + + var new_results = this.perform(text) + + if(new_results.length === 0){ + this.setTip(strings.search.noResults, true) + return + }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) + fragment.appendChild(result) + 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){ + this.remove(true) + }else if(e.which === 1){ + var songEl = e.target.closest(".song-search-result") + if(songEl){ + var songId = parseInt(songEl.dataset.songId) + this.proceed(songId) + } + } + } + + keyPress(pressed, name, event, repeat){ + if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { + this.remove(true) + if(event){ + event.preventDefault() + } + }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){ + this.setActive(null) + this.input.focus() + }else if(Number.isInteger(this.active)){ + this.setActive(this.active + 1) + }else{ + this.setActive(0) + } + }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){ + 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)){ + this.setActive(this.active - 1) + }else{ + this.setActive(this.results.length - 1) + } + }else if(name === "confirm"){ + if(Number.isInteger(this.active)){ + this.proceed(parseInt(this.results[this.active].dataset.songId)) + }else{ + this.onInput() + } + } + } + + redraw(){ + if(this.opened && this.container){ + var vmin = Math.min(innerWidth, lastHeight) / 100 + if(this.vmin !== vmin){ + this.container.style.setProperty("--vmin", vmin + "px") + this.vmin = vmin + } + }else{ + this.vmin = null + } + } + + clean(){ + loader.screen.removeChild(this.style) + fuzzysort.cleanup() + delete this.container + delete this.style + delete this.songSelect + } +} \ No newline at end of file diff --git a/public/src/js/settings.js b/public/src/js/settings.js index e9c9f2d..f52e817 100644 --- a/public/src/js/settings.js +++ b/public/src/js/settings.js @@ -217,15 +217,18 @@ class SettingsView{ constructor(...args){ this.init(...args) } - init(touchEnabled, tutorial, songId, toSetting, settingsItems){ + init(touchEnabled, tutorial, songId, toSetting, settingsItems, noSoundStart){ this.touchEnabled = touchEnabled this.tutorial = tutorial this.songId = songId this.customSettings = !!settingsItems this.settingsItems = settingsItems || settings.items + this.locked = false loader.changePage("settings", tutorial) - assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992) + if(!noSoundStart){ + assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992) + } this.defaultButton = document.getElementById("settings-default") this.viewOuter = this.getElement("view-outer") if(touchEnabled){ @@ -377,16 +380,48 @@ class SettingsView{ this.items.push(outputObject) this.getValue(i, valueDiv) } - this.items.push({ - id: "default", - settingBox: this.defaultButton - }) - this.addTouch(this.defaultButton, this.defaultSettings.bind(this)) + var selectBack = this.items.length === 0 + if(this.customSettings){ + var form = document.createElement("form") + this.browse = document.createElement("input") + this.browse.id = "browse" + this.browse.type = "file" + this.browse.multiple = true + this.browse.accept = ".taikoweb.js" + pageEvents.add(this.browse, "change", this.browseChange.bind(this)) + form.appendChild(this.browse) + loader.screen.appendChild(form) + this.browseButton = document.createElement("div") + this.browseButton.classList.add("taibtn", "stroke-sub") + this.defaultButton.parentNode.insertBefore(this.browseButton, this.defaultButton) + this.items.push({ + id: "browse", + settingBox: this.browseButton + }) + this.addTouch(this.browseButton, () => { + this.playSound("se_don") + this.browse.click() + }) + } + this.showDefault = !this.customSettings || plugins.allPlugins.filter(obj => obj.plugin.imported).length + if(this.showDefault){ + this.items.push({ + id: "default", + settingBox: this.defaultButton + }) + this.addTouch(this.defaultButton, this.defaultSettings.bind(this)) + }else{ + this.defaultButton.parentNode.removeChild(this.defaultButton) + } this.items.push({ id: "back", settingBox: this.endButton }) this.addTouch(this.endButton, this.onEnd.bind(this)) + if(selectBack){ + this.selected = this.items.length - 1 + this.endButton.classList.add("selected") + } if(!this.customSettings){ this.gamepadSettings = document.getElementById("settings-gamepad") @@ -606,6 +641,9 @@ class SettingsView{ valueDiv.innerText = value } setValue(name){ + if(this.locked){ + return + } var promise var current = this.settingsItems[name] if(current.getItem){ @@ -674,6 +712,9 @@ class SettingsView{ }) } keyPressed(pressed, name, event, repeat){ + if(this.locked){ + return + } if(pressed){ if(!this.pressedKeys[name]){ this.pressedKeys[name] = this.getMS() + 300 @@ -693,6 +734,11 @@ class SettingsView{ this.onEnd() }else if(selected.id === "default"){ this.defaultSettings() + }else if(selected.id === "browse"){ + if(event){ + this.playSound("se_don") + this.browse.click() + } }else{ this.setValue(selected.id) } @@ -700,7 +746,7 @@ class SettingsView{ selected.settingBox.classList.remove("selected") do{ this.selected = this.mod(this.items.length, this.selected + ((name === "right" || name === "down") ? 1 : -1)) - }while(this.items[this.selected].id === "default" && name !== "left") + }while((this.items[this.selected].id === "default" || this.items[this.selected].id === "browse") && name !== "left") selected = this.items[this.selected] selected.settingBox.classList.add("selected") this.scrollTo(selected.settingBox) @@ -1027,7 +1073,9 @@ class SettingsView{ defaultSettings(){ if(this.customSettings){ plugins.unloadImported() - return this.onEnd() + this.clean(true) + this.playSound("se_don") + return setTimeout(() => this.restart(), 500) } if(this.mode === "keyboard"){ this.keyboardBack(this.items[this.selected]) @@ -1046,6 +1094,31 @@ class SettingsView{ this.drumSounds = settings.getItem("latency").drumSounds this.playSound("se_don") } + browseChange(event){ + this.locked = true + var files = [] + for(var i = 0; i < event.target.files.length; i++){ + files.push(new LocalFile(event.target.files[i])) + } + var customSongs = new CustomSongs(this.touchEnabled, true) + customSongs.importLocal(files).then(() => { + this.clean(true) + return this.restart() + }).catch(e => { + if(e){ + var message = e.message + if(e.name === "nosongs"){ + message = strings.plugins.noPlugins + } + if(message){ + alert(message) + } + } + this.locked = false + this.browse.form.reset() + return Promise.resolve() + }) + } onEnd(){ if(this.mode === "number"){ this.numberBack(this.items[this.selected]) @@ -1063,6 +1136,12 @@ class SettingsView{ } }, 500) } + restart(){ + if(this.mode === "number"){ + this.numberBack(this.items[this.selected]) + } + return new SettingsView(this.touchEnabled, this.tutorial, this.songId, undefined, this.customSettings ? plugins.getSettings() : undefined, true) + } getLocalTitle(title, titleLang){ if(titleLang){ for(var id in titleLang){ @@ -1109,14 +1188,18 @@ class SettingsView{ setStrings(){ this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings) this.setAltText(this.endButton, strings.settings.ok) - if(!this.customSettings){ + if(this.customSettings){ + this.setAltText(this.browseButton, strings.plugins.browse) + }else{ this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name) this.setAltText(this.gamepadEndButton, strings.settings.ok) this.setAltText(this.latencyTitle, strings.settings.latency.name) this.setAltText(this.latencyDefaultButton, strings.settings.default) this.setAltText(this.latencyEndButton, strings.settings.ok) } - this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default) + if(this.showDefault){ + this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default) + } } setAltText(element, text){ element.innerText = text @@ -1154,11 +1237,13 @@ class SettingsView{ getMS(){ return Date.now() } - clean(){ + clean(noSoundStop){ this.redrawRunning = false this.keyboard.clean() this.gamepad.clean() - assets.sounds["bgm_settings"].stop() + if(!noSoundStop){ + assets.sounds["bgm_settings"].stop() + } pageEvents.remove(window, ["mouseup", "touchstart", "touchmove", "touchend", "blur"], this.windowSymbol) if(this.customSettings){ pageEvents.remove(window, "language-change", this.windowSymbol) @@ -1176,7 +1261,12 @@ class SettingsView{ if(this.defaultButton){ delete this.defaultButton } - if(!this.customSettings){ + if(this.customSettings){ + pageEvents.remove(this.browse, "change") + this.removeTouch(this.browseButton) + delete this.browse + delete this.browseButton + }else{ this.removeTouch(this.gamepadSettings) this.removeTouch(this.gamepadEndButton) this.removeTouch(this.gamepadBox) @@ -1204,8 +1294,12 @@ class SettingsView{ delete this.latencyEndButton if(this.resolution !== settings.getItem("resolution")){ for(var i in assets.image){ - if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_")){ - URL.revokeObjectURL(assets.image[i].src) + if(i === "touch_drum" || i.startsWith("bg_song_") || i.startsWith("bg_stage_") || i.startsWith("bg_don_") || i.startsWith("results_")){ + var img = assets.image[i] + URL.revokeObjectURL(img.src) + if(img.parentNode){ + img.parentNode.removeChild(img) + } delete assets.image[i] } } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 29a4cf2..5e891cc 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -92,22 +92,6 @@ class SongSelect{ } this.songSkin["default"].sort = songSkinLength + 1 - this.searchStyle = document.createElement("style") - var searchCss = [] - Object.keys(this.songSkin).forEach(key => { - var skin = this.songSkin[key] - if("id" in skin || key === "default"){ - var id = "id" in skin ? ("cat" + skin.id) : key - - searchCss.push('.song-search-' + id + ' { background-color: ' + skin.background + ' }') - searchCss.push('.song-search-' + id + '::before { border: 0.4em solid ' + skin.border[0] + ' ; border-bottom-color: ' + skin.border[1] + ' ; border-right-color: ' + skin.border[1] + ' }') - searchCss.push('.song-search-' + id + ' .song-search-result-title::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }') - searchCss.push('.song-search-' + id + ' .song-search-result-subtitle::before { -webkit-text-stroke: 0.4em ' + skin.outline + ' }') - } - }) - this.searchStyle.appendChild(document.createTextNode(searchCss.join("\n"))) - loader.screen.appendChild(this.searchStyle) - this.font = strings.font this.songs = [] @@ -194,14 +178,12 @@ class SongSelect{ category: strings.random }) } - if(plugins.hasSettings()){ - this.songs.push({ - title: strings.plugins.title, - skin: this.songSkin.plugins, - action: "plugins", - category: strings.random - }) - } + this.songs.push({ + title: strings.plugins.title, + skin: this.songSkin.plugins, + action: "plugins", + category: strings.random + }) this.songs.push({ title: strings.back, @@ -246,6 +228,8 @@ class SongSelect{ this.currentSongCache = new CanvasCache(noSmoothing) this.nameplateCache = new CanvasCache(noSmoothing) + this.search = new Search(this) + this.difficulty = [strings.easy, strings.normal, strings.hard, strings.oni] this.difficultyId = ["easy", "normal", "hard", "oni", "ura"] @@ -257,7 +241,6 @@ class SongSelect{ this.selectedSong = 0 this.selectedDiff = 0 this.lastCurrentSong = {} - this.searchEnabled = true this.lastRandom = false assets.sounds["bgm_songsel"].playLoop(0.1, false, 0, 1.442, 3.506) @@ -434,44 +417,14 @@ class SongSelect{ this.state.showWarning = false this.showWarning = false } - }else if (this.search){ - if(name === "back" || (event && event.keyCode && event.keyCode === 70 && ctrl)) { - this.removeSearch(true) - if(event){ event.preventDefault() } - }else if(name === "down" && this.search.results.length){ - if(this.search.input == document.activeElement && this.search.results){ - this.searchSetActive(0) - }else if(this.search.active === this.search.results.length-1){ - this.searchSetActive(null) - this.search.input.focus() - }else if(Number.isInteger(this.search.active)){ - this.searchSetActive(this.search.active+1) - }else{ - this.searchSetActive(0) - } - }else if(name === "up" && this.search.results.length){ - if(this.search.input == document.activeElement && this.search.results){ - this.searchSetActive(this.search.results.length-1) - }else if(this.search.active === 0){ - this.searchSetActive(null) - this.search.input.focus() - setTimeout(() => { - this.search.input.setSelectionRange(this.search.input.value.length, this.search.input.value.length) - }, 0) - }else if(Number.isInteger(this.search.active)){ - this.searchSetActive(this.search.active-1) - }else{ - this.searchSetActive(this.search.results.length-1) - } - }else if(name === "confirm"){ - if(Number.isInteger(this.search.active)){ - this.searchProceed(parseInt(this.search.results[this.search.active].dataset.songId)) - } - } + }else if(this.search.opened){ + this.search.keyPress(pressed, name, event, repeat) }else if(this.state.screen === "song"){ if(event && event.keyCode && event.keyCode === 70 && ctrl){ - this.displaySearch() - if(event){ event.preventDefault() } + this.search.display() + if(event){ + event.preventDefault() + } }else if(name === "confirm"){ this.toSelectDifficulty() }else if(name === "back"){ @@ -504,8 +457,10 @@ class SongSelect{ } }else if(this.state.screen === "difficulty"){ if(event && event.keyCode && event.keyCode === 70 && ctrl){ - this.displaySearch() - if(event){ event.preventDefault() } + this.search.display() + if(event){ + event.preventDefault() + } }else if(name === "confirm"){ if(this.selectedDiff === 0){ this.toSongSelect() @@ -528,8 +483,10 @@ class SongSelect{ } }else if(this.state.screen === "title" || this.state.screen === "titleFadeIn"){ if(event && event.keyCode && event.keyCode === 70 && ctrl){ - this.displaySearch() - if(event){ event.preventDefault() } + this.search.display() + if(event){ + event.preventDefault() + } } } } @@ -627,7 +584,7 @@ class SongSelect{ if(408 < mouse.x && mouse.x < 872 && 470 < mouse.y && mouse.y < 550){ moveTo = "showWarning" } - }else if(this.state.screen === "song" && !this.search){ + }else if(this.state.screen === "song" && !this.search.opened){ if(20 < mouse.y && mouse.y < 90 && 410 < mouse.x && mouse.x < 880 && (mouse.x < 540 || mouse.x > 750)){ moveTo = mouse.x < 640 ? "categoryPrev" : "categoryNext" }else if(!p2.session && 60 < mouse.x && mouse.x < 332 && 640 < mouse.y && mouse.y < 706 && gameConfig.accounts){ @@ -792,7 +749,7 @@ class SongSelect{ } } }else if(this.state.locked === 0 || fromP2){ - this.removeSearch() + this.search.remove() if(currentSong.courses){ if(currentSong.unloaded){ return @@ -815,7 +772,6 @@ class SongSelect{ } pageEvents.send("song-select-difficulty", currentSong) }else if(currentSong.action === "back"){ - this.clean() this.toTitleScreen() }else if(currentSong.action === "random"){ do{ @@ -827,7 +783,7 @@ class SongSelect{ this.toSelectDifficulty(false, playVoice=false) pageEvents.send("song-select-random") }else if(currentSong.action === "search"){ - this.displaySearch(true) + this.search.display(true) }else if(currentSong.action === "tutorial"){ this.toTutorial() }else if(currentSong.action === "about"){ @@ -1116,8 +1072,8 @@ class SongSelect{ this.selectableText = "" - if(this.search && this.searchContainer){ - this.searchInput() + if(this.search.opened && this.search.container){ + this.search.onInput() } }else if(!document.hasFocus() && !p2.session){ if(this.state.focused){ @@ -1146,15 +1102,7 @@ class SongSelect{ var screen = this.state.screen var selectedWidth = this.songAsset.width - if(this.search && this.searchContainer){ - var vmin = Math.min(innerWidth, lastHeight) / 100 - if(this.vmin !== vmin){ - this.searchContainer.style.setProperty("--vmin", vmin + "px") - this.vmin = vmin - } - }else{ - this.vmin = null - } + this.search.redraw() if(this.wheelScrolls !== 0 && !this.state.locked && ms >= this.wheelTimer + 20) { if(p2.session){ @@ -2726,520 +2674,6 @@ class SongSelect{ } return addedSong } - - createSearchResult(result, resultWidth, fontSize){ - var song = result.obj - var title = this.getLocalTitle(song.title, song.title_lang) - var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) - - var id = "default" - if(song.category_id){ - var cat = assets.categories.find(cat => cat.id === song.category_id) - 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){ - 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") - courseDiv.classList.add("song-search-result-course", "song-search-result-" + course) - if (song.courses[course]) { - var crown = "noclear" - if (scoreStorage.scores[song.hash]) { - if (scoreStorage.scores[song.hash][course]) { - crown = scoreStorage.scores[song.hash][course].crown || "noclear" - } - } - var courseCrown = document.createElement("div") - courseCrown.classList.add("song-search-result-crown", "song-search-result-" + crown) - var courseStars = document.createElement("div") - courseStars.classList.add("song-search-result-stars") - courseStars.innerText = song.courses[course].stars + '★' - - courseDiv.appendChild(courseCrown) - courseDiv.appendChild(courseStars) - } else { - courseDiv.classList.add("song-search-result-hidden") - } - - resultDiv.appendChild(courseDiv) - }) - - this.ctx.font = (1.2 * fontSize) + "px " + strings.font - var titleWidth = this.ctx.measureText(title).width - var titleRatio = resultWidth / titleWidth - if(titleRatio < 1){ - resultInfoTitle.style.transform = "scale(" + titleRatio + ", 1)" - } - if(subtitle){ - this.ctx.font = (0.8 * 1.2 * fontSize) + "px " + strings.font - var subtitleWidth = this.ctx.measureText(subtitle).width - var subtitleRatio = resultWidth / subtitleWidth - if(subtitleRatio < 1){ - resultInfoSubtitle.style.transform = "scale(" + subtitleRatio + ", 1)" - } - } - - return resultDiv - } - - highlightResult(text, result){ - var fragment = document.createDocumentFragment() - var ranges = (result ? result.ranges : null) || [] - var lastIdx = 0 - ranges.forEach(range => { - if(lastIdx !== range[0]){ - fragment.appendChild(document.createTextNode(text.slice(lastIdx, range[0]))) - } - var span = document.createElement("span") - span.classList.add("highlighted-text") - span.innerText = text.slice(range[0], range[1] + 1) - fragment.appendChild(span) - lastIdx = range[1] + 1 - }) - if(text.length !== lastIdx){ - fragment.appendChild(document.createTextNode(text.slice(lastIdx))) - } - return fragment - } - - searchSetActive(idx){ - this.playSound("se_ka") - var active = this.search.div.querySelector(":scope .song-search-result-active") - if(active){ - active.classList.remove("song-search-result-active") - } - - if(idx === null){ - this.search.active = null - return - } - - var el = this.search.results[idx] - this.search.input.blur() - el.classList.add("song-search-result-active") - this.scrollTo(el) - - this.search.active = idx - } - - 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)){ - parentNode.scrollTop += selectedPosTop - parent.top - }else{ - var selectedPosBottom = selected.top + selected.height * 1.5 - parent.top - if(Math.floor(selectedPosBottom) > Math.floor(parent.height)){ - parentNode.scrollTop += selectedPosBottom - parent.height - } - } - } - - displaySearch(fromButton=false){ - if(!this.searchEnabled){ - return - } - if(this.search){ - return this.removeSearch(true) - } - - this.search = {results: []} - this.search.div = document.createElement("div") - this.search.div.innerHTML = assets.pages["search"] - - this.searchContainer = this.search.div.querySelector(":scope #song-search-container") - if(this.touchEnabled){ - this.searchContainer.classList.add("touch-enabled") - } - pageEvents.add(this.searchContainer, ["mousedown", "touchstart"], this.searchClick.bind(this)) - - this.search.input = this.search.div.querySelector(":scope #song-search-input") - this.search.input.setAttribute("placeholder", strings.search.searchInput) - pageEvents.add(this.search.input, ["input"], this.searchInput.bind(this)) - - this.playSound("se_pause") - loader.screen.appendChild(this.search.div) - this.setSearchTip() - cancelTouch = false - noResizeRoot = true - if(this.songs[this.selectedSong].courses){ - snd.previewGain.setVolumeMul(0.5) - }else if(this.bgmEnabled){ - snd.musicGain.setVolumeMul(0.5) - } - - setTimeout(() => { - this.search.input.focus() - this.search.input.setSelectionRange(0, this.search.input.value.length) - }, 10) - - var lastQuery = localStorage.getItem("lastSearchQuery") - if(lastQuery){ - this.search.input.value = lastQuery - this.search.input.dispatchEvent(new Event('input', {value: lastQuery})) - } - } - - removeSearch(byUser=false){ - if(this.search){ - if(byUser){ - this.playSound("se_cancel") - } - - pageEvents.remove(this.search.div.querySelector(":scope #song-search-container"), - ["mousedown", "touchstart"]) - pageEvents.remove(this.search.input, ["input"]) - - this.search.div.remove() - delete this.search - cancelTouch = true - noResizeRoot = false - if(this.songs[this.selectedSong].courses){ - snd.previewGain.setVolumeMul(1) - }else if(this.bgmEnabled){ - snd.musicGain.setVolumeMul(1) - } - } - } - - setSearchTip(tip, error=false){ - if(this.search.tip){ - this.search.tip.remove() - delete this.search.tip - } - - if(!tip){ - tip = strings.search.tip + " " + strings.search.tips[Math.floor(Math.random() * strings.search.tips.length)] - } - - var resultsDiv = this.search.div.querySelector(":scope #song-search-results") - resultsDiv.innerHTML = "" - this.search.results = [] - - this.search.tip = document.createElement("div") - this.search.tip.setAttribute("id", "song-search-tip") - this.search.tip.innerText = tip - this.search.div.querySelector(":scope #song-search").appendChild(this.search.tip) - - if(error){ - this.search.tip.classList.add("song-search-tip-error") - } - } - - parseRange(string){ - var range = string.split("-") - if(range.length == 1){ - var min = parseInt(range[0]) || 0 - 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 - } - } - - performSearch(query){ - var results = [] - var filters = {} - - var querySplit = query.split(" ") - var editedSplit = query.split(" ") - querySplit.forEach(word => { - if(word.length > 0){ - var parts = word.toLowerCase().split(":") - 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) { filters[parts[0]] = range } - break - case "extreme": - var range = this.parseRange(parts[1]) - if (range) { filters.oni = this.parseRange(parts[1]) } - break - case "clear": - case "silver": - case "gold": - case "genre": - case "lyrics": - case "creative": - case "played": - case "maker": - case "diverge": - filters[parts[0]] = parts[1] - break - } - - editedSplit.splice(editedSplit.indexOf(word), 1) - } - } - }) - - query = editedSplit.join(" ").trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "") - - var totalFilters = Object.keys(filters).length - 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){ - 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){ - passedFilters++ - } - break - case "clear": - case "silver": - case "gold": - 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)){ - passedFilters++ - } - }) - } else { - var score = scoreStorage.scores[song.hash] - 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)){ - passedFilters++ - } - break - case "lyrics": - if((value === "yes" && song.lyrics) || (value === "no" && !song.lyrics)){ - passedFilters++ - } - break - case "creative": - if((value === "yes" && song.maker) || (value === "no" && !song.maker)){ - passedFilters++ - } - break - case "maker": - 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())){ - passedFilters++ - } - break - case "diverge": - var branch = Object.values(song.courses).find(course => course && course.branch) - if((value === "yes" && branch) || (value === "no" && !branch)){ - passedFilters++ - } - break - } - }) - - if(passedFilters === totalFilters){ - results.push(song) - } - } - - var maxResults = totalFilters > 0 && !query ? 100 : 50 - - if(query){ - results = fuzzysort.go(query, results, { - keys: ["titlePrepared", "subtitlePrepared"], - allowTypo: true, - limit: maxResults, - scoreFn: a => { - if(a[0]){ - var score0 = a[0].score - a[0].ranges = this.indexesToRanges(a[0].indexes) - 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){ - rangeAmount-- - score0 -= 1000 - } - lastIdx = range[1] - }) - var index = a[0].target.toLowerCase().indexOf(query) - if(index !== -1){ - a[0].ranges = [[index, index + query.length - 1]] - }else if(rangeAmount > a[0].indexes.length / 2){ - score0 = -Infinity - a[0].ranges = null - }else if(rangeAmount !== 1){ - score0 -= 9000 - } - } - } - if(a[1]){ - var score1 = a[1].score - 1000 - a[1].ranges = this.indexesToRanges(a[1].indexes) - 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){ - rangeAmount-- - score1 -= 1000 - } - lastIdx = range[1] - }) - var index = a[1].target.indexOf(query) - if(index !== -1){ - a[1].ranges = [[index, index + query.length - 1]] - }else if(rangeAmount > a[1].indexes.length / 2){ - score1 = -Infinity - a[1].ranges = null - }else if(rangeAmount !== 1){ - score1 -= 9000 - } - } - } - if(a[0]){ - return a[1] ? Math.max(score0, score1) : score0 - }else{ - return a[1] ? score1 : -Infinity - } - } - }) - }else{ - results = results.map(result => { - return {obj: result} - }).slice(0, maxResults) - } - - return results - } - - indexesToRanges(indexes){ - var ranges = [] - var range - indexes.forEach(idx => { - if(range && range[1] === idx - 1){ - range[1] = idx - }else{ - range = [idx, idx] - ranges.push(range) - } - }) - return ranges - } - - searchInput(){ - var text = this.search.input.value - localStorage.setItem("lastSearchQuery", text) - text = text.toLowerCase() - - if(text.length === 0){ - this.setSearchTip() - return - } - - var new_results = this.performSearch(text) - - if (new_results.length === 0) { - this.setSearchTip(strings.search.noResults, true) - return - } else if (this.search.tip) { - this.search.tip.remove() - delete this.search.tip - } - - var resultsDiv = this.search.div.querySelector(":scope #song-search-results") - resultsDiv.innerHTML = "" - this.search.results = [] - - var fontSize = parseFloat(getComputedStyle(this.search.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.ctx.save() - - var fragment = document.createDocumentFragment() - new_results.forEach(result => { - var result = this.createSearchResult(result, resultWidth, fontSize) - fragment.appendChild(result) - this.search.results.push(result) - }) - resultsDiv.appendChild(fragment) - - this.ctx.restore() - } - - searchClick(e){ - if((e.target.id === "song-search-container" || e.target.id === "song-search-close") && e.which === 1){ - this.removeSearch(true) - }else if(e.which === 1){ - var songEl = e.target.closest(".song-search-result") - if(songEl){ - var songId = parseInt(songEl.dataset.songId) - this.searchProceed(songId) - } - } - } - - searchProceed(songId){ - var song = this.songs.find(song => song.id === songId) - this.removeSearch() - this.playBgm(false) - - var songIndex = this.songs.findIndex(song => song.id === songId) - this.setSelectedSong(songIndex) - this.toSelectDifficulty() - } onusers(response){ var p2InSong = false @@ -3268,17 +2702,17 @@ class SongSelect{ if(this.state.screen !== "difficulty"){ this.toSelectDifficulty({player: response.value.player}) } - this.searchEnabled = false + this.search.enabled = false p2InSong = true - this.removeSearch() + this.search.remove() } } } }) } - if(!this.searchEnabled && !p2InSong){ - this.searchEnabled = true + if(!this.search.enabled && !p2InSong){ + this.search.enabled = true } } onsongsel(response){ @@ -3404,6 +2838,7 @@ class SongSelect{ this.sessionCache.clean() this.currentSongCache.clean() this.nameplateCache.clean() + this.search.clean() assets.sounds["bgm_songsel"].stop() if(!this.bgmEnabled){ snd.musicGain.fadeIn() @@ -3425,13 +2860,8 @@ class SongSelect{ pageEvents.remove(this.touchFullBtn, "click") delete this.touchFullBtn } - if(this.searchStyle){ - loader.screen.removeChild(this.searchStyle) - } delete this.selectable delete this.ctx delete this.canvas - delete this.searchContainer - delete this.searchStyle } } diff --git a/public/src/js/strings.js b/public/src/js/strings.js index f641f66..9fca720 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -1331,6 +1331,17 @@ var translations = { version: { ja: "Ver. %s", en: "Version %s" + }, + browse: { + ja: "参照する…", + en: "Browse...", + cn: "浏览…", + tw: "開啟檔案…", + ko: "찾아보기…" + }, + noPlugins: { + ja: null, + en: "No .taikoweb.js plugin files have been found in the provided file list." } }, search: { diff --git a/public/src/js/titlescreen.js b/public/src/js/titlescreen.js index 45408d6..a1238d7 100644 --- a/public/src/js/titlescreen.js +++ b/public/src/js/titlescreen.js @@ -8,6 +8,7 @@ class Titlescreen{ if(!songId){ loader.changePage("titlescreen", false) + loader.screen.style.backgroundImage = "" this.titleScreen = document.getElementById("title-screen") this.proceed = document.getElementById("title-proceed") @@ -75,8 +76,9 @@ class Titlescreen{ } pageEvents.remove(p2, "message") if(this.customFolder && !fromP2 && !assets.customSongs){ - var customSongs = new CustomSongs(this.touched, true) + var customSongs = new CustomSongs(this.touched, true, true) var soundPlayed = false + var noError = true var promises = [] var allFiles = [] this.customFolder.forEach(file => { @@ -95,6 +97,13 @@ class Titlescreen{ setTimeout(() => { new SongSelect(false, false, this.touched, this.songId) }, 500) + noError = false + }).then(() => { + if(noError){ + setTimeout(() => { + new SongSelect("customSongs", false, this.touchEnabled) + }, 500) + } }) }else{ setTimeout(() => { diff --git a/public/src/js/view.js b/public/src/js/view.js index 694e767..2005924 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -12,8 +12,9 @@ if(noSmoothing){ this.ctx.imageSmoothingEnabled = false } - if(resolution === "lowest"){ - this.canvas.style.imageRendering = "pixelated" + this.multiplayer = this.controller.multiplayer + if(this.multiplayer !== 2 && resolution === "lowest"){ + document.getElementById("game").classList.add("pixelated") } this.gameDiv = document.getElementById("game") @@ -97,7 +98,6 @@ this.branchCache = new CanvasCache(noSmoothing) this.nameplateCache = new CanvasCache(noSmoothing) - this.multiplayer = this.controller.multiplayer if(this.multiplayer === 2){ this.player = p2.player === 2 ? 1 : 2 }else{ diff --git a/templates/index.html b/templates/index.html index a3ad2f1..5731815 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,6 +7,8 @@ + + @@ -22,7 +24,7 @@