From 27c8526c2ac91e8ee37e6893b5eb35c1f4206d8a Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Fri, 11 Mar 2022 14:20:22 +0300 Subject: [PATCH 01/13] Gpicker API Changes Hopefully that is what is being changed, I do not think there is a way to test this properly until the old API closes down Resources: - https://developers.googleblog.com/2022/03/gis-jsweb-authz-migration.html - https://developers.google.com/drive/api/v3/quickstart/js --- public/src/js/customsongs.js | 6 ++++-- public/src/js/gpicker.js | 35 +++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index 50ddd5e..7a5a155 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -313,8 +313,10 @@ class CustomSongs{ return Promise.reject(e) } }).finally(() => { - var addRemove = !gpicker || !gpicker.oauthToken ? "add" : "remove" - this.linkGdriveAccount.classList[addRemove]("hiddenbtn") + if(this.linkGdriveAccount){ + var addRemove = !gpicker || !gpicker.oauthToken ? "add" : "remove" + this.linkGdriveAccount.classList[addRemove]("hiddenbtn") + } }) } gdriveAccount(event){ diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js index f5fb1ab..c7c0590 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -9,6 +9,7 @@ class Gpicker{ this.scope = "https://www.googleapis.com/auth/drive.readonly" this.folder = "application/vnd.google-apps.folder" this.filesUrl = "https://www.googleapis.com/drive/v3/files/" + this.discoveryDocs = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"] this.resolveQueue = [] this.queueActive = false } @@ -138,7 +139,9 @@ class Gpicker{ if(!this.auth){ return new Promise((resolve, reject) => { gapi.auth2.init({ + apiKey: this.apiKey, clientId: this.oauthClientId, + discoveryDocs: this.discoveryDocs, fetch_basic_profile: false, scope: this.scope }).then(() => { @@ -164,22 +167,30 @@ class Gpicker{ return Promise.resolve() } return this.getAuth(errorCallback).then(auth => { - var user = force || auth.currentUser.get() - if(force || !this.checkScope(user)){ + if(!force && auth.isSignedIn.get() && this.checkScope()){ + return Promise.resolve() + }else{ lockedCallback(false) - return auth.signIn(force ? { - prompt: "select_account" - } : undefined).then(user => { - if(this.checkScope(user)){ - lockedCallback(true) - }else{ - return Promise.reject("cancel") - } - }, () => Promise.reject("cancel")) + return new Promise((resolve, reject) => + auth.signIn({ + prompt: force ? "select_account" : "consent", + scope: this.scope + }).then(resolve, reject) + ) } + }).then(() => { + if(this.checkScope()){ + lockedCallback(true) + }else{ + return Promise.reject("cancel") + } + }, e => { + console.error(e) + Promise.reject("cancel") }) } - checkScope(user){ + checkScope(){ + var user = this.auth.currentUser.get() if(user.hasGrantedScopes(this.scope)){ this.oauthToken = user.getAuthResponse(true).access_token return this.oauthToken From 9c31d5b8a0843e355dfa1f1489462f0648bb9c27 Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Fri, 11 Mar 2022 17:34:00 +0300 Subject: [PATCH 02/13] Use Google 3P authorization --- public/src/js/customsongs.js | 3 ++ public/src/js/gpicker.js | 90 ++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 51 deletions(-) diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index 7a5a155..e9f2d86 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -516,6 +516,9 @@ class CustomSongs{ pageEvents.remove(document, ["dragover", "dragleave", "drop"]) delete this.dropzone } + if(gpicker){ + gpicker.tokenResolve = null + } delete this.browse delete this.linkLocalFolder delete this.linkGdriveFolder diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js index c7c0590..5f6eaa0 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -9,9 +9,9 @@ class Gpicker{ this.scope = "https://www.googleapis.com/auth/drive.readonly" this.folder = "application/vnd.google-apps.folder" this.filesUrl = "https://www.googleapis.com/drive/v3/files/" - this.discoveryDocs = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"] this.resolveQueue = [] this.queueActive = false + this.clientCallbackBind = this.clientCallback.bind(this) } browse(lockedCallback, errorCallback){ return this.loadApi() @@ -124,9 +124,12 @@ class Gpicker{ if(window.gapi && gapi.client && gapi.client.drive){ return Promise.resolve() } - return loader.loadScript("https://apis.google.com/js/api.js") - .then(() => new Promise((resolve, reject) => - gapi.load("auth2:picker:client", { + var promises = [ + loader.loadScript("https://apis.google.com/js/api.js"), + loader.loadScript("https://accounts.google.com/gsi/client") + ] + return Promise.all(promises).then(() => new Promise((resolve, reject) => + gapi.load("picker:client", { callback: resolve, onerror: reject }) @@ -135,68 +138,53 @@ class Gpicker{ gapi.client.load("drive", "v3").then(resolve, reject) )) } - getAuth(errorCallback=()=>{}){ - if(!this.auth){ - return new Promise((resolve, reject) => { - gapi.auth2.init({ - apiKey: this.apiKey, - clientId: this.oauthClientId, - discoveryDocs: this.discoveryDocs, - fetch_basic_profile: false, - scope: this.scope - }).then(() => { - this.auth = gapi.auth2.getAuthInstance() - resolve(this.auth) - }, e => { - if(e.details){ - var errorStr = strings.gpicker.authError.replace("%s", e.details) - if(/cookie/i.test(e.details)){ - errorStr += "\n\n" + strings.gpicker.cookieError - } - errorCallback(errorStr) - } - reject(e) - }) - }) + getClient(errorCallback=()=>{}, force){ + var obj = { + client_id: this.oauthClientId, + scope: this.scope, + callback: this.clientCallbackBind + } + if(force){ + if(!this.clientForce){ + obj.select_account = true + this.clientForce = google.accounts.oauth2.initTokenClient(obj) + } + return this.clientForce }else{ - return Promise.resolve(this.auth) + if(!this.client){ + this.client = google.accounts.oauth2.initTokenClient(obj) + } + return this.client + } + } + clientCallback(tokenResponse){ + this.tokenResponse = tokenResponse + this.oauthToken = tokenResponse.access_token + if(this.oauthToken && this.tokenResolve){ + this.tokenResolve() } } getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){ if(this.oauthToken && !force){ return Promise.resolve() } - return this.getAuth(errorCallback).then(auth => { - if(!force && auth.isSignedIn.get() && this.checkScope()){ - return Promise.resolve() - }else{ - lockedCallback(false) - return new Promise((resolve, reject) => - auth.signIn({ - prompt: force ? "select_account" : "consent", - scope: this.scope - }).then(resolve, reject) - ) - } - }).then(() => { + var client = this.getClient(errorCallback, force) + var promise = new Promise(resolve => { + this.tokenResolve = resolve + }) + lockedCallback(false) + client.requestAccessToken() + return promise.then(() => { + this.tokenResolve = null if(this.checkScope()){ lockedCallback(true) }else{ return Promise.reject("cancel") } - }, e => { - console.error(e) - Promise.reject("cancel") }) } checkScope(){ - var user = this.auth.currentUser.get() - if(user.hasGrantedScopes(this.scope)){ - this.oauthToken = user.getAuthResponse(true).access_token - return this.oauthToken - }else{ - return false - } + return google.accounts.oauth2.hasGrantedAnyScope(this.tokenResponse, this.scope) } switchAccounts(lockedCallback, errorCallback){ return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true)) From 407f1f35cd0c13fd2bbadac328670a36a0627128 Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Fri, 11 Mar 2022 17:45:05 +0300 Subject: [PATCH 03/13] Update privacy --- templates/privacy.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/templates/privacy.txt b/templates/privacy.txt index 0496f2f..9816989 100644 --- a/templates/privacy.txt +++ b/templates/privacy.txt @@ -23,10 +23,7 @@ You can use the Google Drive integration to let Taiko Web make your Taiko chart Applications that integrate with a Google account must declare their intent by requesting permissions. These permissions to your account must be granted in order for Taiko Web to integrate with Google accounts. Below is a list of these permissions and why they are required. At no time will Taiko Web request or have access to your Google account password. -3.1 "Associate you with your personal info on Google" Permission -Required for Google Sign-In to provide Taiko Web with a non-identifiable user authentication token. No other information provided by this permission is used. - -3.2 "See and download all your Google Drive files" Permission +3.1 "See and download all your Google Drive files" Permission When selecting a folder with the Google Drive file picker, Taiko Web instructs your Browser to recursively download all the files of that folder directly into your computer's memory. Limitation of Google Drive's permission model requires us to request access to all your Google Drive files, however, Taiko Web will only access the selected folder and its children, and only when requested. File parsing is handled locally; none of your Google Drive files is ever sent to our servers or third parties. {% endif %}{% if config.email %} {% if integration %}4{% else %}3{% endif %}. Contact Info From 7967ff1b0932089ec37b4ef5fd60553eb7a0fae4 Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Sun, 13 Mar 2022 18:31:42 +0300 Subject: [PATCH 04/13] Fix fixing courses with p1 and p2 notes --- public/src/js/parsetja.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index 5a966d4..9291bb2 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -67,13 +67,14 @@ if((name === "start" || name === "start p1") && !inSong){ inSong = true - if(!hasSong){ + if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){ if(!(courseName in courses)){ courses[courseName] = {} } - for(var name in currentCourse){ - if(name !== "branch"){ - courses[courseName][name] = currentCourse[name] + courses[courseName].startName = name + for(var opt in currentCourse){ + if(opt !== "branch"){ + courses[courseName][opt] = currentCourse[opt] } } courses[courseName].start = lineNum + 1 From c553a15f1e25ec857b891e30f17e60faf7046279 Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Sun, 13 Mar 2022 18:51:17 +0300 Subject: [PATCH 05/13] Fix the previous commit --- public/src/js/parsetja.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/src/js/parsetja.js b/public/src/js/parsetja.js index 9291bb2..ab20e9c 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -68,6 +68,7 @@ inSong = true if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){ + hasSong = false if(!(courseName in courses)){ courses[courseName] = {} } 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 06/13] 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 @@