├── icon.icns ├── icon.ico ├── app ├── visuals │ ├── icon200.png │ ├── ffftp-512.png │ ├── ffftp200w.png │ └── icons │ │ ├── file.png │ │ ├── folder.png │ │ ├── folder.svg │ │ └── new-connection.svg ├── app.js ├── directives │ ├── showFocus.directive.js │ └── konsole.directive.js ├── services │ ├── analytics.service.js │ ├── konsole.service.js │ └── ftp.service.js ├── filters │ └── fileSize.filter.js ├── stylesheets │ ├── less │ │ ├── variables.less │ │ ├── reset.less │ │ ├── animations.less │ │ ├── modals.less │ │ ├── console.less │ │ ├── menu.less │ │ └── main.less │ └── main.css ├── templates │ └── directives │ │ └── konsole.template.html ├── pages │ └── main.html └── controllers │ └── base.js ├── .gitignore ├── .editorconfig ├── README.md ├── package.json ├── main.js ├── index.html ├── Gruntfile.js └── .jshintrc /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/icon.icns -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/icon.ico -------------------------------------------------------------------------------- /app/visuals/icon200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/app/visuals/icon200.png -------------------------------------------------------------------------------- /app/visuals/ffftp-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/app/visuals/ffftp-512.png -------------------------------------------------------------------------------- /app/visuals/ffftp200w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/app/visuals/ffftp200w.png -------------------------------------------------------------------------------- /app/visuals/icons/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/app/visuals/icons/file.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | releases/ 4 | .idea/ 5 | .DS_Store 6 | build/ 7 | -------------------------------------------------------------------------------- /app/visuals/icons/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchas/ffftp/HEAD/app/visuals/icons/folder.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = crlf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | tab_width = 2 12 | trim_trailing_whitespace = true 13 | 14 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app', ['ngRoute', 'ngMaterial', 'ngAnimate', 'ngDraggable']) 5 | .config(['$routeProvider', ($routeProvider) => { 6 | $routeProvider 7 | .when('/', { 8 | templateUrl: './app/pages/main.html', 9 | controller: 'homeCtrl' 10 | }) 11 | .otherwise({redirectTo: '/'}); 12 | }]) 13 | .constant('PROD', false); 14 | })(angular); 15 | 16 | -------------------------------------------------------------------------------- /app/directives/showFocus.directive.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .directive('showFocus', showFocusDirective); 6 | 7 | function showFocusDirective($timeout) { 8 | return (scope, element, attrs) => { 9 | scope.$watch(attrs.showFocus, 10 | (newValue) => { 11 | $timeout(() => { 12 | newValue && element.focus(); 13 | }); 14 | }, true); 15 | }; 16 | } 17 | })(angular); 18 | 19 | -------------------------------------------------------------------------------- /app/services/analytics.service.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .factory('analyticsService', ['PROD', analyticsService]); 6 | 7 | function analyticsService(PROD) { 8 | const ua = require('universal-analytics'), 9 | visitor = ua('UA-88669012-1'); 10 | 11 | function track(uri) { 12 | if (PROD) { 13 | visitor.pageview(uri).send(); 14 | console.log(`Tracking ${visitor}`); 15 | } 16 | } 17 | 18 | return { 19 | track 20 | } 21 | } 22 | })(angular); 23 | -------------------------------------------------------------------------------- /app/filters/fileSize.filter.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .filter('fileSize', fileSizeFilter); 6 | 7 | function fileSizeFilter() { 8 | return (bytes, precision) => { 9 | if (bytes === 0 || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-'; 10 | if (typeof precision === undefined) precision = 1; 11 | const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'], 12 | number = Math.floor(Math.log(bytes) / Math.log(1024)); 13 | 14 | return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number]; 15 | }; 16 | } 17 | })(angular); 18 | -------------------------------------------------------------------------------- /app/stylesheets/less/variables.less: -------------------------------------------------------------------------------- 1 | // "main": "main.less" 2 | @import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'); 3 | @import 'http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css'; 4 | @import url('https://fonts.googleapis.com/css?family=Inconsolata:400,700'); 5 | 6 | // Fonts 7 | @ionicons: 'Ionicons'; 8 | @sans: 'Roboto', 'Helvetica', 'Arial' sans-serif; 9 | @code: 'Inconsolata', monospace; 10 | 11 | // Colors 12 | @black: #081D38; 13 | @mediumblack: #32384A; 14 | @lightblack: #474E65; 15 | @white: #FFF; 16 | @blue: #0080FF; 17 | @grey: #E9EEF2; 18 | @blue-text: #939EBC; 19 | 20 | @red: #FF4F5E; 21 | @green: #25E552; 22 | 23 | // UI 24 | @transition: 0.2s; 25 | -------------------------------------------------------------------------------- /app/templates/directives/konsole.template.html: -------------------------------------------------------------------------------- 1 |
2 | {{ konsole.message }} 3 | {{ konsole.unread }} 4 |
5 |
6 |
7 |
8 |
{{ line.message }}
9 |
10 |
11 | -------------------------------------------------------------------------------- /app/services/konsole.service.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .factory('konsoleService', ['$rootScope', konsoleService]); 6 | 7 | function konsoleService($rootScope) { 8 | function subscribe(scope, callback) { 9 | const handler = $rootScope.$on('konsole-new-message-event', callback); 10 | scope.$on('$destroy', handler); 11 | } 12 | 13 | function notify(message) { 14 | $rootScope.$emit('konsole-new-message-event', message); 15 | } 16 | 17 | function addMessage(colour, message) { 18 | notify({colour, message}); 19 | } 20 | 21 | return { 22 | subscribe, 23 | notify, 24 | addMessage 25 | } 26 | } 27 | })(angular); 28 | -------------------------------------------------------------------------------- /app/visuals/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![N|Solid](https://raw.githubusercontent.com/mitchas/ffftp/master/app/visuals/icon200.png) 2 | # **FFFTP** 3 | A minimal FTP manager built on Electron. 4 | - Explore files 5 | - Move, delete, upload files 6 | - Drag and drop 7 | - Save connection to favorites 8 | 9 | # Not maintained!***** 10 | 11 | I haven't worked ont this for a while. I may get back into it, but maybe not. Feel free to check out the forks. **It does still work, though.** 12 | 13 | ## Building & running it locally: 14 | - Clone the repo. 15 | - cd to the directory 16 | - make sure electron is installed `npm install electron` 17 | - install and run with `npm install && npm start` 18 | 19 | ## Packaging it (.app for mac, .exe for windows, or for linux) 20 | - Install [Electron Packager Interactive](https://github.com/Urucas/electron-packager-interactive) with `npm install -g electron-packager-interactive` 21 | - run `epi` 22 | - Follow steps 23 | - Icon is `./icon.ico' 24 | -------------------------------------------------------------------------------- /app/directives/konsole.directive.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .directive('konsole', ['konsoleService', konsoleDirective]); 6 | 7 | function konsoleDirective() { 8 | return { 9 | restrict: 'E', 10 | templateUrl: './app/templates/directives/konsole.template.html', 11 | controllerAs: 'konsole', 12 | bindToController: true, 13 | controller: function($scope, konsoleService) { 14 | let konsole = this; 15 | 16 | konsole.unread = 0; 17 | konsole.messages = []; 18 | 19 | konsole.openConsole = () => { 20 | konsole.unread = 0; 21 | konsole.fullConsole = true; 22 | }; 23 | 24 | konsoleService.subscribe($scope, (event, msg) => { 25 | konsole.messageClass = msg.color; 26 | konsole.message = msg.message; 27 | konsole.messages.push({'color': msg.color, 'message': msg.message}); 28 | konsole.unread++; 29 | }); 30 | } 31 | } 32 | } 33 | })(angular); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ffftp", 3 | "productName": "ffftp", 4 | "version": "0.0.6", 5 | "description": "A minimal FTP client built on Electron", 6 | "main": "main.js", 7 | "scripts": { 8 | "start": "electron main.js" 9 | }, 10 | "keywords": [ 11 | "Electron", 12 | "FTP", 13 | "Windows" 14 | ], 15 | "author": "Mitch Samuels", 16 | "homepage": "https://ffftp.site", 17 | "devDependencies": { 18 | "electron": "^1.4.10", 19 | "electron-packager": "^7.0.4", 20 | "grunt": "^1.0.1", 21 | "grunt-comment-toggler": "^0.2.2", 22 | "grunt-contrib-copy": "^1.0.0", 23 | "grunt-contrib-uglify": "^1.0.1", 24 | "grunt-search": "^0.1.8" 25 | }, 26 | "dependencies": { 27 | "angular": "^1.5.7", 28 | "angular-animate": "^1.5.7", 29 | "angular-aria": "^1.5.7", 30 | "angular-material": "^1.0.9", 31 | "angular-route": "^1.5.7", 32 | "asar": "^0.11.0", 33 | "directory-tree": "^1.1.1", 34 | "electron-json-storage": "^2.1.0", 35 | "jsftp": "^1.5.5", 36 | "jsftp-rmr": "0.0.1", 37 | "ng-draggable": "^1.0.0", 38 | "universal-analytics": "^0.4.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/stylesheets/less/reset.less: -------------------------------------------------------------------------------- 1 | //"main": "main.less" 2 | 3 | html, body, div, span, applet, object, iframe, 4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 5 | a, abbr, acronym, address, big, cite, code, 6 | del, dfn, em, img, ins, kbd, q, s, samp, 7 | small, strike, strong, sub, sup, tt, var, 8 | b, u, i, center, 9 | dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td, 12 | article, aside, canvas, details, embed, 13 | figure, figcaption, footer, header, hgroup, 14 | menu, nav, output, ruby, section, summary, 15 | time, mark, audio, video { 16 | margin: 0; 17 | padding: 0; 18 | border: 0; 19 | font-size: 100%; 20 | font: inherit; 21 | vertical-align: baseline; 22 | } 23 | /* HTML5 display-role reset for older browsers */ 24 | article, aside, details, figcaption, figure, 25 | footer, header, hgroup, menu, nav, section { 26 | display: block; 27 | } 28 | body { 29 | line-height: 1; 30 | } 31 | ol, ul { 32 | list-style: none; 33 | } 34 | blockquote, q { 35 | quotes: none; 36 | } 37 | blockquote:before, blockquote:after, 38 | q:before, q:after { 39 | content: ''; 40 | content: none; 41 | } 42 | table { 43 | border-collapse: collapse; 44 | border-spacing: 0; 45 | } 46 | -------------------------------------------------------------------------------- /app/services/ftp.service.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .service('ftpService', ftpService); 6 | 7 | function ftpService() { 8 | return { 9 | connect: connect 10 | }; 11 | 12 | const JsFtp = require('jsftp'), 13 | Ftp = require('jsftp-rmr')(JsFtp); 14 | let ftp; 15 | 16 | ftp.on('error', (data) => { 17 | consoleService('red', data); 18 | // $scope.emptyMessage = 'Error connecting.' 19 | console.error(data); 20 | }); 21 | 22 | ftp.on('lookup', (data) => { 23 | consoleService('red', `Lookup error: ${data}`); 24 | // $scope.emptyMessage = 'Error connecting.' 25 | console.error(`Lookup error: ${data}`); 26 | }); 27 | 28 | function connect({ftpHost, ftpPort, ftpUsername, ftpPassword}) { 29 | // $scope.showingMenu = false; 30 | 31 | ftp = new Ftp({ 32 | host: ftpHost, 33 | port: ftpPort, 34 | user: ftpUsername, 35 | pass: ftpPassword 36 | }); 37 | 38 | consoleService('white', `Connected to ${ftp.host}`); 39 | 40 | changeDir(); 41 | splitPath(); 42 | } 43 | 44 | function changeDir() { 45 | 46 | } 47 | 48 | function splitPath() { 49 | 50 | } 51 | } 52 | })(angular); 53 | -------------------------------------------------------------------------------- /app/visuals/icons/new-connection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const app = require('electron').app, // Module to control application life. 2 | BrowserWindow = require('electron').BrowserWindow; // Module to create native browser window. 3 | 4 | // Keep a global reference of the window object, if you don't, the window will 5 | // be closed automatically when the JavaScript object is garbage collected. 6 | let mainWindow = null; 7 | 8 | // Quit when all windows are closed. 9 | app.on('window-all-closed', function () { 10 | // On OS X it is common for applications and their menu bar 11 | // to stay active until the user quits explicitly with Cmd + Q 12 | // if (process.platform != 'darwin') { 13 | app.quit(); 14 | // } 15 | }); 16 | 17 | // This method will be called when Electron has finished 18 | // initialization and is ready to create browser windows. 19 | app.on('ready', function () { 20 | // Create the browser window. 21 | mainWindow = new BrowserWindow({width: 1000, height: 600, icon: __dirname + '/icon.ico'}); 22 | 23 | // and load the index.html of the app. 24 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 25 | 26 | // Open the DevTools. 27 | // mainWindow.openDevTools(); 28 | 29 | // Emitted when the window is closed. 30 | mainWindow.on('closed', function () { 31 | // Dereference the window object, usually you would store windows 32 | // in an array if your app supports multi windows, this is the time 33 | // when you should delete the corresponding element. 34 | mainWindow = null; 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ffftp 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |

Loading...

36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /app/stylesheets/less/animations.less: -------------------------------------------------------------------------------- 1 | // "main": "main.less" 2 | 3 | // .ng-hide-add { animation:0.2s fadeOut ease; } // Hide 4 | // .ng-hide-remove { animation:0.2s fadeIn ease; } // Show 5 | 6 | // .ng-fade 7 | .ng-fade{ 8 | &.ng-hide-add{animation: 0.15s fadeOut ease !important;} 9 | &.ng-hide-remove{ animation: 0.15s fadeIn ease !important; } 10 | } 11 | // .slideTop 12 | .slideTop{ 13 | &.ng-hide-add{animation: 0.2s slideOutToTop ease !important;} 14 | &.ng-hide-remove{ animation: 0.2s slideInFromTop ease !important; } 15 | } 16 | // .slideBottom 17 | .slideBottom{ 18 | &.ng-hide-add{animation: 0.2s slideOutToBottom ease !important;} 19 | &.ng-hide-remove{ animation: 0.2s slideInFromBottom ease !important; } 20 | } 21 | // .slideBottom 22 | .zoom{ 23 | &.ng-hide-add{animation: 0.2s zoomOut ease !important;} 24 | &.ng-hide-remove{ animation: 0.2s zoomIn ease !important; } 25 | } 26 | 27 | // From Top 28 | @keyframes slideInFromTop { 29 | 0% {top: -100%; opacity: 1;} 30 | 100% {top: 0; opacity: 1} 31 | } 32 | @keyframes slideOutToTop { 33 | 0% {top: 0; opacity: 1;} 34 | 100% {top: -100%; opacity: 1;} 35 | } 36 | // From Bottom 37 | @keyframes slideInFromBottom { 38 | 0% {bottom: -100%; opacity: 0;} 39 | 100% {bottom: 0; opacity: 1} 40 | } 41 | @keyframes slideOutToBottom { 42 | 0% {bottom: 0; opacity: 1;} 43 | 100% {bottom: -100%; opacity: 0;} 44 | } 45 | // Zoom From Center 46 | @keyframes zoomIn { 47 | 0% {transform: scale(0.85); opacity: 0;} 48 | 100% {transform: scale(1); opacity: 1} 49 | } 50 | @keyframes zoomOut { 51 | 0% {transform: scale(1); opacity: 1;} 52 | 100% {transform: scale(0.85); opacity: 0;} 53 | } 54 | 55 | 56 | @keyframes fadeIn { 57 | 0% {opacity: 0;} 58 | 100% {opacity: 1} 59 | } 60 | @keyframes fadeOut { 61 | 0% {opacity: 1;} 62 | 100% {opacity: 0;} 63 | } 64 | -------------------------------------------------------------------------------- /app/stylesheets/less/modals.less: -------------------------------------------------------------------------------- 1 | // "main": "main.less" 2 | 3 | .modal(){ 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | background-color: @white; 11 | z-index: 5000; 12 | } 13 | .modal-buttons{ 14 | display: block; 15 | width: 90%; 16 | max-width: 280px; 17 | margin: 0 auto; 18 | text-align: right; 19 | box-sizing: border-box; 20 | padding: 15px 0 0 0; 21 | 22 | &.center{ 23 | text-align: center; 24 | } 25 | 26 | button{ 27 | font-size: 12pt; 28 | text-transform: lowercase; 29 | letter-spacing: -0.5px; 30 | font-weight: 400; 31 | color: @white; 32 | background-color: @black; 33 | border: none; 34 | box-sizing: border-box; 35 | padding: 8px 16px; 36 | border-radius: 4px; 37 | &:hover{ 38 | cursor: pointer; 39 | transition: @transition; 40 | } 41 | 42 | &.cancel{ 43 | background-color: fade(@black, 20%); 44 | margin-right: 10px; 45 | &:hover{ 46 | background-color: fade(@black, 40%); 47 | } 48 | } 49 | &.proceed{ 50 | background-color: darken(@green, 6%); 51 | &:hover{ 52 | background-color: darken(@green, 12%); 53 | } 54 | } 55 | &.danger{ 56 | background-color: @red; 57 | &:hover{ 58 | background-color: darken(@red, 5%); 59 | } 60 | } 61 | } 62 | } 63 | 64 | div.rename, 65 | div.newfolder, 66 | div.confirmdelete{ 67 | .modal(); 68 | flex-direction: column; 69 | justify-content: center; 70 | text-align: center; 71 | 72 | h1{ 73 | font-size: 22pt; 74 | box-sizing: border-box; 75 | padding: 0 0 15px 0; 76 | } 77 | 78 | input{ 79 | display: block; 80 | width: 90%; 81 | max-width: 280px; 82 | margin: 0 auto; 83 | box-sizing: border-box; 84 | padding: 12px 20px; 85 | font-weight: 300; 86 | border: 1px solid fade(@black, 50%); 87 | border-radius: 4px; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/stylesheets/less/console.less: -------------------------------------------------------------------------------- 1 | // "main": "main.less" 2 | div.cancel-operation{ 3 | position: fixed; 4 | right: 0; 5 | bottom: 32px; 6 | box-sizing: border-box; 7 | padding: 5px 8px; 8 | background-color: fade(@red, 80%); 9 | color: @white; 10 | font-family: @code; 11 | font-size: 10pt; 12 | transition: @transition; 13 | 14 | &:hover{ 15 | cursor: pointer; 16 | background-color: @red; 17 | transition: @transition; 18 | } 19 | 20 | &.withconsole{ 21 | bottom: 220px; 22 | } 23 | } 24 | div.console-preview{ 25 | display: block; 26 | position: fixed; 27 | bottom: 0; 28 | left: 0; 29 | z-index: 2000; 30 | box-sizing: border-box; 31 | padding: 7px 35px 8px 35px; 32 | color: @white; 33 | font-family: @code; 34 | font-size: 10pt; 35 | background-color: fade(@black, 90%); 36 | width: 100%; 37 | transition: @transition; 38 | font-weight: bolder; 39 | letter-spacing: 0.5px; 40 | height: 32px; 41 | .message{ 42 | line-height: 2em; 43 | position: relative; 44 | top: -5px; 45 | } 46 | 47 | &.red{color: @red;} 48 | &.green{color: @green;} 49 | &.white{color: @white;} 50 | &.blue{color: lighten(@blue, 10%);} 51 | 52 | i{ 53 | font-size: 12pt; 54 | margin-right: 10px; 55 | position: relative; 56 | top: 1px; 57 | } 58 | 59 | &:hover{ 60 | cursor: pointer; 61 | background-color: fade(@black, 100%); 62 | transition: @transition; 63 | } 64 | 65 | .console-unread{ 66 | position: absolute; 67 | float: right; 68 | right: 10px; 69 | bottom: 7px; 70 | background-color: fade(@white, 90%); 71 | display: flex; 72 | height: 17px; 73 | width: auto; 74 | box-sizing: border-box; 75 | text-align: center; 76 | box-sizing: border-box; 77 | padding: 1px 5px 0 5px; 78 | justify-content: center; 79 | flex-direction: column; 80 | transition: @transition; 81 | color: @black; 82 | } 83 | .cancel-operation{ 84 | display: none; 85 | background-color: @red; 86 | color: @white; 87 | transition: @transition; 88 | cursor: pointer; 89 | } 90 | } 91 | 92 | 93 | div.console{ 94 | display: block; 95 | position: fixed; 96 | background-color: fade(@black, 90%); 97 | box-sizing: border-box; 98 | padding: 0 0 35px 0; 99 | z-index: 2000; 100 | width: 100%; 101 | color: @white; 102 | box-shadow: 0px -4px 10px 0px fade(@black, 16%); 103 | height: 220px; 104 | font-family: @code; 105 | font-size: 10pt; 106 | bottom: 0; 107 | left: 0; 108 | 109 | div.console-minimize{ 110 | display: block; 111 | width: 100%; 112 | text-align: center; 113 | font-size: 14pt; 114 | box-sizing: border-box; 115 | padding: 5px 0 5px 0; 116 | margin-bottom: 15px; 117 | transition: @transition; 118 | position: absolute; 119 | z-index: 250; 120 | background-color: transparent; 121 | &:hover{ 122 | cursor: pointer; 123 | transition: @transition; 124 | background-color: fade(@black, 50%); 125 | } 126 | } 127 | 128 | div.console-messages{ 129 | display: block; 130 | height: auto; 131 | height: 185px; 132 | overflow: auto; 133 | padding: 35px 20px 0 0; 134 | width: 100%; 135 | 136 | div.line{ 137 | display: block; 138 | width: 100%; 139 | box-sizing: border-box; 140 | box-sizing: border-box; 141 | padding: 2px 35px; 142 | letter-spacing: 0.5px; 143 | 144 | 145 | &:last-child{ 146 | margin-bottom: 25px; 147 | } 148 | 149 | &.red{color: @red;} 150 | &.green{color: @green;} 151 | &.white{color: @white;} 152 | &.blue{color: @blue;} 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.loadNpmTasks('grunt-contrib-copy'); 3 | grunt.loadNpmTasks('grunt-contrib-uglify'); 4 | grunt.loadNpmTasks('grunt-search'); 5 | grunt.loadNpmTasks('grunt-comment-toggler'); 6 | 7 | const packager = require('electron-packager'); 8 | 9 | const BUILD_DIR = 'build/'; 10 | const DIST_DIR = 'dist/'; 11 | const APP_NAME = 'APP Name'; 12 | const PLATFORM = 'all'; 13 | const ARCH = 'all'; 14 | const ELECTRON_VERSION = '1.2.3'; 15 | const USE_ASAR = true; 16 | 17 | const toggleCommentsFiles = {files:{}}; 18 | toggleCommentsFiles.files[BUILD_DIR + 'index.html'] = 'index.html'; 19 | 20 | // Project configuration. 21 | grunt.initConfig( 22 | { 23 | pkg: grunt.file.readJSON('package.json'), 24 | uglify: { 25 | production: { 26 | files: [ 27 | { 28 | src : 'scripts/**/*.js', 29 | dest : BUILD_DIR + '/scripts/app.angular.min.js' 30 | }, 31 | { 32 | expand: true, 33 | src: BUILD_DIR + 'main.js' 34 | } 35 | ] 36 | } 37 | }, 38 | copy: { 39 | electron_app: { 40 | files: [ 41 | { 42 | expand: true, 43 | src: ['index.html', 'main.js', 'package.json'], 44 | dest: BUILD_DIR 45 | } 46 | ] 47 | }, 48 | angular_app_html: { 49 | files: [ 50 | { 51 | expand: true, 52 | src: ['scripts/**/*.html'], 53 | dest: BUILD_DIR 54 | } 55 | ] 56 | } 57 | }, 58 | search: { 59 | node_modules_dependencies: { 60 | files: { 61 | src: ['index.html'] 62 | }, 63 | options: { 64 | searchString: /="node_modules\/.*"/g, 65 | logFormat: "custom", 66 | onMatch: function (match) { 67 | const filePath = match.match.substring( 68 | 2, 69 | match.match.length - 1 70 | ); 71 | grunt.file.copy(filePath, BUILD_DIR + filePath); 72 | console.log('Found dependency: ' + filePath) 73 | }, 74 | customLogFormatCallback: function (params) { 75 | } 76 | } 77 | } 78 | }, 79 | toggleComments: { 80 | customOptions: { 81 | padding: 1, 82 | removeCommands: false 83 | }, 84 | files: toggleCommentsFiles 85 | } 86 | } 87 | ); 88 | 89 | grunt.registerTask( 90 | 'build', 91 | [ 92 | 'clean', 93 | 'copy:electron_app', 94 | 'copy:angular_app_html', 95 | 'toggleComments', 96 | 'search:node_modules_dependencies', 97 | 'uglify:production', 98 | 'package', 99 | 'fix_default_app' 100 | ] 101 | ); 102 | 103 | // Clean the build directory 104 | grunt.registerTask( 105 | 'clean', 106 | function () { 107 | if (grunt.file.isDir(BUILD_DIR)) { 108 | grunt.file.delete(BUILD_DIR, {force: true}); 109 | } 110 | } 111 | ); 112 | 113 | grunt.registerTask( 114 | 'package', 115 | function () { 116 | const done = this.async(); 117 | packager( 118 | { 119 | dir: BUILD_DIR, 120 | out: DIST_DIR, 121 | name: APP_NAME, 122 | platform: PLATFORM, 123 | arch: ARCH, 124 | version: ELECTRON_VERSION, 125 | asar: USE_ASAR 126 | }, 127 | function (err) { 128 | if (err) { 129 | grunt.warn(err); 130 | return; 131 | } 132 | 133 | done(); 134 | } 135 | ); 136 | } 137 | ); 138 | 139 | // Used as a temporary fix for: 140 | // https://github.com/maxogden/electron-packager/issues/49 141 | grunt.registerTask( 142 | 'fix_default_app', 143 | function () { 144 | 145 | const default_apps = grunt.file.expand( 146 | DIST_DIR + APP_NAME + '*/resources/default_app' 147 | ); 148 | 149 | default_apps.forEach( 150 | function (folder) { 151 | console.log('Removing ' + folder); 152 | grunt.file.delete(folder); 153 | } 154 | ); 155 | } 156 | ); 157 | }; 158 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr": 50, // {int} Maximum error before stopping 3 | 4 | // Enforcing 5 | "bitwise": true, // true: Prohibit bitwise operators (&, |, ^, etc.) 6 | "camelcase": true, // true: Identifiers must be in camelCase 7 | "curly": false, // true: Require {} for every new block or scope 8 | "eqeqeq": true, // true: Require triple equals (===) for comparison 9 | "forin": true, // true: Require filtering for..in loops with obj.hasOwnProperty() 10 | "freeze": true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 11 | "immed": true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 12 | "latedef": true, // true: Require variables/functions to be defined before being used 13 | "newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()` 14 | "noarg": true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 15 | "noempty": true, // true: Prohibit use of empty blocks 16 | "nonbsp": true, // true: Prohibit "non-breaking whitespace" characters. 17 | "nonew": false, // true: Prohibit use of constructors for side-effects (without assignment) 18 | "plusplus": false, // true: Prohibit use of `++` and `--` 19 | "quotmark": "single", // Quotation mark consistency: 20 | // false : do nothing (default) 21 | // true : ensure whatever is used is consistent 22 | // "single" : require single quotes 23 | // "double" : require double quotes 24 | "undef": true, // true: Require all non-global variables to be declared (prevents global leaks) 25 | "unused": true, // Unused variables: 26 | // true : all variables, last function parameter 27 | // "vars" : all variables only 28 | // "strict" : all variables, all function parameters 29 | "strict": true, // true: Requires all functions run in ES5 Strict Mode 30 | "maxparams": false, // {int} Max number of formal params allowed per function 31 | "maxdepth": false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements": false, // {int} Max number statements per function 33 | "maxcomplexity": false, // {int} Max cyclomatic complexity per function 34 | "maxlen": false, // {int} Max number of characters per line 35 | "varstmt": true, // true: Disallow any var statements. Only `let` and `const` are allowed. 36 | 37 | // Relaxing 38 | "asi": false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss": false, // true: Tolerate assignments where comparisons would be expected 40 | "debug": false, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull": false, // true: Tolerate use of `== null` 42 | "esversion": 6, // {int} Specify the ECMAScript version to which the code must adhere. 43 | "moz": false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 44 | // (ex: `for each`, multiple try/catch, function expression…) 45 | "evil": false, // true: Tolerate use of `eval` and `new Function()` 46 | "expr": false, // true: Tolerate `ExpressionStatement` as Programs 47 | "funcscope": true, // true: Tolerate defining variables inside control statements 48 | "globalstrict": false, // true: Allow global "use strict" (also enables 'strict') 49 | "iterator": false, // true: Tolerate using the `__iterator__` property 50 | "lastsemic": false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 51 | "laxbreak": false, // true: Tolerate possibly unsafe line breakings 52 | "laxcomma": false, // true: Tolerate comma-first style coding 53 | "loopfunc": false, // true: Tolerate functions being defined in loops 54 | "multistr": false, // true: Tolerate multi-line strings 55 | "noyield": false, // true: Tolerate generator functions with no yield statement in them. 56 | "notypeof": false, // true: Tolerate invalid typeof operator values 57 | "proto": false, // true: Tolerate using the `__proto__` property 58 | "scripturl": false, // true: Tolerate script-targeted URLs 59 | "shadow": false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 60 | "sub": true, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 61 | "supernew": false, // true: Tolerate `new function () { ... };` and `new Object;` 62 | "validthis": false, // true: Tolerate using this in a non-constructor function 63 | 64 | // Environments 65 | "browser": true, // Web Browser (window, document, etc) 66 | "browserify": false, // Browserify (node.js code in the browser) 67 | "couch": false, // CouchDB 68 | "devel": true, // Development/debugging (alert, confirm, etc) 69 | "dojo": false, // Dojo Toolkit 70 | "jasmine": false, // Jasmine 71 | "jquery": false, // jQuery 72 | "mocha": true, // Mocha 73 | "mootools": false, // MooTools 74 | "node": true, // Node.js 75 | "nonstandard": false, // Widely adopted globals (escape, unescape, etc) 76 | "phantom": false, // PhantomJS 77 | "prototypejs": false, // Prototype and Scriptaculous 78 | "qunit": false, // QUnit 79 | "rhino": false, // Rhino 80 | "shelljs": false, // ShellJS 81 | "typed": false, // Globals for typed array constructions 82 | "worker": false, // Web Workers 83 | "wsh": false, // Windows Scripting Host 84 | "yui": false, // Yahoo User Interface 85 | 86 | // Custom Globals 87 | "globals": { // additional predefined global variables 88 | "angular": true 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /app/stylesheets/less/menu.less: -------------------------------------------------------------------------------- 1 | // "main": "main.less" 2 | 3 | 4 | .menu{ 5 | display: flex; 6 | position: fixed; 7 | justify-content: space-between; 8 | flex-direction: column; 9 | text-align: center; 10 | top: 0; 11 | left: 0; 12 | box-sizing: border-box; 13 | padding: 30px 0; 14 | height: 100%; 15 | width: 100%; 16 | box-sizing: border-box; 17 | z-index: 5000; 18 | background-color: @white; 19 | 20 | .menu-branding{ 21 | display: block; 22 | margin: 0 auto; 23 | height: auto; 24 | width: 80px; 25 | img{ 26 | width: 50px; 27 | height: auto; 28 | } 29 | } 30 | 31 | 32 | i.close-menu, 33 | i.menu-open-notifications{ 34 | display: flex; 35 | width: 40px; 36 | height: 40px; 37 | justify-content: center; 38 | flex-direction: column; 39 | font-size: 38pt; 40 | text-align: center; 41 | color: @black; 42 | position: absolute; 43 | left: 15px; 44 | top: 15px; 45 | transition: @transition; 46 | 47 | &:hover{ 48 | cursor: pointer; 49 | color: fade(@black, 50%); 50 | transition: @transition; 51 | } 52 | } 53 | 54 | div.menu-notifications{ 55 | display: flex; 56 | justify-content: space-between; 57 | position: absolute; 58 | bottom: 0px; 59 | left: 0; 60 | background-color: @grey; 61 | width: 100%; 62 | color: @black; 63 | font-size: 10pt; 64 | text-align: center; 65 | line-height: 1.4em; 66 | letter-spacing: 1px; 67 | font-weight: 500; 68 | 69 | div.message{ 70 | flex-grow: 3; 71 | box-sizing: border-box; 72 | transition: @transition; 73 | padding: 8px 15px 8px 45px; 74 | &:hover{ 75 | background-color: darken(@grey, 5%); 76 | cursor: pointer; 77 | transition: @transition; 78 | } 79 | } 80 | div.ignore{ 81 | padding: 8px 15px; 82 | box-sizing: border-box; 83 | transition: @transition; 84 | color: @black; 85 | &:hover{ 86 | transition: @transition; 87 | cursor: pointer; 88 | color: @red; 89 | } 90 | } 91 | } 92 | 93 | 94 | // Connection form 95 | form.connection-form{ 96 | display: block; 97 | width: 100%; 98 | 99 | div.form-row{ 100 | display: flex; 101 | width: 100%; 102 | justify-content: center; 103 | box-sizing: border-box; 104 | padding: 10px 35px; 105 | max-width: 850px; 106 | margin: 0 auto; 107 | 108 | &.flex-end{ 109 | justify-content: flex-end; 110 | } 111 | 112 | div.form-item{ 113 | label{ 114 | display: block; 115 | box-sizing: border-box; 116 | padding: 4px 0 0 0; 117 | font-weight: 400; 118 | width: 90%; 119 | margin: 0 auto; 120 | font-size: 10pt; 121 | text-transform: lowercase; 122 | color: fade(@black, 50%); 123 | } 124 | input{ 125 | display: block; 126 | width: 90%; 127 | max-width: 280px; 128 | margin: 0 auto; 129 | box-sizing: border-box; 130 | text-align: center; 131 | padding: 12px 8px; 132 | font-weight: 300; 133 | border: 1px solid fade(@black, 50%); 134 | border-radius: 4px; 135 | 136 | &[type='number']{ 137 | padding-left: 35px; 138 | } 139 | } 140 | 141 | button{ 142 | .button(); 143 | 144 | &.favorites{ 145 | background-color: fade(@black, 10%); 146 | color: fade(@black, 50%); 147 | margin-right: 20px; 148 | 149 | i{ 150 | position: relative; 151 | vertical-align: middle; 152 | padding-right: 5px; 153 | width: 14px; 154 | display: inline-block; 155 | } 156 | &:hover{ 157 | background-color: fade(@blue, 30%); 158 | } 159 | &.active{ 160 | background-color: @blue; 161 | color: @white; 162 | &:hover{ 163 | background-color: darken(@blue, 5%); 164 | color: @white; 165 | } 166 | } 167 | } 168 | 169 | &:disabled{ 170 | background-color: @red; 171 | &:hover{ 172 | background-color: @red; 173 | cursor: default; 174 | } 175 | } 176 | 177 | } 178 | } 179 | } 180 | } 181 | 182 | // Favorites 183 | .favorites-list{ 184 | display: block; 185 | text-align: left; 186 | width: 100%; 187 | box-sizing: border-box; 188 | padding: 10px 45px; 189 | max-width: 850px; 190 | margin: 0 auto; 191 | 192 | .favorites-header{ 193 | display: block; 194 | box-sizing: border-box; 195 | padding: 4px 0 15px 0; 196 | font-weight: 400; 197 | width: 100%; 198 | margin: 0 auto; 199 | font-size: 14pt; 200 | text-transform: lowercase; 201 | color: fade(@black, 50%); 202 | } 203 | 204 | .favorite{ 205 | display: inline-block; 206 | box-sizing: border-box; 207 | letter-spacing: 1px; 208 | color: @white; 209 | border-radius: 4px; 210 | font-size: 12pt; 211 | text-transform: lowercase; 212 | letter-spacing: -0.5px; 213 | margin: 0 15px 25px 0; 214 | transition: @transition; 215 | height: 35px; 216 | 217 | .name{ 218 | background-color: @blue; 219 | display: block; 220 | border-radius: 4px; 221 | width: auto; 222 | height: auto; 223 | box-sizing: border-box; 224 | padding: 12px 25px 12px 25px; 225 | &:hover{ 226 | cursor: pointer; 227 | background-color: darken(@blue, 5%); 228 | color: @white; 229 | transition: @transition; 230 | } 231 | } 232 | .delete{ 233 | width: 100%; 234 | display: block; 235 | color: fade(@black, 30%); 236 | font-size: 10pt; 237 | box-sizing: border-box; 238 | padding: 2px 0 0 0; 239 | text-align: center; 240 | transition: @transition; 241 | text-align: center; 242 | &:hover{ 243 | cursor: pointer; 244 | color: @red; 245 | transition: @transition; 246 | } 247 | } 248 | 249 | } 250 | 251 | .edit-favorites{ 252 | display: block; 253 | box-sizing: border-box; 254 | padding: 25px 0 0 0; 255 | 256 | button{ 257 | .button(); 258 | background-color: fade(@black, 10%); 259 | color: fade(@black, 50%); 260 | &:hover{ 261 | background-color: fade(@black, 15%); 262 | } 263 | } 264 | } 265 | } 266 | 267 | 268 | 269 | div.menu-footer{ 270 | display: block; 271 | width: 100%; 272 | text-align: center; 273 | box-sizing: border-box; 274 | padding: 6px 0; 275 | font-size: 8pt; 276 | font-weight: 600; 277 | color: fade(@black, 30%); 278 | text-transform: lowercase; 279 | background-color: @white; 280 | a{ 281 | transition: @transition; 282 | color: fade(@blue, 50%); 283 | &:hover{ 284 | color: @blue; 285 | transition: @transition; 286 | } 287 | } 288 | } 289 | 290 | 291 | 292 | } 293 | -------------------------------------------------------------------------------- /app/pages/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | root 13 | 14 | 15 | 16 | {{level}} 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | 34 |
35 | Name 36 | Size 37 | Date 38 |
39 |
45 | 51 | {{file.name}} 52 | {{file.size | fileSize}} 53 | {{file.time | date}} 54 |
55 |
56 | {{emptyMessage}} 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 67 | 140 | 141 | 144 |
145 |
146 | 147 | 151 |
152 |
153 | 154 |
155 |
156 | 157 | 161 |
162 |
163 | 164 |
165 |
166 |

are you sure?

167 | 171 |
172 |
173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /app/stylesheets/less/main.less: -------------------------------------------------------------------------------- 1 | //"out": "../main.css", "compress": true 2 | 3 | @import 'reset.less'; 4 | @import 'variables.less'; 5 | @import 'modals.less'; 6 | @import 'menu.less'; 7 | @import 'console.less'; 8 | @import 'animations.less'; 9 | 10 | *, *:hover, *:active, *:focus{ 11 | outline: none; 12 | text-decoration: none; 13 | } 14 | 15 | 16 | html, 17 | body{ 18 | font-family: @sans; 19 | -webkit-font-smoothing: subpixel-antialiased; 20 | color: @black; 21 | background-color: @white; 22 | } 23 | 24 | // 25 | // Universal Elements 26 | // 27 | h1{ 28 | display: block; 29 | font-size: 26pt; 30 | font-weight: 100; 31 | } 32 | 33 | 34 | div.app-wrapper{ 35 | display: block; 36 | width: 100%; 37 | height: 100%; 38 | max-height: 100%; 39 | box-sizing: border-box; 40 | } 41 | 42 | 43 | 44 | 45 | 46 | 47 | div.workspace{ 48 | box-sizing: border-box; 49 | // padding: 140px 280px 25px 5%; 50 | padding: 110px 5% 25px 5%; 51 | max-width: 1200px; 52 | margin: 0 auto; 53 | display: flex; 54 | 55 | &.consolePadding{ 56 | padding-bottom: 220px !important; 57 | } 58 | 59 | div.workspace-nav{ 60 | display: block; 61 | box-sizing: border-box; 62 | padding: 25px 0 25px 0; 63 | width: 100%; 64 | top: 0; 65 | left: 0; 66 | z-index: 250; 67 | position: fixed; 68 | background-color: fade(@white, 80%); 69 | 70 | .ion-navicon{ 71 | display: flex; 72 | width: 40px; 73 | height: 40px; 74 | justify-content: center; 75 | flex-direction: column; 76 | font-size: 24pt; 77 | text-align: center; 78 | background-color: @white; 79 | color: @black; 80 | position: fixed; 81 | top: 15px; 82 | left: 15px; 83 | transition: @transition; 84 | 85 | &:hover{ 86 | cursor: pointer; 87 | color: fade(@black, 50%); 88 | transition: @transition; 89 | } 90 | } 91 | 92 | div.dir{ 93 | display: block; 94 | font-size: 13pt; 95 | font-weight: 500; 96 | width: 100%; 97 | text-align: center; 98 | box-sizing: border-box; 99 | padding: 0 0 15px 0; 100 | 101 | .item{ 102 | display: inline-block; 103 | box-sizing: border-box; 104 | padding: 0 5px; 105 | 106 | span{ 107 | color: @red; 108 | transition: @transition; 109 | 110 | i{ 111 | font-size: 10pt; 112 | color: fade(@red, 50%); 113 | display: inline-block; 114 | box-sizing: border-box; 115 | padding: 0 8px 0 5px; 116 | position: relative; 117 | top: 0.5px; 118 | } 119 | 120 | &:hover{ 121 | cursor: pointer; 122 | transition: @transition; 123 | text-decoration: underline; 124 | } 125 | 126 | 127 | &.current{ 128 | color: @black; 129 | &:hover{ 130 | cursor: default; 131 | color: @black; 132 | text-decoration: none; 133 | } 134 | } 135 | 136 | } 137 | } 138 | } 139 | 140 | div.workspace-buttons{ 141 | display: block; 142 | box-sizing: border-box; 143 | padding: 0 5%; 144 | text-align: center; 145 | 146 | input[type='file']{ 147 | display: none; 148 | } 149 | 150 | button{ 151 | font-size: 11pt; 152 | color: @black; 153 | margin: 0 6px; 154 | background-color: transparent; 155 | border: none; 156 | font-weight: 600; 157 | text-transform: lowercase; 158 | vertical-align: middle; 159 | transition: @transition; 160 | 161 | i{ 162 | font-size: 13pt; 163 | position: relative; 164 | padding-right: 4px; 165 | display: inline-block; 166 | top: 1.25px; 167 | } 168 | 169 | &:hover{ 170 | color: @red; 171 | transition: @transition; 172 | } 173 | 174 | &:disabled{ 175 | color: fade(@black, 40%); 176 | font-weight: 400; 177 | &:hover{ 178 | cursor: default; 179 | color: fade(@black, 40%); 180 | 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | div.workspace-sidebar{ 188 | display: block; 189 | flex-wrap: wrap; 190 | width: 280px; 191 | box-sizing: border-box; 192 | // background-color: red; 193 | padding: 0 0px 0 55px; 194 | 195 | div.inner{ 196 | position: fixed; 197 | 198 | .search{ 199 | display: block; 200 | width: 100%; 201 | margin: 0 auto; 202 | box-sizing: border-box; 203 | padding: 12px 10px; 204 | font-weight: 300; 205 | border: none; 206 | border-bottom: 1px solid fade(@black, 50%); 207 | // border-radius: 4px; 208 | } 209 | 210 | div.sidebar-preview{ 211 | display: block; 212 | width: 100%; 213 | height: 200px; 214 | background-repeat: no-repeat; 215 | background-color: fade(@black, 5%); 216 | margin-top: 35px; 217 | 218 | &.folder{ 219 | background-image: url('../visuals/icons/folder.png'); 220 | background-size: 70%; 221 | background-position: center center; 222 | } 223 | } 224 | 225 | div.sidebar-file-details{ 226 | color: @black; 227 | box-sizing: border-box; 228 | padding: 35px 0 0 0; 229 | font-family: @sans; 230 | 231 | span.name{ 232 | font-weight: 600; 233 | font-size: 14pt; 234 | color: @black; 235 | } 236 | span.path, 237 | span.type{ 238 | display: block; 239 | box-sizing: border-box; 240 | padding: 10px 0 0 0; 241 | font-size: 10pt; 242 | } 243 | } 244 | } 245 | } 246 | 247 | div.file-list{ 248 | display: block; 249 | height: 100%; 250 | box-sizing: border-box; 251 | font-weight: 400; 252 | color: @black; 253 | padding: 0 0 40px 0; 254 | width: 100%; 255 | 256 | .searchInput{ 257 | display: block; 258 | margin: 0 auto; 259 | text-align: center; 260 | background-color: fade(@grey, 50%); 261 | box-sizing: border-box; 262 | padding: 8px 12px; 263 | border: none; 264 | } 265 | 266 | div.header{ 267 | display: flex; 268 | box-sizing: border-box; 269 | padding: 14px; 270 | border-bottom: 1px solid fade(@black, 40%); 271 | font-family: @sans; 272 | font-size: 12pt; 273 | color: fade(@black, 80%); 274 | .name{flex-grow: 4;} 275 | .size, 276 | .time{ 277 | width: 18%; 278 | text-align: right; 279 | } 280 | } 281 | div.file{ 282 | display: flex; 283 | box-sizing: border-box; 284 | padding: 14px; 285 | border-bottom: 1px solid fade(@black, 7.5%); 286 | font-family: @code; 287 | .name{ 288 | flex-grow: 4; 289 | i{ 290 | font-size: 14pt; 291 | margin-right: 10px; 292 | position: relative; 293 | top: 1px; 294 | } 295 | } 296 | .size, 297 | .time{ 298 | width: 18%; 299 | text-align: right; 300 | } 301 | &:hover{ 302 | background-color: fade(@black, 6%); 303 | transition: @transition; 304 | cursor: pointer; 305 | } 306 | &:focus{ 307 | background-color: fade(@black, 12%); 308 | } 309 | } 310 | .empty-message{ 311 | display: block; 312 | width: 100%; 313 | text-align: center; 314 | font-size: 16pt; 315 | color: fade(@black, 30%); 316 | font-weight: 400; 317 | box-sizing: border-box; 318 | padding: 75px 0; 319 | } 320 | } 321 | } 322 | 323 | button:disabled{ 324 | background-color: red; 325 | } 326 | 327 | .error { 328 | color: red; 329 | } 330 | 331 | 332 | 333 | // 334 | // Universal elements 335 | // 336 | .button(){ 337 | font-size: 12pt; 338 | text-transform: lowercase; 339 | letter-spacing: -0.5px; 340 | font-weight: 400; 341 | color: @white; 342 | background-color: darken(@green, 6%); 343 | border: none; 344 | box-sizing: border-box; 345 | padding: 8px 16px; 346 | transition: @transition; 347 | border-radius: 4px; 348 | margin-right: 10px; 349 | &:hover{ 350 | transition: @transition; 351 | cursor: pointer; 352 | background-color: darken(@green, 12%); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /app/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700');@import 'http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css';@import url('https://fonts.googleapis.com/css?family=Inconsolata:400,700');html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}.modal-buttons{display:block;width:90%;max-width:280px;margin:0 auto;text-align:right;box-sizing:border-box;padding:15px 0 0 0}.modal-buttons.center{text-align:center}.modal-buttons button{font-size:12pt;text-transform:lowercase;letter-spacing:-0.5px;font-weight:400;color:#FFF;background-color:#081D38;border:none;box-sizing:border-box;padding:8px 16px;border-radius:4px}.modal-buttons button:hover{cursor:pointer;transition:.2s}.modal-buttons button.cancel{background-color:rgba(8,29,56,0.2);margin-right:10px}.modal-buttons button.cancel:hover{background-color:rgba(8,29,56,0.4)}.modal-buttons button.proceed{background-color:#19d244}.modal-buttons button.proceed:hover{background-color:#16b73c}.modal-buttons button.danger{background-color:#FF4F5E}.modal-buttons button.danger:hover{background-color:#ff3547}div.rename,div.newfolder,div.confirmdelete{display:flex;width:100%;height:100%;position:fixed;top:0;left:0;background-color:#FFF;z-index:5000;flex-direction:column;justify-content:center;text-align:center}div.rename h1,div.newfolder h1,div.confirmdelete h1{font-size:22pt;box-sizing:border-box;padding:0 0 15px 0}div.rename input,div.newfolder input,div.confirmdelete input{display:block;width:90%;max-width:280px;margin:0 auto;box-sizing:border-box;padding:12px 20px;font-weight:300;border:1px solid rgba(8,29,56,0.5);border-radius:4px}.menu{display:flex;position:fixed;justify-content:space-between;flex-direction:column;text-align:center;top:0;left:0;padding:30px 0;height:100%;width:100%;box-sizing:border-box;z-index:5000;background-color:#FFF}.menu .menu-branding{display:block;margin:0 auto;height:auto;width:80px}.menu .menu-branding img{width:50px;height:auto}.menu i.close-menu,.menu i.menu-open-notifications{display:flex;width:40px;height:40px;justify-content:center;flex-direction:column;font-size:38pt;text-align:center;color:#081D38;position:absolute;left:15px;top:15px;transition:.2s}.menu i.close-menu:hover,.menu i.menu-open-notifications:hover{cursor:pointer;color:rgba(8,29,56,0.5);transition:.2s}.menu div.menu-notifications{display:flex;justify-content:space-between;position:absolute;bottom:0;left:0;background-color:#E9EEF2;width:100%;color:#081D38;font-size:10pt;text-align:center;line-height:1.4em;letter-spacing:1px;font-weight:500}.menu div.menu-notifications div.message{flex-grow:3;box-sizing:border-box;transition:.2s;padding:8px 15px 8px 45px}.menu div.menu-notifications div.message:hover{background-color:#d9e2e9;cursor:pointer;transition:.2s}.menu div.menu-notifications div.ignore{padding:8px 15px;box-sizing:border-box;transition:.2s;color:#081D38}.menu div.menu-notifications div.ignore:hover{transition:.2s;cursor:pointer;color:#FF4F5E}.menu form.connection-form{display:block;width:100%}.menu form.connection-form div.form-row{display:flex;width:100%;justify-content:center;box-sizing:border-box;padding:10px 35px;max-width:850px;margin:0 auto}.menu form.connection-form div.form-row.flex-end{justify-content:flex-end}.menu form.connection-form div.form-row div.form-item label{display:block;box-sizing:border-box;padding:4px 0 0 0;font-weight:400;width:90%;margin:0 auto;font-size:10pt;text-transform:lowercase;color:rgba(8,29,56,0.5)}.menu form.connection-form div.form-row div.form-item input{display:block;width:90%;max-width:280px;margin:0 auto;box-sizing:border-box;text-align:center;padding:12px 8px;font-weight:300;border:1px solid rgba(8,29,56,0.5);border-radius:4px}.menu form.connection-form div.form-row div.form-item input[type='number']{padding-left:35px}.menu form.connection-form div.form-row div.form-item button{font-size:12pt;text-transform:lowercase;letter-spacing:-0.5px;font-weight:400;color:#FFF;background-color:#19d244;border:none;box-sizing:border-box;padding:8px 16px;transition:.2s;border-radius:4px;margin-right:10px}.menu form.connection-form div.form-row div.form-item button:hover{transition:.2s;cursor:pointer;background-color:#16b73c}.menu form.connection-form div.form-row div.form-item button.favorites{background-color:rgba(8,29,56,0.1);color:rgba(8,29,56,0.5);margin-right:20px}.menu form.connection-form div.form-row div.form-item button.favorites i{position:relative;vertical-align:middle;padding-right:5px;width:14px;display:inline-block}.menu form.connection-form div.form-row div.form-item button.favorites:hover{background-color:rgba(0,128,255,0.3)}.menu form.connection-form div.form-row div.form-item button.favorites.active{background-color:#0080FF;color:#FFF}.menu form.connection-form div.form-row div.form-item button.favorites.active:hover{background-color:#0073e6;color:#FFF}.menu form.connection-form div.form-row div.form-item button:disabled{background-color:#FF4F5E}.menu form.connection-form div.form-row div.form-item button:disabled:hover{background-color:#FF4F5E;cursor:default}.menu .favorites-list{display:block;text-align:left;width:100%;box-sizing:border-box;padding:10px 45px;max-width:850px;margin:0 auto}.menu .favorites-list .favorites-header{display:block;box-sizing:border-box;padding:4px 0 15px 0;font-weight:400;width:100%;margin:0 auto;font-size:14pt;text-transform:lowercase;color:rgba(8,29,56,0.5)}.menu .favorites-list .favorite{display:inline-block;box-sizing:border-box;letter-spacing:1px;color:#FFF;border-radius:4px;font-size:12pt;text-transform:lowercase;letter-spacing:-0.5px;margin:0 15px 25px 0;transition:.2s;height:35px}.menu .favorites-list .favorite .name{background-color:#0080FF;display:block;border-radius:4px;width:auto;height:auto;box-sizing:border-box;padding:12px 25px 12px 25px}.menu .favorites-list .favorite .name:hover{cursor:pointer;background-color:#0073e6;color:#FFF;transition:.2s}.menu .favorites-list .favorite .delete{width:100%;display:block;color:rgba(8,29,56,0.3);font-size:10pt;box-sizing:border-box;padding:2px 0 0 0;transition:.2s;text-align:center}.menu .favorites-list .favorite .delete:hover{cursor:pointer;color:#FF4F5E;transition:.2s}.menu .favorites-list .edit-favorites{display:block;box-sizing:border-box;padding:25px 0 0 0}.menu .favorites-list .edit-favorites button{font-size:12pt;text-transform:lowercase;letter-spacing:-0.5px;font-weight:400;color:#FFF;background-color:#19d244;border:none;box-sizing:border-box;padding:8px 16px;transition:.2s;border-radius:4px;margin-right:10px;background-color:rgba(8,29,56,0.1);color:rgba(8,29,56,0.5)}.menu .favorites-list .edit-favorites button:hover{transition:.2s;cursor:pointer;background-color:#16b73c}.menu .favorites-list .edit-favorites button:hover{background-color:rgba(8,29,56,0.15)}.menu div.menu-footer{display:block;width:100%;text-align:center;box-sizing:border-box;padding:6px 0;font-size:8pt;font-weight:600;color:rgba(8,29,56,0.3);text-transform:lowercase;background-color:#FFF}.menu div.menu-footer a{transition:.2s;color:rgba(0,128,255,0.5)}.menu div.menu-footer a:hover{color:#0080FF;transition:.2s}div.cancel-operation{position:fixed;right:0;bottom:32px;box-sizing:border-box;padding:5px 8px;background-color:rgba(255,79,94,0.8);color:#FFF;font-family:'Inconsolata',monospace;font-size:10pt;transition:.2s}div.cancel-operation:hover{cursor:pointer;background-color:#FF4F5E;transition:.2s}div.cancel-operation.withconsole{bottom:220px}div.console-preview{display:block;position:fixed;bottom:0;left:0;z-index:2000;box-sizing:border-box;padding:7px 35px 8px 35px;color:#FFF;font-family:'Inconsolata',monospace;font-size:10pt;background-color:rgba(8,29,56,0.9);width:100%;transition:.2s;font-weight:bolder;letter-spacing:.5px;height:32px}div.console-preview .message{line-height:2em;position:relative;top:-5px}div.console-preview.red{color:#FF4F5E}div.console-preview.green{color:#25E552}div.console-preview.white{color:#FFF}div.console-preview.blue{color:#39f}div.console-preview i{font-size:12pt;margin-right:10px;position:relative;top:1px}div.console-preview:hover{cursor:pointer;background-color:#081d38;transition:.2s}div.console-preview .console-unread{position:absolute;float:right;right:10px;bottom:7px;background-color:rgba(255,255,255,0.9);display:flex;height:17px;width:auto;text-align:center;box-sizing:border-box;padding:1px 5px 0 5px;justify-content:center;flex-direction:column;transition:.2s;color:#081D38}div.console-preview .cancel-operation{display:none;background-color:#FF4F5E;color:#FFF;transition:.2s;cursor:pointer}div.console{display:block;position:fixed;background-color:rgba(8,29,56,0.9);box-sizing:border-box;padding:0 0 35px 0;z-index:2000;width:100%;color:#FFF;box-shadow:0 -4px 10px 0 rgba(8,29,56,0.16);height:220px;font-family:'Inconsolata',monospace;font-size:10pt;bottom:0;left:0}div.console div.console-minimize{display:block;width:100%;text-align:center;font-size:14pt;box-sizing:border-box;padding:5px 0 5px 0;margin-bottom:15px;transition:.2s;position:absolute;z-index:250;background-color:transparent}div.console div.console-minimize:hover{cursor:pointer;transition:.2s;background-color:rgba(8,29,56,0.5)}div.console div.console-messages{display:block;height:auto;height:185px;overflow:auto;padding:35px 20px 0 0;width:100%}div.console div.console-messages div.line{display:block;width:100%;box-sizing:border-box;padding:2px 35px;letter-spacing:.5px}div.console div.console-messages div.line:last-child{margin-bottom:25px}div.console div.console-messages div.line.red{color:#FF4F5E}div.console div.console-messages div.line.green{color:#25E552}div.console div.console-messages div.line.white{color:#FFF}div.console div.console-messages div.line.blue{color:#0080FF}.ng-fade.ng-hide-add{animation:.15s fadeOut ease !important}.ng-fade.ng-hide-remove{animation:.15s fadeIn ease !important}.slideTop.ng-hide-add{animation:.2s slideOutToTop ease !important}.slideTop.ng-hide-remove{animation:.2s slideInFromTop ease !important}.slideBottom.ng-hide-add{animation:.2s slideOutToBottom ease !important}.slideBottom.ng-hide-remove{animation:.2s slideInFromBottom ease !important}.zoom.ng-hide-add{animation:.2s zoomOut ease !important}.zoom.ng-hide-remove{animation:.2s zoomIn ease !important}@keyframes slideInFromTop{0%{top:-100%;opacity:1}100%{top:0;opacity:1}}@keyframes slideOutToTop{0%{top:0;opacity:1}100%{top:-100%;opacity:1}}@keyframes slideInFromBottom{0%{bottom:-100%;opacity:0}100%{bottom:0;opacity:1}}@keyframes slideOutToBottom{0%{bottom:0;opacity:1}100%{bottom:-100%;opacity:0}}@keyframes zoomIn{0%{transform:scale(.85);opacity:0}100%{transform:scale(1);opacity:1}}@keyframes zoomOut{0%{transform:scale(1);opacity:1}100%{transform:scale(.85);opacity:0}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}*,*:hover,*:active,*:focus{outline:none;text-decoration:none}html,body{font-family:'Roboto','Helvetica','Arial' sans-serif;-webkit-font-smoothing:subpixel-antialiased;color:#081D38;background-color:#FFF}h1{display:block;font-size:26pt;font-weight:100}div.app-wrapper{display:block;width:100%;height:100%;max-height:100%;box-sizing:border-box}div.workspace{box-sizing:border-box;padding:110px 5% 25px 5%;max-width:1200px;margin:0 auto;display:flex}div.workspace.consolePadding{padding-bottom:220px !important}div.workspace div.workspace-nav{display:block;box-sizing:border-box;padding:25px 0 25px 0;width:100%;top:0;left:0;z-index:250;position:fixed;background-color:rgba(255,255,255,0.8)}div.workspace div.workspace-nav .ion-navicon{display:flex;width:40px;height:40px;justify-content:center;flex-direction:column;font-size:24pt;text-align:center;background-color:#FFF;color:#081D38;position:fixed;top:15px;left:15px;transition:.2s}div.workspace div.workspace-nav .ion-navicon:hover{cursor:pointer;color:rgba(8,29,56,0.5);transition:.2s}div.workspace div.workspace-nav div.dir{display:block;font-size:13pt;font-weight:500;width:100%;text-align:center;box-sizing:border-box;padding:0 0 15px 0}div.workspace div.workspace-nav div.dir .item{display:inline-block;box-sizing:border-box;padding:0 5px}div.workspace div.workspace-nav div.dir .item span{color:#FF4F5E;transition:.2s}div.workspace div.workspace-nav div.dir .item span i{font-size:10pt;color:rgba(255,79,94,0.5);display:inline-block;box-sizing:border-box;padding:0 8px 0 5px;position:relative;top:.5px}div.workspace div.workspace-nav div.dir .item span:hover{cursor:pointer;transition:.2s;text-decoration:underline}div.workspace div.workspace-nav div.dir .item span.current{color:#081D38}div.workspace div.workspace-nav div.dir .item span.current:hover{cursor:default;color:#081D38;text-decoration:none}div.workspace div.workspace-nav div.workspace-buttons{display:block;box-sizing:border-box;padding:0 5%;text-align:center}div.workspace div.workspace-nav div.workspace-buttons input[type='file']{display:none}div.workspace div.workspace-nav div.workspace-buttons button{font-size:11pt;color:#081D38;margin:0 6px;background-color:transparent;border:none;font-weight:600;text-transform:lowercase;vertical-align:middle;transition:.2s}div.workspace div.workspace-nav div.workspace-buttons button i{font-size:13pt;position:relative;padding-right:4px;display:inline-block;top:1.25px}div.workspace div.workspace-nav div.workspace-buttons button:hover{color:#FF4F5E;transition:.2s}div.workspace div.workspace-nav div.workspace-buttons button:disabled{color:rgba(8,29,56,0.4);font-weight:400}div.workspace div.workspace-nav div.workspace-buttons button:disabled:hover{cursor:default;color:rgba(8,29,56,0.4)}div.workspace div.workspace-sidebar{display:block;flex-wrap:wrap;width:280px;box-sizing:border-box;padding:0 0 0 55px}div.workspace div.workspace-sidebar div.inner{position:fixed}div.workspace div.workspace-sidebar div.inner .search{display:block;width:100%;margin:0 auto;box-sizing:border-box;padding:12px 10px;font-weight:300;border:none;border-bottom:1px solid rgba(8,29,56,0.5)}div.workspace div.workspace-sidebar div.inner div.sidebar-preview{display:block;width:100%;height:200px;background-repeat:no-repeat;background-color:rgba(8,29,56,0.05);margin-top:35px}div.workspace div.workspace-sidebar div.inner div.sidebar-preview.folder{background-image:url('../visuals/icons/folder.png');background-size:70%;background-position:center center}div.workspace div.workspace-sidebar div.inner div.sidebar-file-details{color:#081D38;box-sizing:border-box;padding:35px 0 0 0;font-family:'Roboto','Helvetica','Arial' sans-serif}div.workspace div.workspace-sidebar div.inner div.sidebar-file-details span.name{font-weight:600;font-size:14pt;color:#081D38}div.workspace div.workspace-sidebar div.inner div.sidebar-file-details span.path,div.workspace div.workspace-sidebar div.inner div.sidebar-file-details span.type{display:block;box-sizing:border-box;padding:10px 0 0 0;font-size:10pt}div.workspace div.file-list{display:block;height:100%;box-sizing:border-box;font-weight:400;color:#081D38;padding:0 0 40px 0;width:100%}div.workspace div.file-list .searchInput{display:block;margin:0 auto;text-align:center;background-color:rgba(233,238,242,0.5);box-sizing:border-box;padding:8px 12px;border:none}div.workspace div.file-list div.header{display:flex;box-sizing:border-box;padding:14px;border-bottom:1px solid rgba(8,29,56,0.4);font-family:'Roboto','Helvetica','Arial' sans-serif;font-size:12pt;color:rgba(8,29,56,0.8)}div.workspace div.file-list div.header .name{flex-grow:4}div.workspace div.file-list div.header .size,div.workspace div.file-list div.header .time{width:18%;text-align:right}div.workspace div.file-list div.file{display:flex;box-sizing:border-box;padding:14px;border-bottom:1px solid rgba(8,29,56,0.075);font-family:'Inconsolata',monospace}div.workspace div.file-list div.file .name{flex-grow:4}div.workspace div.file-list div.file .name i{font-size:14pt;margin-right:10px;position:relative;top:1px}div.workspace div.file-list div.file .size,div.workspace div.file-list div.file .time{width:18%;text-align:right}div.workspace div.file-list div.file:hover{background-color:rgba(8,29,56,0.06);transition:.2s;cursor:pointer}div.workspace div.file-list div.file:focus{background-color:rgba(8,29,56,0.12)}div.workspace div.file-list .empty-message{display:block;width:100%;text-align:center;font-size:16pt;color:rgba(8,29,56,0.3);font-weight:400;box-sizing:border-box;padding:75px 0}button:disabled{background-color:red}.error{color:red} 2 | -------------------------------------------------------------------------------- /app/controllers/base.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app') 5 | .controller('homeCtrl', ['$scope', '$timeout', '$interval', '$http', 'konsoleService', 'analyticsService', homeController]); 6 | 7 | function homeController($scope, $timeout, $interval, $http, konsoleService, analyticsService) { 8 | analyticsService.track('/'); 9 | 10 | const fs = require('fs'); 11 | 12 | const JsFtp = require('jsftp'), 13 | Ftp = require('jsftp-rmr')(JsFtp); 14 | let ftp; 15 | 16 | // Get computer OS 17 | const os = require('os'); 18 | let isWindowsOS = false, 19 | dirSeperator = '/'; 20 | if (os.platform() === 'win32') { 21 | isWindowsOS = true; 22 | dirSeperator = '\\'; 23 | } 24 | 25 | // $scope.remote = require('electron').remote; 26 | // $scope.dialog = remote.require('dialog'); 27 | const remote = require('electron').remote, 28 | dialog = require('electron').dialog; 29 | 30 | $scope.path = '.'; 31 | $scope.emptyMessage = 'Loading...'; 32 | $scope.fullConsole = false; 33 | $scope.showingMenu = true; 34 | konsoleService.addMessage('Click to expand console.'); 35 | 36 | $scope.editingFavorites = false; 37 | 38 | $scope.fileSelected = false; 39 | $scope.saveFavorite = false; 40 | 41 | 42 | const shell = require('electron').shell, 43 | pjson = require('./package.json'); 44 | $scope.appVersion = pjson.version; 45 | // console.log(require('electron').remote.app.getVersion()); 46 | 47 | // Get update notifications from ffftp.site 48 | $http({ 49 | method: 'GET', 50 | url: 'http://www.ffftp.site/appupdate.json' 51 | }).then((data) => { 52 | if (data.version !== $scope.appVersion.toString()) { 53 | $scope.showUpdate = true; 54 | } 55 | }, () => { 56 | console.log('Error getting update notification'); 57 | }); 58 | 59 | $scope.updateApp = () => { 60 | shell.openExternal('http://ffftp.site/download/' + $scope.appVersion); 61 | }; 62 | 63 | // Load Favorites 64 | const storage = require('electron-json-storage'); 65 | $scope.favorites = []; 66 | storage.has('favorites', (error, hasKey) => { 67 | if (error) throw error; 68 | 69 | if (hasKey) { 70 | storage.get('favorites', (error, data) => { 71 | if (error) throw error; 72 | 73 | $timeout(() => { 74 | $scope.favorites = data; 75 | console.log('FAVORITES'); 76 | console.log(data); 77 | }, 0); 78 | }); 79 | } else { 80 | console.log('No favs'); 81 | } 82 | }); 83 | 84 | // On favorite click 85 | $scope.loadFavorite = (index) => { 86 | $scope.ftpHost = $scope.favorites[index].host; 87 | $scope.ftpPort = $scope.favorites[index].port; 88 | $scope.ftpUsername = $scope.favorites[index].user; 89 | $scope.ftpPassword = $scope.favorites[index].pass; 90 | $scope.favoriteName = $scope.favorites[index].name; 91 | $scope.connect(); 92 | }; 93 | $scope.deleteFavorite = (index) => { 94 | $scope.favorites.splice(index, 1); 95 | $scope.saveFavoritesToStorage(); 96 | }; 97 | 98 | 99 | // Connect to ftp 100 | $scope.connect = () => { 101 | $scope.showingMenu = false; 102 | 103 | if ($scope.saveFavorite) { 104 | $scope.newFavorite = { 105 | name: $scope.favoriteName, 106 | host: $scope.ftpHost, 107 | port: $scope.ftpPort, 108 | user: $scope.ftpUsername, 109 | pass: $scope.ftpPassword 110 | }; 111 | $scope.favorites.push($scope.newFavorite); 112 | $scope.saveFavoritesToStorage(); 113 | } 114 | 115 | $scope.saveFavorite = false; 116 | 117 | ftp = new Ftp({ 118 | host: $scope.ftpHost, 119 | port: $scope.ftpPort, 120 | user: $scope.ftpUsername, 121 | pass: $scope.ftpPassword 122 | }); 123 | 124 | ftp.on('error', (data) => { 125 | konsoleService.addMessage('red', data); 126 | $scope.emptyMessage = 'Error connecting.' 127 | console.error(data); 128 | }); 129 | 130 | ftp.on('lookup', (data) => { 131 | konsoleService.addMessage('red', `Lookup error: ${data}`); 132 | $scope.emptyMessage = 'Error connecting.' 133 | console.error(`Lookup error: ${data}`); 134 | }); 135 | 136 | konsoleService.addMessage('white', `Connected to ${ftp.host}`); 137 | 138 | // Start Scripts 139 | $scope.changeDir(); 140 | $scope.splitPath(); 141 | }; 142 | 143 | $scope.saveFavoritesToStorage = () => { 144 | storage.set('favorites', $scope.favorites, (error) => { 145 | if (error) throw error; 146 | }); 147 | }; 148 | $scope.deleteFavs = () => { 149 | storage.clear((error) => { 150 | if (error) throw error; 151 | }); 152 | }; 153 | 154 | // Change directory 155 | $scope.changeDir = () => { 156 | $scope.searchFiles = ''; 157 | if ($scope.showCancelOperation) { 158 | return; 159 | } else { 160 | $scope.fileSelected = false; 161 | ftp.ls($scope.path, (err, res) => { 162 | $timeout(() => { 163 | $scope.files = res; 164 | $scope.splitPath(); 165 | $scope.emptyMessage = `There's nothin' here`; 166 | if ($scope.path !== '.') { 167 | konsoleService.addMessage('white', `Navigated to ${$scope.path}`); 168 | } 169 | }, 0); 170 | }); 171 | } 172 | }; 173 | 174 | // Go into a directory (double click folder); 175 | $scope.intoDir = (dir) => { 176 | if ($scope.selectedFileType === 0) { // If file, do nothing but select 177 | return; 178 | } else { 179 | $scope.emptyMessage = 'Loading...'; 180 | $scope.path = `${$scope.path}/${dir}`; 181 | $scope.changeDir(); 182 | } 183 | }; 184 | 185 | // Go up a directory - button on nav 186 | $scope.upDir = () => { 187 | $scope.path = $scope.path.substring(0, $scope.path.lastIndexOf('/')); 188 | $scope.changeDir(); 189 | }; 190 | 191 | // Click a breadcrumb to go up multiple directories 192 | $scope.breadCrumb = (index) => { 193 | $scope.path = '.'; 194 | for (let i = 1; i <= index; i++) { 195 | $scope.path = `${$scope.path}/${$scope.pathArray[i]}`; 196 | } 197 | console.log($scope.path); 198 | $scope.changeDir(); 199 | }; 200 | 201 | // Split paths for use in breadcrumbs 202 | $scope.splitPath = () => { 203 | $scope.pathArray = new Array(); 204 | $scope.pathArray = $scope.path.split('/'); 205 | }; 206 | 207 | // Select a file to modify 208 | $scope.selectTimer = () => { 209 | $scope.fileToFile = true; 210 | $timeout(() => { 211 | $scope.fileToFile = false; 212 | }, 200); 213 | }; 214 | $scope.selectFile = (name, filetype) => { 215 | $scope.fileSelected = true; 216 | $scope.selectedFileName = name; 217 | $scope.selectedFileType = filetype; 218 | $scope.selectedFilePath = `${$scope.path}/${name}`; 219 | console.log($scope.selectedFileName); 220 | }; 221 | $scope.clearSelected = () => { 222 | $timeout(() => { 223 | if (!$scope.fileToFile) $scope.fileSelected = false; 224 | }, 200); 225 | }; 226 | 227 | // Create a new folder 228 | $scope.showingNewFolder = false; 229 | $scope.newFolder = () => { 230 | $scope.showingNewFolder = false; 231 | ftp.raw('mkd', `${$scope.path}/${$scope.newFolderName}`, (err, data) => { 232 | $scope.changeDir(); 233 | $scope.newFolderName = ''; 234 | if (err) { 235 | konsoleService.addMessage("red", err); 236 | } else { 237 | konsoleService.addMessage("white", data.text); 238 | } 239 | }); 240 | }; 241 | 242 | // Delete a file or folder depending on file type 243 | $scope.deleteFile = () => { 244 | console.log(`TYPE: ${$scope.selectedFileType}`); 245 | console.log(`NAME: ${$scope.selectedFileName}`); 246 | console.log(`PATH: ${$scope.path}`); 247 | $scope.showingConfirmDelete = false; 248 | console.log(`DELETING ${$scope.path}/${$scope.selectedFileName}`); 249 | if ($scope.selectedFileType === 0) { // 0 is file 250 | ftp.raw('dele', `${$scope.path}/${$scope.selectedFileName}`, (err, data) => { 251 | if (err) return konsoleService.addMessage('red', err); 252 | $scope.changeDir(); 253 | konsoleService.addMessage('green', data.text); 254 | }); 255 | } else if ($scope.selectedFileType === 1) { // Everything else is folder 256 | ftp.rmr(`${$scope.path}/${$scope.selectedFileName}`, (err) => { 257 | ftp.raw('rmd', `${$scope.path}/${$scope.selectedFileName}`, (err, data) => { 258 | if (err) return konsoleService.addMessage('red', err); 259 | $scope.changeDir(); 260 | konsoleService.addMessage('green', data.text); 261 | }); 262 | }); 263 | } 264 | }; 265 | 266 | // Rename a file or folder 267 | $scope.renameFile = () => { 268 | if (!$scope.showingRename) { 269 | $scope.fileRenameInput = $scope.selectedFileName; 270 | $scope.showingRename = true; 271 | } else { 272 | ftp.rename(`${$scope.path}/${$scope.selectedFileName}`, `${$scope.path}/${$scope.fileRenameInput}`, (err, res) => { 273 | if (!err) { 274 | $scope.showingRename = false; 275 | konsoleService.addMessage('green', `Renamed ${$scope.selectedFileName} to ${$scope.fileRenameInput}`); 276 | $scope.changeDir(); 277 | } else { 278 | konsoleService.addMessage('red', err); 279 | } 280 | }); 281 | } 282 | }; 283 | 284 | // Download a file 285 | $scope.chooseDownloadDirectory = () => { 286 | document.getElementById('chooseDownloadDirectory').click(); 287 | }; 288 | $scope.saveDownloadPath = () => { 289 | $scope.downloadPath = document.getElementById('chooseDownloadDirectory').files[0].path; 290 | console.log($scope.downloadPath); 291 | $scope.downloadFiles(); 292 | }; 293 | $scope.downloadFiles = () => { 294 | console.log('downloadFiles'); 295 | 296 | if ($scope.selectedFileType === 0) { // If file, download right away 297 | $scope.saveFileToDisk($scope.selectedFilePath, $scope.selectedFileName); 298 | } else if ($scope.selectedFileType === 1) { // if folder, index folders and files 299 | $scope.foldersToCreate = []; 300 | $scope.filesToDownload = []; 301 | $scope.getDownloadTree($scope.selectedFilePath); 302 | $scope.downloadTime = 0; 303 | $scope.showCancelOperation = true; 304 | $scope.downloadInterval = $interval(() => { 305 | $scope.downloadTime++; 306 | }, 1000); // Download Timer 307 | $scope.gettingDownloadReady = true; 308 | $scope.watchDownloadProcess(); 309 | } else { // else unknown file type 310 | konsoleService.addMessage('red', `Unable to download file ${$scope.selectedFileName}. Unknown file type.`); 311 | } 312 | }; 313 | 314 | // Checks every 400ms if download tree is still processing 315 | $scope.watchDownloadProcess = () => { 316 | $timeout(() => { 317 | if ($scope.gettingDownloadReady) { 318 | $scope.watchDownloadProcess; 319 | } else { 320 | $scope.processFiles(); 321 | } 322 | }, 400); 323 | }; 324 | 325 | // Get download tree loops through all folders and files, and adds them to arrays. 326 | // Current directory folders are added to the tempfolders array 327 | $scope.getDownloadTree = (path) => { 328 | $scope.tempFolders = []; 329 | $scope.tempPath = path; 330 | $scope.gettingDownloadReady = true; // Reset because still working 331 | 332 | ftp.ls(path, (err, res) => { 333 | console.log(res); 334 | for (let i = 0, item; item = res[i]; i++) { 335 | if (item.type === 1) { // if folder, push to full array and temp 336 | $scope.foldersToCreate.push({'path': path, 'name': item.name}); 337 | $scope.tempFolders.push({'path': path, 'name': item.name}); 338 | } else if (item.type === 0) { // if file, push to file array 339 | $scope.filesToDownload.push({'path': path, 'name': item.name}); 340 | } 341 | } 342 | $scope.gettingDownloadReady = false; 343 | for (let x = 0, folder; folder = $scope.tempFolders[x]; x++) { // for each folder, getDownloadTree again and index those. Same process 344 | console.log(`FOLDER PATH: ${folder.path}`); 345 | $scope.getDownloadTree(`${folder.path}/${folder.name}`); 346 | } 347 | }); 348 | }; 349 | 350 | // Once getDownloadTree is finished, this is called 351 | $scope.processFiles = () => { 352 | //First create base folder 353 | fs.mkdir(`${$scope.downloadPath}${dirSeperator}${$scope.selectedFileName}`); 354 | //Then create all folders within 355 | for (let i = 0, folder; folder = $scope.foldersToCreate[i]; i++) { // Create all empty folders 356 | const newfolderpath = `${folder.path}${dirSeperator}${folder.name}`; 357 | fs.mkdir(`${$scope.downloadPath}${dirSeperator}${$scope.selectedFileName + newfolderpath.replace($scope.selectedFilePath, '')}`); 358 | } 359 | 360 | 361 | // Then begin downloading files individually 362 | $scope.downloadFileZero = 0; 363 | $scope.saveAllFilesToDisk(); 364 | }; 365 | 366 | $scope.saveAllFilesToDisk = () => { 367 | if ($scope.filesToDownload[$scope.downloadFileZero]) { 368 | const filepath = $scope.filesToDownload[$scope.downloadFileZero].path, 369 | filename = $scope.filesToDownload[$scope.downloadFileZero].name, 370 | absoluteFilePath = filepath.substring(filepath.indexOf('/') + 1) + '/' + filename; 371 | 372 | const from = `${filepath}/${filename}`; 373 | 374 | const newfilepath = `${filepath}${dirSeperator}${filename}`; 375 | let to = `${$scope.downloadPath}${dirSeperator}${$scope.selectedFileName + newfilepath.replace($scope.selectedFilePath, '')}`; 376 | konsoleService.addMessage('white', `Downloading ${filename} to ${$scope.downloadPath}${dirSeperator}${$scope.selectedFileName + newfilepath.replace($scope.selectedFilePath, '')}`); 377 | 378 | ftp.get(from, to, (hadErr) => { 379 | if (hadErr) { 380 | konsoleService.addMessage('red', `Error downloading ${filename}... ${hadErr}`); 381 | } else { 382 | konsoleService.addMessage('white', 'Done.'); 383 | } 384 | $scope.downloadFileZero++; 385 | $scope.changeDir(); 386 | $scope.saveAllFilesToDisk(); // do it again until all files are downloaded 387 | }); 388 | } else { // once finished 389 | $timeout(() => { 390 | $scope.changeDir(); 391 | $interval.cancel($scope.downloadInterval); 392 | $scope.showCancelOperation = false; 393 | konsoleService.addMessage('blue', `Downloaded ${$scope.filesToDownload.length} files in ${$scope.foldersToCreate.length} directories in ${$scope.downloadTime} seconds.`); 394 | }, 200); 395 | } 396 | }; 397 | 398 | // Download file if single file - not folder 399 | $scope.saveFileToDisk = (filepath, filename) => { 400 | const from = filepath; 401 | let to = `${$scope.downloadPath}\\${filename}`; 402 | console.log(`DOWNLOADING: ${from} TO: ${to}`); 403 | ftp.get(from, to, (hadErr) => { 404 | if (hadErr) { 405 | konsoleService.addMessage('red', `Error downloading ${filename}`); 406 | } else { 407 | konsoleService.addMessage('green', `Successfully downloaded ${filename}`); 408 | } 409 | }); 410 | }; 411 | 412 | // File Uploading 413 | document.ondragover = document.ondrop = (ev) => { 414 | ev.preventDefault(); 415 | }; 416 | document.body.ondrop = (ev) => { 417 | $scope.dragged = ev.dataTransfer.files; 418 | 419 | konsoleService.addMessage('white', 'Getting file tree...'); 420 | $scope.folderTree = []; 421 | $scope.baseUploadPath = $scope.path; 422 | 423 | $scope.foldersArray = []; 424 | $scope.filesArray = []; 425 | 426 | $scope.uploadTime = 0; 427 | $scope.uploadInterval = $interval(() => { 428 | $scope.uploadTime++; 429 | }, 1000); 430 | $scope.showCancelOperation = true; 431 | 432 | for (let i = 0, f; f = $scope.dragged[i]; i++) { 433 | $scope.folderTree.push(dirTree($scope.dragged[i].path)); 434 | } 435 | 436 | $scope.baselocalpath = $scope.dragged[0].path.substring(0, $scope.dragged[0].path.lastIndexOf(dirSeperator)); 437 | 438 | $scope.gatherFiles($scope.folderTree); 439 | $timeout(() => { 440 | $scope.uploadEverything(); 441 | }, 1000); 442 | 443 | ev.preventDefault(); 444 | }; 445 | 446 | $scope.gatherFiles = (tree) => { 447 | if (!tree.length) { 448 | console.log("No folders"); 449 | } 450 | $scope.nestedTree = []; 451 | for (let i = 0, f; f = tree[i]; i++) { 452 | if (tree[i].extension) { // if file 453 | $scope.filesArray.push({'name': tree[i].name, 'path': tree[i].path}); 454 | } else { // if folder 455 | $scope.foldersArray.push({'name': tree[i].name, 'path': tree[i].path}); 456 | if (tree[i].children.length) { 457 | console.log(`HAS CHILDREN: ${tree[i].name}`); 458 | for (let x = 0, y; y = tree[i].children[x]; x++) { 459 | $scope.nestedTree.push(dirTree(y.path)); 460 | } 461 | $scope.gatherFiles($scope.nestedTree); 462 | } 463 | } 464 | } 465 | }; 466 | 467 | $scope.uploadEverything = () => { 468 | console.log($scope.foldersArray); 469 | console.log($scope.filesArray); 470 | konsoleService.addMessage('white', `Uploading ${$scope.foldersArray.length} folders and ${$scope.filesArray.length} files...`); 471 | $scope.filezero = 0; 472 | $scope.folderzero = 0; 473 | $scope.mkDirs(); 474 | }; 475 | $scope.mkDirs = () => { 476 | if ($scope.foldersArray[$scope.folderzero]) { 477 | const localpath = $scope.foldersArray[$scope.folderzero].path, 478 | uploadpath = $scope.baseUploadPath; 479 | 480 | $scope.dirToCreate = uploadpath + localpath.replace($scope.baselocalpath, '').replace(/\\/g, '/'); 481 | konsoleService.addMessage('white', `Creating folder ${$scope.dirToCreate}...`) 482 | 483 | ftp.raw('mkd', $scope.dirToCreate, (err, data) => { 484 | // $scope.changeDir(); 485 | if (err) { 486 | konsoleService.addMessage(err); 487 | } 488 | else { 489 | konsoleService.addMessage(data.text); 490 | } 491 | $scope.folderzero++; 492 | $scope.mkDirs(); 493 | }); 494 | } else { 495 | $timeout(() => { 496 | $scope.changeDir(); 497 | $scope.upFiles(); 498 | }, 200); 499 | } 500 | }; 501 | $scope.upFiles = () => { 502 | if ($scope.filesArray[$scope.filezero]) { 503 | const localpath = $scope.filesArray[$scope.filezero].path, 504 | uploadpath = $scope.baseUploadPath; 505 | $scope.fileToUpload = uploadpath + localpath.replace($scope.baselocalpath, '').replace(/\\/g, '/'); 506 | konsoleService.addMessage('white', `Uploading ${$scope.fileToUpload}...`); 507 | 508 | ftp.put(localpath, $scope.fileToUpload, (hadError) => { 509 | if (!hadError) { 510 | konsoleService.addMessage('white', `Successfully uploaded ${localpath} to ${$scope.fileToUpload}`); 511 | } else { 512 | konsoleService.addMessage('red', `Error Uploading ${$scope.fileToUpload}`); 513 | } 514 | $scope.filezero++; 515 | $scope.changeDir(); 516 | $scope.upFiles(); 517 | }); 518 | } else { 519 | $timeout(() => { 520 | $interval.cancel($scope.uploadInterval); 521 | $scope.showCancelOperation = false; 522 | $scope.changeDir(); 523 | konsoleService.addMessage('blue', `File transfer completed in ${$scope.uploadTime} seconds.`); 524 | }, 200); 525 | } 526 | }; 527 | 528 | // Drag to move files 529 | // Unused for now 530 | $scope.onDragComplete = (path) => { 531 | console.log(`MOVING: ${path}`); 532 | }; 533 | $scope.onDropComplete = (path) => { 534 | console.log(`MOVE TO: ${path}`); 535 | }; 536 | 537 | // Keyboard Shortcuts 538 | window.document.onkeydown = (e) => { 539 | if (!e) e = event; 540 | if (e.keyCode === 27) { // esc 541 | $timeout(() => { 542 | console.log('esc pressed'); 543 | if (!$scope.showingRename && !$scope.showingNewFolder && !$scope.showingMenu) $scope.fullConsole = false; 544 | $scope.showingRename = false; 545 | $scope.showingMenu = false; 546 | $scope.showingNewFolder = false; 547 | }, 0); 548 | } 549 | if (e.keyCode === 8 || e.keyCode === 46) { // esc 550 | $timeout(() => { 551 | if ($scope.fileSelected) $scope.showingConfirmDelete = true; 552 | }, 0); 553 | } 554 | }; 555 | 556 | // Electron Menu 557 | const Menu = remote.Menu; 558 | 559 | const template = [ 560 | { 561 | label: 'ffftp', 562 | submenu: [{ 563 | label: 'About', 564 | accelerator: 'CmdOrCtrl+H', 565 | click: (item, focusedWindow) => { 566 | shell.openExternal('http://ffftp.site'); 567 | } 568 | }, 569 | { 570 | label: 'Close', 571 | accelerator: 'CmdOrCtrl+Q', 572 | role: 'close' 573 | }] 574 | }, 575 | { 576 | label: 'Action', 577 | submenu: [{ 578 | label: 'Connect', 579 | accelerator: 'CmdOrCtrl+R', 580 | click: () => { 581 | $timeout(() => { 582 | $scope.showingMenu = true; 583 | }, 0); 584 | } 585 | }, 586 | { 587 | label: 'Up directory', 588 | accelerator: 'CmdOrCtrl+U', 589 | click: () => { 590 | if ($scope.path === '.') { 591 | $scope.console('red', 'You are in the root directory.') 592 | } 593 | else { 594 | $scope.upDir(); 595 | } 596 | } 597 | }, 598 | { 599 | label: 'New folder', 600 | accelerator: 'CmdOrCtrl+N', 601 | click: () => { 602 | $timeout(() => { 603 | $scope.showingNewFolder = true; 604 | }, 0); 605 | } 606 | }] 607 | }, 608 | { 609 | label: 'View', 610 | submenu: [{ 611 | label: 'Reload', 612 | accelerator: 'CmdOrCtrl+R', 613 | click: (item, focusedWindow) => { 614 | if (focusedWindow) 615 | focusedWindow.reload(); 616 | } 617 | }, 618 | { 619 | label: 'Full screen', 620 | accelerator: (() => { 621 | if (process.platform === 'darwin') 622 | return 'Ctrl+Command+F'; 623 | else 624 | return 'F11'; 625 | })(), 626 | click: (item, focusedWindow) => { 627 | if (focusedWindow) 628 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 629 | } 630 | }] 631 | }, 632 | { 633 | label: 'Edit', 634 | submenu: [ 635 | {label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:'}, 636 | {label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:'}, 637 | {type: 'separator'}, 638 | {label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:'}, 639 | {label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:'}, 640 | {label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:'}, 641 | {label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:'} 642 | ] 643 | }, 644 | { 645 | label: 'Dev', 646 | submenu: [ 647 | { 648 | label: 'Dev tools', 649 | accelerator: (() => { 650 | if (process.platform === 'darwin') 651 | return 'Alt+Command+I'; 652 | else 653 | return 'Ctrl+Shift+I'; 654 | })(), 655 | click: (item, focusedWindow) => { 656 | if (focusedWindow) 657 | focusedWindow.toggleDevTools(); 658 | } 659 | }, { 660 | label: 'Github', 661 | click: (item, focusedWindow) => { 662 | shell.openExternal('http://github.com/mitchas/ffftp'); 663 | } 664 | } 665 | ] 666 | } 667 | ]; 668 | 669 | const menu = Menu.buildFromTemplate(template); 670 | Menu.setApplicationMenu(menu); 671 | } 672 | })(angular); 673 | 674 | --------------------------------------------------------------------------------