539 lines
21 KiB
JavaScript
539 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
/* global angular */
|
|
/* global sanitize, isModalVisible */
|
|
|
|
angular.module('Application').component('filetree', {
|
|
bindings: {
|
|
backendId: '<',
|
|
backendType: '<',
|
|
view: '<',
|
|
clipboard: '<',
|
|
onUploadFile: '&',
|
|
onUploadFolder: '&',
|
|
onNewFile: '&',
|
|
onNewFolder: '&',
|
|
onRenameEntry: '&',
|
|
onExtractEntry: '&',
|
|
onChownEntries: '&',
|
|
onDeleteEntries: '&',
|
|
onCopyEntries: '&',
|
|
onCutEntries: '&',
|
|
onPasteEntries: '&'
|
|
},
|
|
templateUrl: 'components/filetree.html?<%= revision %>',
|
|
controller: [ '$scope', '$translate', '$timeout', 'Client', FileTreeController ]
|
|
});
|
|
|
|
function FileTreeController($scope, $translate, $timeout, Client) {
|
|
var ctrl = this;
|
|
|
|
$scope.backendId = this.backendId;
|
|
$scope.backendType = this.backendType;
|
|
$scope.view = this.view;
|
|
|
|
$scope.busy = true;
|
|
$scope.busyRefresh = false;
|
|
$scope.client = Client;
|
|
$scope.cwd = null;
|
|
$scope.cwdParts = [];
|
|
$scope.rootDirLabel = '';
|
|
$scope.entries = [];
|
|
$scope.selected = []; // holds selected entries
|
|
$scope.dropToBody = false;
|
|
$scope.applicationLink = '';
|
|
|
|
// register so parent can call child
|
|
$scope.$parent.registerChild($scope);
|
|
|
|
function isArchive(f) {
|
|
return f.match(/\.tgz$/) ||
|
|
f.match(/\.tar$/) ||
|
|
f.match(/\.7z$/) ||
|
|
f.match(/\.zip$/) ||
|
|
f.match(/\.tar\.gz$/) ||
|
|
f.match(/\.tar\.xz$/) ||
|
|
f.match(/\.tar\.bz2$/);
|
|
}
|
|
|
|
$scope.menuOptions = []; // shown for entries
|
|
$scope.menuOptionsBlank = []; // shown for empty space in folder
|
|
|
|
function sort() {
|
|
return $scope.entries.sort(function (a, b) {
|
|
if (a.fileName.toLowerCase() < b.fileName.toLowerCase()) return -1;
|
|
return 1;
|
|
}).sort(function (a, b) {
|
|
if ((a.isDirectory && b.isDirectory) || (!a.isDirectory && !b.isDirectory)) return 0;
|
|
if (a.isDirectory && !b.isDirectory) return -1;
|
|
return 1;
|
|
});
|
|
}
|
|
|
|
function openPath(path) {
|
|
path = sanitize(path);
|
|
|
|
// we always show the parent path, even if overlayed by a mediaviewer
|
|
var parentPath = sanitize(path + '/..');
|
|
|
|
$scope.busy = true;
|
|
|
|
$scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
|
|
|
|
// nothing changes here, mostly triggered when editor is closed
|
|
if ($scope.cwd === path) {
|
|
$scope.busy = false;
|
|
return;
|
|
}
|
|
|
|
if ($scope.cwd === parentPath) {
|
|
var entry = $scope.entries.find(function (e) { return path === sanitize(parentPath + '/' + e.fileName); });
|
|
if (!entry) return Client.error('No such file or folder: ' + path);
|
|
|
|
if (entry.isDirectory) {
|
|
$scope.cwd = path;
|
|
$scope.selected = [];
|
|
|
|
// refresh will set busy to false once done
|
|
$scope.refresh();
|
|
} else if (entry.isFile) {
|
|
var mimeType = Mimer().get(entry.fileName);
|
|
var mimeGroup = mimeType.split('/')[0];
|
|
|
|
if (mimeGroup === 'video' || mimeGroup === 'image') {
|
|
$scope.mediaViewer.show(entry);
|
|
} else if (mimeType === 'application/pdf') {
|
|
Client.filesGet($scope.backendId, $scope.backendType, path, 'open', function (error) { if (error) return Client.error(error); });
|
|
} else if (mimeGroup === 'text' || mimeGroup === 'application') {
|
|
$scope.$parent.textEditor.show($scope.cwd, entry);
|
|
} else {
|
|
Client.filesGet($scope.backendId, $scope.backendType, path, 'open', function (error) { if (error) return Client.error(error); });
|
|
}
|
|
|
|
$scope.busy = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Client.filesGet($scope.backendId, $scope.backendType, parentPath, 'data', function (error, result) {
|
|
if (error) return setTimeout(function () { openPath(path); }, 2000); // try again in some time
|
|
|
|
// call itself now that we know
|
|
$scope.cwd = parentPath;
|
|
|
|
$scope.entries = result.entries;
|
|
amendIcons();
|
|
sort();
|
|
|
|
openPath(path);
|
|
});
|
|
}
|
|
|
|
$scope.isSelected = function (entry) {
|
|
return $scope.selected.indexOf(entry) !== -1;
|
|
};
|
|
|
|
function download(entry) {
|
|
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
|
|
|
|
Client.filesGet($scope.backendId, $scope.backendType, filePath, 'download', function (error) {
|
|
if (error) return Client.error(error);
|
|
});
|
|
}
|
|
|
|
$scope.dragStart = function ($event, entry) {
|
|
var filePaths = $scope.selected.map(function (entry) { return sanitize($scope.cwd + '/' + entry.fileName); });
|
|
$event.originalEvent.dataTransfer.setData('application/cloudron-filemanager', JSON.stringify(filePaths));
|
|
};
|
|
|
|
$scope.dragEnter = function ($event, entry) {
|
|
$event.originalEvent.stopPropagation();
|
|
$event.originalEvent.preventDefault();
|
|
|
|
// if entry is string, we come from breadcrumb
|
|
if (entry && typeof entry === 'string') $event.currentTarget.classList.add('entry-hovered');
|
|
else if (entry && entry.isDirectory) entry.hovered = true;
|
|
else $scope.dropToBody = true;
|
|
|
|
$event.originalEvent.dataTransfer.dropEffect = 'move';
|
|
};
|
|
|
|
$scope.dragExit = function ($event, entry) {
|
|
$event.originalEvent.stopPropagation();
|
|
$event.originalEvent.preventDefault();
|
|
|
|
// if entry is string, we come from breadcrumb
|
|
if (entry && typeof entry === 'string') $event.currentTarget.classList.remove('entry-hovered');
|
|
else if (entry && entry.isDirectory) entry.hovered = false;
|
|
$scope.dropToBody = false;
|
|
|
|
$event.originalEvent.dataTransfer.dropEffect = 'move';
|
|
};
|
|
|
|
$scope.drop = function (event, entry) {
|
|
event.originalEvent.stopPropagation();
|
|
event.originalEvent.preventDefault();
|
|
|
|
$scope.dropToBody = false;
|
|
|
|
if (!event.originalEvent.dataTransfer.items[0]) return;
|
|
|
|
var targetFolder;
|
|
if (entry === null) targetFolder = $scope.cwd + '/';
|
|
else if (typeof entry === 'string') targetFolder = sanitize(entry);
|
|
else targetFolder = sanitize($scope.cwd + '/' + (entry && entry.isDirectory ? entry.fileName : ''));
|
|
|
|
var dataTransfer = event.originalEvent.dataTransfer;
|
|
var dragContent = dataTransfer.getData('application/cloudron-filemanager');
|
|
|
|
// check if we have internal drag'n'drop
|
|
if (dragContent) {
|
|
var moved = 0;
|
|
|
|
// we expect a JSON.stringified Array here
|
|
try {
|
|
dragContent = JSON.parse(dragContent);
|
|
} catch (e) {
|
|
console.error('Wrong drag content.', e);
|
|
return;
|
|
}
|
|
|
|
// move files
|
|
async.eachLimit(dragContent, 5, function (oldFilePath, callback) {
|
|
var fileName = oldFilePath.split('/').slice(-1);
|
|
var newFilePath = sanitize(targetFolder + '/' + fileName);
|
|
|
|
// if we drop the item on itself
|
|
if (oldFilePath === targetFolder) return callback();
|
|
|
|
// if nothing changes
|
|
if (newFilePath === oldFilePath) return callback();
|
|
|
|
moved++;
|
|
|
|
// TODO this will overwrite files in destination!
|
|
Client.filesRename($scope.backendId, $scope.backendType, oldFilePath, newFilePath, callback);
|
|
}, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
// only refresh if anything has changed
|
|
if (moved) $scope.refresh();
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// 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 = dataTransfer.items[0].webkitGetAsEntry();
|
|
if (folderItem.isFile) return $scope.$parent.uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
|
|
} catch (e) {
|
|
return $scope.$parent.uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
|
|
}
|
|
|
|
// if we got here we have a folder drop and a modern browser
|
|
// now traverse the folder tree and create a file list
|
|
var fileList = [];
|
|
function traverseFileTree(item, path, callback) {
|
|
if (item.isFile) {
|
|
// Get file
|
|
item.file(function (file) {
|
|
fileList.push(file);
|
|
callback();
|
|
});
|
|
} else if (item.isDirectory) {
|
|
// Get folder contents
|
|
var dirReader = item.createReader();
|
|
dirReader.readEntries(function (entries) {
|
|
async.each(entries, function (entry, callback) {
|
|
traverseFileTree(entry, path + item.name + '/', callback);
|
|
}, callback);
|
|
});
|
|
}
|
|
}
|
|
|
|
traverseFileTree(folderItem, '', function (error) {
|
|
if (error) return console.error(error);
|
|
|
|
$scope.$parent.uploadFiles(fileList, targetFolder, false);
|
|
});
|
|
};
|
|
|
|
$scope.refresh = function () {
|
|
$scope.$parent.refresh();
|
|
};
|
|
|
|
function amendIcons() {
|
|
$scope.entries.forEach(function (e) {
|
|
e.icon = 'fa-file';
|
|
e.previewUrl = null;
|
|
|
|
if (e.isDirectory) e.icon = 'fa-folder';
|
|
if (e.isSymbolicLink) e.icon = 'fa-link';
|
|
if (e.isFile) {
|
|
var mimeType = Mimer().get(e.fileName.toLowerCase());
|
|
var mimeGroup = mimeType.split('/')[0];
|
|
if (mimeGroup === 'text') e.icon = 'fa-file-alt';
|
|
// if (mimeGroup === 'image') e.icon = 'fa-file-image';
|
|
if (mimeGroup === 'image') {
|
|
e.icon = 'fa-file-image';
|
|
e.previewUrl = Client.filesGetLink($scope.backendId, $scope.backendType, sanitize($scope.cwd + '/' + e.fileName));
|
|
}
|
|
if (mimeGroup === 'video') e.icon = 'fa-file-video';
|
|
if (mimeGroup === 'audio') e.icon = 'fa-file-audio';
|
|
if (mimeType === 'text/csv') e.icon = 'fa-file-csv';
|
|
if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf';
|
|
}
|
|
});
|
|
}
|
|
|
|
// called from the parent
|
|
$scope.onRefresh = function () {
|
|
$scope.selected = [];
|
|
$scope.busy = true;
|
|
$scope.busyRefresh = true;
|
|
|
|
Client.filesGet($scope.backendId, $scope.backendType, $scope.cwd, 'data', function (error, result) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.entries = result.entries;
|
|
amendIcons();
|
|
sort();
|
|
|
|
$scope.busyRefresh = false;
|
|
$scope.busy = false;
|
|
});
|
|
};
|
|
|
|
$scope.open = function (entry) {
|
|
openPath(sanitize($scope.cwd + '/' + entry.fileName));
|
|
};
|
|
|
|
$scope.goDirectoryUp = function () {
|
|
openPath(sanitize($scope.cwd + '/..'));
|
|
};
|
|
|
|
$scope.changeDirectory = function (path) {
|
|
openPath(sanitize(path));
|
|
};
|
|
|
|
$scope.onClearSelection = function ($event) {
|
|
// we don't stop propagation if targets don't match we got the whole list click event
|
|
if ($event.currentTarget !== $event.target) return;
|
|
|
|
$scope.selected = [];
|
|
};
|
|
|
|
$scope.onMousedown = function ($event, entry) {
|
|
var i = $scope.selected.indexOf(entry);
|
|
var multi = ($event.ctrlKey || $event.metaKey);
|
|
var shift = $event.shiftKey;
|
|
|
|
if (shift) {
|
|
if ($scope.selected.length === 0) {
|
|
$scope.selected = [ entry ];
|
|
} else {
|
|
var pos = $scope.entries.indexOf(entry);
|
|
var selectedPositions = $scope.selected.map(function (s) { return $scope.entries.indexOf(s); }).sort();
|
|
|
|
if (pos < selectedPositions[0]) {
|
|
$scope.selected = $scope.entries.slice(pos, selectedPositions[0]+1);
|
|
} else if (selectedPositions[1] && pos > selectedPositions[1]) {
|
|
$scope.selected = $scope.entries.slice(selectedPositions[1], pos+1);
|
|
} else {
|
|
$scope.selected = $scope.entries.slice(selectedPositions[0], pos+1);
|
|
}
|
|
}
|
|
} else if (multi) {
|
|
if (i === -1) {
|
|
$scope.selected.push(entry);
|
|
} else if ($event.button === 0) { // only do this on left click
|
|
$scope.selected.splice(i, 1);
|
|
}
|
|
} else {
|
|
$scope.selected = [ entry ];
|
|
}
|
|
};
|
|
|
|
$scope.onEntryContextMenu = function ($event, entry) {
|
|
if ($scope.selected.indexOf(entry) !== -1) return;
|
|
$scope.selected.push(entry);
|
|
};
|
|
|
|
$scope.actionSelectAll = function () {
|
|
$scope.selected = $scope.entries.slice();
|
|
};
|
|
|
|
// just events to the parent controller
|
|
$scope.onUploadFile = function () { ctrl.onUploadFile({ cwd: $scope.cwd }); };
|
|
$scope.onUploadFolder = function () { ctrl.onUploadFolder({ cwd: $scope.cwd }); };
|
|
$scope.onNewFile = function () { ctrl.onNewFile({ cwd: $scope.cwd }); };
|
|
$scope.onNewFolder = function () { ctrl.onNewFolder({ cwd: $scope.cwd }); };
|
|
|
|
$scope.mediaViewer = {
|
|
type: '',
|
|
src: '',
|
|
entry: null,
|
|
|
|
show: function (entry) {
|
|
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
|
|
|
|
$scope.mediaViewer.entry = entry;
|
|
$scope.mediaViewer.type = Mimer().get(entry.fileName).split('/')[0];
|
|
$scope.mediaViewer.src = Client.filesGetLink($scope.backendId, $scope.backendType, filePath);
|
|
|
|
$('#mediaViewerModal-' + $scope.$id).modal('show');
|
|
},
|
|
|
|
close: function () {
|
|
// set an empty pixel image to bust the cached img to avoid flickering on slow load
|
|
$scope.mediaViewer.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==';
|
|
|
|
$('#mediaViewerModal-' + $scope.$id).modal('hide');
|
|
}
|
|
};
|
|
|
|
$translate(['filemanager.list.menu.edit', 'filemanager.list.menu.cut', 'filemanager.list.menu.copy', 'filemanager.list.menu.paste', 'filemanager.list.menu.rename', 'filemanager.list.menu.chown', 'filemanager.list.menu.extract', 'filemanager.list.menu.download', 'filemanager.list.menu.delete' ]).then(function (tr) {
|
|
$scope.menuOptions = [
|
|
{
|
|
text: tr['filemanager.list.menu.edit'],
|
|
displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && !entry.isSymbolicLink; },
|
|
enabled: function () { return $scope.selected.length === 1; },
|
|
hasBottomDivider: true,
|
|
click: function ($itemScope, $event, entry) { $scope.open(entry); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.cut'],
|
|
click: function ($itemScope, $event, entry) { ctrl.onCutEntries({ cwd: $scope.cwd, entries: $scope.selected.slice() }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.copy'],
|
|
click: function ($itemScope, $event, entry) { ctrl.onCopyEntries({ cwd: $scope.cwd, entries: $scope.selected.slice() }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.paste'],
|
|
hasBottomDivider: true,
|
|
enabled: function () { return ctrl.clipboard.length; },
|
|
click: function ($itemScope, $event, entry) { ctrl.onPasteEntries({ cwd: $scope.cwd, entry: entry }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.rename'],
|
|
enabled: function () { return $scope.selected.length === 1; },
|
|
click: function ($itemScope, $event, entry) { ctrl.onRenameEntry({ cwd: $scope.cwd, entry: entry }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.chown'],
|
|
click: function ($itemScope, $event, entry) { ctrl.onChownEntries({ cwd: $scope.cwd, entries: $scope.selected }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.extract'],
|
|
displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && isArchive(entry.fileName); },
|
|
click: function ($itemScope, $event, entry) { ctrl.onExtractEntry({ cwd: $scope.cwd, entry: entry }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.download'],
|
|
enabled: function () { return $scope.selected.length === 1; },
|
|
click: function ($itemScope, $event, entry) { download(entry); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.delete'],
|
|
hasTopDivider: true,
|
|
click: function ($itemScope, $event, entry) { ctrl.onDeleteEntries({ cwd: $scope.cwd, entries: $scope.selected }); }
|
|
}
|
|
];
|
|
});
|
|
|
|
$translate(['filemanager.toolbar.newFile', 'filemanager.toolbar.newFolder', 'filemanager.list.menu.paste', 'filemanager.list.menu.selectAll' ]).then(function (tr) {
|
|
$scope.menuOptionsBlank = [
|
|
{
|
|
text: tr['filemanager.toolbar.newFile'],
|
|
click: function ($itemScope, $event) { ctrl.onNewFile({ cwd: $scope.cwd }); }
|
|
}, {
|
|
text: tr['filemanager.toolbar.newFolder'],
|
|
click: function ($itemScope, $event) { ctrl.onNewFolder({ cwd: $scope.cwd }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.paste'],
|
|
hasTopDivider: true,
|
|
hasBottomDivider: true,
|
|
enabled: function () { return ctrl.clipboard.length; },
|
|
click: function ($itemScope, $event) { ctrl.onPasteEntries({ cwd: $scope.cwd, entry: null }); }
|
|
}, {
|
|
text: tr['filemanager.list.menu.selectAll'],
|
|
click: function ($itemScope, $event) { $scope.actionSelectAll(); }
|
|
}
|
|
];
|
|
});
|
|
|
|
function scrollInView(element) {
|
|
if (!element) return;
|
|
|
|
// This assumes the DOM tree being that rigid
|
|
function isVisible(ele) {
|
|
var container = ele.parentElement.parentElement.parentElement;
|
|
var eleTop = ele.offsetTop;
|
|
var eleBottom = eleTop + ele.clientHeight;
|
|
|
|
var containerTop = container.scrollTop;
|
|
var containerBottom = containerTop + container.clientHeight;
|
|
|
|
// The element is fully visible in the container
|
|
return (
|
|
(eleTop >= containerTop && eleBottom <= containerBottom) ||
|
|
// Some part of the element is visible in the container
|
|
(eleTop < containerTop && containerTop < eleBottom) ||
|
|
(eleTop < containerBottom && containerBottom < eleBottom)
|
|
);
|
|
}
|
|
|
|
if (!isVisible(element)) element.scrollIntoView();
|
|
}
|
|
|
|
function openSelected() {
|
|
if (!$scope.selected.length) return;
|
|
|
|
$scope.open($scope.selected[0]);
|
|
}
|
|
|
|
function selectNext() {
|
|
var entries = sort();
|
|
|
|
if (!$scope.selected.length) return $scope.selected = [ entries[0] ];
|
|
|
|
var curIndex = $scope.entries.indexOf($scope.selected[0]);
|
|
if (curIndex !== -1 && curIndex < $scope.entries.length-1) {
|
|
var entry = entries[++curIndex];
|
|
$scope.selected = [ entry ];
|
|
scrollInView(document.querySelector('[entry-hashkey="' + entry['$$hashKey'] + '"]'));
|
|
}
|
|
}
|
|
|
|
function selectPrev() {
|
|
var entries = sort();
|
|
|
|
if (!$scope.selected.length) return $scope.selected = [ entries.slice(-1) ];
|
|
|
|
var curIndex = $scope.entries.indexOf($scope.selected[0]);
|
|
if (curIndex !== -1 && curIndex !== 0) {
|
|
var entry = entries[--curIndex];
|
|
$scope.selected = [ entry ];
|
|
scrollInView(document.querySelector('[entry-hashkey="' + entry['$$hashKey'] + '"]'));
|
|
}
|
|
}
|
|
|
|
openPath('.');
|
|
|
|
$('.file-list').on('scroll', function (event) {
|
|
if (event.target.scrollTop > 10) event.target.classList.add('top-scroll-indicator');
|
|
else event.target.classList.remove('top-scroll-indicator');
|
|
});
|
|
|
|
// handle shortcuts
|
|
window.addEventListener('keydown', function (event) {
|
|
if ($scope.$parent.activeView !== $scope.view || $scope.$parent.viewerOpen || isModalVisible()) return;
|
|
|
|
if (event.key === 'ArrowDown') {
|
|
$scope.$apply(selectNext);
|
|
} else if (event.key === 'ArrowUp') {
|
|
$scope.$apply(selectPrev);
|
|
} else if (event.key === 'Enter') {
|
|
$scope.$apply(openSelected);
|
|
} else if (event.key === 'Backspace') {
|
|
if ($scope.view === 'fileTree') $scope.goDirectoryUp();
|
|
}
|
|
});
|
|
}
|