Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 308 additions & 68 deletions Packages/StandaloneFileBrowser/Plugins/StandaloneFileBrowser.jslib
Original file line number Diff line number Diff line change
@@ -1,87 +1,327 @@
var StandaloneFileBrowserWebGLPlugin = {
// Open file.
// gameObjectNamePtr: Unique GameObject name. Required for calling back unity with SendMessage.
// methodNamePtr: Callback method name on given GameObject.
// filter: Filter files. Example filters:
// Match all image files: "image/*"
// Match all video files: "video/*"
// Match all audio files: "audio/*"
// Custom: ".plist, .xml, .yaml"
// multiselect: Allows multiple file selection
UploadFile: function(gameObjectNamePtr, methodNamePtr, filterPtr, multiselect) {
gameObjectName = UTF8ToString(gameObjectNamePtr);
methodName = UTF8ToString(methodNamePtr);
filter = UTF8ToString(filterPtr);

// Delete if element exist
var fileInput = document.getElementById(gameObjectName)
if (fileInput) {
document.body.removeChild(fileInput);
$SFBHelpers: {
initialized: false,
state: null,

getGlobal: function() {
if (typeof globalThis !== 'undefined') return globalThis;
if (typeof self !== 'undefined') return self;
if (typeof window !== 'undefined') return window;
return this;
},

ensureState: function() {
if (this.initialized) return this.state;
var globalObj = this.getGlobal();
if (!globalObj.__sfbWebGLState) {
globalObj.__sfbWebGLState = {
nextHandleId: 0,
activeOpenRequest: null,
activeSaveRequest: null,
filesByHandle: {}
};
}
this.state = globalObj.__sfbWebGLState;
this.initialized = true;
return this.state;
},

getState: function() {
return this.ensureState();
},

createHandleId: function() {
var state = this.getState();
state.nextHandleId += 1;
return 'handle-' + state.nextHandleId;
},

createOpenResponse: function(requestId, status, files, errorMessage) {
return {
type: 'open',
requestId: requestId || '',
status: status || 'error',
files: files || [],
errorMessage: errorMessage || ''
};
},

createSaveResponse: function(requestId, status, errorMessage) {
return {
type: 'save',
requestId: requestId || '',
status: status || 'error',
errorMessage: errorMessage || ''
};
},

sendMessage: function(gameObjectName, methodName, payload) {
SendMessage(gameObjectName, methodName, JSON.stringify(payload));
},

revokeHandle: function(handleId) {
var state = this.getState();
if (!handleId || !state.filesByHandle[handleId]) return;
var fileEntry = state.filesByHandle[handleId];
delete state.filesByHandle[handleId];
if (fileEntry.objectUrl) {
URL.revokeObjectURL(fileEntry.objectUrl);
}
},

revokeAllHandles: function() {
var state = this.getState();
for (var handleId in state.filesByHandle) {
if (state.filesByHandle.hasOwnProperty(handleId)) {
this.revokeHandle(handleId);
}
}
},

cleanupOpenRequest: function(request) {
if (!request) return;

if (request.input) {
if (request.changeHandler) request.input.removeEventListener('change', request.changeHandler);
if (request.cancelHandler) request.input.removeEventListener('cancel', request.cancelHandler);
if (request.input.parentNode) request.input.parentNode.removeChild(request.input);
}
var state = this.getState();
if (state.activeOpenRequest === request) state.activeOpenRequest = null;
},

completeOpenRequest: function(request, payload) {
if (!request || request.completed) return;
request.completed = true;
this.cleanupOpenRequest(request);
this.sendMessage(request.gameObjectName, request.methodName, payload);
},

cleanupSaveRequest: function(request, revokeObjectUrlAsync) {
if (!request) return;
if (request.anchor && request.anchor.parentNode) {
request.anchor.parentNode.removeChild(request.anchor);
}
var objectUrl = request.objectUrl;
request.anchor = null;
request.objectUrl = null;
var state = this.getState();
if (state.activeSaveRequest === request) state.activeSaveRequest = null;
if (!objectUrl) return;
if (revokeObjectUrlAsync) {
window.setTimeout(function() { URL.revokeObjectURL(objectUrl); }, 0);
return;
}
URL.revokeObjectURL(objectUrl);
},

completeSaveRequest: function(request, payload) {
if (!request || request.completed) return;
request.completed = true;
this.cleanupSaveRequest(request, payload && payload.status === 'success');
this.sendMessage(request.gameObjectName, request.methodName, payload);
}
},

StandaloneFileBrowserWebGLOpenFilePanel: function(gameObjectNamePtr, methodNamePtr, requestIdPtr, filterPtr, multiselect) {
var helpers = SFBHelpers;

fileInput = document.createElement('input');
fileInput.setAttribute('id', gameObjectName);
fileInput.setAttribute('type', 'file');
fileInput.setAttribute('style','display:none;');
fileInput.setAttribute('style','visibility:hidden;');
if (multiselect) {
fileInput.setAttribute('multiple', '');
var gameObjectName = UTF8ToString(gameObjectNamePtr);
var methodName = UTF8ToString(methodNamePtr);
var requestId = UTF8ToString(requestIdPtr);
var filter = UTF8ToString(filterPtr);
var state = helpers.getState();

if (state.activeOpenRequest) {
helpers.sendMessage(
gameObjectName,
methodName,
helpers.createOpenResponse(requestId, 'busy', [], '')
);
return;
}
if (filter) {
fileInput.setAttribute('accept', filter);

if (typeof navigator !== 'undefined' && navigator.userActivation && navigator.userActivation.isActive === false) {
var errorMessage = 'Browser file picker must be triggered from a user gesture.';
if (typeof console !== 'undefined' && console.error) console.error(errorMessage);
helpers.sendMessage(
gameObjectName,
methodName,
helpers.createOpenResponse(requestId, 'error', [], errorMessage)
);
return;
}
fileInput.onclick = function (event) {
// File dialog opened
this.value = null;

var request = {
gameObjectName: gameObjectName,
methodName: methodName,
requestId: requestId,
input: null,
changeHandler: null,
cancelHandler: null,
focusHandler: null,
completed: false
};
fileInput.onchange = function (event) {
// multiselect works
var urls = [];
for (var i = 0; i < event.target.files.length; i++) {
urls.push(URL.createObjectURL(event.target.files[i]));
}
// File selected
SendMessage(gameObjectName, methodName, urls.join());
state.activeOpenRequest = request;

// Remove after file selected
document.body.removeChild(fileInput);
}
document.body.appendChild(fileInput);
try {
var fileInput = document.createElement('input');
request.input = fileInput;

fileInput.setAttribute('type', 'file');
fileInput.setAttribute('style', 'position:fixed;left:-10000px;top:-10000px;opacity:0;pointer-events:none;');

if (multiselect) fileInput.setAttribute('multiple', '');
if (filter) fileInput.setAttribute('accept', filter);

fileInput.onclick = function() { this.value = ''; };

request.changeHandler = function(event) {
var selectedFiles = event.target && event.target.files ? event.target.files : [];
var files = [];

for (var i = 0; i < selectedFiles.length; i++) {
var browserFile = selectedFiles[i];
var handleId = helpers.createHandleId();
var objectUrl = URL.createObjectURL(browserFile);
var fileState = helpers.getState();

fileState.filesByHandle[handleId] = {
objectUrl: objectUrl,
name: browserFile.name || '',
size: browserFile.size || 0,
mimeType: browserFile.type || '',
lastModifiedUnixMs: browserFile.lastModified || 0
};

files.push({
handleId: handleId,
objectUrl: objectUrl,
name: browserFile.name || '',
size: String(browserFile.size || 0),
mimeType: browserFile.type || '',
lastModifiedUnixMs: String(browserFile.lastModified || 0)
});
}

helpers.completeOpenRequest(
request,
helpers.createOpenResponse(
request.requestId,
files.length > 0 ? 'success' : 'cancelled',
files,
''
)
);
};

request.cancelHandler = function() {
helpers.completeOpenRequest(
request,
helpers.createOpenResponse(request.requestId, 'cancelled', [], '')
);
};

fileInput.addEventListener('change', request.changeHandler);
fileInput.addEventListener('cancel', request.cancelHandler);

fileInput.click();
document.body.appendChild(fileInput);
fileInput.click();
} catch (error) {
helpers.completeOpenRequest(
request,
helpers.createOpenResponse(
request.requestId,
'error',
[],
error && error.message ? error.message : 'Browser file open failed.'
)
);
}
},

// Save file
// DownloadFile method does not open SaveFileDialog like standalone builds, its just allows user to download file
// gameObjectNamePtr: Unique GameObject name. Required for calling back unity with SendMessage.
// methodNamePtr: Callback method name on given GameObject.
// filenamePtr: Filename with extension
// byteArray: byte[]
// byteArraySize: byte[].Length
DownloadFile: function(gameObjectNamePtr, methodNamePtr, filenamePtr, byteArray, byteArraySize) {
gameObjectName = UTF8ToString(gameObjectNamePtr);
methodName = UTF8ToString(methodNamePtr);
filename = UTF8ToString(filenamePtr);

var bytes = new Uint8Array(byteArraySize);
for (var i = 0; i < byteArraySize; i++) {
bytes[i] = HEAPU8[byteArray + i];
StandaloneFileBrowserWebGLSaveFile: function(gameObjectNamePtr, methodNamePtr, requestIdPtr, fileNamePtr, mimeTypePtr, byteArray, byteArraySize) {
var helpers = SFBHelpers;

var gameObjectName = UTF8ToString(gameObjectNamePtr);
var methodName = UTF8ToString(methodNamePtr);
var requestId = UTF8ToString(requestIdPtr);
var fileName = UTF8ToString(fileNamePtr);
var mimeType = UTF8ToString(mimeTypePtr);
var state = helpers.getState();

if (state.activeSaveRequest) {
helpers.sendMessage(
gameObjectName,
methodName,
helpers.createSaveResponse(requestId, 'busy', '')
);
return;
}

var downloader = window.document.createElement('a');
downloader.setAttribute('id', gameObjectName);
downloader.href = window.URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));
downloader.download = filename;
document.body.appendChild(downloader);
var request = {
gameObjectName: gameObjectName,
methodName: methodName,
requestId: requestId,
anchor: null,
objectUrl: null,
completed: false
};
state.activeSaveRequest = request;

try {
if (typeof navigator !== 'undefined' && navigator.userActivation && navigator.userActivation.isActive === false) {
helpers.completeSaveRequest(
request,
helpers.createSaveResponse(
request.requestId,
'blocked-by-browser',
'Browser download must be triggered from a user gesture.'
)
);
return;
}

var bytes = new Uint8Array(byteArraySize);
if (byteArraySize > 0) bytes.set(HEAPU8.subarray(byteArray, byteArray + byteArraySize));

document.onmouseup = function() {
downloader.click();
document.body.removeChild(downloader);
document.onmouseup = null;
var blob = new Blob([bytes], { type: mimeType || 'application/octet-stream' });
request.objectUrl = URL.createObjectURL(blob);
request.anchor = document.createElement('a');
request.anchor.setAttribute('download', fileName);
request.anchor.setAttribute('style', 'display:none;');
request.anchor.href = request.objectUrl;
document.body.appendChild(request.anchor);
request.anchor.click();

SendMessage(gameObjectName, methodName);
helpers.completeSaveRequest(
request,
helpers.createSaveResponse(request.requestId, 'success', '')
);
} catch (error) {
helpers.completeSaveRequest(
request,
helpers.createSaveResponse(
request.requestId,
'error',
error && error.message ? error.message : 'Browser file save failed.'
)
);
}
},

StandaloneFileBrowserWebGLReleaseFile: function(handleIdPtr) {
var helpers = SFBHelpers;
var handleId = UTF8ToString(handleIdPtr);
helpers.revokeHandle(handleId);
},

StandaloneFileBrowserWebGLReleaseAllFiles: function() {
var helpers = SFBHelpers;
helpers.revokeAllHandles();
}
};

autoAddDeps(StandaloneFileBrowserWebGLPlugin, '$SFBHelpers');

mergeInto(LibraryManager.library, StandaloneFileBrowserWebGLPlugin);
Loading