diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index 93d176a..44b4bbb 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -4,6 +4,21 @@ function readFile(file, arrayBuffer, encoding){ reader[arrayBuffer ? "readAsArrayBuffer" : "readAsText"](file, encoding) return promise } +function filePermission(file){ + return file.queryPermission().then(response => { + if(response === "granted"){ + return file + }else{ + return file.requestPermission().then(response => { + if(response === "granted"){ + return file + }else{ + return Promise.reject(file) + } + }) + } + }) +} class RemoteFile{ constructor(url){ this.url = url @@ -54,6 +69,23 @@ class LocalFile{ return Promise.resolve(this.file) } } +class FilesystemFile{ + constructor(file, path){ + this.file = file + this.path = path + this.url = this.path + this.name = file.name + } + arrayBuffer(){ + return this.blob().then(blob => blob.arrayBuffer()) + } + read(encoding){ + return this.blob().then(blob => readFile(blob, false, encoding)) + } + blob(){ + return filePermission(this.file).then(file => file.getFile()) + } +} class GdriveFile{ constructor(fileObj){ this.path = fileObj.path diff --git a/public/src/js/assets.js b/public/src/js/assets.js index c8b23cb..f449487 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -35,7 +35,8 @@ var assets = { "account.js", "lyrics.js", "customsongs.js", - "abstractfile.js" + "abstractfile.js", + "idb.js" ], "css": [ "main.css", diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index 6edc60c..ab98800 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -1,12 +1,23 @@ class CustomSongs{ - constructor(touchEnabled){ + constructor(touchEnabled, noPage){ + this.loaderDiv = document.createElement("div") + this.loaderDiv.innerHTML = assets.pages["loadsong"] + var loadingText = this.loaderDiv.querySelector("#loading-text") + this.setAltText(loadingText, strings.loading) + + this.locked = false + this.mode = "main" + + if(noPage){ + this.noPage = true + return + } + this.touchEnabled = touchEnabled loader.changePage("customsongs", true) if(touchEnabled){ 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) @@ -19,7 +30,7 @@ class CustomSongs{ this.items = [] this.linkLocalFolder = document.getElementById("link-localfolder") - this.hasLocal = "webkitdirectory" in HTMLInputElement.prototype && !(/Android|iPhone|iPad/.test(navigator.userAgent)) + this.hasLocal = (typeof showDirectoryPicker === "function" || "webkitdirectory" in HTMLInputElement.prototype) && !(/Android|iPhone|iPad/.test(navigator.userAgent)) this.selected = -1 if(this.hasLocal){ @@ -68,12 +79,7 @@ class CustomSongs{ this.selected = this.items.length - 1 } - this.loaderDiv = document.createElement("div") - this.loaderDiv.innerHTML = assets.pages["loadsong"] - var loadingText = this.loaderDiv.querySelector("#loading-text") - this.setAltText(loadingText, strings.loading) - - if(DataTransferItem.prototype.webkitGetAsEntry){ + if(DataTransferItem.prototype.getAsFileSystemHandle || DataTransferItem.prototype.webkitGetAsEntry){ this.dropzone = document.getElementById("dropzone") var dropContent = this.dropzone.getElementsByClassName("view-content")[0] dropContent.innerText = strings.customSongs.dropzone @@ -142,7 +148,19 @@ class CustomSongs{ return } this.changeSelected(this.linkLocalFolder) - this.browse.click() + if(typeof showDirectoryPicker === "function"){ + return showDirectoryPicker().then(file => { + this.walkFilesystem(file).then(files => this.importLocal(files)).then(e => { + db.setItem("customFolder", file) + }).catch(e => { + if(e !== "cancel"){ + return Promise.reject(e) + } + }) + }, () => {}) + }else{ + this.browse.click() + } } browseChange(event){ var files = [] @@ -151,6 +169,24 @@ class CustomSongs{ } this.importLocal(files) } + walkFilesystem(dir, path=dir.name + "/", output=[]){ + return filePermission(dir).then(dir => { + var values = dir.values() + var walkValues = () => values.next().then(generator => { + if(generator.done){ + return output + } + var file = generator.value + if(file.kind === "directory"){ + return this.walkFilesystem(file, path + file.name + "/", output).then(() => walkValues()) + }else{ + output.push(new FilesystemFile(file, path + file.name)) + return walkValues() + } + }) + return walkValues() + }, () => Promise.resolve()) + } filesDropped(event){ event.preventDefault() this.dropzone.classList.remove("dragover") @@ -158,46 +194,69 @@ class CustomSongs{ 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 allFiles = [] 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)) + var dropLength = event.dataTransfer.items.length + for(var i = 0; i < dropLength; i++){ + var item = event.dataTransfer.items[i] + let promise + if(item.getAsFileSystemHandle){ + promise = item.getAsFileSystemHandle().then(file => { + if(file.kind === "directory"){ + return this.walkFilesystem(file).then(files => { + if(files.length && dropLength === 1){ + db.setItem("customFolder", file) + } + return files + }) + }else{ + return [new FilesystemFile(file, file.name)] + } + }) + }else{ + var entry = item.webkitGetAsEntry() + if(entry){ + promise = this.walkEntry(entry) + } + } + if(promise){ + dropPromises.push(promise.then(files => { + allFiles = allFiles.concat(files) + })) } } - Promise.all(dropPromises).then(() => this.importLocal(files)) + Promise.all(dropPromises).then(() => this.importLocal(allFiles)) + } + walkEntry(entry, path="", output=[]){ + return new Promise(resolve => { + if(entry.isFile){ + entry.file(file => { + output.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(this.walkEntry(entries[i], path + entry.name + "/", output)) + } + return Promise.all(dirPromises).then(resolve) + }, resolve) + }else{ + return resolve() + } + }).then(() => output) } importLocal(files){ if(!files.length){ - return + return Promise.resolve("cancel") } this.locked = true this.loading(true) var importSongs = new ImportSongs() - importSongs.load(files).then(this.songsLoaded.bind(this), e => { + return importSongs.load(files).then(this.songsLoaded.bind(this), e => { this.browse.parentNode.reset() this.locked = false this.loading(false) @@ -315,7 +374,7 @@ class CustomSongs{ var length = songs.length assets.songs = songs assets.customSongs = true - assets.customSelected = 0 + assets.customSelected = this.noPage ? +localStorage.getItem("customSelected") : 0 } assets.sounds["se_don"].play() this.clean() @@ -393,15 +452,18 @@ class CustomSongs{ touched = this.touchEnabled } this.clean() - assets.sounds[confirm ? "se_don" : "se_cancel"].play() - setTimeout(() => { + if(!this.noPage){ + assets.sounds[confirm ? "se_don" : "se_cancel"].play() + } + return new Promise(resolve => setTimeout(() => { new SongSelect("customSongs", false, touched) - }, 500) + resolve() + }, 500)) } showError(text){ this.locked = false this.loading(false) - if(this.mode === "error"){ + if(this.noPage || this.mode === "error"){ return } this.mode = "error" @@ -418,6 +480,10 @@ class CustomSongs{ assets.sounds[confirm ? "se_don" : "se_cancel"].play() } clean(){ + delete this.loaderDiv + if(this.noPage){ + return + } this.keyboard.clean() this.gamepad.clean() pageEvents.remove(this.browse, "change") @@ -443,7 +509,6 @@ class CustomSongs{ delete this.linkPrivacy delete this.endButton delete this.items - delete this.loaderDiv delete this.errorDiv delete this.errorContent delete this.errorEnd diff --git a/public/src/js/idb.js b/public/src/js/idb.js new file mode 100644 index 0000000..6ca2640 --- /dev/null +++ b/public/src/js/idb.js @@ -0,0 +1,51 @@ +class IDB{ + constructor(name, store){ + this.name = name + this.store = store + } + init(){ + if(this.db){ + return Promise.resolve(this.db) + } + var request = indexedDB.open(this.name) + request.onupgradeneeded = event => { + var db = event.target.result + db.createObjectStore(this.store) + } + return this.promise(request).then(result => { + this.db = result + return this.db + }, target => + console.warn("DB error", target) + ) + } + promise(request){ + return new Promise((resolve, reject) => { + return pageEvents.race(request, "success", "error").then(response => { + if(response.type === "success"){ + return resolve(event.target.result) + }else{ + return reject(event.target) + } + }) + }) + } + transaction(method, ...args){ + return this.init().then(db => + db.transaction(this.store, "readwrite").objectStore(this.store)[method](...args) + ).then(this.promise.bind(this)) + } + getItem(name){ + return this.transaction("get", name) + } + setItem(name, value){ + return this.transaction("put", value, name) + } + removeItem(name){ + return this.transaction("delete", name) + } + removeDB(){ + delete this.db + return indexedDB.deleteDatabase(this.name) + } +} diff --git a/public/src/js/loader.js b/public/src/js/loader.js index b077deb..9053d99 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -252,6 +252,7 @@ class Loader{ settings = new Settings() pageEvents.setKbd() scoreStorage = new ScoreStorage() + db = new IDB("taiko", "store") Promise.all(this.promises).then(() => { if(this.error){ diff --git a/public/src/js/main.js b/public/src/js/main.js index b314147..2077fcd 100644 --- a/public/src/js/main.js +++ b/public/src/js/main.js @@ -90,6 +90,7 @@ var settings var scoreStorage var account = {} var gpicker +var db 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 f775e07..6478ef3 100644 --- a/public/src/js/songselect.js +++ b/public/src/js/songselect.js @@ -210,7 +210,7 @@ class SongSelect{ if(!assets.customSongs && !fromTutorial && !("selectedSong" in localStorage) && !songId){ fromTutorial = touchEnabled ? "about" : "tutorial" } - if(p2.session){ + if(p2.session || assets.customSongs && "customSelected" in localStorage){ fromTutorial = false } @@ -231,7 +231,7 @@ class SongSelect{ if(songIdIndex !== -1){ this.selectedSong = songIdIndex }else if(assets.customSongs){ - this.selectedSong = assets.customSelected + this.selectedSong = Math.min(Math.max(0, assets.customSelected), this.songs.length - 1) }else if((!p2.session || fadeIn) && "selectedSong" in localStorage){ this.selectedSong = Math.min(Math.max(0, localStorage["selectedSong"] |0), this.songs.length - 1) } @@ -508,7 +508,7 @@ class SongSelect{ moveTo = "account" }else if(p2.session && 438 < mouse.x && mouse.x < 834 && mouse.y > 603){ moveTo = "session" - }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket.readyState === 1 && !assets.customSongs){ + }else if(!p2.session && mouse.x > 641 && mouse.y > 603 && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){ moveTo = "session" }else{ var moveTo = this.songSelMouse(mouse.x, mouse.y) @@ -739,6 +739,7 @@ class SongSelect{ try{ if(assets.customSongs){ assets.customSelected = this.selectedSong + localStorage["customSelected"] = this.selectedSong }else{ localStorage["selectedSong"] = this.selectedSong } @@ -832,7 +833,7 @@ class SongSelect{ this.state.moveHover = null }else{ localStorage["selectedSong"] = this.selectedSong - + this.playSound("se_don") this.clean() setTimeout(() => { @@ -850,6 +851,8 @@ class SongSelect{ setTimeout(() => { new SongSelect("customSongs", false, this.touchEnabled) }, 500) + localStorage.removeItem("customSelected") + db.removeItem("customFolder") pageEvents.send("import-songs-default") }else{ localStorage["selectedSong"] = this.selectedSong @@ -1174,6 +1177,7 @@ class SongSelect{ this.state.locked = 2 if(assets.customSongs){ assets.customSelected = this.selectedSong + localStorage["customSelected"] = this.selectedSong }else if(!p2.session){ try{ localStorage["selectedSong"] = this.selectedSong @@ -2097,7 +2101,7 @@ class SongSelect{ ctx.lineTo(x + 4, y + 4) ctx.lineTo(x + 4, y + h) ctx.fill() - if(screen !== "difficulty" && p2.socket.readyState === 1 && !assets.customSongs){ + if(screen !== "difficulty" && p2.socket && p2.socket.readyState === 1 && !assets.customSongs){ var elapsed = (ms - this.state.screenMS) % 3100 var fade = 1 if(!p2.session && screen === "song"){ diff --git a/public/src/js/titlescreen.js b/public/src/js/titlescreen.js index aa98e4d..d1d3b2a 100644 --- a/public/src/js/titlescreen.js +++ b/public/src/js/titlescreen.js @@ -1,6 +1,7 @@ class Titlescreen{ constructor(songId){ this.songId = songId + db.getItem("customFolder").then(folder => this.customFolder = folder) if(!songId){ loader.changePage("titlescreen", false) @@ -50,7 +51,7 @@ class Titlescreen{ onPressed(pressed, name){ if(pressed){ - if(name === "gamepadConfirm" && snd.buffer.context.state === "suspended"){ + if(name === "gamepadConfirm" && (snd.buffer.context.state === "suspended" || this.customFolder)){ return } this.titleScreen.style.cursor = "auto" @@ -62,18 +63,28 @@ class Titlescreen{ goNext(fromP2){ if(p2.session && !fromP2){ p2.send("songsel") - }else if(fromP2 || localStorage.getItem("tutorial") === "true"){ - if(this.touched){ - localStorage.setItem("tutorial", "true") - } - pageEvents.remove(p2, "message") - setTimeout(() => { - new SongSelect(false, false, this.touched, this.songId) - }, 500) }else{ - setTimeout(() => { - new SettingsView(this.touched, true, this.songId) - }, 500) + if(fromP2 || this.customFolder || localStorage.getItem("tutorial") === "true"){ + if(this.touched){ + localStorage.setItem("tutorial", "true") + } + pageEvents.remove(p2, "message") + if(this.customFolder && !fromP2 && !assets.customSongs){ + var customSongs = new CustomSongs(this.touched, true) + customSongs.walkFilesystem(this.customFolder).then(files => customSongs.importLocal(files)).catch(() => { + db.removeItem("customFolder") + new SongSelect(false, false, this.touched, this.songId) + }) + }else{ + setTimeout(() => { + new SongSelect(false, false, this.touched, this.songId) + }, 500) + } + }else{ + setTimeout(() => { + new SettingsView(this.touched, true, this.songId) + }, 500) + } } } setLang(lang, noEvent){