taiko-web/public/src/js/gpicker.js
KatieFrogs e43c4afceb Lyrics, search, and other fixes
- #LYRIC
  - Parse #LYRIC commands and apply them to all difficulties that do not have them
  - #LYRIC command now supports branches
  - Fix last #LYRIC at the end of the chart getting ignored
- Fix the glitchy dragging and dropping of files on the custom song importing page
- Fix Ctrl and Shift keys getting stuck on song select when switching tabs with Ctrl(+Shift)+Tab
- Search
  - Fix the search box "random:yes" query to randomize the entire results and not just the first 50
  - Add "all:yes" query to the search box to remove the result limit and display all of the results
  - Fix searching for an invalid query (like "cleared:yes" or ":") unexpectedly returning all the songs
  - Fix pressing Q then jumping to a song through search not unmuting the sound
  - Pressing the search key on mobile will hide the keyboard
  - Fix search tips changing rapidly when the window is resized
- Use comments instead of `######` in the issue template so that the warning does not appear in the issue
- Fix TJA MAKER: url between angle brackets not working
- Add a check for Class field declarations in the browser support warning
- Fix gpicker getting stuck if a network error occurs
- Fix not being able to replace some assets using a "taiko-web assets" folder
- Fix selectable song title not being aligned with the game if the game window is too wide
- Allow plugin developers to use the "select" type for the settings options
  - It uses "options" array and "options_lang" object
- Fix plugins not getting removed from the plugin list on syntax error
- Fix error messages not working if a default plugin is broken
- Fix the start of default plugins not stopping the page from loading on error
- Fix not being able to scroll the plugins screen on mobile
2022-07-15 16:00:43 +02:00

286 lines
8.0 KiB
JavaScript

class Gpicker{
constructor(...args){
this.init(...args)
}
init(){
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
this.clientCallbackBind = this.clientCallback.bind(this)
}
browse(lockedCallback, errorCallback){
return this.loadApi(lockedCallback, errorCallback)
.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=[]) => {
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){
folders.push({
path: path + file.name,
id: file.id
})
}else{
output.push(new GdriveFile({
path: path + file.name,
name: file.name,
id: file.id
}))
}
}
}
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
}
}
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(lockedCallback=()=>{}, errorCallback=()=>{}){
if(window.gapi && gapi.client && gapi.client.drive){
return Promise.resolve()
}
var promises = [
loader.loadScript("https://apis.google.com/js/api.js"),
loader.loadScript("https://accounts.google.com/gsi/client")
]
var apiLoaded = false
return Promise.all(promises).then(() => new Promise((resolve, reject) =>
gapi.load("picker:client", {
callback: resolve,
onerror: reject
})
))
.then(() => new Promise((resolve, reject) => {
setTimeout(() => {
if(!apiLoaded){
lockedCallback(false)
}
}, 3000)
return gapi.client.load("drive", "v3").then(resolve, reject)
})).then(() => {
apiLoaded = true
lockedCallback(true)
}).catch(e => {
errorCallback(Array.isArray(e) ? e[0] : e)
return Promise.reject("cancel")
})
}
getClient(errorCallback=()=>{}, force){
var obj = {
client_id: this.oauthClientId,
scope: this.scope,
callback: this.clientCallbackBind
}
if(force){
if(!this.clientForce){
obj.select_account = true
this.clientForce = google.accounts.oauth2.initTokenClient(obj)
}
return this.clientForce
}else{
if(!this.client){
this.client = google.accounts.oauth2.initTokenClient(obj)
}
return this.client
}
}
clientCallback(tokenResponse){
this.tokenResponse = tokenResponse
this.oauthToken = tokenResponse && tokenResponse.access_token
if(this.oauthToken && this.tokenResolve){
this.tokenResolve()
}
}
getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){
if(this.oauthToken && !force){
return Promise.resolve()
}
var client = this.getClient(errorCallback, force)
var promise = new Promise(resolve => {
this.tokenResolve = resolve
})
lockedCallback(false)
client.requestAccessToken()
return promise.then(() => {
this.tokenResolve = null
if(this.checkScope()){
lockedCallback(true)
}else{
return Promise.reject("cancel")
}
})
}
checkScope(){
return google.accounts.oauth2.hasGrantedAnyScope(this.tokenResponse, this.scope)
}
switchAccounts(lockedCallback, errorCallback){
return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true))
}
displayPicker(callback){
var picker = gapi.picker.api
new picker.PickerBuilder()
.setDeveloperKey(this.apiKey)
.setAppId(this.projectNumber)
.setOAuthToken(this.oauthToken)
.setLocale(strings.gpicker.locale)
.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, responseType, retry){
var url = this.filesUrl + id + "?alt=media"
return this.queue().then(this.getToken.bind(this)).then(() =>
loader.ajax(url, request => {
if(responseType){
request.responseType = responseType
}
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, responseType, true)
}else{
return reject()
}
}, reject)
}
return reject()
})
)
}
sleep(time){
return new Promise(resolve => setTimeout(resolve, time))
}
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)
}
}
}