X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=frontend%2Fjs%2Fapp.js;h=f532bc1e508515a9196329474af9e53450644ce9;hb=2d28c88dd7a6765a6d2ed54758bbb4c3da999331;hp=de61dcf8f522aa48c49d929feba1e52e0891bc30;hpb=9b7a26fc3708ac42d7d29c4329adbde465d29220;p=perso%2FImmae%2FProjets%2FNodejs%2FSurfer.git diff --git a/frontend/js/app.js b/frontend/js/app.js index de61dcf..f532bc1 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,56 +1,54 @@ (function () { 'use strict'; -function getProfile(accessToken, callback) { - callback = callback || function (error) { if (error) console.error(error); }; +/* global superagent */ +/* global Vue */ +/* global $ */ +/* global filesize */ +// poor man's async +function asyncForEach(items, handler, callback) { + var cur = 0; + + if (items.length === 0) return callback(); + + (function iterator() { + handler(items[cur], function (error) { + if (error) return callback(error); + if (cur >= items.length-1) return callback(); + ++cur; + + iterator(); + }); + })(); +} + +function initWithToken(accessToken) { superagent.get('/api/profile').query({ access_token: accessToken }).end(function (error, result) { - app.busy = false; + app.ready = true; - if (error && !error.response) return callback(error); + if (error && !error.response) return console.error(error); if (result.statusCode !== 200) { delete localStorage.accessToken; - return callback('Invalid access token'); + return; } localStorage.accessToken = accessToken; app.session.username = result.body.username; app.session.valid = true; - callback(); - }); -} - -function login(username, password) { - username = username || app.loginData.username; - password = password || app.loginData.password; - - app.busy = true; - - superagent.post('/api/login').send({ username: username, password: password }).end(function (error, result) { - app.busy = false; + superagent.get('/api/settings').query({ access_token: localStorage.accessToken }).end(function (error, result) { + if (error) console.error(error); - if (error) return console.error(error); - if (result.statusCode === 401) return console.error('Invalid credentials'); + app.folderListingEnabled = !!result.body.folderListingEnabled; - getProfile(result.body.accessToken, function (error) { - if (error) return console.error(error); + loadDirectory(decode(window.location.hash.slice(1))); - loadDirectory(window.location.hash.slice(1)); + app.refreshAccessTokens(); }); }); } -function logout() { - superagent.post('/api/logout').query({ access_token: localStorage.accessToken }).end(function (error) { - if (error) console.error(error); - - app.session.valid = false; - - delete localStorage.accessToken; - }); -} - function sanitize(filePath) { filePath = '/' + filePath; return filePath.replace(/\/+/g, '/'); @@ -69,7 +67,7 @@ var mimeTypes = { text: [ '.txt', '.md' ], pdf: [ '.pdf' ], html: [ '.html', '.htm', '.php' ], - video: [ '.mp4', '.mpg', '.mpeg', '.ogg', '.mkv' ] + video: [ '.mp4', '.mpg', '.mpeg', '.ogg', '.mkv', '.avi', '.mov' ] }; function getPreviewUrl(entry, basePath) { @@ -85,10 +83,26 @@ function getPreviewUrl(entry, basePath) { return path + 'unknown.png'; } +// simple extension detection, does not work with double extension like .tar.gz +function getExtension(entry) { + if (entry.isFile) return entry.filePath.slice(entry.filePath.lastIndexOf('.') + 1); + return ''; +} + function refresh() { loadDirectory(app.path); } +function logout() { + superagent.post('/api/logout').query({ access_token: localStorage.accessToken }).end(function (error) { + if (error) console.error(error); + + app.session.valid = false; + + delete localStorage.accessToken; + }); +} + function loadDirectory(filePath) { app.busy = true; @@ -103,6 +117,9 @@ function loadDirectory(filePath) { result.body.entries.sort(function (a, b) { return a.isDirectory && b.isFile ? -1 : 1; }); app.entries = result.body.entries.map(function (entry) { entry.previewUrl = getPreviewUrl(entry, filePath); + entry.extension = getExtension(entry); + entry.rename = false; + entry.filePathNew = entry.filePath; return entry; }); app.path = filePath; @@ -115,246 +132,382 @@ function loadDirectory(filePath) { // update in case this was triggered from code window.location.hash = app.path; - - Vue.nextTick(function () { - $(function () { - $('[data-toggle="tooltip"]').tooltip(); - }); - }); }); } -function open(entry) { - var path = sanitize(app.path + '/' + entry.filePath); +function open(row, column, event) { + // ignore item open on row clicks if we are renaming this entry + if (row.rename) return; - if (entry.isDirectory) { + var path = sanitize(app.path + '/' + row.filePath); + + if (row.isDirectory) { window.location.hash = path; return; } - window.open(encode(path)); -} + app.activeEntry = row; + app.activeEntry.fullPath = encode(sanitize(app.path + '/' + row.filePath)); + app.previewDrawerVisible = true + + // need to wait for DOM element to exist + setTimeout(function () { + $('iframe').on('load', function (e) { + if (!e.target.contentWindow.document.body) return; -function up() { - window.location.hash = sanitize(app.path.split('/').slice(0, -1).filter(function (p) { return !!p; }).join('/')); + e.target.contentWindow.document.body.style.display = 'flex' + e.target.contentWindow.document.body.style.justifyContent = 'center' + }); + }, 0); } function uploadFiles(files) { if (!files || !files.length) return; - app.uploadStatus = { - busy: true, - count: files.length, - done: 0, - percentDone: 0 - }; + app.uploadStatus.busy = true; + app.uploadStatus.count = files.length; + app.uploadStatus.size = 0; + app.uploadStatus.done = 0; + app.uploadStatus.percentDone = 0; + + for (var i = 0; i < files.length; ++i) { + app.uploadStatus.size += files[i].size; + } - function uploadFile(file) { - var path = encode(sanitize(app.path + '/' + file.name)); + asyncForEach(files, function (file, callback) { + var path = encode(sanitize(app.path + '/' + (file.webkitRelativePath || file.name))); var formData = new FormData(); formData.append('file', file); - superagent.post('/api/files' + path).query({ access_token: localStorage.accessToken }).send(formData).end(function (error, result) { - if (result && result.statusCode === 401) return logout(); - if (result && result.statusCode !== 201) console.error('Error uploading file: ', result.statusCode); - if (error) console.error(error); + var finishedUploadSize = app.uploadStatus.done; - app.uploadStatus.done += 1; - app.uploadStatus.percentDone = Math.round(app.uploadStatus.done / app.uploadStatus.count * 100); + superagent.post('/api/files' + path) + .query({ access_token: localStorage.accessToken }) + .send(formData) + .on('progress', function (event) { + // only handle upload events + if (!(event.target instanceof XMLHttpRequestUpload)) return; - if (app.uploadStatus.done >= app.uploadStatus.count) { - app.uploadStatus = { - busy: false, - count: 0, - done: 0, - percentDone: 100 - }; + app.uploadStatus.done = finishedUploadSize + event.loaded; + var tmp = Math.round(app.uploadStatus.done / app.uploadStatus.size * 100); + app.uploadStatus.percentDone = tmp > 100 ? 100 : tmp; + }).end(function (error, result) { + if (result && result.statusCode === 401) return logout(); + if (result && result.statusCode !== 201) return callback('Error uploading file: ', result.statusCode); + if (error) return callback(error); - refresh(); - } + callback(); }); - } + }, function (error) { + if (error) console.error(error); - for(var i = 0; i < app.uploadStatus.count; ++i) { - uploadFile(files[i]); - } + app.uploadStatus.busy = false; + app.uploadStatus.count = 0; + app.uploadStatus.size = 0; + app.uploadStatus.done = 0; + app.uploadStatus.percentDone = 100; + + refresh(); + }); } -function upload() { - $(app.$els.upload).on('change', function () { +function dragOver(event) { + event.stopPropagation(); + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; +} - // detach event handler - $(app.$els.upload).off('change'); +function drop(event) { + event.stopPropagation(); + event.preventDefault(); - uploadFiles(app.$els.upload.files || []); - }); + if (!event.dataTransfer.items[0]) return; - // reset the form first to make the change handler retrigger even on the same file selected - $('#fileUploadForm')[0].reset(); + // figure if a folder was dropped on a modern browser, in this case the first would have to be a directory + var folderItem; + try { + folderItem = event.dataTransfer.items[0].webkitGetAsEntry(); + if (folderItem.isFile) return uploadFiles(event.dataTransfer.files); + } catch (e) { + return uploadFiles(event.dataTransfer.files); + } - app.$els.upload.click(); -} + // if we got here we have a folder drop and a modern browser + // now traverse the folder tree and create a file list + app.uploadStatus.busy = true; + app.uploadStatus.uploadListCount = 0; + + var fileList = []; + function traverseFileTree(item, path, callback) { + if (item.isFile) { + // Get file + item.file(function (file) { + fileList.push(file); + ++app.uploadStatus.uploadListCount; + callback(); + }); + } else if (item.isDirectory) { + // Get folder contents + var dirReader = item.createReader(); + dirReader.readEntries(function (entries) { + asyncForEach(entries, function (entry, callback) { + traverseFileTree(entry, path + item.name + '/', callback); + }, callback); + }); + } + } + + traverseFileTree(folderItem, '', function (error) { + app.uploadStatus.busy = false; + app.uploadStatus.uploadListCount = 0; + + if (error) return console.error(error); -function delAsk(entry) { - $('#modalDelete').modal('show'); - app.deleteData = entry; + uploadFiles(fileList); + }); } -function del(entry) { - app.busy = true; +var app = new Vue({ + el: '#app', + data: { + ready: false, + busy: false, + origin: window.location.origin, + uploadStatus: { + busy: false, + count: 0, + done: 0, + percentDone: 50, + uploadListCount: 0 + }, + path: '/', + pathParts: [], + session: { + valid: false + }, + folderListingEnabled: false, + loginData: { + username: '', + password: '', + busy: false + }, + previewDrawerVisible: false, + activeEntry: {}, + entries: [], + accessTokens: [], + accessTokensDialogVisible: false + }, + methods: { + onLogin: function () { + var that = this; - var path = encode(sanitize(app.path + '/' + entry.filePath)); + that.loginData.busy = true; - superagent.del('/api/files' + path).query({ access_token: localStorage.accessToken, recursive: true }).end(function (error, result) { - app.busy = false; + superagent.post('/api/login').send({ username: that.loginData.username, password: that.loginData.password }).end(function (error, result) { + that.loginData.busy = false; - if (result && result.statusCode === 401) return logout(); - if (result && result.statusCode !== 200) return console.error('Error deleting file: ', result.statusCode); - if (error) return console.error(error); + if (error && !result) return that.$message.error(error.message); + if (result.statusCode === 401) return that.$message.error('Wrong username or password'); - refresh(); + initWithToken(result.body.accessToken); + }); + }, + onOptionsMenu: function (command) { + if (command === 'folderListing') { + superagent.put('/api/settings').send({ folderListingEnabled: this.folderListingEnabled }).query({ access_token: localStorage.accessToken }).end(function (error) { + if (error) console.error(error); + }); + } else if (command === 'about') { + this.$msgbox({ + title: 'About Surfer', + message: 'Surfer is a static file server written by Cloudron.

The source code is licensed under MIT and available here.', + dangerouslyUseHTMLString: true, + confirmButtonText: 'OK', + showCancelButton: false, + type: 'info', + center: true + }).then(function () {}).catch(function () {}); + } else if (command === 'logout') { + logout(); + } else if (command === 'apiAccess') { + this.accessTokensDialogVisible = true; + } + }, + onDownload: function (entry) { + if (entry.isDirectory) return; + window.location.href = encode('/api/files/' + sanitize(this.path + '/' + entry.filePath)) + '?access_token=' + localStorage.accessToken; + }, + onUpload: function () { + var that = this; - $('#modalDelete').modal('hide'); - }); -} + $(this.$refs.upload).on('change', function () { + // detach event handler + $(that.$refs.upload).off('change'); + uploadFiles(that.$refs.upload.files || []); + }); -function renameAsk(entry) { - app.renameData.entry = entry; - app.renameData.error = null; - app.renameData.newFilePath = entry.filePath; + // reset the form first to make the change handler retrigger even on the same file selected + this.$refs.upload.value = ''; + this.$refs.upload.click(); + }, + onUploadFolder: function () { + var that = this; - $('#modalRename').modal('show'); -} + $(this.$refs.uploadFolder).on('change', function () { + // detach event handler + $(that.$refs.uploadFolder).off('change'); + uploadFiles(that.$refs.uploadFolder.files || []); + }); -function rename(data) { - app.busy = true; + // reset the form first to make the change handler retrigger even on the same file selected + this.$refs.uploadFolder.value = ''; + this.$refs.uploadFolder.click(); + }, + onDelete: function (entry) { + var that = this; - var path = encode(sanitize(app.path + '/' + data.entry.filePath)); - var newFilePath = sanitize(app.path + '/' + data.newFilePath); + var title = 'Really delete ' + (entry.isDirectory ? 'folder ' : '') + entry.filePath; + this.$confirm('', title, { confirmButtonText: 'Yes', cancelButtonText: 'No' }).then(function () { + var path = encode(sanitize(that.path + '/' + entry.filePath)); - superagent.put('/api/files' + path).query({ access_token: localStorage.accessToken }).send({ newFilePath: newFilePath }).end(function (error, result) { - app.busy = false; + superagent.del('/api/files' + path).query({ access_token: localStorage.accessToken, recursive: true }).end(function (error, result) { + if (result && result.statusCode === 401) return logout(); + if (result && result.statusCode !== 200) return that.$message.error('Error deleting file: ' + result.statusCode); + if (error) return that.$message.error(error.message); - if (result && result.statusCode === 401) return logout(); - if (result && result.statusCode !== 200) return console.error('Error renaming file: ', result.statusCode); - if (error) return console.error(error); + refresh(); + }); + }).catch(function () {}); + }, + onRename: function (entry, scope) { + if (entry.rename) return entry.rename = false; - refresh(); + entry.rename = true; - $('#modalRename').modal('hide'); - }); -} + Vue.nextTick(function () { + var elem = document.getElementById('filePathRenameInputId-' + scope.$index); + elem.focus(); -function createDirectoryAsk() { - $('#modalcreateDirectory').modal('show'); - app.createDirectoryData = ''; - app.createDirectoryError = null; -} + if (typeof elem.selectionStart != "undefined") { + elem.selectionStart = 0; + elem.selectionEnd = entry.filePath.lastIndexOf('.'); + } + }); + }, + onRenameEnd: function (entry) { + entry.rename = false; + entry.filePathNew = entry.filePath; + }, + onRenameSubmit: function (entry) { + var that = this; -function createDirectory(name) { - app.busy = true; - app.createDirectoryError = null; + entry.rename = false; - var path = encode(sanitize(app.path + '/' + name)); + if (entry.filePathNew === entry.filePath) return; - superagent.post('/api/files' + path).query({ access_token: localStorage.accessToken, directory: true }).end(function (error, result) { - app.busy = false; + var path = encode(sanitize(this.path + '/' + entry.filePath)); + var newFilePath = sanitize(this.path + '/' + entry.filePathNew); - if (result && result.statusCode === 401) return logout(); - if (result && result.statusCode === 403) { - app.createDirectoryError = 'Name not allowed'; - return; - } - if (result && result.statusCode === 409) { - app.createDirectoryError = 'Directory already exists'; - return; - } - if (result && result.statusCode !== 201) return console.error('Error creating directory: ', result.statusCode); - if (error) return console.error(error); + superagent.put('/api/files' + path).query({ access_token: localStorage.accessToken }).send({ newFilePath: newFilePath }).end(function (error, result) { + if (result && result.statusCode === 401) return logout(); + if (result && result.statusCode !== 200) return that.$message.error('Error renaming file: ' + result.statusCode); + if (error) return that.$message.error(error.message); - app.createDirectoryData = ''; - refresh(); + entry.filePath = entry.filePathNew; + }); + }, + onNewFolder: function () { + var that = this; + + var title = 'Create New Folder'; + this.$prompt('', title, { confirmButtonText: 'Yes', cancelButtonText: 'No', inputPlaceholder: 'new foldername' }).then(function (data) { + var path = encode(sanitize(that.path + '/' + data.value)); + + superagent.post('/api/files' + path).query({ access_token: localStorage.accessToken, directory: true }).end(function (error, result) { + if (result && result.statusCode === 401) return logout(); + if (result && result.statusCode === 403) return that.$message.error('Folder name not allowed'); + if (result && result.statusCode === 409) return that.$message.error('Folder already exists'); + if (result && result.statusCode !== 201) return that.$message.error('Error creating folder: ' + result.statusCode); + if (error) return that.$message.error(error.message); + + refresh(); + }); + }).catch(function () {}); + }, + refreshAccessTokens: function () { + var that = this; - $('#modalcreateDirectory').modal('hide'); - }); -} + superagent.get('/api/tokens').query({ access_token: localStorage.accessToken }).end(function (error, result) { + if (error && !result) return that.$message.error(error.message); -function dragOver(event) { - event.preventDefault(); -} + that.accessTokens = result.body.accessTokens; + }); + }, + onCopyAccessToken: function (event) { + event.target.select(); + document.execCommand('copy'); -function drop(event) { - event.preventDefault(); - uploadFiles(event.dataTransfer.files || []); -} + this.$message({ type: 'success', message: 'Access token copied to clipboard' }); + }, + onCreateAccessToken: function () { + var that = this; -Vue.filter('prettyDate', function (value) { - var d = new Date(value); - return d.toDateString(); -}); + superagent.post('/api/tokens').query({ access_token: localStorage.accessToken }).end(function (error, result) { + if (error && !result) return that.$message.error(error.message); -Vue.filter('prettyFileSize', function (value) { - return filesize(value); -}); + that.refreshAccessTokens(); + }); + }, + onDeleteAccessToken: function (token) { + var that = this; + + this.$confirm('All actions from apps using this token will fail!', 'Really delete this access token?', { confirmButtonText: 'Yes Delete', cancelButtonText: 'No' }).then(function () { + superagent.delete('/api/tokens/' + token).query({ access_token: localStorage.accessToken }).end(function (error, result) { + if (error && !result) return that.$message.error(error.message); + + that.refreshAccessTokens(); + }); + }).catch(function () {}); -var app = new Vue({ - el: '#app', - data: { - busy: true, - uploadStatus: { - busy: false, - count: 0, - done: 0, - percentDone: 50 }, - path: '/', - pathParts: [], - session: { - valid: false + prettyDate: function (row, column, cellValue, index) { + var date = new Date(cellValue), + diff = (((new Date()).getTime() - date.getTime()) / 1000), + day_diff = Math.floor(diff / 86400); + + if (isNaN(day_diff) || day_diff < 0) + return; + + return day_diff === 0 && ( + diff < 60 && 'just now' || + diff < 120 && '1 minute ago' || + diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' || + diff < 7200 && '1 hour ago' || + diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') || + day_diff === 1 && 'Yesterday' || + day_diff < 7 && day_diff + ' days ago' || + day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' || + day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' || + Math.round( day_diff / 365 ) + ' years ago'; }, - loginData: {}, - deleteData: {}, - renameData: { - entry: {}, - error: null, - newFilePath: '' + prettyFileSize: function (row, column, cellValue, index) { + return filesize(cellValue); }, - createDirectoryData: '', - createDirectoryError: null, - entries: [] - }, - methods: { - login: login, - logout: logout, loadDirectory: loadDirectory, + onUp: function () { + window.location.hash = sanitize(this.path.split('/').slice(0, -1).filter(function (p) { return !!p; }).join('/')); + }, open: open, - up: up, - upload: upload, - delAsk: delAsk, - del: del, - renameAsk: renameAsk, - rename: rename, - createDirectoryAsk: createDirectoryAsk, - createDirectory: createDirectory, drop: drop, dragOver: dragOver } }); -window.app = app; - -getProfile(localStorage.accessToken); +initWithToken(localStorage.accessToken); $(window).on('hashchange', function () { - loadDirectory(window.location.hash.slice(1)); -}); - -// setup all the dialog focus handling -['modalcreateDirectory'].forEach(function (id) { - $('#' + id).on('shown.bs.modal', function () { - $(this).find("[autofocus]:first").focus(); - }); + loadDirectory(decode(window.location.hash.slice(1))); }); })();