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/css/view.css b/public/src/css/view.css index 709009f..66a90fa 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -452,3 +452,21 @@ kbd{ #dropzone.dragover{ opacity: 1; } +.plugin-browse-button{ + position: relative; + overflow: hidden; +} +#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/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 50ddd5e..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,13 +311,15 @@ 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) } }).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){ @@ -369,7 +374,7 @@ class CustomSongs{ open("privacy") } loading(show){ - if(this.noPage){ + if(this.noLoading){ return } if(show){ @@ -385,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){ @@ -472,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" @@ -514,6 +525,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 f5fb1ab..f46f457 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -11,6 +11,7 @@ class Gpicker{ this.filesUrl = "https://www.googleapis.com/drive/v3/files/" this.resolveQueue = [] this.queueActive = false + this.clientCallbackBind = this.clientCallback.bind(this) } browse(lockedCallback, errorCallback){ return this.loadApi() @@ -123,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 }) @@ -134,58 +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({ - clientId: this.oauthClientId, - 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 && 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 => { - var user = force || auth.currentUser.get() - if(force || !this.checkScope(user)){ - 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")) + 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") } }) } - checkScope(user){ - if(user.hasGrantedScopes(this.scope)){ - this.oauthToken = user.getAuthResponse(true).access_token - return this.oauthToken - }else{ - return false - } + checkScope(){ + return google.accounts.oauth2.hasGrantedAnyScope(this.tokenResponse, this.scope) } switchAccounts(lockedCallback, errorCallback){ return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true)) @@ -221,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 => { @@ -239,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/main.js b/public/src/js/main.js index 0570845..e5602fc 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -104,7 +104,7 @@ var kanaPairs = [["っきゃ","ッキャ"],["っきゅ","ッキュ"],["っきょ ["ば","バ"],["び","ビ"],["ぶ","ブ"],["べ","ベ"],["ぼ","ボ"],["ぱ","パ"],["ぴ","パ"],["ぷ","プ"],["ぺ","ペ"],["ぽ","ポ"],["ゔ","ヴ"]] 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/parsetja.js b/public/src/js/parsetja.js index 5a966d4..ab20e9c 100644 --- a/public/src/js/parsetja.js +++ b/public/src/js/parsetja.js @@ -67,13 +67,15 @@ if((name === "start" || name === "start p1") && !inSong){ inSong = true - if(!hasSong){ + if(!hasSong || name === "start" && courses[courseName] && courses[courseName].startName !== "start"){ + hasSong = false 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 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..b171920 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,46 @@ 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 = "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) + this.browseButton = document.createElement("div") + 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.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 +639,9 @@ class SettingsView{ valueDiv.innerText = value } setValue(name){ + if(this.locked){ + return + } var promise var current = this.settingsItems[name] if(current.getItem){ @@ -674,6 +710,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 +732,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 +744,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 +1071,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 +1092,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 +1134,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 +1186,19 @@ 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.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) 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 +1236,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 +1260,12 @@ class SettingsView{ if(this.defaultButton){ delete this.defaultButton } - if(!this.customSettings){ + if(this.customSettings){ + pageEvents.remove(this.browse, "change") + delete this.browse + delete this.browseButton + delete this.browseText + }else{ this.removeTouch(this.gamepadSettings) this.removeTouch(this.gamepadEndButton) this.removeTouch(this.gamepadBox) @@ -1204,8 +1293,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 f80d9cd..cd7a5db 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,531 +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 - } - } - - normalizeSearch(string){ - string = string - .replace('’', '\'').replace('“', '"').replace('”', '"') - - kanaPairs.forEach(pair => { - string = string.replace(pair[1], pair[0]) - }) - - return string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "") - } - - 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 = this.normalizeSearch(editedSplit.join(" ").trim()) - - 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 @@ -3279,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){ @@ -3415,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() @@ -3436,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 %} 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 diff --git a/tools/setup.sh b/tools/setup.sh new file mode 100644 index 0000000..022de21 --- /dev/null +++ b/tools/setup.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -euo pipefail + +sudo apt update +sudo apt install -y git python3-pip python3-virtualenv python3-venv 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 + 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 + +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 +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 | tr -d '"') +echo +echo "Setup complete! You should be able to access your taiko-web instance at http://$IP" +echo