From 3fea149353793d772e3914eba86cc03394b4a911 Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Thu, 29 Oct 2020 08:07:56 +0300 Subject: [PATCH 1/7] ImportSongs: Add Google Drive support - Adds a new page for importing custom songs, where it is possible to pick a local folder (desktop only) or a Google Drive folder (desktop and Android) - This feature is disabled on iOS due to the lack of OGG audio support in the browser - In order to not get rate limited, a TJA file is parsed for metadata only when the song is clicked in the song selection, rather than all at once at import time - The instance maintainer will need to provide the API credentials in the config.py file to enable this feature - This requires a new project to be created at console.cloud.google.com - Drive API will have to be enabled - API and OAuth keys should be created - API key can be restricted to only have Google Drive and Google Picker APIs - OAuth Client ID should have Web Application type and JavaScript origins set - Editing the OAuth consent screen to have a name and icon is recommended - It is semi-required to submit the consent screen for verification as the permission to download all of the Drive files will be asked. - Note that the email of the maintainer is publicly visible on the consent screen - The project number can be found in the IAM & Admin settings page --- app.py | 3 +- config.example.py | 8 + public/src/css/loadsong.css | 1 + public/src/js/abstractfile.js | 87 +++++++++ public/src/js/assets.js | 7 +- public/src/js/controller.js | 16 +- public/src/js/customsongs.js | 216 ++++++++++++++++++++++ public/src/js/gpicker.js | 185 +++++++++++++++++++ public/src/js/importsongs.js | 288 ++++++++++++++---------------- public/src/js/loader.js | 128 +++++++------ public/src/js/loadsong.js | 110 ++++-------- public/src/js/main.js | 1 + public/src/js/songselect.js | 207 +++++++++++++-------- public/src/js/soundbuffer.js | 25 +-- public/src/js/strings.js | 51 ++++-- public/src/views/customsongs.html | 12 ++ public/src/views/songselect.html | 1 - 17 files changed, 940 insertions(+), 406 deletions(-) create mode 100644 public/src/js/abstractfile.js create mode 100644 public/src/js/customsongs.js create mode 100644 public/src/js/gpicker.js create mode 100644 public/src/views/customsongs.html diff --git a/app.py b/app.py index 7eb8b55..b772539 100644 --- a/app.py +++ b/app.py @@ -119,7 +119,8 @@ def get_config(): 'assets_baseurl': config.ASSETS_BASEURL, 'email': config.EMAIL, 'accounts': config.ACCOUNTS, - 'custom_js': config.CUSTOM_JS + 'custom_js': config.CUSTOM_JS, + 'google_credentials': config.GOOGLE_CREDENTIALS } if not config_out.get('songs_baseurl'): diff --git a/config.example.py b/config.example.py index 478aa77..b6c3aa8 100644 --- a/config.example.py +++ b/config.example.py @@ -33,3 +33,11 @@ SECRET_KEY = 'change-me' # Git repository base URL. URL = 'https://github.com/bui/taiko-web/' + +# Google Drive API. +GOOGLE_CREDENTIALS = { + 'gdrive_enabled': False, + 'api_key': '', + 'oauth_client_id': '', + 'project_number': '' +} diff --git a/public/src/css/loadsong.css b/public/src/css/loadsong.css index 1a18c5a..05ee398 100644 --- a/public/src/css/loadsong.css +++ b/public/src/css/loadsong.css @@ -19,6 +19,7 @@ border-radius: 5px; border: 3px solid white; color: #fff; + z-index: 1; } #loading-don{ width: 10vw; diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js new file mode 100644 index 0000000..00f5f07 --- /dev/null +++ b/public/src/js/abstractfile.js @@ -0,0 +1,87 @@ +class RemoteFile{ + constructor(url){ + this.url = url + try{ + this.path = new URL(url).pathname + }catch(e){ + this.path = url + } + if(this.path.startsWith("/")){ + this.path = this.path.slice(1) + } + this.name = this.path + var index = this.name.lastIndexOf("/") + if(index !== -1){ + this.name = this.name.slice(index + 1) + } + } + arrayBuffer(){ + return loader.ajax(this.url, request => { + request.responseType = "arraybuffer" + }) + } + read(encoding){ + if(encoding){ + return this.arrayBuffer().then(response => + new TextDecoder(encoding).decode(response) + ) + }else{ + return loader.ajax(this.url) + } + } +} +class LocalFile{ + constructor(file){ + this.file = file + this.path = file.webkitRelativePath + this.url = this.path + this.name = file.name + } + arrayBuffer(){ + var reader = new FileReader() + var promise = pageEvents.load(reader).then(event => event.target.result) + reader.readAsArrayBuffer(this.file) + return promise + } + read(encoding){ + var reader = new FileReader() + var promise = pageEvents.load(reader).then(event => event.target.result) + reader.readAsText(this.file, encoding) + return promise + } +} +class GdriveFile{ + constructor(fileObj){ + this.path = fileObj.path + this.name = fileObj.name + this.id = fileObj.id + this.url = gpicker.filesUrl + this.id + "?alt=media" + } + arrayBuffer(){ + return gpicker.downloadFile(this.id, true) + } + read(encoding){ + if(encoding){ + return this.arrayBuffer().then(response => + new TextDecoder(encoding).decode(response) + ) + }else{ + return gpicker.downloadFile(this.id) + } + } +} +class CachedFile{ + constructor(contents, oldFile){ + this.contents = contents + this.oldFile = oldFile + this.path = oldFile.path + this.name = oldFile.name + this.url = oldFile.url + } + arrayBuffer(){ + return Promise.resolve(this.contents) + } + read(encoding){ + return this.arrayBuffer() + } +} diff --git a/public/src/js/assets.js b/public/src/js/assets.js index dd9830c..9a0ef01 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -33,7 +33,9 @@ var assets = { "settings.js", "scorestorage.js", "account.js", - "lyrics.js" + "lyrics.js", + "customsongs.js", + "abstractfile.js" ], "css": [ "main.css", @@ -144,7 +146,8 @@ var assets = { "session.html", "settings.html", "account.html", - "login.html" + "login.html", + "customsongs.html" ], "songs": [], diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 3740bf2..978b316 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -246,23 +246,15 @@ class Controller{ var songObj = assets.songs.find(song => song.id === this.selectedSong.folder) var promises = [] if(songObj.chart && songObj.chart !== "blank"){ - var reader = new FileReader() - promises.push(pageEvents.load(reader).then(event => { - this.songData = event.target.result.replace(/\0/g, "").split("\n") + promises.push(songObj.chart.read(this.selectedSong.type === "tja" ? "sjis" : undefined).then(data => { + this.songData = data.replace(/\0/g, "").split("\n") return Promise.resolve() })) - if(this.selectedSong.type === "tja"){ - reader.readAsText(songObj.chart, "sjis") - }else{ - reader.readAsText(songObj.chart) - } } if(songObj.lyricsFile){ - var reader = new FileReader() - promises.push(pageEvents.load(reader).then(event => { + promises.push(songObj.lyricsFile.read().then(event => { songObj.lyricsData = event.target.result - }, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath) - reader.readAsText(songObj.lyricsFile) + }, () => Promise.resolve()), songObj.lyricsFile.path) } Promise.all(promises).then(resolve) } diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js new file mode 100644 index 0000000..b95c7d7 --- /dev/null +++ b/public/src/js/customsongs.js @@ -0,0 +1,216 @@ +class CustomSongs{ + constructor(touchEnabled){ + this.touchEnabled = touchEnabled + loader.changePage("customsongs", true) + if(touchEnabled){ + this.getElement("view-outer").classList.add("touch-enabled") + } + this.locked = false + + var tutorialTitle = this.getElement("view-title") + this.setAltText(tutorialTitle, strings.customSongs.title) + + var tutorialContent = this.getElement("view-content") + strings.customSongs.description.forEach(string => { + tutorialContent.appendChild(document.createTextNode(string)) + tutorialContent.appendChild(document.createElement("br")) + }) + + this.items = [] + this.linkLocalFolder = document.getElementById("link-localfolder") + this.hasLocal = "webkitdirectory" in HTMLInputElement.prototype && !(/Android/.test(navigator.userAgent)) + if(this.hasLocal){ + this.browse = document.getElementById("browse") + pageEvents.add(this.browse, "change", this.browseChange.bind(this)) + this.setAltText(this.linkLocalFolder, strings.customSongs.localFolder) + pageEvents.add(this.linkLocalFolder, ["mousedown", "touchstart"], this.localFolder.bind(this)) + this.items.push(this.linkLocalFolder) + }else{ + this.linkLocalFolder.parentNode.removeChild(this.linkLocalFolder) + } + + this.linkGdriveFolder = document.getElementById("link-gdrivefolder") + if(gameConfig.google_credentials.gdrive_enabled){ + this.setAltText(this.linkGdriveFolder, strings.customSongs.gdriveFolder) + pageEvents.add(this.linkGdriveFolder, ["mousedown", "touchstart"], this.gdriveFolder.bind(this)) + this.items.push(this.linkGdriveFolder) + }else{ + this.linkGdriveFolder.parentNode.removeChild(this.linkGdriveFolder) + } + + this.endButton = this.getElement("view-end-button") + this.setAltText(this.endButton, strings.session.cancel) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + this.items.push(this.endButton) + this.selected = this.items.length - 1 + + this.loaderDiv = document.createElement("div") + this.loaderDiv.innerHTML = assets.pages["loadsong"] + var loadingText = this.loaderDiv.querySelector("#loading-text") + loadingText.appendChild(document.createTextNode(strings.loading)) + loadingText.setAttribute("alt", strings.loading) + + this.keyboard = new Keyboard({ + confirm: ["enter", "space", "don_l", "don_r"], + previous: ["left", "up", "ka_l"], + next: ["right", "down", "ka_r"], + back: ["escape"] + }, this.keyPressed.bind(this)) + this.gamepad = new Gamepad({ + confirmPad: ["b", "ls", "rs"], + previous: ["u", "l", "lb", "lt", "lsu", "lsl"], + next: ["d", "r", "rb", "rt", "lsd", "lsr"], + back: ["start", "a"] + }, this.keyPressed.bind(this)) + + pageEvents.send("custom-songs") + } + getElement(name){ + return loader.screen.getElementsByClassName(name)[0] + } + setAltText(element, text){ + element.innerText = text + element.setAttribute("alt", text) + } + localFolder(){ + if(this.locked){ + return + } + this.browse.click() + } + browseChange(event){ + var files = [] + for(var i = 0; i < event.target.files.length; i++){ + files.push(new LocalFile(event.target.files[i])) + } + if(!files.length){ + return + } + this.locked = true + this.loading(true) + + var importSongs = new ImportSongs() + importSongs.load(files).then(this.songsLoaded.bind(this), e => { + this.browse.parentNode.reset() + this.locked = false + this.loading(false) + if(e !== "cancel"){ + return Promise.reject(e) + } + }) + } + gdriveFolder(){ + if(this.locked){ + return + } + this.locked = true + this.loading(true) + var importSongs = new ImportSongs(true) + if(!gpicker){ + var gpickerPromise = loader.loadScript("/src/js/gpicker.js").then(() => { + gpicker = new Gpicker() + }) + }else{ + var gpickerPromise = Promise.resolve() + } + gpickerPromise.then(() => { + return gpicker.browse(locked => { + this.locked = locked + this.loading(locked) + }) + }).then(files => importSongs.load(files)) + .then(this.songsLoaded.bind(this)) + .catch(e => { + this.locked = false + this.loading(false) + if(e !== "cancel"){ + return Promise.reject(e) + } + }) + } + loading(show){ + if(show){ + loader.screen.appendChild(this.loaderDiv) + }else{ + loader.screen.removeChild(this.loaderDiv) + } + } + songsLoaded(songs){ + var length = songs.length + assets.songs = songs + assets.customSongs = true + assets.customSelected = 0 + assets.sounds["se_don"].play() + this.clean() + setTimeout(() => { + new SongSelect("customSongs", false, this.touchEnabled) + pageEvents.send("import-songs", length) + }, 500) + } + keyPressed(pressed, name){ + if(!pressed || this.locked){ + return + } + var selected = this.items[this.selected] + if(name === "confirm" || name === "confirmPad"){ + if(selected === this.endButton){ + this.onEnd() + }else if(name !== "confirmPad"){ + if(selected === this.linkLocalFolder){ + assets.sounds["se_don"].play() + this.localFolder() + }else if(selected === this.linkGdriveFolder){ + assets.sounds["se_don"].play() + this.gdriveFolder() + } + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + }else if(name === "back"){ + this.onEnd() + } + } + mod(length, index){ + return ((index % length) + length) % length + } + onEnd(event){ + if(this.locked){ + return + } + var touched = false + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + touched = true + }else if(event.which !== 1){ + return + } + } + this.clean() + assets.sounds["se_don"].play() + setTimeout(() => { + new SongSelect("customSongs", false, touched) + }, 500) + } + clean(){ + this.keyboard.clean() + this.gamepad.clean() + pageEvents.remove(this.browse, "change") + if(this.hasLocal){ + pageEvents.remove(this.linkLocalFolder, ["mousedown", "touchstart"]) + } + if(gameConfig.google_credentials.gdrive_enabled){ + pageEvents.remove(this.linkGdriveFolder, ["mousedown", "touchstart"]) + } + pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + delete this.browse + delete this.linkLocalFolder + delete this.linkGdriveFolder + delete this.endButton + delete this.items + delete this.loaderDiv + } +} diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js new file mode 100644 index 0000000..0707b6c --- /dev/null +++ b/public/src/js/gpicker.js @@ -0,0 +1,185 @@ +class Gpicker{ + constructor(){ + this.apiKey = gameConfig.google_credentials.api_key + this.oauthClientId = gameConfig.google_credentials.oauth_client_id + this.projectNumber = gameConfig.google_credentials.project_number + 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.resolveQueue = [] + this.queueActive = false + } + browse(lockedCallback){ + return this.loadApi() + .then(() => this.getToken(lockedCallback)) + .then(() => new Promise((resolve, reject) => { + this.displayPicker(data => { + if(data.action === "picked"){ + var file = data.docs[0] + var walk = (files, output=[]) => { + var batch = null + for(var i = 0; i < files.length; i++){ + var path = files[i].path ? files[i].path + "/" : "" + var list = files[i].list + for(var j = 0; j < list.length; j++){ + var file = list[j] + if(file.mimeType === this.folder){ + if(!batch){ + batch = gapi.client.newBatch() + } + batch.add(gapi.client.drive.files.list({ + q: "'" + file.id + "' in parents", + orderBy: "name_natural" + }), { + id: path + file.name + }) + }else{ + output.push(new GdriveFile({ + path: path + file.name, + name: file.name, + id: file.id + })) + } + } + } + if(batch){ + return this.queue() + .then(() => batch.then(responses => { + var files = [] + for(var path in responses.result){ + files.push({path: path, list: responses.result[path].result.files}) + } + return walk(files, output) + })) + }else{ + return output + } + } + if(file.mimeType === this.folder){ + return walk([{list: [file]}]).then(resolve, reject) + }else{ + return reject("cancel") + } + }else if(data.action === "cancel"){ + return reject("cancel") + } + }) + })) + } + loadApi(){ + if(window.gapi && gapi.client && gapi.client.drive){ + return + } + return loader.loadScript("https://apis.google.com/js/api.js") + .then(() => new Promise((resolve, reject) => + gapi.load("auth2:picker:client", { + callback: resolve, + onerror: reject + }) + )) + .then(() => new Promise((resolve, reject) => + gapi.client.load("drive", "v3").then(resolve, reject) + )) + } + getToken(lockedCallback){ + if(this.oauthToken){ + return + } + if(!this.auth){ + var authPromise = gapi.auth2.init({ + clientId: this.oauthClientId, + fetch_basic_profile: false, + scope: this.scope + }).then(() => { + this.auth = gapi.auth2.getAuthInstance() + }, e => { + if(e.details){ + alert(strings.gpicker.authError.replace("%s", e.details)) + } + return Promise.reject(e) + }) + }else{ + var authPromise = Promise.resolve() + } + return authPromise.then(() => { + var user = this.auth.currentUser.get() + if(!this.checkScope(user)){ + lockedCallback(false) + this.auth.signIn().then(user => { + if(this.checkScope(user)){ + 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 + } + } + displayPicker(callback){ + var picker = gapi.picker.api + new picker.PickerBuilder() + .setDeveloperKey(this.apiKey) + .setAppId(this.projectNumber) + .setOAuthToken(this.oauthToken) + .hideTitleBar() + .addView(new picker.DocsView("folders") + .setLabel(strings.gpicker.myDrive) + .setParent("root") + .setSelectFolderEnabled(true) + .setMode("grid") + ) + .addView(new picker.DocsView("folders") + .setLabel(strings.gpicker.starred) + .setStarred(true) + .setSelectFolderEnabled(true) + .setMode("grid") + ) + .addView(new picker.DocsView("folders") + .setLabel(strings.gpicker.sharedWithMe) + .setOwnedByMe(false) + .setSelectFolderEnabled(true) + .setMode("list") + ) + .setCallback(callback) + .setSize(Infinity, Infinity) + .build() + .setVisible(true) + } + downloadFile(id, arrayBuffer){ + return this.queue().then(() => + loader.ajax(this.filesUrl + id + "?alt=media", request => { + if(arrayBuffer){ + request.responseType = "arraybuffer" + } + request.setRequestHeader("Authorization", "Bearer " + this.oauthToken) + }) + ) + } + queue(){ + return new Promise(resolve => { + this.resolveQueue.push(resolve) + if(!this.queueActive){ + this.queueActive = true + this.queueTimer = setInterval(this.parseQueue.bind(this), 100) + this.parseQueue() + } + }) + } + parseQueue(){ + if(this.resolveQueue.length){ + var resolve = this.resolveQueue.shift() + resolve() + }else{ + this.queueActive = false + clearInterval(this.queueTimer) + } + } +} diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index 1687e0b..f076032 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -1,32 +1,10 @@ class ImportSongs{ - constructor(songSelect, event){ - this.songSelect = songSelect - this.songSelect.redrawRunning = false - this.songSelect.pointer(false) - - this.loaderDiv = document.createElement("div") - this.loaderDiv.innerHTML = assets.pages["loadsong"] - loader.screen.appendChild(this.loaderDiv) - var loadingText = document.getElementById("loading-text") - loadingText.appendChild(document.createTextNode(strings.loading)) - loadingText.setAttribute("alt", strings.loading) - - var files = [] - for(var i = 0; i < event.target.files.length; i++){ - files.push(event.target.files[i]) - } - var extensionRegex = /\.[^\/]+$/ - files.sort((a, b) => { - var path1 = a.webkitRelativePath.replace(extensionRegex, "") - var path2 = b.webkitRelativePath.replace(extensionRegex, "") - return path1 > path2 ? 1 : -1 - }) - + constructor(limited, otherFiles){ + this.limited = limited this.tjaFiles = [] this.osuFiles = [] this.assetFiles = {} - var metaFiles = [] - this.otherFiles = {} + this.otherFiles = otherFiles || {} this.songs = [] this.stylesheet = [] this.songTitle = {} @@ -38,7 +16,6 @@ "oni": 3, "ura": 4 } - this.categoryAliases = {} assets.categories.forEach(cat => { this.categoryAliases[cat.title.toLowerCase()] = cat.id @@ -53,7 +30,6 @@ } } }) - this.assetSelectors = { "bg-pattern-1": ".pattern-bg", "bg_genre_0": "#song-select", @@ -66,11 +42,20 @@ "bg_stage_2": ".song-stage-2", "bg_stage_3": ".song-stage-3" } + } + load(files){ + var extensionRegex = /\.[^\/]+$/ + files.sort((a, b) => { + var path1 = a.path.replace(extensionRegex, "") + var path2 = b.path.replace(extensionRegex, "") + return path1 > path2 ? 1 : -1 + }) + var metaFiles = [] for(var i = 0; i < files.length; i++){ var file = files[i] var name = file.name.toLowerCase() - var path = file.webkitRelativePath.toLowerCase() + var path = file.path.toLowerCase() if(name.endsWith(".tja")){ this.tjaFiles.push({ file: file, @@ -81,13 +66,13 @@ file: file, index: i }) - }else if(name === "genre.ini" || name === "box.def" || name === "songtitle.txt"){ - var level = (file.webkitRelativePath.match(/\//g) || []).length + }else if(!this.limited && (name === "genre.ini" || name === "box.def" || name === "songtitle.txt")){ + var level = (file.path.match(/\//g) || []).length metaFiles.push({ file: file, level: (level * 2) + (name === "genre.ini" ? 1 : 0) }) - }else if(path.indexOf("/taiko-web assets/") !== -1){ + }else if(!this.limited && path.indexOf("/taiko-web assets/") !== -1){ if(!(name in this.assetFiles)){ this.assetFiles[name] = file } @@ -97,32 +82,30 @@ } var metaPromises = [] - metaFiles.forEach(fileObj => { metaPromises.push(this.addMeta(fileObj)) }) - Promise.all(metaPromises).then(() => { + return Promise.all(metaPromises).then(() => { var songPromises = [] this.tjaFiles.forEach(fileObj => { - songPromises.push(this.addTja(fileObj)) + songPromises.push(this.addTja(fileObj).catch(e => console.warn(e))) }) this.osuFiles.forEach(fileObj => { - songPromises.push(this.addOsu(fileObj)) + songPromises.push(this.addOsu(fileObj).catch(e => console.warn(e))) }) songPromises.push(this.addAssets()) - Promise.all(songPromises).then(this.loaded.bind(this)) - }) + return Promise.all(songPromises) + }).then(this.loaded.bind(this)) } addMeta(fileObj){ var file = fileObj.file var level = fileObj.level var name = file.name.toLowerCase() - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => { - var data = event.target.result.replace(/\0/g, "").split("\n") + return file.read(name === "songtitle.txt" ? undefined : "sjis").then(data => { + var data = data.replace(/\0/g, "").split("\n") var category if(name === "genre.ini"){ var key @@ -177,9 +160,9 @@ } } if(category){ - var metaPath = file.webkitRelativePath.toLowerCase().slice(0, file.name.length * -1) + var metaPath = file.path.toLowerCase().slice(0, file.name.length * -1) var filesLoop = fileObj => { - var tjaPath = fileObj.file.webkitRelativePath.toLowerCase().slice(0, fileObj.file.name.length * -1) + var tjaPath = fileObj.file.path.toLowerCase().slice(0, fileObj.file.name.length * -1) if(tjaPath.startsWith(metaPath) && (!("categoryLevel" in fileObj) || fileObj.categoryLevel < level)){ if(category.toLowerCase() in this.categoryAliases){ fileObj.category_id = this.categoryAliases[category.toLowerCase()] @@ -192,13 +175,9 @@ this.tjaFiles.forEach(filesLoop) this.osuFiles.forEach(filesLoop) } - }).catch(() => {}) - if(name === "songtitle.txt"){ - reader.readAsText(file) - }else{ - reader.readAsText(file, "sjis") - } - return promise + }).catch(e => { + console.warn(e) + }) } addTja(fileObj){ @@ -206,24 +185,33 @@ var index = fileObj.index var category = fileObj.category var category_id = fileObj.category_id - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => { - var data = event.target.result.replace(/\0/g, "").split("\n") + if(!this.limited){ + var filePromise = file.read("sjis") + }else{ + var filePromise = Promise.resolve() + } + return filePromise.then(dataRaw => { + var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] var tja = new ParseTja(data, "oni", 0, 0, true) var songObj = { id: index + 1, order: index + 1, + title: file.name.slice(0, file.name.lastIndexOf(".")), type: "tja", chart: file, courses: {}, - music: "muted" + music: "muted", + custom: true + } + if(this.limited){ + songObj.unloaded = true } var coursesAdded = false var titleLang = {} var titleLangAdded = false var subtitleLangAdded = false var subtitleLang = {} - var dir = file.webkitRelativePath.toLowerCase() + var dir = file.path.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) for(var diff in tja.metadata){ var meta = tja.metadata[diff] @@ -321,19 +309,19 @@ songObj.category_id = category_id } } - if(coursesAdded){ + if(coursesAdded || songObj.unloaded){ this.songs[index] = songObj } - var hash = md5.base64(event.target.result).slice(0, -2) - songObj.hash = hash - scoreStorage.songTitles[songObj.title] = hash - var score = scoreStorage.get(hash, false, true) - if(score){ - score.title = songObj.title + if(!this.limited){ + var hash = md5.base64(dataRaw).slice(0, -2) + songObj.hash = hash + scoreStorage.songTitles[songObj.title] = hash + var score = scoreStorage.get(hash, false, true) + if(score){ + score.title = songObj.title + } } - }).catch(() => {}) - reader.readAsText(file, "sjis") - return promise + }) } addOsu(fileObj){ @@ -341,11 +329,15 @@ var index = fileObj.index var category = fileObj.category var category_id = fileObj.category_id - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => { - var data = event.target.result.replace(/\0/g, "").split("\n") - var osu = new ParseOsu(data, "oni", 0, 0, true); - var dir = file.webkitRelativePath.toLowerCase() + if(!this.limited){ + var filePromise = file.read() + }else{ + var filePromise = Promise.resolve() + } + return filePromise.then(dataRaw => { + var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] + var osu = new ParseOsu(data, "oni", 0, 0, true) + var dir = file.path.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) var songObj = { id: index + 1, @@ -356,14 +348,17 @@ subtitle_lang: { en: osu.metadata.Artist || osu.metadata.ArtistUnicode }, - preview: osu.generalInfo.PreviewTime / 1000, - courses: { - oni:{ - stars: parseInt(osu.difficulty.overallDifficulty) || 0, - branch: false - } - }, - music: this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] || "muted" + preview: osu.generalInfo.PreviewTime ? osu.generalInfo.PreviewTime / 1000 : 0, + courses: {}, + music: (osu.generalInfo.AudioFilename ? this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] : "") || "muted" + } + if(!this.limited){ + songObj.courses.oni = { + stars: parseInt(osu.difficulty.overallDifficulty) || 0, + branch: false + } + }else{ + songObj.unloaded = true } var filename = file.name.slice(0, file.name.lastIndexOf(".")) var title = osu.metadata.TitleUnicode || osu.metadata.Title || file.name.slice(0, file.name.lastIndexOf(".")) @@ -375,7 +370,7 @@ } songObj.title = title + suffix songObj.title_lang = { - en: (osu.metadata.Title || osu.metadata.TitleUnicode) + suffix + en: (osu.metadata.Title || osu.metadata.TitleUnicode || title) + suffix } }else{ songObj.title = filename @@ -389,72 +384,68 @@ }else{ songObj.category_id = category_id } - var hash = md5.base64(event.target.result).slice(0, -2) - songObj.hash = hash - scoreStorage.songTitles[songObj.title] = hash - var score = scoreStorage.get(hash, false, true) - if(score){ - score.title = songObj.title + if(!this.limited){ + var hash = md5.base64(dataRaw).slice(0, -2) + songObj.hash = hash + scoreStorage.songTitles[songObj.title] = hash + var score = scoreStorage.get(hash, false, true) + if(score){ + score.title = songObj.title + } } - }).catch(() => {}) - reader.readAsText(file) - return promise + }) } addAssets(){ - return new Promise((resolve, reject) => { - var promises = [] - for(let name in this.assetFiles){ - let id = this.getFilename(name) - var file = this.assetFiles[name] - if(name === "vectors.json"){ - var reader = new FileReader() - promises.push(pageEvents.load(reader).then(() => response => { - vectors = JSON.parse(response) - })) - reader.readAsText(file) - } - if(assets.img.indexOf(name) !== -1){ - let image = document.createElement("img") - promises.push(pageEvents.load(image).then(() => { - if(id in this.assetSelectors){ - var selector = this.assetSelectors[id] - this.stylesheet.push(selector + '{background-image:url("' + image.src + '")}') - } - })) - image.id = name - image.src = URL.createObjectURL(file) - loader.assetsDiv.appendChild(image) - assets.image[id].parentNode.removeChild(assets.image[id]) - assets.image[id] = image - } - if(assets.audioSfx.indexOf(name) !== -1){ - assets.sounds[id].clean() - promises.push(this.loadSound(file, name, snd.sfxGain)) - } - if(assets.audioMusic.indexOf(name) !== -1){ - assets.sounds[id].clean() - promises.push(this.loadSound(file, name, snd.musicGain)) - } - if(assets.audioSfxLR.indexOf(name) !== -1){ - assets.sounds[id + "_p1"].clean() - assets.sounds[id + "_p2"].clean() - promises.push(this.loadSound(file, name, snd.sfxGain).then(sound => { - assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) - assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) - })) - } - if(assets.audioSfxLoud.indexOf(name) !== -1){ - assets.sounds[id].clean() - promises.push(this.loadSound(file, name, snd.sfxLoudGain)) - } + var promises = [] + for(let name in this.assetFiles){ + let id = this.getFilename(name) + var file = this.assetFiles[name] + if(name === "vectors.json"){ + promises.push(file.read().then(() => response => { + vectors = JSON.parse(response) + })) } - Promise.all(promises).then(resolve, reject) - }) + if(assets.img.indexOf(name) !== -1){ + let image = document.createElement("img") + promises.push(pageEvents.load(image).then(() => { + if(id in this.assetSelectors){ + var selector = this.assetSelectors[id] + this.stylesheet.push(selector + '{background-image:url("' + image.src + '")}') + } + })) + image.id = name + image.src = URL.createObjectURL(file) + loader.assetsDiv.appendChild(image) + assets.image[id].parentNode.removeChild(assets.image[id]) + assets.image[id] = image + } + if(assets.audioSfx.indexOf(name) !== -1){ + assets.sounds[id].clean() + promises.push(this.loadSound(file, name, snd.sfxGain)) + } + if(assets.audioMusic.indexOf(name) !== -1){ + assets.sounds[id].clean() + promises.push(this.loadSound(file, name, snd.musicGain)) + } + if(assets.audioSfxLR.indexOf(name) !== -1){ + assets.sounds[id + "_p1"].clean() + assets.sounds[id + "_p2"].clean() + promises.push(this.loadSound(file, name, snd.sfxGain).then(sound => { + assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) + assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) + })) + } + if(assets.audioSfxLoud.indexOf(name) !== -1){ + assets.sounds[id].clean() + promises.push(this.loadSound(file, name, snd.sfxLoudGain)) + } + } + return Promise.all(promises) } loadSound(file, name, gain){ var id = this.getFilename(name) - return gain.load(file, true).then(sound => { + return gain.load(file).then(sound => { assets.sounds[id] = sound }) } @@ -463,7 +454,7 @@ } getCategory(file, exclude){ - var path = file.webkitRelativePath.toLowerCase().split("/") + var path = file.path.toLowerCase().split("/") for(var i = path.length - 2; i >= 0; i--){ var hasTitle = false for(var j in exclude){ @@ -532,24 +523,14 @@ document.head.appendChild(style) } if(this.songs.length){ - var length = this.songs.length - assets.songs = this.songs - assets.customSongs = true - assets.customSelected = 0 - assets.sounds["se_don"].play() - this.songSelect.clean() - setTimeout(() => { - loader.screen.removeChild(this.loaderDiv) - this.clean() - new SongSelect("browse", false, this.songSelect.touchEnabled) - pageEvents.send("import-songs", length) - }, 500) + if(this.limited){ + assets.otherFiles = this.otherFiles + } + return Promise.resolve(this.songs) }else{ - loader.screen.removeChild(this.loaderDiv) - this.songSelect.browse.parentNode.reset() - this.songSelect.redrawRunning = true - this.clean() + return Promise.reject("cancel") } + this.clean() } joinPath(){ @@ -587,7 +568,6 @@ } clean(){ - delete this.loaderDiv delete this.songs delete this.tjaFiles delete this.osuFiles diff --git a/public/src/js/loader.js b/public/src/js/loader.js index fee5647..e8e7321 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -25,21 +25,13 @@ class Loader{ this.loaderPercentage = document.querySelector("#loader .percentage") this.loaderProgress = document.querySelector("#loader .progress") - var queryString = gameConfig._version.commit_short ? "?" + gameConfig._version.commit_short : "" + this.queryString = gameConfig._version.commit_short ? "?" + gameConfig._version.commit_short : "" if(gameConfig.custom_js){ - var script = document.createElement("script") - var url = gameConfig.custom_js + queryString - this.addPromise(pageEvents.load(script), url) - script.src = url - document.head.appendChild(script) + this.addPromise(this.loadScript(gameConfig.custom_js), gameConfig.custom_js) } assets.js.forEach(name => { - var script = document.createElement("script") - var url = "/src/js/" + name + queryString - this.addPromise(pageEvents.load(script), url) - script.src = url - document.head.appendChild(script) + this.addPromise(this.loadScript("/src/js/" + name), "/src/js/" + name) }) var pageVersion = versionLink.href @@ -53,20 +45,19 @@ class Loader{ gameConfig._version.commit && versionLink.href.indexOf(gameConfig._version.commit) === -1 ){ - // Version in the config does not match version on the page - reject() + reject("Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")") } var cssCount = document.styleSheets.length + assets.css.length assets.css.forEach(name => { var stylesheet = document.createElement("link") stylesheet.rel = "stylesheet" - stylesheet.href = "/src/css/" + name + queryString + 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 + queryString + stylesheet.href = gameConfig.assets_baseurl + name + this.queryString document.head.appendChild(stylesheet) }) var checkStyles = () => { @@ -77,7 +68,7 @@ class Loader{ } var interval = setInterval(checkStyles, 100) checkStyles() - }), "Version on the page and config does not match\n(page: " + pageVersion + ",\nconfig: "+ gameConfig._version.commit + ")") + })) for(var name in assets.fonts){ var url = gameConfig.assets_baseurl + "fonts/" + assets.fonts[name] @@ -99,12 +90,12 @@ class Loader{ assets.views.forEach(name => { var id = this.getFilename(name) - var url = "/src/views/" + name + queryString + var url = "/src/views/" + name + this.queryString this.addPromise(this.ajax(url).then(page => { assets.pages[id] = page }), url) }) - + this.addPromise(this.ajax("/api/categories").then(cats => { assets.categories = JSON.parse(cats) assets.categories.forEach(cat => { @@ -113,9 +104,9 @@ class Loader{ delete cat.song_skin cat.songSkin.infoFill = cat.songSkin.info_fill delete cat.songSkin.info_fill - } - }); - + } + }) + assets.categories.push({ title: "default", songSkin: { @@ -127,18 +118,17 @@ class Loader{ }) }), "/api/categories") - this.addPromise(this.ajax("/api/songs").then(songs => { - assets.songsDefault = JSON.parse(songs) - assets.songs = assets.songsDefault - }), "/api/songs") - - var url = gameConfig.assets_baseurl + "img/vectors.json" + queryString + var url = gameConfig.assets_baseurl + "img/vectors.json" + this.queryString this.addPromise(this.ajax(url).then(response => { vectors = JSON.parse(response) }), url) this.afterJSCount = - ["blurPerformance"].length + + [ + "/api/songs", + "blurPerformance", + "categories" + ].length + assets.audioSfx.length + assets.audioMusic.length + assets.audioSfxLR.length + @@ -149,21 +139,51 @@ class Loader{ if(this.error){ return } - + + this.addPromise(this.ajax("/api/songs").then(songs => { + songs = JSON.parse(songs) + songs.forEach(song => { + var directory = gameConfig.songs_baseurl + song.id + "/" + song.music = new RemoteFile(directory + "main.mp3") + if(song.type === "tja"){ + song.chart = new RemoteFile(directory + "main.tja") + }else{ + song.chart = {separateDiff: true} + for(var diff in song.courses){ + if(song.courses[diff]){ + song.chart[diff] = new RemoteFile(directory + diff + ".osu") + } + } + } + if(song.lyrics){ + song.lyricsFile = new RemoteFile(gameConfig.songs_baseurl + song.id + "main.vtt") + } + if(song.preview > 0){ + song.previewMusic = new RemoteFile(directory + "preview.mp3") + } + }) + assets.songsDefault = songs + assets.songs = assets.songsDefault + }), "/api/songs") + + var categoryPromises = [] assets.categories //load category backgrounds to DOM - .filter(cat=>cat.songSkin && cat.songSkin.bg_img) - .forEach(cat=>{ + .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 - this.addPromise(pageEvents.load(image), url) + categoryPromises.push(pageEvents.load(image).catch(response => { + this.errorMsg(response, url) + })) image.id = name image.src = url this.assetsDiv.appendChild(image) assets.image[id] = image - }) - + }) + this.addPromise(Promise.all(categoryPromises)) + snd.buffer = new SoundBuffer() snd.musicGain = snd.buffer.createGain() snd.sfxGain = snd.buffer.createGain() @@ -300,14 +320,13 @@ class Loader{ this.callback(songId) pageEvents.send("ready", readyEvent) }) - }, this.errorMsg.bind(this)) - }) + }, this.errorMsg.bind(this)) + }) } addPromise(promise, url){ this.promises.push(promise) promise.then(this.assetLoaded.bind(this), response => { this.errorMsg(response, url) - return Promise.resolve() }) } soundUrl(name){ @@ -315,7 +334,7 @@ class Loader{ } loadSound(name, gain){ var id = this.getFilename(name) - return gain.load(this.soundUrl(name)).then(sound => { + return gain.load(new RemoteFile(this.soundUrl(name))).then(sound => { assets.sounds[id] = sound }) } @@ -415,21 +434,28 @@ class Loader{ this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg") } ajax(url, customRequest){ - return new Promise((resolve, reject) => { - var request = new XMLHttpRequest() - request.open("GET", url) - pageEvents.load(request).then(() => { - if(request.status === 200){ - resolve(request.response) - }else{ - reject() - } - }, reject) - if(customRequest){ - customRequest(request) + var request = new XMLHttpRequest() + request.open("GET", url) + var promise = pageEvents.load(request).then(() => { + if(request.status === 200){ + return request.response + }else{ + return Promise.reject(`${url} (${request.status})`) } - request.send() }) + if(customRequest){ + customRequest(request) + } + request.send() + return promise + } + loadScript(url){ + var script = document.createElement("script") + var url = url + this.queryString + var promise = pageEvents.load(script) + script.src = url + document.head.appendChild(script) + return promise } getCsrfToken(){ return this.ajax("api/csrftoken").then(response => { diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index d3835f2..aad65d7 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -34,10 +34,10 @@ class LoadSong{ run(){ var song = this.selectedSong var id = song.folder + var songObj this.promises = [] - if(song.folder !== "calibration"){ + if(id !== "calibration"){ assets.sounds["v_start"].play() - var songObj assets.songs.forEach(song => { if(song.id === id){ songObj = song @@ -50,11 +50,12 @@ class LoadSong{ } }) }else{ - var songObj = { - "music": "muted", - "chart": "blank" + songObj = { + music: "muted", + custom: true } } + this.songObj = songObj song.songBg = this.randInt(1, 5) song.songStage = this.randInt(1, 3) @@ -99,14 +100,14 @@ class LoadSong{ } let img = document.createElement("img") let force = imgLoad[i].type === "song" && this.touchEnabled - if(!songObj.music && (this.imgScale !== 1 || force)){ + if(!songObj.custom && (this.imgScale !== 1 || force)){ img.crossOrigin = "Anonymous" } let promise = pageEvents.load(img) this.addPromise(promise.then(() => { return this.scaleImg(img, filename, prefix, force) - }), songObj.music ? filename + ".png" : skinBase + filename + ".png") - if(songObj.music){ + }), songObj.custom ? filename + ".png" : skinBase + filename + ".png") + if(songObj.custom){ img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) }else{ img.src = skinBase + filename + ".png" @@ -115,57 +116,29 @@ class LoadSong{ } this.loadSongBg(id) - var url = gameConfig.songs_baseurl + id + "/main.mp3" - this.addPromise(new Promise((resolve, reject) => { - if(songObj.sound){ - songObj.sound.gain = snd.musicGain - resolve() - }else if(!songObj.music){ - snd.musicGain.load(url).then(sound => { - songObj.sound = sound - resolve() - }, reject) - }else if(songObj.music !== "muted"){ - snd.musicGain.load(songObj.music, true).then(sound => { - songObj.sound = sound - resolve() - }, reject) - }else{ - resolve() - } - }), songObj.music ? songObj.music.webkitRelativePath : url) - if(songObj.chart){ - if(songObj.chart === "blank"){ - this.songData = "" - }else{ - var reader = new FileReader() - this.addPromise(pageEvents.load(reader).then(event => { - this.songData = event.target.result.replace(/\0/g, "").split("\n") - }), songObj.chart.webkitRelativePath) - if(song.type === "tja"){ - reader.readAsText(songObj.chart, "sjis") - }else{ - reader.readAsText(songObj.chart) - } - } - if(songObj.lyricsFile && settings.getItem("showLyrics")){ - var reader = new FileReader() - this.addPromise(pageEvents.load(reader).then(event => { - songObj.lyricsData = event.target.result - }, () => Promise.resolve()), songObj.lyricsFile.webkitRelativePath) - reader.readAsText(songObj.lyricsFile) - } - }else{ - var url = this.getSongPath(song) - this.addPromise(loader.ajax(url).then(data => { + if(songObj.sound){ + songObj.sound.gain = snd.musicGain + }else if(songObj.music !== "muted"){ + this.addPromise(snd.musicGain.load(songObj.music).then(sound => { + songObj.sound = sound + }), songObj.music.url) + } + var chart = songObj.chart + if(chart.separateDiff){ + var chartDiff = this.selectedSong.difficulty + chart = chart[chartDiff] + } + if(chart){ + this.addPromise(chart.read(song.type === "tja" ? "sjis" : "").then(data => { this.songData = data.replace(/\0/g, "").split("\n") - }), url) - if(song.lyrics && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){ - var url = this.getSongDir(song) + "main.vtt" - this.addPromise(loader.ajax(url).then(data => { - songObj.lyricsData = data - }), url) - } + }), chart.url) + }else{ + this.songData = "" + } + if(songObj.lyricsFile && !songObj.lyricsData && !this.multiplayer && (!this.touchEnabled || this.autoPlayEnabled) && settings.getItem("showLyrics")){ + this.addPromise(songObj.lyricsFile.read().then(data => { + songObj.lyricsData = data + }, () => {}), songObj.lyricsFile.url) } if(this.touchEnabled && !assets.image["touch_drum"]){ let img = document.createElement("img") @@ -289,17 +262,6 @@ class LoadSong{ randInt(min, max){ return Math.floor(Math.random() * (max - min + 1)) + min } - getSongDir(selectedSong){ - return gameConfig.songs_baseurl + selectedSong.folder + "/" - } - getSongPath(selectedSong){ - var directory = this.getSongDir(selectedSong) - if(selectedSong.type === "tja"){ - return directory + "main.tja" - }else{ - return directory + selectedSong.difficulty + ".osu" - } - } setupMultiplayer(){ var song = this.selectedSong @@ -326,13 +288,14 @@ class LoadSong{ this.selectedSong2[i] = this.selectedSong[i] } this.selectedSong2.difficulty = event.value.diff - if(song.type === "tja"){ + var chart = this.songObj.chart + var chartDiff = this.selectedSong2.difficulty + if(song.type === "tja" || !chart || !chart.separateDiff || !chart[chartDiff]){ this.startMultiplayer() }else{ - loader.ajax(this.getSongPath(this.selectedSong2)).then(data => { + chart[chartDiff].read(song.type === "tja" ? "sjis" : "").then(data => { this.song2Data = data.replace(/\0/g, "").split("\n") - this.startMultiplayer() - }, () => { + }, () => {}).then(() => { this.startMultiplayer() }) } @@ -389,6 +352,7 @@ class LoadSong{ } clean(){ delete this.promises + delete this.songObj pageEvents.remove(p2, "message") if(this.cancelButton){ pageEvents.remove(this.cancelButton, ["mousedown", "touchstart"]) diff --git a/public/src/js/main.js b/public/src/js/main.js index 254caa1..6e9386d 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -89,6 +89,7 @@ var vectors var settings var scoreStorage var account = {} +var gpicker pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => { if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){ diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 43ca8d8..64f6bfc 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -49,7 +49,7 @@ class SongSelect{ border: ["#dec4fd", "#a543ef"], outline: "#a741ef" }, - "browse": { + "customSongs": { sort: 0, background: "#fab5d3", border: ["#ffe7ef", "#d36aa2"], @@ -80,42 +80,7 @@ class SongSelect{ this.songs = [] for(let song of assets.songs){ - var title = this.getLocalTitle(song.title, song.title_lang) - var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) - var skin = null - var categoryName = "" - var originalCategory = "" - if(song.category_id !== null && song.category_id !== undefined){ - var category = assets.categories.find(cat => cat.id === song.category_id) - var categoryName = this.getLocalTitle(category.title, category.title_lang) - var originalCategory = category.title - var skin = this.songSkin[category.title] - }else if(song.category){ - var categoryName = song.category - var originalCategory = song.category - } - this.songs.push({ - id: song.id, - title: title, - originalTitle: song.title, - subtitle: subtitle, - skin: skin || this.songSkin.default, - courses: song.courses, - originalCategory: originalCategory, - category: categoryName, - category_id: song.category_id, - preview: song.preview || 0, - type: song.type, - offset: song.offset, - songSkin: song.song_skin || {}, - music: song.music, - volume: song.volume, - maker: song.maker, - canJump: true, - hash: song.hash || song.title, - order: song.order, - lyrics: song.lyrics - }) + this.songs.push(this.addSong(song)) } this.songs.sort((a, b) => { var catA = a.originalCategory in this.songSkin ? this.songSkin[a.originalCategory] : this.songSkin.default @@ -170,17 +135,26 @@ class SongSelect{ action: "settings", category: strings.random }) - if("webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent))){ - this.browse = document.getElementById("browse") - pageEvents.add(this.browse, "change", this.browseChange.bind(this)) - + + var showCustom = false + if(gameConfig.google_credentials.gdrive_enabled){ + if(!(/iPhone|iPad/.test(navigator.userAgent))){ + showCustom = true + } + }else{ + if("webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent))){ + showCustom = true + } + } + if(showCustom){ this.songs.push({ - title: assets.customSongs ? strings.defaultSongList : strings.browse, - skin: this.songSkin.browse, - action: "browse", + title: assets.customSongs ? strings.customSongs.default : strings.customSongs.title, + skin: this.songSkin.customSongs, + action: "customSongs", category: strings.random }) } + this.songs.push({ title: strings.back, skin: this.songSkin.back, @@ -501,11 +475,11 @@ class SongSelect{ event.preventDefault() if(this.state.screen === "song" && this.redrawRunning){ var currentSong = this.songs[this.selectedSong] - if(currentSong.action === "browse"){ + if(currentSong.action === "customSongs"){ var mouse = this.mouseOffset(event.changedTouches[0].pageX, event.changedTouches[0].pageY) var moveBy = this.songSelMouse(mouse.x, mouse.y) if(moveBy === 0){ - this.toBrowse() + this.toCustomSongs() } } } @@ -668,10 +642,6 @@ class SongSelect{ } } - browseChange(event){ - new ImportSongs(this, event) - } - toSelectDifficulty(fromP2){ var currentSong = this.songs[this.selectedSong] if(p2.session && !fromP2 && currentSong.action !== "random"){ @@ -686,6 +656,9 @@ class SongSelect{ } }else if(this.state.locked === 0 || fromP2){ if(currentSong.courses){ + if(currentSong.unloaded){ + return + } this.state.screen = "difficulty" this.state.screenMS = this.getMS() this.state.locked = true @@ -721,8 +694,8 @@ class SongSelect{ this.toAbout() }else if(currentSong.action === "settings"){ this.toSettings() - }else if(currentSong.action === "browse"){ - this.toBrowse() + }else if(currentSong.action === "customSongs"){ + this.toCustomSongs() } } this.pointer(false) @@ -857,18 +830,25 @@ class SongSelect{ }, 500) } } - toBrowse(){ + toCustomSongs(){ if(assets.customSongs){ assets.customSongs = false assets.songs = assets.songsDefault + delete assets.otherFiles this.playSound("se_don") this.clean() setTimeout(() => { - new SongSelect("browse", false, this.touchEnabled) + new SongSelect("customSongs", false, this.touchEnabled) }, 500) pageEvents.send("import-songs-default") }else{ - this.browse.click() + localStorage["selectedSong"] = this.selectedSong + + this.playSound("se_don") + this.clean() + setTimeout(() => { + new CustomSongs(this.touchEnabled) + }, 500) } } @@ -2420,30 +2400,31 @@ class SongSelect{ } }else{ songObj = {id: id} - - var previewFilename = prvTime > 0 ? "/preview.mp3" : "/main.mp3" - - var loadPreview = previewFilename => { - return snd.previewGain.load(gameConfig.songs_baseurl + id + previewFilename) - } - - new Promise((resolve, reject) => { - if(!currentSong.music){ - songObj.preview_time = 0 - loadPreview(previewFilename).catch(() => { - songObj.preview_time = prvTime - return loadPreview("/main.mp3") - }).then(resolve, reject) - }else if(currentSong.music !== "muted"){ + if(currentSong.previewMusic){ + songObj.preview_time = 0 + var promise = snd.previewGain.load(currentSong.previewMusic).catch(() => { songObj.preview_time = prvTime - snd.previewGain.load(currentSong.music, true).then(resolve, reject) - } - }).then(sound => { - if(currentId === this.previewId){ + return snd.previewGain.load(currentSong.music) + }) + }else if(currentSong.unloaded){ + var promise = this.getUnloaded(this.selectedSong, songObj) + }else if(currentSong.sound){ + songObj.preview_time = prvTime + currentSong.sound.gain = snd.previewGain + var promise = Promise.resolve(currentSong.sound) + }else if(currentSong.music !== "muted"){ + songObj.preview_time = prvTime + var promise = snd.previewGain.load(currentSong.music) + }else{ + return + } + promise.then(sound => { + if(currentId === this.previewId || loadOnly){ songObj.preview_sound = sound - this.preview = sound - this.previewLoaded(startLoad, songObj.preview_time, currentSong.volume) - + if(!loadOnly){ + this.preview = sound + this.previewLoaded(startLoad, songObj.preview_time, currentSong.volume) + } var oldPreview = this.previewList.shift() if(oldPreview){ oldPreview.preview_sound.clean() @@ -2452,6 +2433,10 @@ class SongSelect{ }else{ sound.clean() } + }).catch(e => { + if(e !== "cancel"){ + return Promise.reject(e) + } }) } } @@ -2483,6 +2468,72 @@ class SongSelect{ snd.musicGain.fadeOut(0.4) } } + getUnloaded(selectedSong, songObj){ + var currentSong = this.songs[selectedSong] + var file = currentSong.chart + var importSongs = new ImportSongs(false, assets.otherFiles) + return file.read(currentSong.type === "tja" ? "sjis" : "").then(data => { + currentSong.chart = new CachedFile(data, file) + return importSongs.addTja({ + file: currentSong.chart, + index: 0 + }) + }).then(() => { + var imported = importSongs.songs[0] + importSongs.clean() + imported.id = currentSong.id + imported.order = currentSong.order + delete imported.song_skin + songObj.preview_time = imported.preview + if(imported.music){ + return snd.previewGain.load(imported.music).then(sound => { + imported.sound = sound + var index = assets.songs.findIndex(song => song.id === currentSong.id) + if(index !== -1){ + assets.songs[index] = imported + } + this.songs[selectedSong] = this.addSong(imported) + return sound.copy() + }) + }else{ + return Promise.reject("cancel") + } + }) + } + addSong(song){ + var title = this.getLocalTitle(song.title, song.title_lang) + var subtitle = this.getLocalTitle(title === song.title ? song.subtitle : "", song.subtitle_lang) + var skin = null + var categoryName = "" + var originalCategory = "" + if(song.category_id !== null && song.category_id !== undefined){ + var category = assets.categories.find(cat => cat.id === song.category_id) + var categoryName = this.getLocalTitle(category.title, category.title_lang) + var originalCategory = category.title + var skin = this.songSkin[category.title] + }else if(song.category){ + var categoryName = song.category + var originalCategory = song.category + } + var addedSong = { + title: title, + originalTitle: song.title, + subtitle: subtitle, + skin: skin || this.songSkin.default, + originalCategory: originalCategory, + category: categoryName, + preview: song.preview || 0, + songSkin: song.song_skin || {}, + canJump: true, + hash: song.hash || song.title + } + for(var i in song){ + if(!(i in addedSong)){ + addedSong[i] = song[i] + } + } + return addedSong + } onusers(response){ this.songs.forEach(song => { @@ -2657,8 +2708,6 @@ class SongSelect{ pageEvents.remove(this.touchFullBtn, "click") delete this.touchFullBtn } - pageEvents.remove(this.browse, "change") - delete this.browse delete this.selectable delete this.ctx delete this.canvas diff --git a/public/src/js/soundbuffer.js b/public/src/js/soundbuffer.js index 3329802..4a37e86 100644 --- a/public/src/js/soundbuffer.js +++ b/public/src/js/soundbuffer.js @@ -5,24 +5,11 @@ pageEvents.add(window, ["click", "touchend", "keypress"], this.pageClicked.bind(this)) this.gainList = [] } - load(url, local, gain){ - if(local){ - var reader = new FileReader() - var loadPromise = pageEvents.load(reader).then(event => { - return event.target.result - }) - reader.readAsArrayBuffer(url) - }else{ - var loadPromise = loader.ajax(url, request => { - request.responseType = "arraybuffer" - }) - } - return loadPromise.then(response => { + load(file, gain){ + return file.arrayBuffer().then(response => { return new Promise((resolve, reject) => { return this.context.decodeAudioData(response, resolve, reject) - }).catch(error => { - throw [error, url] - }) + }).catch(error => Promise.reject([error, file.url])) }).then(buffer => { return new Sound(gain || {soundBuffer: this}, buffer) }) @@ -90,8 +77,8 @@ class SoundGain{ } this.setVolume(1) } - load(url, local){ - return this.soundBuffer.load(url, local, this) + load(url){ + return this.soundBuffer.load(url, this) } convertTime(time, absolute){ return this.soundBuffer.convertTime(time, absolute) @@ -134,7 +121,7 @@ class Sound{ this.sources = new Set() } copy(gain){ - return new Sound(gain, this.buffer) + return new Sound(gain || this.gain, this.buffer) } getTime(){ return this.soundBuffer.getTime() diff --git a/public/src/js/strings.js b/public/src/js/strings.js index cca613e..8443f74 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -102,20 +102,6 @@ var translations = { tw: "遊戲設定", ko: "게임 설정" }, - browse: { - ja: "参照する…", - en: "Browse…", - cn: "浏览…", - tw: "開啟檔案…", - ko: "찾아보기…" - }, - defaultSongList: { - ja: "デフォルト曲リスト", - en: "Default Song List", - cn: "默认歌曲列表", - tw: "默認歌曲列表", - ko: "기본 노래 목록" - }, songOptions: { ja: "演奏オプション", en: "Song Options", @@ -1071,6 +1057,43 @@ var translations = { cn: "带歌词", tw: "帶歌詞", ko: "가사가있는" + }, + customSongs: { + title: { + en: "Custom Song List", + }, + default: { + ja: "デフォルト曲リスト", + en: "Default Song List", + cn: "默认歌曲列表", + tw: "默認歌曲列表", + ko: "기본 노래 목록" + }, + description: { + en: [ + "Pick a folder with Taiko chart files in TJA format to play on a custom song list!" + ] + }, + localFolder: { + en: "Local Folder..." + }, + gdriveFolder: { + en: "Google Drive..." + } + }, + gpicker: { + myDrive: { + en: "My Drive" + }, + starred: { + en: "Starred" + }, + sharedWithMe: { + en: "Shared with me" + }, + authError: { + en: "Auth error: %s" + } } } var allStrings = {} diff --git a/public/src/views/customsongs.html b/public/src/views/customsongs.html new file mode 100644 index 0000000..585a233 --- /dev/null +++ b/public/src/views/customsongs.html @@ -0,0 +1,12 @@ +
+
+
+
+
+ + +
+
+
+
+
diff --git a/public/src/views/songselect.html b/public/src/views/songselect.html index d63c409..0c47814 100644 --- a/public/src/views/songselect.html +++ b/public/src/views/songselect.html @@ -2,5 +2,4 @@
-
From f590222568dcc98a827f268f3c641cb179b3149c Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Thu, 29 Oct 2020 09:04:36 +0300 Subject: [PATCH 2/7] Fix metadata loading, loading animation, osu charts, and gpicker - Speed up metadata loading by not waiting for the audio - Make loading animation bigger on mobile - Fix osu charts imported with gdrive - Fix gpicker not appearing after cancelling --- public/src/css/loadsong.css | 10 +++++----- public/src/js/gpicker.js | 7 +++++-- public/src/js/songselect.js | 19 ++++++++++--------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/public/src/css/loadsong.css b/public/src/css/loadsong.css index 05ee398..f4e5489 100644 --- a/public/src/css/loadsong.css +++ b/public/src/css/loadsong.css @@ -13,8 +13,8 @@ flex-direction: column; justify-content: center; align-items: center; - width: 20vw; - height: 15vw; + width: 20vmax; + height: 15vmax; background: rgba(0, 0, 0, 0.75); border-radius: 5px; border: 3px solid white; @@ -22,14 +22,14 @@ z-index: 1; } #loading-don{ - width: 10vw; - height: calc(10vw / 120 * 115); + width: 10vmax; + height: calc(10vmax / 120 * 115); background-size: contain; background-repeat: no-repeat; } .loading-text{ position: relative; - font-size: 1.5vw; + font-size: 1.5vmax; text-align: center; z-index: 1; } diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js index 0707b6c..bcd94af 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -21,6 +21,9 @@ class Gpicker{ for(var i = 0; i < files.length; i++){ var path = files[i].path ? files[i].path + "/" : "" var list = files[i].list + if(!list){ + continue + } for(var j = 0; j < list.length; j++){ var file = list[j] if(file.mimeType === this.folder){ @@ -68,7 +71,7 @@ class Gpicker{ } loadApi(){ if(window.gapi && gapi.client && gapi.client.drive){ - return + return Promise.resolve() } return loader.loadScript("https://apis.google.com/js/api.js") .then(() => new Promise((resolve, reject) => @@ -83,7 +86,7 @@ class Gpicker{ } getToken(lockedCallback){ if(this.oauthToken){ - return + return Promise.resolve() } if(!this.auth){ var authPromise = gapi.auth2.init({ diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 64f6bfc..0e68284 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -2407,7 +2407,7 @@ class SongSelect{ return snd.previewGain.load(currentSong.music) }) }else if(currentSong.unloaded){ - var promise = this.getUnloaded(this.selectedSong, songObj) + var promise = this.getUnloaded(this.selectedSong, songObj, currentId) }else if(currentSong.sound){ songObj.preview_time = prvTime currentSong.sound.gain = snd.previewGain @@ -2468,13 +2468,13 @@ class SongSelect{ snd.musicGain.fadeOut(0.4) } } - getUnloaded(selectedSong, songObj){ + getUnloaded(selectedSong, songObj, currentId){ var currentSong = this.songs[selectedSong] var file = currentSong.chart var importSongs = new ImportSongs(false, assets.otherFiles) return file.read(currentSong.type === "tja" ? "sjis" : "").then(data => { currentSong.chart = new CachedFile(data, file) - return importSongs.addTja({ + return importSongs[currentSong.type === "tja" ? "addTja" : "addOsu"]({ file: currentSong.chart, index: 0 }) @@ -2485,14 +2485,15 @@ class SongSelect{ imported.order = currentSong.order delete imported.song_skin songObj.preview_time = imported.preview - if(imported.music){ + var index = assets.songs.findIndex(song => song.id === currentSong.id) + if(index !== -1){ + assets.songs[index] = imported + } + this.songs[selectedSong] = this.addSong(imported) + if(imported.music && currentId === this.previewId){ return snd.previewGain.load(imported.music).then(sound => { imported.sound = sound - var index = assets.songs.findIndex(song => song.id === currentSong.id) - if(index !== -1){ - assets.songs[index] = imported - } - this.songs[selectedSong] = this.addSong(imported) + this.songs[selectedSong].sound = sound return sound.copy() }) }else{ From 9aa73752879d9a5abda8c3824a20ca0f28127124 Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Thu, 29 Oct 2020 11:56:58 +0300 Subject: [PATCH 3/7] Fix lyrics --- public/src/js/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/js/loader.js b/public/src/js/loader.js index e8e7321..8119243 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -156,7 +156,7 @@ class Loader{ } } if(song.lyrics){ - song.lyricsFile = new RemoteFile(gameConfig.songs_baseurl + song.id + "main.vtt") + song.lyricsFile = new RemoteFile(directory + "main.vtt") } if(song.preview > 0){ song.previewMusic = new RemoteFile(directory + "preview.mp3") From c5ce5104f142bd260eb02e002453de7de775fdbf Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Thu, 29 Oct 2020 15:21:53 +0300 Subject: [PATCH 4/7] Fix charts with utf8-bom encoding --- public/src/js/abstractfile.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index 00f5f07..982ac1c 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -62,9 +62,12 @@ class GdriveFile{ } read(encoding){ if(encoding){ - return this.arrayBuffer().then(response => - new TextDecoder(encoding).decode(response) - ) + return this.arrayBuffer().then(response => { + var reader = new FileReader() + var promise = pageEvents.load(reader).then(event => event.target.result) + reader.readAsText(new Blob([response]), encoding) + return promise + }) }else{ return gpicker.downloadFile(this.id) } From 180ec58adbc093d46ac5e41453e94e3bdeccbba2 Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Sat, 31 Oct 2020 14:47:42 +0300 Subject: [PATCH 5/7] Changed look of song loading, fix custom game assets and song skins, fix auth error - Change the way a selected song appears while it is loading the metadata - Fix custom taikowebskin - Fix importing custom game assets (local only) - Get the oauth token again on auth error --- public/src/js/about.js | 2 +- public/src/js/abstractfile.js | 39 ++++++++++++++++++++--------------- public/src/js/controller.js | 8 +++---- public/src/js/customsongs.js | 10 +++++---- public/src/js/gpicker.js | 26 +++++++++++++++++++---- public/src/js/importsongs.js | 30 ++++++++++++++++++++------- public/src/js/loader.js | 19 ++++++++++------- public/src/js/loadsong.js | 4 +++- public/src/js/songselect.js | 20 ++++++++---------- public/src/js/view.js | 4 ++-- 10 files changed, 103 insertions(+), 59 deletions(-) diff --git a/public/src/js/about.js b/public/src/js/about.js index a2712c7..5e3dc97 100644 --- a/public/src/js/about.js +++ b/public/src/js/about.js @@ -138,7 +138,7 @@ if(gamepads[i]){ var gamepadDiag = [] gamepadDiag.push(gamepads[i].id) - gamepadDiag.push("buttons: " + gamepads[i].buttons.length) + gamepadDiag.push("buttons: " + gamepads[i].buttons.length) gamepadDiag.push("axes: " + gamepads[i].axes.length) diag.push("Gamepad #" + (i + 1) + ": " + gamepadDiag.join(", ")) } diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index 982ac1c..07908d4 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -1,3 +1,9 @@ +function readFile(file, arrayBuffer, encoding){ + var reader = new FileReader() + var promise = pageEvents.load(reader).then(event => event.target.result) + reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding) + return promise +} class RemoteFile{ constructor(url){ this.url = url @@ -22,13 +28,14 @@ class RemoteFile{ } read(encoding){ if(encoding){ - return this.arrayBuffer().then(response => - new TextDecoder(encoding).decode(response) - ) + return this.blob().then(blob => readFile(blob, false, encoding)) }else{ return loader.ajax(this.url) } } + blob(){ + return this.arrayBuffer().then(response => new Blob([response])) + } } class LocalFile{ constructor(file){ @@ -38,16 +45,13 @@ class LocalFile{ this.name = file.name } arrayBuffer(){ - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => event.target.result) - reader.readAsArrayBuffer(this.file) - return promise + return readFile(this.file, true) } read(encoding){ - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => event.target.result) - reader.readAsText(this.file, encoding) - return promise + return readFile(this.file, false, encoding) + } + blob(){ + return Promise.resolve(this.file) } } class GdriveFile{ @@ -62,16 +66,14 @@ class GdriveFile{ } read(encoding){ if(encoding){ - return this.arrayBuffer().then(response => { - var reader = new FileReader() - var promise = pageEvents.load(reader).then(event => event.target.result) - reader.readAsText(new Blob([response]), encoding) - return promise - }) + return this.blob().then(blob => readFile(blob, false, encoding)) }else{ return gpicker.downloadFile(this.id) } } + blob(){ + return this.arrayBuffer().then(response => new Blob([response])) + } } class CachedFile{ constructor(contents, oldFile){ @@ -87,4 +89,7 @@ class CachedFile{ read(encoding){ return this.arrayBuffer() } + blob(){ + return this.arrayBuffer().then(response => new Blob([response])) + } } diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 978b316..9294df9 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -40,12 +40,12 @@ class Controller{ this.parsedSongData = new ParseOsu(songData, selectedSong.difficulty, selectedSong.stars, selectedSong.offset) } this.offset = this.parsedSongData.soundOffset - + var maxCombo = this.parsedSongData.circles.filter(circle => ["don", "ka", "daiDon", "daiKa"].indexOf(circle.type) > -1 && (!circle.branch || circle.branch.name == "master")).length if (maxCombo >= 50) { - var comboVoices = ["v_combo_50"].concat([...Array(Math.floor(maxCombo/100)).keys()].map(i => "v_combo_" + (i + 1)*100)) + var comboVoices = ["v_combo_50"].concat(Array.from(Array(Math.min(50, Math.floor(maxCombo / 100))), (d, i) => "v_combo_" + ((i + 1) * 100))) var promises = [] - + comboVoices.forEach(name => { if (!assets.sounds[name + "_p1"]) { promises.push(loader.loadSound(name + ".wav", snd.sfxGain).then(sound => { @@ -54,7 +54,7 @@ class Controller{ })) } }) - + Promise.all(promises) } diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index b95c7d7..006661c 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -136,10 +136,12 @@ class CustomSongs{ } } songsLoaded(songs){ - var length = songs.length - assets.songs = songs - assets.customSongs = true - assets.customSelected = 0 + if(songs){ + var length = songs.length + assets.songs = songs + assets.customSongs = true + assets.customSelected = 0 + } assets.sounds["se_don"].play() this.clean() setTimeout(() => { diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js index bcd94af..65eb2de 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -84,7 +84,7 @@ class Gpicker{ gapi.client.load("drive", "v3").then(resolve, reject) )) } - getToken(lockedCallback){ + getToken(lockedCallback=()=>{}){ if(this.oauthToken){ return Promise.resolve() } @@ -156,13 +156,31 @@ class Gpicker{ .build() .setVisible(true) } - downloadFile(id, arrayBuffer){ - return this.queue().then(() => - loader.ajax(this.filesUrl + id + "?alt=media", request => { + downloadFile(id, arrayBuffer, 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" } request.setRequestHeader("Authorization", "Bearer " + this.oauthToken) + }, true).then(event => { + var request = event.target + var reject = () => Promise.reject(`${url} (${request.status})`) + if(request.status === 200){ + return request.response + }else if(request.status === 401 && !retry){ + return new Response(request.response).json().then(response => { + var e = response.error + if(e && e.errors[0].reason === "authError"){ + delete this.oauthToken + return this.downloadFile(id, arrayBuffer, true) + }else{ + return reject() + } + }, reject) + } + return reject() }) ) } diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index f076032..cc8c154 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -7,7 +7,7 @@ this.otherFiles = otherFiles || {} this.songs = [] this.stylesheet = [] - this.songTitle = {} + this.songTitle = this.otherFiles.songTitle || {} this.uraRegex = /\s*[\((]裏[\))]$/ this.courseTypes = { "easy": 0, @@ -42,6 +42,7 @@ "bg_stage_2": ".song-stage-2", "bg_stage_3": ".song-stage-3" } + this.comboVoices = ["v_combo_50"].concat(Array.from(Array(50), (d, i) => "v_combo_" + ((i + 1) * 100))) } load(files){ var extensionRegex = /\.[^\/]+$/ @@ -66,13 +67,13 @@ file: file, index: i }) - }else if(!this.limited && (name === "genre.ini" || name === "box.def" || name === "songtitle.txt")){ + }else if(!this.limited && (name === "genre.ini" || name === "box.def") || name === "songtitle.txt"){ var level = (file.path.match(/\//g) || []).length metaFiles.push({ file: file, level: (level * 2) + (name === "genre.ini" ? 1 : 0) }) - }else if(!this.limited && path.indexOf("/taiko-web assets/") !== -1){ + }else if(!this.limited && (path.indexOf("/taiko-web assets/") !== -1 || path.indexOf("taiko-web assets/") === 0)){ if(!(name in this.assetFiles)){ this.assetFiles[name] = file } @@ -401,12 +402,13 @@ for(let name in this.assetFiles){ let id = this.getFilename(name) var file = this.assetFiles[name] + var index = name.lastIndexOf(".") if(name === "vectors.json"){ - promises.push(file.read().then(() => response => { + promises.push(file.read().then(response => { vectors = JSON.parse(response) })) } - if(assets.img.indexOf(name) !== -1){ + if(name.endsWith(".png")){ let image = document.createElement("img") promises.push(pageEvents.load(image).then(() => { if(id in this.assetSelectors){ @@ -415,9 +417,12 @@ } })) image.id = name - image.src = URL.createObjectURL(file) + image.src = URL.createObjectURL(file.blob()) loader.assetsDiv.appendChild(image) - assets.image[id].parentNode.removeChild(assets.image[id]) + var oldImage = assets.image[id] + if(oldImage && oldImage.parentNode){ + oldImage.parentNode.removeChild(oldImage) + } assets.image[id] = image } if(assets.audioSfx.indexOf(name) !== -1){ @@ -440,6 +445,13 @@ assets.sounds[id].clean() promises.push(this.loadSound(file, name, snd.sfxLoudGain)) } + if(this.comboVoices.indexOf(id) !== -1){ + promises.push(snd.sfxGain.load(file).then(sound => { + assets.sounds[id] = sound + assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) + assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) + })) + } } return Promise.all(promises) } @@ -525,8 +537,11 @@ if(this.songs.length){ if(this.limited){ assets.otherFiles = this.otherFiles + assets.otherFiles.songTitle = this.songTitle } return Promise.resolve(this.songs) + }else if(Object.keys(this.assetFiles).length){ + return Promise.resolve() }else{ return Promise.reject("cancel") } @@ -571,6 +586,7 @@ delete this.songs delete this.tjaFiles delete this.osuFiles + delete this.assetFiles delete this.otherFiles } } diff --git a/public/src/js/loader.js b/public/src/js/loader.js index 8119243..d3a277f 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -433,16 +433,19 @@ class Loader{ this.screen.innerHTML = assets.pages[name] this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg") } - ajax(url, customRequest){ + ajax(url, customRequest, customResponse){ var request = new XMLHttpRequest() request.open("GET", url) - var promise = pageEvents.load(request).then(() => { - if(request.status === 200){ - return request.response - }else{ - return Promise.reject(`${url} (${request.status})`) - } - }) + var promise = pageEvents.load(request) + if(!customResponse){ + promise = promise.then(() => { + if(request.status === 200){ + return request.response + }else{ + return Promise.reject(`${url} (${request.status})`) + } + }) + } if(customRequest){ customRequest(request) } diff --git a/public/src/js/loadsong.js b/public/src/js/loadsong.js index aad65d7..66b9080 100644 --- a/public/src/js/loadsong.js +++ b/public/src/js/loadsong.js @@ -108,7 +108,9 @@ class LoadSong{ return this.scaleImg(img, filename, prefix, force) }), songObj.custom ? filename + ".png" : skinBase + filename + ".png") if(songObj.custom){ - img.src = URL.createObjectURL(song.songSkin[filename + ".png"]) + this.addPromise(song.songSkin[filename + ".png"].blob().then(blob => { + img.src = URL.createObjectURL(blob) + })) }else{ img.src = skinBase + filename + ".png" } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index 0e68284..af8e038 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -1090,7 +1090,7 @@ class SongSelect{ } if(screen === "song"){ - if(this.songs[this.selectedSong].courses){ + if(this.songs[this.selectedSong].courses && !this.songs[this.selectedSong].unloaded){ selectedWidth = this.songAsset.selectedWidth } @@ -1101,11 +1101,11 @@ class SongSelect{ var resize2 = changeSpeed - resize var scroll = resize2 - resize - scrollDelay * 2 var elapsed = ms - this.state.moveMS - + if(this.state.catJump || (this.state.move && ms > this.state.moveMS + resize2 - scrollDelay)){ var isJump = this.state.catJump var previousSelectedSong = this.selectedSong - + if(!isJump){ this.playSound("se_ka", 0, this.lastMoveBy) this.selectedSong = this.mod(this.songs.length, this.selectedSong + this.state.move) @@ -1165,7 +1165,7 @@ class SongSelect{ if(this.songs[this.selectedSong].action !== "back"){ var cat = this.songs[this.selectedSong].originalCategory - this.drawBackground(cat) + this.drawBackground(cat) } } if(this.state.moveMS && ms < this.state.moveMS + changeSpeed){ @@ -1291,7 +1291,7 @@ class SongSelect{ highlight = 1 } var selectedSkin = this.songSkin.selected - if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3){ + if(screen === "title" || screen === "titleFadeIn" || this.state.locked === 3 || currentSong.unloaded){ selectedSkin = currentSong.skin highlight = 2 }else if(songSelMoving){ @@ -2278,7 +2278,7 @@ class SongSelect{ }else{ this.songSelect.style.backgroundImage = "url('" + assets.image["bg_genre_def"].src + "')" } - } + } drawClosedSong(config){ var ctx = config.ctx @@ -2476,20 +2476,18 @@ class SongSelect{ currentSong.chart = new CachedFile(data, file) return importSongs[currentSong.type === "tja" ? "addTja" : "addOsu"]({ file: currentSong.chart, - index: 0 + index: currentSong.id }) }).then(() => { - var imported = importSongs.songs[0] + var imported = importSongs.songs[currentSong.id] importSongs.clean() - imported.id = currentSong.id - imported.order = currentSong.order - delete imported.song_skin songObj.preview_time = imported.preview var index = assets.songs.findIndex(song => song.id === currentSong.id) if(index !== -1){ assets.songs[index] = imported } this.songs[selectedSong] = this.addSong(imported) + this.state.moveMS = this.getMS() - this.songSelecting.speed * this.songSelecting.resize if(imported.music && currentId === this.previewId){ return snd.previewGain.load(imported.music).then(sound => { imported.sound = sound diff --git a/public/src/js/view.js b/public/src/js/view.js index 5c5711f..686bdc0 100644 --- a/public/src/js/view.js +++ b/public/src/js/view.js @@ -308,8 +308,8 @@ w: _w, h: _h, radius: 11 }) - ctx.fill() - + ctx.fill() + this.draw.layeredText({ ctx: ctx, text: selectedSong.category, From 5094b0bc70cedbca1620e4d4913ce0f7d55ba28e Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Wed, 4 Nov 2020 03:12:46 +0300 Subject: [PATCH 6/7] Add folder dropping, fix rate limits - Add folder drag and drop support - Do expodential retrying if rate limited, allowing upload of very large drive folders - Do not import deleted files - Move the upload buttons to their own line - Notify when no TJA files have been found - Add more translations --- public/src/css/view.css | 30 ++++- public/src/js/abstractfile.js | 4 +- public/src/js/autoscore.js | 1 - public/src/js/controller.js | 4 +- public/src/js/customsongs.js | 196 ++++++++++++++++++++++++++---- public/src/js/gpicker.js | 91 +++++++++++--- public/src/js/importsongs.js | 7 +- public/src/js/songselect.js | 4 +- public/src/js/strings.js | 54 +++++++- public/src/js/viewassets.js | 4 +- public/src/views/customsongs.html | 16 ++- 11 files changed, 345 insertions(+), 66 deletions(-) diff --git a/public/src/css/view.css b/public/src/css/view.css index 9819f52..6971dac 100644 --- a/public/src/css/view.css +++ b/public/src/css/view.css @@ -108,6 +108,14 @@ kbd{ .left-buttons .taibtn{ margin-right: 0.4em; } +.center-buttons{ + display: flex; + justify-content: center; + margin: 1.5em 0; +} +.center-buttons .taibtn{ + margin: 0 0.2em; +} .diag-txt textarea, .diag-txt iframe{ width: 100%; @@ -217,7 +225,8 @@ kbd{ z-index: 1; } #settings-gamepad, -#settings-latency{ +#settings-latency, +#customsongs-error{ display: none; } #settings-gamepad .view{ @@ -289,7 +298,8 @@ kbd{ .latency-buttons span:active{ background-color: #946013; } -.left-buttons .taibtn{ +.left-buttons .taibtn, +.center-buttons .taibtn{ z-index: 1; } .accountpass-form, @@ -403,3 +413,19 @@ kbd{ font-size: 1em; padding: 0.2em; } +#customsongs-error .view, +#dropzone .view{ + width: 600px; +} +#dropzone{ + pointer-events: none; + opacity: 0; + transition: opacity 0.5s; +} +#dropzone .view-content{ + font-size: 2em; + text-align: center; +} +#dropzone.dragover{ + opacity: 1; +} diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index 07908d4..93d176a 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -38,9 +38,9 @@ class RemoteFile{ } } class LocalFile{ - constructor(file){ + constructor(file, path){ this.file = file - this.path = file.webkitRelativePath + this.path = path || file.webkitRelativePath this.url = this.path this.name = file.name } diff --git a/public/src/js/autoscore.js b/public/src/js/autoscore.js index 7345c20..45443f6 100644 --- a/public/src/js/autoscore.js +++ b/public/src/js/autoscore.js @@ -165,7 +165,6 @@ class AutoScore { GetMaxCombo() { var combo = 0; for (var circle of this.circles) { - //alert(this.IsCommonCircle(circle)); if (this.IsCommonCircle(circle) && (!circle.branch || circle.branch.name === "master")) { combo++; } diff --git a/public/src/js/controller.js b/public/src/js/controller.js index 9294df9..645012a 100644 --- a/public/src/js/controller.js +++ b/public/src/js/controller.js @@ -252,8 +252,8 @@ class Controller{ })) } if(songObj.lyricsFile){ - promises.push(songObj.lyricsFile.read().then(event => { - songObj.lyricsData = event.target.result + promises.push(songObj.lyricsFile.read().then(result => { + songObj.lyricsData = result }, () => Promise.resolve()), songObj.lyricsFile.path) } Promise.all(promises).then(resolve) diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index 006661c..b0a2a0a 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -6,6 +6,7 @@ class CustomSongs{ this.getElement("view-outer").classList.add("touch-enabled") } this.locked = false + this.mode = "main" var tutorialTitle = this.getElement("view-title") this.setAltText(tutorialTitle, strings.customSongs.title) @@ -40,21 +41,55 @@ class CustomSongs{ this.endButton = this.getElement("view-end-button") this.setAltText(this.endButton, strings.session.cancel) - pageEvents.add(this.endButton, ["mousedown", "touchstart"], this.onEnd.bind(this)) + pageEvents.add(this.endButton, ["mousedown", "touchstart"], event => this.onEnd(event, true)) this.items.push(this.endButton) this.selected = this.items.length - 1 this.loaderDiv = document.createElement("div") this.loaderDiv.innerHTML = assets.pages["loadsong"] var loadingText = this.loaderDiv.querySelector("#loading-text") - loadingText.appendChild(document.createTextNode(strings.loading)) - loadingText.setAttribute("alt", strings.loading) + this.setAltText(loadingText, strings.loading) + + if(DataTransferItem.prototype.webkitGetAsEntry){ + this.dropzone = document.getElementById("dropzone") + var dropContent = this.dropzone.getElementsByClassName("view-content")[0] + dropContent.innerText = strings.customSongs.dropzone + this.dragging = false + pageEvents.add(document, "dragover", event => { + event.preventDefault() + if(!this.locked){ + event.dataTransfer.dropEffect = "copy" + this.dropzone.classList.add("dragover") + this.dragging = true + }else{ + event.dataTransfer.dropEffect = "none" + } + }) + pageEvents.add(document, "dragleave", () => { + this.dropzone.classList.remove("dragover") + this.dragging = false + }) + pageEvents.add(document, "drop", this.filesDropped.bind(this)) + } + + this.errorDiv = document.getElementById("customsongs-error") + pageEvents.add(this.errorDiv, ["mousedown", "touchstart"], event => { + if(event.target === event.currentTarget){ + this.hideError() + } + }) + var errorTitle = this.errorDiv.getElementsByClassName("view-title")[0] + this.setAltText(errorTitle, strings.customSongs.importError) + this.errorContent = this.errorDiv.getElementsByClassName("view-content")[0] + this.errorEnd = this.errorDiv.getElementsByClassName("view-end-button")[0] + this.setAltText(this.errorEnd, strings.tutorial.ok) + pageEvents.add(this.errorEnd, ["mousedown", "touchstart"], () => this.hideError(true)) this.keyboard = new Keyboard({ confirm: ["enter", "space", "don_l", "don_r"], previous: ["left", "up", "ka_l"], next: ["right", "down", "ka_r"], - back: ["escape"] + backEsc: ["escape"] }, this.keyPressed.bind(this)) this.gamepad = new Gamepad({ confirmPad: ["b", "ls", "rs"], @@ -73,9 +108,17 @@ class CustomSongs{ element.setAttribute("alt", text) } localFolder(){ - if(this.locked){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked || this.mode !== "main"){ return } + this.changeSelected(this.linkLocalFolder) this.browse.click() } browseChange(event){ @@ -83,6 +126,47 @@ class CustomSongs{ for(var i = 0; i < event.target.files.length; i++){ files.push(new LocalFile(event.target.files[i])) } + this.importLocal(files) + } + filesDropped(event){ + event.preventDefault() + this.dropzone.classList.remove("dragover") + this.dragging = false + if(this.locked){ + return + } + var files = [] + var walk = (entry, path="") => { + return new Promise(resolve => { + if(entry.isFile){ + entry.file(file => { + files.push(new LocalFile(file, path + file.name)) + return resolve() + }, resolve) + }else if(entry.isDirectory){ + var dirReader = entry.createReader() + dirReader.readEntries(entries => { + var dirPromises = [] + for(var i = 0; i < entries.length; i++){ + dirPromises.push(walk(entries[i], path + entry.name + "/")) + } + return Promise.all(dirPromises).then(resolve) + }, resolve) + }else{ + return resolve() + } + }) + } + var dropPromises = [] + for(var i = 0; i < event.dataTransfer.items.length; i++){ + var entry = event.dataTransfer.items[i].webkitGetAsEntry() + if(entry){ + dropPromises.push(walk(entry)) + } + } + Promise.all(dropPromises).then(() => this.importLocal(files)) + } + importLocal(files){ if(!files.length){ return } @@ -94,15 +178,25 @@ class CustomSongs{ this.browse.parentNode.reset() this.locked = false this.loading(false) - if(e !== "cancel"){ + if(e === "nosongs"){ + this.showError(strings.customSongs.noSongs) + }else if(e !== "cancel"){ return Promise.reject(e) } }) } - gdriveFolder(){ - if(this.locked){ + gdriveFolder(event){ + if(event){ + if(event.type === "touchstart"){ + event.preventDefault() + }else if(event.which !== 1){ + return + } + } + if(this.locked || this.mode !== "main"){ return } + this.changeSelected(this.linkGdriveFolder) this.locked = true this.loading(true) var importSongs = new ImportSongs(true) @@ -117,13 +211,17 @@ class CustomSongs{ return gpicker.browse(locked => { this.locked = locked this.loading(locked) + }, error => { + this.showError(error) }) }).then(files => importSongs.load(files)) .then(this.songsLoaded.bind(this)) .catch(e => { this.locked = false this.loading(false) - if(e !== "cancel"){ + if(e === "nosongs"){ + this.showError(strings.customSongs.noSongs) + }else if(e !== "cancel"){ return Promise.reject(e) } }) @@ -154,32 +252,48 @@ class CustomSongs{ return } var selected = this.items[this.selected] - if(name === "confirm" || name === "confirmPad"){ - if(selected === this.endButton){ - this.onEnd() - }else if(name !== "confirmPad"){ - if(selected === this.linkLocalFolder){ - assets.sounds["se_don"].play() - this.localFolder() - }else if(selected === this.linkGdriveFolder){ - assets.sounds["se_don"].play() - this.gdriveFolder() + if(this.mode === "main"){ + if(name === "confirm" || name === "confirmPad"){ + if(selected === this.endButton){ + this.onEnd(null, true) + }else if(name !== "confirmPad"){ + if(selected === this.linkLocalFolder){ + assets.sounds["se_don"].play() + this.localFolder() + }else if(selected === this.linkGdriveFolder){ + assets.sounds["se_don"].play() + this.gdriveFolder() + } + } + }else if(name === "previous" || name === "next"){ + selected.classList.remove("selected") + this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.items[this.selected].classList.add("selected") + assets.sounds["se_ka"].play() + }else if(name === "back" || name === "backEsc"){ + if(!this.dragging || name !== "backEsc"){ + this.onEnd() } } - }else if(name === "previous" || name === "next"){ + }else if(this.mode === "error"){ + if(name === "confirm" || name === "confirmPad" || name === "back" || name === "backEsc"){ + this.hideError(name === "confirm" || name === "confirmPad") + } + } + } + changeSelected(button){ + var selected = this.items[this.selected] + if(selected !== button){ selected.classList.remove("selected") - this.selected = this.mod(this.items.length, this.selected + (name === "next" ? 1 : -1)) + this.selected = this.items.findIndex(item => item === button) this.items[this.selected].classList.add("selected") - assets.sounds["se_ka"].play() - }else if(name === "back"){ - this.onEnd() } } mod(length, index){ return ((index % length) + length) % length } - onEnd(event){ - if(this.locked){ + onEnd(event, confirm){ + if(this.locked || this.mode !== "main"){ return } var touched = false @@ -190,13 +304,32 @@ class CustomSongs{ }else if(event.which !== 1){ return } + }else{ + touched = this.touchEnabled } this.clean() - assets.sounds["se_don"].play() + assets.sounds[confirm ? "se_don" : "se_cancel"].play() setTimeout(() => { new SongSelect("customSongs", false, touched) }, 500) } + showError(text){ + if(this.mode === "error"){ + return + } + this.mode = "error" + this.errorContent.innerText = text + this.errorDiv.style.display = "flex" + assets.sounds["se_pause"].play() + } + hideError(confirm){ + if(this.mode !== "error"){ + return + } + this.mode = "main" + this.errorDiv.style.display = "" + assets.sounds[confirm ? "se_don" : "se_cancel"].play() + } clean(){ this.keyboard.clean() this.gamepad.clean() @@ -208,11 +341,20 @@ class CustomSongs{ pageEvents.remove(this.linkGdriveFolder, ["mousedown", "touchstart"]) } pageEvents.remove(this.endButton, ["mousedown", "touchstart"]) + pageEvents.remove(this.errorDiv, ["mousedown", "touchstart"]) + pageEvents.remove(this.errorEnd, ["mousedown", "touchstart"]) + if(DataTransferItem.prototype.webkitGetAsEntry){ + pageEvents.remove(document, ["dragover", "dragleave", "drop"]) + delete this.dropzone + } delete this.browse delete this.linkLocalFolder delete this.linkGdriveFolder delete this.endButton delete this.items delete this.loaderDiv + delete this.errorDiv + delete this.errorContent + delete this.errorEnd } } diff --git a/public/src/js/gpicker.js b/public/src/js/gpicker.js index 65eb2de..776f403 100644 --- a/public/src/js/gpicker.js +++ b/public/src/js/gpicker.js @@ -9,15 +9,17 @@ class Gpicker{ this.resolveQueue = [] this.queueActive = false } - browse(lockedCallback){ + browse(lockedCallback, errorCallback){ return this.loadApi() - .then(() => this.getToken(lockedCallback)) + .then(() => this.getToken(lockedCallback, errorCallback)) .then(() => new Promise((resolve, reject) => { this.displayPicker(data => { if(data.action === "picked"){ var file = data.docs[0] + var folders = [] + var rateLimit = -1 + var lastBatch = 0 var walk = (files, output=[]) => { - var batch = null for(var i = 0; i < files.length; i++){ var path = files[i].path ? files[i].path + "/" : "" var list = files[i].list @@ -27,14 +29,9 @@ class Gpicker{ for(var j = 0; j < list.length; j++){ var file = list[j] if(file.mimeType === this.folder){ - if(!batch){ - batch = gapi.client.newBatch() - } - batch.add(gapi.client.drive.files.list({ - q: "'" + file.id + "' in parents", - orderBy: "name_natural" - }), { - id: path + file.name + folders.push({ + path: path + file.name, + id: file.id }) }else{ output.push(new GdriveFile({ @@ -45,14 +42,64 @@ class Gpicker{ } } } - if(batch){ - return this.queue() - .then(() => batch.then(responses => { - var files = [] - for(var path in responses.result){ - files.push({path: path, list: responses.result[path].result.files}) + var batchList = [] + for(var i = 0; i < folders.length && batchList.length < 100; i++){ + if(!folders[i].listed){ + folders[i].pos = i + folders[i].listed = true + batchList.push(folders[i]) + } + } + if(batchList.length){ + var batch = gapi.client.newBatch() + batchList.forEach(folder => { + var req = { + q: "'" + folder.id + "' in parents and trashed = false", + orderBy: "name_natural" + } + if(folder.pageToken){ + req.pageToken = folder.pageToken + } + batch.add(gapi.client.drive.files.list(req), {id: folder.pos}) + }) + if(lastBatch + batchList.length > 100){ + var waitPromise = this.sleep(1000) + }else{ + var waitPromise = Promise.resolve() + } + return waitPromise.then(() => this.queue()).then(() => batch.then(responses => { + var files = [] + var rateLimited = false + for(var i in responses.result){ + var result = responses.result[i].result + if(result.error){ + if(result.error.errors[0].domain !== "usageLimits"){ + console.warn(result) + }else if(!rateLimited){ + rateLimited = true + rateLimit++ + folders.push({ + path: folders[i].path, + id: folders[i].id, + pageToken: folders[i].pageToken + }) + } + }else{ + if(result.nextPageToken){ + folders.push({ + path: folders[i].path, + id: folders[i].id, + pageToken: result.nextPageToken + }) + } + files.push({path: folders[i].path, list: result.files}) + } + } + if(rateLimited){ + return this.sleep(Math.pow(2, rateLimit) * 1000).then(() => walk(files, output)) + }else{ + return walk(files, output) } - return walk(files, output) })) }else{ return output @@ -84,7 +131,7 @@ class Gpicker{ gapi.client.load("drive", "v3").then(resolve, reject) )) } - getToken(lockedCallback=()=>{}){ + getToken(lockedCallback=()=>{}, errorCallback=()=>{}){ if(this.oauthToken){ return Promise.resolve() } @@ -97,7 +144,7 @@ class Gpicker{ this.auth = gapi.auth2.getAuthInstance() }, e => { if(e.details){ - alert(strings.gpicker.authError.replace("%s", e.details)) + errorCallback(strings.gpicker.authError.replace("%s", e.details)) } return Promise.reject(e) }) @@ -132,6 +179,7 @@ class Gpicker{ .setDeveloperKey(this.apiKey) .setAppId(this.projectNumber) .setOAuthToken(this.oauthToken) + .setLocale(strings.gpicker.locale) .hideTitleBar() .addView(new picker.DocsView("folders") .setLabel(strings.gpicker.myDrive) @@ -184,6 +232,9 @@ class Gpicker{ }) ) } + sleep(time){ + return new Promise(resolve => setTimeout(resolve, time)) + } queue(){ return new Promise(resolve => { this.resolveQueue.push(resolve) diff --git a/public/src/js/importsongs.js b/public/src/js/importsongs.js index cc8c154..2e5b673 100644 --- a/public/src/js/importsongs.js +++ b/public/src/js/importsongs.js @@ -417,10 +417,13 @@ } })) image.id = name - image.src = URL.createObjectURL(file.blob()) + promises.push(file.blob().then(blob => { + image.src = URL.createObjectURL(blob) + })) loader.assetsDiv.appendChild(image) var oldImage = assets.image[id] if(oldImage && oldImage.parentNode){ + URL.revokeObjectURL(oldImage.src) oldImage.parentNode.removeChild(oldImage) } assets.image[id] = image @@ -543,7 +546,7 @@ }else if(Object.keys(this.assetFiles).length){ return Promise.resolve() }else{ - return Promise.reject("cancel") + return Promise.reject("nosongs") } this.clean() } diff --git a/public/src/js/songselect.js b/public/src/js/songselect.js index af8e038..409f291 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -586,7 +586,7 @@ class SongSelect{ }) } }else if(this.state.locked !== 1 || fromP2){ - if(this.songs[this.selectedSong].courses && (this.state.locked === 0 || fromP2)){ + if(this.songs[this.selectedSong].courses && !this.songs[this.selectedSong].unloaded && (this.state.locked === 0 || fromP2)){ this.state.moveMS = ms }else{ this.state.moveMS = ms - this.songSelecting.speed * this.songSelecting.resize @@ -2222,7 +2222,7 @@ class SongSelect{ ] this.draw.layeredText({ ctx: ctx, - text: strings.ok, + text: strings.tutorial.ok, x: _x, y: _y + 18, width: _w, diff --git a/public/src/js/strings.js b/public/src/js/strings.js index 8443f74..62ca46c 100644 --- a/public/src/js/strings.js +++ b/public/src/js/strings.js @@ -1060,7 +1060,11 @@ var translations = { }, customSongs: { title: { + ja: "カスタム曲リスト", en: "Custom Song List", + cn: "自定义歌曲列表", + tw: "自定義歌曲列表", + ko: "맞춤 노래 목록" }, default: { ja: "デフォルト曲リスト", @@ -1075,21 +1079,61 @@ var translations = { ] }, localFolder: { - en: "Local Folder..." + ja: "ローカルフォルダ...", + en: "Local Folder...", + cn: "本地文件夹...", + tw: "本地文件夾...", + ko: "로컬 폴더..." }, gdriveFolder: { - en: "Google Drive..." + ja: "Google ドライブ...", + en: "Google Drive...", + cn: "Google云端硬盘...", + tw: "Google雲端硬碟...", + ko: "구글 드라이브..." + }, + dropzone: { + ja: "ここにファイルをドロップ", + en: "Drop files here", + cn: "将文件拖至此处", + tw: "將文件拖至此處", + ko: "파일을 여기에 드롭" + }, + importError: { + en: "Import Error" + }, + noSongs: { + en: "No Taiko chart files have been found in the provided folder." } }, gpicker: { + locale: { + ja: "ja", + en: "en-GB", + cn: "zh-CN", + tw: "zh-TW", + ko: "ko" + }, myDrive: { - en: "My Drive" + ja: "マイドライブ", + en: "My Drive", + cn: "我的云端硬盘", + tw: "我的雲端硬碟", + ko: "내 드라이브" }, starred: { - en: "Starred" + ja: "スター付き", + en: "Starred", + cn: "已加星标", + tw: "已加星號", + ko: "중요 문서함" }, sharedWithMe: { - en: "Shared with me" + ja: "共有アイテム", + en: "Shared with me", + cn: "与我共享", + tw: "與我共用", + ko: "공유 문서함" }, authError: { en: "Auth error: %s" diff --git a/public/src/js/viewassets.js b/public/src/js/viewassets.js index 5158611..62ca25b 100644 --- a/public/src/js/viewassets.js +++ b/public/src/js/viewassets.js @@ -175,7 +175,9 @@ class ViewAssets{ }) } clean(){ - this.don.clean() + if(this.don){ + this.don.clean() + } delete this.ctx delete this.don delete this.fire diff --git a/public/src/views/customsongs.html b/public/src/views/customsongs.html index 585a233..f7f4dcf 100644 --- a/public/src/views/customsongs.html +++ b/public/src/views/customsongs.html @@ -1,12 +1,24 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
From 1d025848c43f7b771b5d8302b0e00ad26ee35b07 Mon Sep 17 00:00:00 2001 From: LoveEevee Date: Wed, 4 Nov 2020 03:16:45 +0300 Subject: [PATCH 7/7] Fix local folder button --- public/src/js/customsongs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index b0a2a0a..d985c24 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -107,7 +107,7 @@ class CustomSongs{ element.innerText = text element.setAttribute("alt", text) } - localFolder(){ + localFolder(event){ if(event){ if(event.type === "touchstart"){ event.preventDefault()