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 @@
{% if version.version and version.commit_short and version.commit %} - taiko-web ver.{{version.version}} ({{version.commit_short}}) + taiko-web ver.{{version.version}} ({{version.commit_short}}) {% else %} taiko-web (unknown version) {% endif %} From b9d990a64350caddac81573b1eaf62912fb77b9e Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Wed, 16 Mar 2022 19:04:51 +0300 Subject: [PATCH 07/13] Plugins: Fix browse button on iOS --- public/src/css/view.css | 17 +++++++++++++++++ public/src/js/main.js | 2 +- public/src/js/settings.js | 17 ++++++++--------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/public/src/css/view.css b/public/src/css/view.css index 709009f..0a296c8 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -452,3 +452,20 @@ kbd{ #dropzone.dragover{ opacity: 1; } +.plugin-browse-button{ + position: relative; +} +#plugin-browse{ + position: absolute; + font-size: inherit; + top: -0.1em; + left: -0.1em; + right: -0.1em; + bottom: -0.1em; + border-radius: 0.5em; + opacity: 0; + cursor: pointer; +} +#plugin-browse::-webkit-file-upload-button{ + cursor: pointer; +} diff --git a/public/src/js/main.js b/public/src/js/main.js index 702d29c..f12196b 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -97,7 +97,7 @@ var plugins var noResizeRoot = false pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { - if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ + if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT" && (event.target.tagName !== "INPUT" || event.target.type !== "file")){ event.preventDefault() } }) diff --git a/public/src/js/settings.js b/public/src/js/settings.js index f52e817..b171920 100644 --- a/public/src/js/settings.js +++ b/public/src/js/settings.js @@ -384,24 +384,22 @@ class SettingsView{ if(this.customSettings){ var form = document.createElement("form") this.browse = document.createElement("input") - this.browse.id = "browse" + this.browse.id = "plugin-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.browseButton.classList.add("taibtn", "stroke-sub", "plugin-browse-button") + this.browseText = document.createTextNode("") + this.browseButton.appendChild(this.browseText) + this.browseButton.appendChild(form) 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){ @@ -1189,7 +1187,8 @@ class SettingsView{ this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings) this.setAltText(this.endButton, strings.settings.ok) if(this.customSettings){ - this.setAltText(this.browseButton, strings.plugins.browse) + this.browseText.data = strings.plugins.browse + this.browseButton.setAttribute("alt", strings.plugins.browse) }else{ this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name) this.setAltText(this.gamepadEndButton, strings.settings.ok) @@ -1263,9 +1262,9 @@ class SettingsView{ } if(this.customSettings){ pageEvents.remove(this.browse, "change") - this.removeTouch(this.browseButton) delete this.browse delete this.browseButton + delete this.browseText }else{ this.removeTouch(this.gamepadSettings) this.removeTouch(this.gamepadEndButton) From 81464df356c02f71a8fe29ff91642164628a1c75 Mon Sep 17 00:00:00 2001 From: KatieFrogs <23621460+KatieFrogs@users.noreply.github.com> Date: Wed, 16 Mar 2022 19:18:55 +0300 Subject: [PATCH 08/13] Fix browse button becoming too big --- public/src/css/view.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/src/css/view.css b/public/src/css/view.css index 0a296c8..66a90fa 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -454,6 +454,7 @@ kbd{ } .plugin-browse-button{ position: relative; + overflow: hidden; } #plugin-browse{ position: absolute; From bbee87638af16192398e7ed9e94268ac79efc1a8 Mon Sep 17 00:00:00 2001 From: Bui Date: Mon, 21 Mar 2022 04:09:24 +0000 Subject: [PATCH 09/13] add setup script for debian-based systems --- tools/setup.sh | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tools/setup.sh diff --git a/tools/setup.sh b/tools/setup.sh new file mode 100644 index 0000000..f5bc3ad --- /dev/null +++ b/tools/setup.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail + +sudo apt update +sudo apt upgrade -y + +sudo apt install -y unzip git python3-pip python3-virtualenv nginx ffmpeg redis supervisor + +if [[ -r /etc/os-release ]]; then + . /etc/os-release + if [[ $ID = ubuntu ]]; then + REPO="https://repo.mongodb.org/apt/ubuntu $VERSION_CODENAME/mongodb-org/5.0 multiverse" + elif [[ $ID = debian ]]; then + if [[ $VERSION_CODENAME = bullseye ]]; then + VERSION_CODENAME=buster # MongoDB does not provide packages for Debian 11 yet + fi + REPO="https://repo.mongodb.org/apt/debian $VERSION_CODENAME/mongodb-org/5.0 main" + else + echo "Unsupported distribution $ID" + exit 1 + fi +else + echo "Not running a distribution with /etc/os-release available" + exit 1 +fi + +wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add - +echo "deb [ arch=amd64,arm64 ] $REPO" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list + +sudo apt update +sudo apt install -y mongodb-org + +sudo mkdir -p /srv/taiko-web +sudo chown $USER /srv/taiko-web +git clone https://github.com/bui/taiko-web.git /srv/taiko-web + +cd /srv/taiko-web +tools/get_version.sh +cp tools/hooks/* .git/hooks/ +cp config.example.py config.py +sudo cp tools/nginx.conf /etc/nginx/conf.d/taiko-web.conf + +sudo sed -i 's/^\(\s\{0,\}\)\(include \/etc\/nginx\/sites-enabled\/\*;\)$/\1#\2/g' /etc/nginx/nginx.conf +sudo sed -i 's/}/ application\/wasm wasm;\n}/g' /etc/nginx/mime.types +sudo nginx -s reload + +virtualenv -p python3 .venv +.venv/bin/pip install -r requirements.txt + +sudo mkdir -p /var/log/taiko-web +sudo cp tools/supervisor.conf /etc/supervisor/conf.d/taiko-web.conf +sudo service supervisor restart + +sudo systemctl enable mongod.service +sudo service mongod start + +IP="$(dig +short txt ch whoami.cloudflare @1.0.0.1)" +echo +echo "Setup complete! You should be able to access your taiko-web instance at http://$IP" +echo From 41e11992c9873100a53e3ad69c6d42729aaff249 Mon Sep 17 00:00:00 2001 From: Bui Date: Mon, 21 Mar 2022 04:30:03 +0000 Subject: [PATCH 10/13] fix for ubuntu --- tools/setup.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/setup.sh b/tools/setup.sh index f5bc3ad..b227f5f 100644 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -4,11 +4,14 @@ set -euo pipefail sudo apt update sudo apt upgrade -y -sudo apt install -y unzip git python3-pip python3-virtualenv nginx ffmpeg redis supervisor +sudo apt install -y git python3-pip python3-virtualenv nginx ffmpeg redis supervisor if [[ -r /etc/os-release ]]; then . /etc/os-release if [[ $ID = ubuntu ]]; then + if [[ $VERSION_CODENAME = impish ]]; then + VERSION_CODENAME=focal # MongoDB does not provide packages for Ubuntu 21.10 + fi REPO="https://repo.mongodb.org/apt/ubuntu $VERSION_CODENAME/mongodb-org/5.0 multiverse" elif [[ $ID = debian ]]; then if [[ $VERSION_CODENAME = bullseye ]]; then From b0c35acf3fb83b1fc0c123f4cb347c9c8efba037 Mon Sep 17 00:00:00 2001 From: Bui Date: Mon, 21 Mar 2022 04:33:01 +0000 Subject: [PATCH 11/13] remove apt upgrade and quotes from IP --- tools/setup.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/setup.sh b/tools/setup.sh index b227f5f..44fc93d 100644 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -2,8 +2,6 @@ set -euo pipefail sudo apt update -sudo apt upgrade -y - sudo apt install -y git python3-pip python3-virtualenv nginx ffmpeg redis supervisor if [[ -r /etc/os-release ]]; then @@ -57,7 +55,7 @@ sudo service supervisor restart sudo systemctl enable mongod.service sudo service mongod start -IP="$(dig +short txt ch whoami.cloudflare @1.0.0.1)" +IP=$(dig +short txt ch whoami.cloudflare @1.0.0.1) echo echo "Setup complete! You should be able to access your taiko-web instance at http://$IP" echo From 756968551221677e8bb0d7268210194e0f4259c3 Mon Sep 17 00:00:00 2001 From: Bui Date: Mon, 21 Mar 2022 04:41:04 +0000 Subject: [PATCH 12/13] bash fix --- tools/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/setup.sh b/tools/setup.sh index 44fc93d..ae88dc8 100644 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -55,7 +55,7 @@ sudo service supervisor restart sudo systemctl enable mongod.service sudo service mongod start -IP=$(dig +short txt ch whoami.cloudflare @1.0.0.1) +IP=$(dig +short txt ch whoami.cloudflare @1.0.0.1 | tr -d '"') echo echo "Setup complete! You should be able to access your taiko-web instance at http://$IP" echo From 79ba7eb4a1e01f812aa4d18f63990b490b26fc91 Mon Sep 17 00:00:00 2001 From: Bui Date: Mon, 21 Mar 2022 04:54:17 +0000 Subject: [PATCH 13/13] venv fix for debian? --- tools/setup.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/setup.sh b/tools/setup.sh index ae88dc8..022de21 100644 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -2,7 +2,7 @@ set -euo pipefail sudo apt update -sudo apt install -y git python3-pip python3-virtualenv nginx ffmpeg redis supervisor +sudo apt install -y git python3-pip python3-virtualenv python3-venv nginx ffmpeg redis supervisor if [[ -r /etc/os-release ]]; then . /etc/os-release @@ -45,7 +45,8 @@ sudo sed -i 's/^\(\s\{0,\}\)\(include \/etc\/nginx\/sites-enabled\/\*;\)$/\1#\2/ sudo sed -i 's/}/ application\/wasm wasm;\n}/g' /etc/nginx/mime.types sudo nginx -s reload -virtualenv -p python3 .venv +python3 -m venv .venv +.venv/bin/pip install --upgrade pip wheel setuptools .venv/bin/pip install -r requirements.txt sudo mkdir -p /var/log/taiko-web