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
This commit is contained in:
LoveEevee 2020-11-04 03:12:46 +03:00
parent 180ec58adb
commit 5094b0bc70
11 changed files with 345 additions and 66 deletions

View File

@ -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;
}

View File

@ -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
}

View File

@ -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++;
}

View File

@ -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)

View File

@ -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,9 +252,10 @@ class CustomSongs{
return
}
var selected = this.items[this.selected]
if(this.mode === "main"){
if(name === "confirm" || name === "confirmPad"){
if(selected === this.endButton){
this.onEnd()
this.onEnd(null, true)
}else if(name !== "confirmPad"){
if(selected === this.linkLocalFolder){
assets.sounds["se_don"].play()
@ -171,15 +270,30 @@ class CustomSongs{
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"){
}else if(name === "back" || name === "backEsc"){
if(!this.dragging || name !== "backEsc"){
this.onEnd()
}
}
}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.items.findIndex(item => item === button)
this.items[this.selected].classList.add("selected")
}
}
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
}
}

View File

@ -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)
}
}))
}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)

View File

@ -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()
}

View File

@ -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,

View File

@ -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"

View File

@ -175,7 +175,9 @@ class ViewAssets{
})
}
clean(){
if(this.don){
this.don.clean()
}
delete this.ctx
delete this.don
delete this.fire

View File

@ -1,12 +1,24 @@
<div class="view-outer">
<div class="view">
<div class="view drag-bg">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="left-buttons">
<div class="center-buttons">
<div id="link-localfolder" class="taibtn stroke-sub link-btn"></div>
<div id="link-gdrivefolder" class="taibtn stroke-sub link-btn"></div>
</div>
<div class="view-end-button taibtn stroke-sub selected"></div>
<div class="view-outer shadow-outer" id="dropzone">
<div class="view">
<div class="view-content"></div>
</div>
</div>
<div class="view-outer shadow-outer" id="customsongs-error">
<div class="view">
<div class="view-title stroke-sub"></div>
<div class="view-content"></div>
<div class="view-end-button taibtn stroke-sub selected"></div>
</div>
</div>
</div>
<form><input id="browse" type="file" webkitdirectory multiple></form>
</div>