├── 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 |
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
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 |
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 |
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 |
153 |
154 |
163 |
164 |
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 |
--------------------------------------------------------------------------------