├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── config.js ├── icon-256.png ├── icon.icns ├── icon.ico ├── icon.png ├── icon.psd ├── package.json ├── public ├── index.html ├── scripts │ └── main.js └── stylesheets │ ├── normalize.css │ └── react-incompatible.css ├── screenshots ├── FileCtor-Console.png ├── FileCtor-Files.png └── FileCtor-Snippets.png ├── scripts └── start.js ├── src ├── App.js ├── Router.js ├── component │ ├── Button.js │ ├── ButtonSelect.js │ ├── CodeMirrorEditor.js │ ├── Form.js │ ├── Grid.js │ ├── Link.js │ ├── Navigation.js │ ├── Popup.js │ ├── Popups.js │ ├── Tabs.js │ ├── Titlebar.js │ ├── buttonSelect.css │ ├── buttonSelect │ │ └── List.js │ ├── codeMirrorEditor.css │ ├── fileInspector │ │ └── Files.js │ ├── form.css │ ├── form │ │ ├── Checkbox.js │ │ ├── EmailInput.js │ │ ├── Select.js │ │ ├── TextInput.js │ │ ├── Textarea.js │ │ ├── checkbox.css │ │ └── select.css │ ├── link.css │ ├── navigation.css │ ├── popup.css │ ├── tabs.css │ ├── tabs │ │ ├── Content.js │ │ └── Navigation.js │ └── titlebar.css ├── font │ └── CaveHand.ttf ├── icon │ ├── caret-down.svg │ ├── check.svg │ ├── chevron-circle-down.svg │ ├── chevron-circle-up.svg │ ├── chevron-left-solid.svg │ ├── chevron-right-solid.svg │ ├── close.svg │ ├── code.svg │ ├── cog.svg │ ├── edit.svg │ ├── ellipsis-v.svg │ ├── expand-arrows-alt.svg │ ├── expand-arrows.svg │ ├── eye.svg │ ├── folder-open.svg │ ├── github.svg │ ├── hdd.svg │ ├── home.svg │ ├── level-up-alt.svg │ ├── linkedin.svg │ ├── maximize-clone.svg │ ├── maximize-square.svg │ ├── minimize.svg │ ├── npm.svg │ ├── plus.svg │ ├── question.svg │ ├── refresh.svg │ ├── save.svg │ ├── stack-overflow.svg │ ├── trash-alt.svg │ └── twitter.svg ├── image │ └── tomas-chylyV2-sign.png ├── index.css ├── index.js ├── main.js ├── main │ ├── Api.js │ ├── Config.js │ ├── Console.js │ ├── Reference.js │ ├── api │ │ ├── Console.js │ │ ├── Grid.js │ │ └── functions.js │ ├── install │ │ ├── snippet.js │ │ └── snippet_backup.js │ └── model │ │ ├── Base.js │ │ ├── RxSnippet.js │ │ └── Snippet.js └── view │ ├── About.js │ ├── Console.js │ ├── FileInspector.js │ ├── Reference.js │ ├── Settings.js │ ├── Snippet.js │ ├── about.css │ ├── console.css │ ├── fileInspector.css │ ├── reference.css │ ├── settings.css │ └── snippet.css └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "react-app", 8 | "parserOptions": { 9 | "ecmaVersion": 2017, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | "tab", 16 | {"SwitchCase": 1} 17 | ], 18 | "quotes": [ 19 | "error", 20 | "single" 21 | ], 22 | "semi": [ 23 | "error", 24 | "always" 25 | ], 26 | "no-whitespace-before-property": "off", 27 | "array-callback-return": "off" 28 | } 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # React .gitignore 2 | node_modules/ 3 | build 4 | .DS_Store 5 | *.tgz 6 | my-app* 7 | template/src/__tests__/__snapshots__/ 8 | lerna-debug.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | /.changelog 13 | .npm/ 14 | 15 | # Optional eslint cache 16 | .eslintcache 17 | 18 | # Yarn Integrity file 19 | .yarn-integrity 20 | 21 | # dotenv environment variables file 22 | .env 23 | 24 | # IDEs 25 | .idea 26 | .vscode 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileCtor 2 | 3 | File inspector with interactive javascript console. 4 | 5 | **Notice:** MacOS version is not compatible with Apple Silicon (ARM) 6 | 7 | ![Javascript console](/screenshots/FileCtor-Console.png) 8 | 9 | The idea is to have something similar to other file system managers, e.g. [TotalCommander](https://www.ghisler.com/). But this app is intended for Developers as it allows to run various JS scripts against files and directories. 10 | 11 | ![Files & directory view](/screenshots/FileCtor-Files.png) 12 | 13 | There are multiple script Snippets installed with the app. They will give you some functionality right away, without having to write your own first. Then you can of course write and save your own. 14 | 15 | ![Saved code snippets](/screenshots/FileCtor-Snippets.png) 16 | 17 | ## Contents 18 | 19 | 1. [Installation](#installation) 20 | 2. [Important Notes](#important-notes) 21 | 3. [How to Use](#how-to-use) 22 | 4. [Included script Snippets](#included-script-snippets) 23 | 5. [Roadmap](#roadmap) 24 | 25 | ## Installation 26 | 27 | **Windows** 28 | 29 | 1. Download latest Win32 release from [here](https://github.com/tomaschyly/FileCtor/releases). 30 | 2. Unzip anywhere, then you can copy the folder wherever you want the app to be, there is no installation. 31 | 3. Run the app, optionally create shortcut to use. 32 | 33 | **MacOS** 34 | 35 | 1. Download latest Darwin release from [here](https://github.com/tomaschyly/FileCtor/releases). 36 | 2. Unzip anywhere, then copy app to your Applications folder. 37 | 3. Run the app, optionally create shortcut to use. 38 | 39 | **Ubuntu** 40 | 41 | 1. Download latest Deb release from [here](https://github.com/tomaschyly/FileCtor/releases). 42 | 2. Unzip anywhere, then install the package. 43 | 3. Run the app, optionally create shortcut to use. 44 | 45 | ## Important Notes 46 | 47 | This app is intended to be cross-platform, currently developed and tested on Windows 10, MacOS and Ubuntu 18.04 LTS. 48 | 49 | App is using [Electron](https://electronjs.org/) and [React](https://reactjs.org/) as it was originally meant for me to improve my skill with Electron and learn properly React. 50 | 51 | ## How to Use 52 | 53 | ### Execute script 54 | 1. On the main Files view, click the code button. Either in the files row on the right or at the bottom near the current directory. 55 | 2. 56 | 1. Now you have console open for you with current directory preselected for use by script. 57 | 2. If you used the button inside the files row, then you have also the file/s preselected, but this is not yet fully implemented. 58 | 3. Inside the top box is Javascript editor, here you can write code that will be executed. 59 | 4. If you hit the question mark button, you will see current API reference for global variables and functions. 60 | 5. Execute the code by hitting execute button. **WARNING: be sure that you are in the correct directory and have correct selected files before you execute the script. I will NOT be responsible for any damage that you cause to yor own PC.** 61 | 6. Inside the middle box you will see result and any console.log that you used with your script. Here you will also see error. 62 | 63 | ### Save script 64 | 1. 65 | 1. When you have already open console with written script, click the save button. 66 | 2. Go to the Snippets view and click new snippet button to open the console. Write your script and then save with the save button. 67 | 68 | ### Script API reference 69 | 1. Open the console using any of the previously mentioned ways. 70 | 2. Click the question mark button. 71 | 72 | ### Want more capabilities (variables/methods) 73 | 1. 74 | 1. Write your request in [here](https://github.com/tomaschyly/FileCtor/issues). 75 | 2. Or use the contact form inside the app's About view. 76 | 77 | ## Included script Snippets 78 | 79 | ### Simple Example 80 | 81 | This is just a basic script example that does almost nothing, but demonstrate that execution works. 82 | 83 | ### Rename Files 84 | 85 | Rename files to a new name and append with number if there are more than one. 86 | 87 | ### Rename Files (part of name) 88 | 89 | Rename files to a new name by changing part of name with provided new part. 90 | 91 | ### Rename Host Sql 92 | 93 | Script for renaming host inside Sql query. E.g. rename host of WP website when migrating from Dev to Prod. Should work on large Sql files. 94 | 95 | ### TinyPNG Compress/Resize/Crop Images 96 | 97 | Two scripts for images (PNG & JPG), one can compress them, the other can resize or crop. Crop is intelligent in determining area of interest. You need TinyPNG API key for them to work. 98 | 99 | ## Roadmap 100 | 101 | * Import/Export code 102 | * Localization - **However I know only limited number of languages** 103 | * More files explorer like actions - **LOW PRIORITY** 104 | * More script capabilities - **an ongoing process/dependant on requests** 105 | 106 | * Simple GUI for snippets - **TBD** 107 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const electron = require ('electron'); 2 | const {ENGINE_TYPES} = require ('tch-database'); 3 | const path = require ('path'); 4 | 5 | module.exports = { 6 | storageSystem: ENGINE_TYPES.NeDB, 7 | nedb: { 8 | directory: path.join ('var', 'nedb') 9 | }, 10 | rxDB: { 11 | name: path.join ((electron.app || electron.remote.app).getPath ('userData'), 'var', 'rxDB') 12 | }, 13 | api: { 14 | url: 'https://tomas-chyly.com/wp-admin/admin-ajax.php' 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/icon-256.png -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/icon.icns -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/icon.ico -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/icon.png -------------------------------------------------------------------------------- /icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/icon.psd -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tch-filector", 3 | "productName": "FileCtor", 4 | "version": "1.4.2", 5 | "author": "Tomas Chyly (https://tomas-chyly.com/en/)", 6 | "license": "GPL v3", 7 | "description": "File inspector with interactive javascript console.", 8 | "private": true, 9 | "engines": { 10 | "node": ">=8.0.0" 11 | }, 12 | "homepage": "./", 13 | "main": "src/main.js", 14 | "scripts": { 15 | "debug": "cross-env FILECTOR_DEV=true yarn react-scripts start", 16 | "start": "cross-env FILECTOR_DEV=true node ./scripts/start.js", 17 | "build": "react-scripts build" 18 | }, 19 | "devDependencies": { 20 | "concurrently": "^5.2.0", 21 | "cross-env": "^7.0.2", 22 | "electron": "^9.2.0", 23 | "electron-rebuild": "^1.11.0", 24 | "find-free-port": "^2.0.0", 25 | "react": "^16.13.1", 26 | "react-dom": "^16.13.1", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "^3.4.1", 29 | "react-transition-group": "^4.4.1", 30 | "react-virtualized-auto-sizer": "^1.0.2", 31 | "react-window": "^1.8.5", 32 | "shelljs": "^0.8.4", 33 | "wait-on": "^5.0.1" 34 | }, 35 | "dependencies": { 36 | "axios": "^0.19.2", 37 | "codemirror": "^5.56.0", 38 | "email-validator": "^2.0.4", 39 | "extend": "^3.0.2", 40 | "fs-extra": "^9.0.1", 41 | "nedb": "^1.8.0", 42 | "open": "^7.0.4", 43 | "pouchdb-adapter-node-websql": "^7.0.0", 44 | "rxdb": "^9.1.0", 45 | "rxjs": "^6.5.5", 46 | "sanitize-html": "^1.26.0", 47 | "systeminformation": "^4.26.9", 48 | "tch-database": "^0.5.1", 49 | "tinify": "^1.6.0-beta.2", 50 | "uuid": "^8.1.0", 51 | "write-file-atomic": "^3.0.3" 52 | }, 53 | "browserslist": { 54 | "production": [ 55 | "last 1 chrome version" 56 | ], 57 | "development": [ 58 | "last 1 chrome version" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FileCtor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/scripts/main.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = window.require ('electron'); 2 | 3 | if (typeof (window.TCH) === 'undefined') { 4 | window.TCH = {}; 5 | } 6 | 7 | if (typeof (window.TCH.Main) === 'undefined') { 8 | window.TCH.Main = { 9 | titleBar: null, 10 | navigation: null, 11 | popups: null, 12 | 13 | /** 14 | * Show alert to user. 15 | */ 16 | Alert (message, headline) { 17 | if (typeof (this.popups) !== 'undefined') { 18 | this.popups.Alert (message, headline); 19 | } 20 | }, 21 | 22 | /** 23 | * Show beta popup. 24 | */ 25 | Beta () { 26 | if (typeof (this.popups) !== 'undefined') { 27 | this.popups.Beta (); 28 | } 29 | }, 30 | 31 | /** 32 | * Show popup to ask user for action confirmation. 33 | */ 34 | ConfirmAction (action) { 35 | if (typeof (this.popups) !== 'undefined') { 36 | this.popups.ConfirmAction (action); 37 | } 38 | }, 39 | 40 | /** 41 | * Open console window with parameters. 42 | */ 43 | OpenConsole (params) { 44 | ipcRenderer.send ('console-show', params); 45 | }, 46 | 47 | /** 48 | * Set new document title. 49 | */ 50 | SetTitle (title) { 51 | let element = document.getElementById ('document-title'); 52 | 53 | if (typeof (title) === 'string' && title.length > 0) { 54 | document.title = `${title} - ${element.dataset.title}`; 55 | } else { 56 | document.title = element.dataset.title; 57 | } 58 | 59 | if (this.titleBar !== null) { 60 | this.titleBar.setState ({title: document.title}); 61 | } 62 | }, 63 | 64 | /** 65 | * Show button for opening main window. 66 | */ 67 | ShowMainButton () { 68 | document.getElementById ('titlebar').classList.add ('main-visible'); 69 | }, 70 | 71 | /** 72 | * Hide button for opening main window. 73 | */ 74 | HideMainButton () { 75 | document.getElementById ('titlebar').classList.remove ('main-visible'); 76 | }, 77 | 78 | /** 79 | * Hide navigation. 80 | */ 81 | HideNavigation () { 82 | if (this.navigation !== null) { 83 | this.navigation.Disable (); 84 | } 85 | }, 86 | 87 | /** 88 | * Show button for resetting the window. 89 | */ 90 | ShowResetButton () { 91 | document.getElementById ('titlebar').classList.add ('reset-visible'); 92 | }, 93 | 94 | /** 95 | * Hide button for resetting the window. 96 | */ 97 | HideResetButton () { 98 | document.getElementById ('titlebar').classList.remove ('reset-visible'); 99 | }, 100 | 101 | Utils: { 102 | /** 103 | * Find closest parent node with class or tag name for element. 104 | * @param {Element} element Element from which to search parent 105 | * @param {string} parentClass Class of searched for parent 106 | * @param {string} tagName Tag of searched for parent 107 | * @returns {null|Element} 108 | */ 109 | FindNearestParent (element, parentClass, tagName) { 110 | let parent = element.parentElement; 111 | 112 | do { 113 | if (parent !== null && (typeof (parentClass) !== 'undefined' && parent.classList.contains (parentClass))) { 114 | return parent; 115 | } else if (parent !== null && (typeof (tagName) !== 'undefined' && parent.tagName === tagName)) { 116 | return parent; 117 | } 118 | 119 | parent = parent !== null ? parent.parentElement : null; 120 | } while (parent !== null); 121 | 122 | return null; 123 | }, 124 | 125 | Object: { 126 | /** 127 | * Check if object is empty (no elements). 128 | * @param {object} object Object to check is empty 129 | * @returns {boolean} 130 | */ 131 | IsEmpty (object) { 132 | for (let index in object) { 133 | if (object.hasOwnProperty (index)) { 134 | return false; 135 | } 136 | } 137 | 138 | return true; 139 | }, 140 | 141 | /** 142 | * Map to new Object equivalent to Array.map (). 143 | * @param {object} object Old object to be mapped 144 | * @param {function} filter Callback to filter elements into new object 145 | */ 146 | Map (object, filter) { 147 | const newObject = {}; 148 | 149 | for (let index in object) { 150 | if (object.hasOwnProperty (index)) { 151 | newObject [index] = filter (object [index]); 152 | } 153 | } 154 | 155 | return newObject; 156 | } 157 | } 158 | } 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Correct the font size and margin on `h1` elements within `section` and 29 | * `article` contexts in Chrome, Firefox, and Safari. 30 | */ 31 | 32 | h1 { 33 | font-size: 2em; 34 | margin: 0.67em 0; 35 | } 36 | 37 | /* Grouping content 38 | ========================================================================== */ 39 | 40 | /** 41 | * 1. Add the correct box sizing in Firefox. 42 | * 2. Show the overflow in Edge and IE. 43 | */ 44 | 45 | hr { 46 | box-sizing: content-box; /* 1 */ 47 | height: 0; /* 1 */ 48 | overflow: visible; /* 2 */ 49 | } 50 | 51 | /** 52 | * 1. Correct the inheritance and scaling of font size in all browsers. 53 | * 2. Correct the odd `em` font sizing in all browsers. 54 | */ 55 | 56 | pre { 57 | font-family: monospace, monospace; /* 1 */ 58 | font-size: 1em; /* 2 */ 59 | } 60 | 61 | /* Text-level semantics 62 | ========================================================================== */ 63 | 64 | /** 65 | * Remove the gray background on active links in IE 10. 66 | */ 67 | 68 | a { 69 | background-color: transparent; 70 | } 71 | 72 | /** 73 | * 1. Remove the bottom border in Chrome 57- 74 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 75 | */ 76 | 77 | abbr[title] { 78 | border-bottom: none; /* 1 */ 79 | text-decoration: underline; /* 2 */ 80 | text-decoration: underline dotted; /* 2 */ 81 | } 82 | 83 | /** 84 | * Add the correct font weight in Chrome, Edge, and Safari. 85 | */ 86 | 87 | b, 88 | strong { 89 | font-weight: bolder; 90 | } 91 | 92 | /** 93 | * 1. Correct the inheritance and scaling of font size in all browsers. 94 | * 2. Correct the odd `em` font sizing in all browsers. 95 | */ 96 | 97 | code, 98 | kbd, 99 | samp { 100 | font-family: monospace, monospace; /* 1 */ 101 | font-size: 1em; /* 2 */ 102 | } 103 | 104 | /** 105 | * Add the correct font size in all browsers. 106 | */ 107 | 108 | small { 109 | font-size: 80%; 110 | } 111 | 112 | /** 113 | * Prevent `sub` and `sup` elements from affecting the line height in 114 | * all browsers. 115 | */ 116 | 117 | sub, 118 | sup { 119 | font-size: 75%; 120 | line-height: 0; 121 | position: relative; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -0.25em; 127 | } 128 | 129 | sup { 130 | top: -0.5em; 131 | } 132 | 133 | /* Embedded content 134 | ========================================================================== */ 135 | 136 | /** 137 | * Remove the border on images inside links in IE 10. 138 | */ 139 | 140 | img { 141 | border-style: none; 142 | } 143 | 144 | /* Forms 145 | ========================================================================== */ 146 | 147 | /** 148 | * 1. Change the font styles in all browsers. 149 | * 2. Remove the margin in Firefox and Safari. 150 | */ 151 | 152 | button, 153 | input, 154 | optgroup, 155 | select, 156 | textarea { 157 | font-family: inherit; /* 1 */ 158 | font-size: 100%; /* 1 */ 159 | line-height: 1.15; /* 1 */ 160 | margin: 0; /* 2 */ 161 | } 162 | 163 | /** 164 | * Show the overflow in IE. 165 | * 1. Show the overflow in Edge. 166 | */ 167 | 168 | button, 169 | input { /* 1 */ 170 | overflow: visible; 171 | } 172 | 173 | /** 174 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 175 | * 1. Remove the inheritance of text transform in Firefox. 176 | */ 177 | 178 | button, 179 | select { /* 1 */ 180 | text-transform: none; 181 | } 182 | 183 | /** 184 | * Correct the inability to style clickable types in iOS and Safari. 185 | */ 186 | 187 | button, 188 | [type="button"], 189 | [type="reset"], 190 | [type="submit"] { 191 | -webkit-appearance: button; 192 | } 193 | 194 | /** 195 | * Remove the inner border and padding in Firefox. 196 | */ 197 | 198 | button::-moz-focus-inner, 199 | [type="button"]::-moz-focus-inner, 200 | [type="reset"]::-moz-focus-inner, 201 | [type="submit"]::-moz-focus-inner { 202 | border-style: none; 203 | padding: 0; 204 | } 205 | 206 | /** 207 | * Restore the focus styles unset by the previous rule. 208 | */ 209 | 210 | button:-moz-focusring, 211 | [type="button"]:-moz-focusring, 212 | [type="reset"]:-moz-focusring, 213 | [type="submit"]:-moz-focusring { 214 | outline: 1px dotted ButtonText; 215 | } 216 | 217 | /** 218 | * Correct the padding in Firefox. 219 | */ 220 | 221 | fieldset { 222 | padding: 0.35em 0.75em 0.625em; 223 | } 224 | 225 | /** 226 | * 1. Correct the text wrapping in Edge and IE. 227 | * 2. Correct the color inheritance from `fieldset` elements in IE. 228 | * 3. Remove the padding so developers are not caught out when they zero out 229 | * `fieldset` elements in all browsers. 230 | */ 231 | 232 | legend { 233 | box-sizing: border-box; /* 1 */ 234 | color: inherit; /* 2 */ 235 | display: table; /* 1 */ 236 | max-width: 100%; /* 1 */ 237 | padding: 0; /* 3 */ 238 | white-space: normal; /* 1 */ 239 | } 240 | 241 | /** 242 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 243 | */ 244 | 245 | progress { 246 | vertical-align: baseline; 247 | } 248 | 249 | /** 250 | * Remove the default vertical scrollbar in IE 10+. 251 | */ 252 | 253 | textarea { 254 | overflow: auto; 255 | } 256 | 257 | /** 258 | * 1. Add the correct box sizing in IE 10. 259 | * 2. Remove the padding in IE 10. 260 | */ 261 | 262 | [type="checkbox"], 263 | [type="radio"] { 264 | box-sizing: border-box; /* 1 */ 265 | padding: 0; /* 2 */ 266 | } 267 | 268 | /** 269 | * Correct the cursor style of increment and decrement buttons in Chrome. 270 | */ 271 | 272 | [type="number"]::-webkit-inner-spin-button, 273 | [type="number"]::-webkit-outer-spin-button { 274 | height: auto; 275 | } 276 | 277 | /** 278 | * 1. Correct the odd appearance in Chrome and Safari. 279 | * 2. Correct the outline style in Safari. 280 | */ 281 | 282 | [type="search"] { 283 | -webkit-appearance: textfield; /* 1 */ 284 | outline-offset: -2px; /* 2 */ 285 | } 286 | 287 | /** 288 | * Remove the inner padding in Chrome and Safari on macOS. 289 | */ 290 | 291 | [type="search"]::-webkit-search-decoration { 292 | -webkit-appearance: none; 293 | } 294 | 295 | /** 296 | * 1. Correct the inability to style clickable types in iOS and Safari. 297 | * 2. Change font properties to `inherit` in Safari. 298 | */ 299 | 300 | ::-webkit-file-upload-button { 301 | -webkit-appearance: button; /* 1 */ 302 | font: inherit; /* 2 */ 303 | } 304 | 305 | /* Interactive 306 | ========================================================================== */ 307 | 308 | /* 309 | * Add the correct display in Edge, IE 10+, and Firefox. 310 | */ 311 | 312 | details { 313 | display: block; 314 | } 315 | 316 | /* 317 | * Add the correct display in all browsers. 318 | */ 319 | 320 | summary { 321 | display: list-item; 322 | } 323 | 324 | /* Misc 325 | ========================================================================== */ 326 | 327 | /** 328 | * Add the correct display in IE 10+. 329 | */ 330 | 331 | template { 332 | display: none; 333 | } 334 | 335 | /** 336 | * Add the correct display in IE 10. 337 | */ 338 | 339 | [hidden] { 340 | display: none; 341 | } 342 | -------------------------------------------------------------------------------- /public/stylesheets/react-incompatible.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { width: 16px; border: 2px solid black; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; } 2 | ::-webkit-scrollbar-thumb { background: black; } 3 | ::-webkit-scrollbar-trac { background: white; } 4 | 5 | /*** Dark Mode styles ***/ 6 | .dark-mode ::-webkit-scrollbar { border-color: #666666; } 7 | .dark-mode ::-webkit-scrollbar-thumb { background: #666666; } 8 | -------------------------------------------------------------------------------- /screenshots/FileCtor-Console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/screenshots/FileCtor-Console.png -------------------------------------------------------------------------------- /screenshots/FileCtor-Files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/screenshots/FileCtor-Files.png -------------------------------------------------------------------------------- /screenshots/FileCtor-Snippets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/screenshots/FileCtor-Snippets.png -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | const findFreePort = require ('find-free-port'); 2 | const shell = require ('shelljs'); 3 | 4 | const Starter = { 5 | /** 6 | * Start the react and electron for dev. 7 | */ 8 | async Start () { 9 | const port = await findFreePort (3000, 5001, '127.0.0.1'); 10 | 11 | process.env.FILECTOR_PORT = port; 12 | 13 | shell.exec (`concurrently "cross-env BROWSER=none yarn react-scripts start" "wait-on http://localhost:${port} && electron ."`); 14 | } 15 | }; 16 | 17 | Starter.Start (); 18 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { HashRouter } from 'react-router-dom'; 3 | import TitleBar from './component/Titlebar'; 4 | import Navigation from './component/Navigation'; 5 | import Router from './Router'; 6 | import Popups from './component/Popups'; 7 | import Settings from './view/Settings'; 8 | 9 | const { ipcRenderer } = window.require ('electron'); 10 | const extend = window.require ('extend'); 11 | 12 | window.App_static = { 13 | Instance: null 14 | }; 15 | 16 | class App extends Component { 17 | /** 18 | * App initialization. 19 | */ 20 | constructor (props) { 21 | super (props); 22 | 23 | window.App_static.Instance = this; 24 | 25 | this.state = { 26 | classes: [], 27 | updateAvailable: null 28 | }; 29 | 30 | window.TCH.mainParameters = undefined; 31 | } 32 | 33 | /** 34 | * First rendered to DOM. 35 | */ 36 | componentDidMount () { 37 | this.mainParametersListener = (event, message) => { 38 | window.TCH.mainParameters = message; 39 | 40 | window.TCH.mainParameters.settings = window.TCH.mainParameters.settings !== null ? extend (true, {}, Settings.Defaults (), window.TCH.mainParameters.settings) : Settings.Defaults (); 41 | 42 | let classes = this.state.classes; 43 | if (typeof (message.platform) !== 'undefined') { 44 | classes.push (message.platform); 45 | 46 | document.querySelector ('html').classList.add (message.platform); 47 | } 48 | this.setState ({classes: classes}); 49 | 50 | setTimeout (() => { 51 | this.ClassesBySettings (); 52 | }, 1); 53 | }; 54 | ipcRenderer.on ('main-parameters', this.mainParametersListener); 55 | ipcRenderer.send ('main-parameters'); 56 | 57 | this.nativeThemeUpdatedListener = (event, message) => { 58 | if (typeof window.TCH.mainParameters !== 'undefined') { 59 | window.TCH.mainParameters.osDarkMode = message.osDarkMode; 60 | 61 | setTimeout (() => { 62 | this.ClassesBySettings (); 63 | }, 1); 64 | } 65 | }; 66 | ipcRenderer.on ('native-theme-updated', this.nativeThemeUpdatedListener); 67 | 68 | this.appSettingsSaveListener = (event, message) => { 69 | window.TCH.mainParameters.settings = message !== null ? extend (true, {}, Settings.Defaults (), message) : Settings.Defaults (); 70 | 71 | this.ClassesBySettings (); 72 | }; 73 | ipcRenderer.on ('app-settings-save', this.appSettingsSaveListener); 74 | 75 | this.checkVersionListener = (event, message) => { 76 | if (typeof message.newVersion !== 'undefined') { 77 | this.setState ({ 78 | updateAvailable: { 79 | newVersion: message.newVersion, 80 | downloadUrl: message.downloadUrl 81 | } 82 | }); 83 | } 84 | }; 85 | ipcRenderer.on ('version-check-update', this.checkVersionListener); 86 | ipcRenderer.send ('version-check-update'); 87 | } 88 | 89 | /** 90 | * Called before component is removed from DOM. 91 | */ 92 | componentWillUnmount () { 93 | ipcRenderer.removeListener ('main-parameters', this.mainParametersListener); 94 | delete this.mainParametersListener; 95 | 96 | ipcRenderer.removeListener ('native-theme-updated', this.nativeThemeUpdatedListener); 97 | delete this.nativeThemeUpdatedListener; 98 | 99 | ipcRenderer.removeListener ('app-settings-save', this.appSettingsSaveListener); 100 | delete this.appSettingsSaveListener; 101 | 102 | ipcRenderer.removeListener ('version-check-update', this.checkVersionListener); 103 | delete this.checkVersionListener; 104 | } 105 | 106 | /** 107 | * Render the component into html. 108 | */ 109 | render () { 110 | const {classes, updateAvailable} = this.state; 111 | 112 | if (typeof (window.TCH.mainParameters) === 'undefined') { 113 | return ''; 114 | } else { 115 | return 116 |
117 | 118 | 119 |
120 | 121 |
122 | 123 |
124 |
; 125 | } 126 | } 127 | 128 | /** 129 | * Toggle class on app. 130 | */ 131 | ToggleClass (className) { 132 | const classes = this.state.classes; 133 | 134 | if (classes.includes (className)) { 135 | const index = classes.indexOf (className); 136 | classes.splice (index, 1); 137 | } else { 138 | classes.push (className); 139 | } 140 | 141 | document.querySelector ('body').classList.toggle (className); 142 | 143 | this.setState ({classes: classes}); 144 | } 145 | 146 | /** 147 | * Update app classes by app settings. 148 | */ 149 | ClassesBySettings () { 150 | const {settings} = window.TCH.mainParameters; 151 | 152 | const classes = this.state.classes; 153 | 154 | if (classes.includes ('fancy-font-disabled') && settings.theme.fancyFont) { 155 | this.ToggleClass ('fancy-font-disabled'); 156 | } else if (!classes.includes ('fancy-font-disabled') && !settings.theme.fancyFont) { 157 | this.ToggleClass ('fancy-font-disabled'); 158 | } 159 | 160 | const darkMode = settings.theme.darkMode !== null ? settings.theme.darkMode : window.TCH.mainParameters.osDarkMode; 161 | if (classes.includes ('dark-mode') && !darkMode) { 162 | this.ToggleClass ('dark-mode'); 163 | } else if (!classes.includes ('dark-mode') && darkMode) { 164 | this.ToggleClass ('dark-mode'); 165 | } 166 | } 167 | } 168 | 169 | export default App; 170 | -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import FileInspector from './view/FileInspector'; 4 | import Snippet from './view/Snippet'; 5 | import Settings from './view/Settings'; 6 | import About from './view/About'; 7 | import Console from './view/Console'; 8 | import Reference from './view/Reference'; 9 | 10 | class Router extends Component { 11 | /** 12 | * Render the component into html. 13 | */ 14 | render () { 15 | return 16 | 17 | 18 | 19 | 20 | 21 | 22 | ; 23 | } 24 | } 25 | 26 | export default Router; 27 | -------------------------------------------------------------------------------- /src/component/Button.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Button extends Component { 4 | /** 5 | * Button initialization. 6 | */ 7 | constructor (props) { 8 | super (props); 9 | 10 | this.button = undefined; 11 | this.blurTimeout = null; 12 | } 13 | 14 | /** 15 | * Called before component is removed from DOM. 16 | */ 17 | componentWillUnmount () { 18 | this.button = undefined; 19 | 20 | if (this.blurTimeout !== null) { 21 | clearTimeout (this.blurTimeout); 22 | this.blurTimeout = null; 23 | } 24 | } 25 | 26 | /** 27 | * Render the component into html. 28 | */ 29 | render () { 30 | const {type, children} = this.props; 31 | 32 | this.button = React.createRef (); 33 | 34 | return ; 37 | } 38 | 39 | /** 40 | * OnClick execute onClick action of present and remove focus. 41 | */ 42 | OnClick (e) { 43 | if (typeof (this.props.onClick) !== 'undefined') { 44 | this.props.onClick (e); 45 | } 46 | 47 | if (this.blurTimeout !== null) { 48 | clearTimeout (this.blurTimeout); 49 | this.blurTimeout = null; 50 | } 51 | 52 | this.blurTimeout = setTimeout (() => { 53 | if (typeof (this.button) !== 'undefined') { 54 | this.button.current.blur (); 55 | } 56 | }, 400); 57 | } 58 | } 59 | 60 | export default Button; 61 | -------------------------------------------------------------------------------- /src/component/ButtonSelect.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-whitespace-before-property */ 2 | import './buttonSelect.css'; 3 | 4 | import React, { Component } from 'react'; 5 | import Button from './Button'; 6 | import List from './buttonSelect/List'; 7 | 8 | window.ButtonSelect_static = { 9 | globalHideOptionsListener: undefined, 10 | buttonSelects: [] 11 | }; 12 | 13 | class ButtonSelect extends Component { 14 | /** 15 | * ButtonSelect initialization. 16 | */ 17 | constructor (props) { 18 | super (props); 19 | 20 | if (typeof (window.ButtonSelect_static.globalHideOptionsListener) === 'undefined') { 21 | window.ButtonSelect_static.globalHideOptionsListener = ButtonSelect.GlobalHideAllOptions; 22 | document.querySelector ('body').addEventListener ('click', window.ButtonSelect_static.globalHideOptionsListener); 23 | window.addEventListener ('resize', window.ButtonSelect_static.globalHideOptionsListener); 24 | } 25 | window.ButtonSelect_static.buttonSelects.push (this); 26 | 27 | this.container = undefined; 28 | this.list = undefined; 29 | 30 | this.state = { 31 | optionsVisible: false 32 | }; 33 | } 34 | 35 | /** 36 | * Component was updated. 37 | */ 38 | componentDidUpdate () { 39 | if (this.state.optionsVisible) { 40 | this.OptionsPosition (); 41 | } 42 | } 43 | 44 | /** 45 | * Called before component is removed from DOM. 46 | */ 47 | componentWillUnmount () { 48 | let myIndex = window.ButtonSelect_static.buttonSelects.indexOf (this); 49 | if (myIndex >= 0) { 50 | window.ButtonSelect_static.buttonSelects.splice (myIndex, 1); 51 | } 52 | 53 | this.container = undefined; 54 | this.list = undefined; 55 | } 56 | 57 | /** 58 | * Render the component into html. 59 | */ 60 | render () { 61 | let current = typeof (this.props.icon) !== 'undefined' ? this.props.icon : this.props.value; 62 | 63 | this.container = React.createRef (); 64 | this.list = React.createRef (); 65 | 66 | return
67 | 68 | 69 | 70 |
; 71 | } 72 | 73 | /** 74 | * Toggle options visibility. 75 | */ 76 | ToggleOptions () { 77 | this.setState ({ 78 | optionsVisible: !this.state.optionsVisible 79 | }); 80 | } 81 | 82 | /** 83 | * Hide options of all ButtonSelects. 84 | */ 85 | static GlobalHideAllOptions (e) { 86 | let current = typeof (e.target) !== 'undefined' && typeof (e.target.classList) !== 'undefined' && e.target.classList.contains ('button-select-container') ? e.target : null; 87 | if (current === null && typeof (e.target) !== 'undefined' && typeof (e.target.classList) !== 'undefined') { 88 | current = window.TCH.Main.Utils.FindNearestParent (e.target, 'button-select-container'); 89 | } 90 | 91 | for (let index in window.ButtonSelect_static.buttonSelects) { 92 | if (current !== null && window.ButtonSelect_static.buttonSelects [index].container.current === current) { 93 | continue; 94 | } 95 | 96 | window.ButtonSelect_static.buttonSelects [index].setState ({ 97 | optionsVisible: false 98 | }); 99 | } 100 | } 101 | 102 | /** 103 | * Set correct position to options. 104 | */ 105 | OptionsPosition () { 106 | const container = this.container.current; 107 | const list = this.list.current.container.current; 108 | 109 | list.style.left = ''; 110 | list.style.top = ''; 111 | 112 | const containerOffset = container.getBoundingClientRect (); 113 | const listOffset = list.getBoundingClientRect (); 114 | 115 | let x = containerOffset.x + (containerOffset.width / 2) - (listOffset.width / 2); 116 | let y = containerOffset.y + containerOffset.height; 117 | 118 | const overflowX = window.innerWidth - listOffset.width - x - 2; 119 | if (overflowX < 0) { 120 | x = x - Math.abs (overflowX); 121 | } 122 | 123 | const overflowY = window.innerHeight - listOffset.height - y - 2; 124 | if (overflowY < 0) { 125 | y = y - Math.abs (overflowY); 126 | } 127 | 128 | list.style.left = `${x}px`; 129 | list.style.top = `${y}px`; 130 | } 131 | 132 | /** 133 | * Select item from options. 134 | */ 135 | SelectOption (e) { 136 | if (typeof (this.props.onSelectItem) === 'function') { 137 | this.props.onSelectItem (e); 138 | } 139 | 140 | this.setState ({ 141 | optionsVisible: false 142 | }); 143 | } 144 | } 145 | 146 | export default ButtonSelect; 147 | -------------------------------------------------------------------------------- /src/component/CodeMirrorEditor.js: -------------------------------------------------------------------------------- 1 | import 'codemirror/lib/codemirror.css'; 2 | import 'codemirror/theme/blackboard.css'; 3 | import 'codemirror/theme/darcula.css'; 4 | import 'codemirror/theme/idea.css'; 5 | import 'codemirror/theme/twilight.css'; 6 | import 'codemirror/theme/material-darker.css'; 7 | import 'codemirror/theme/material-ocean.css'; 8 | import 'codemirror/theme/material-palenight.css'; 9 | import 'codemirror/theme/material.css'; 10 | import 'codemirror/addon/hint/show-hint.css'; 11 | import './codeMirrorEditor.css'; 12 | 13 | import React, { Component } from 'react'; 14 | 15 | import CodeMirror from 'codemirror'; 16 | import 'codemirror/mode/javascript/javascript'; 17 | import 'codemirror/addon/hint/show-hint'; 18 | import 'codemirror/addon/hint/javascript-hint'; 19 | 20 | export const THEMES_AS_OPTIONS = [ 21 | {label: 'Blackboard', value: 'blackboard'}, 22 | {label: 'Darcula', value: 'darcula'}, 23 | {label: 'Default', value: 'default'}, 24 | {label: 'Idea', value: 'idea'}, 25 | {label: 'Material', value: 'material'}, 26 | {label: 'Material Darker', value: 'material-darker'}, 27 | {label: 'Material Ocean', value: 'material-ocean'}, 28 | {label: 'Material Palenight', value: 'material-palenight'}, 29 | {label: 'Twilight', value: 'twilight'} 30 | ]; 31 | 32 | class CodeMirrorEditor extends Component { 33 | /** 34 | * CodeMirrorEditor initialization. 35 | */ 36 | constructor (props) { 37 | super (props); 38 | 39 | this.node = undefined; 40 | this.editor = undefined; 41 | } 42 | 43 | /** 44 | * First rendered to DOM. 45 | */ 46 | componentDidMount () { 47 | const {settings} = window.TCH.mainParameters; 48 | 49 | this.editor = CodeMirror (this.node.current, { 50 | value: typeof (this.props.script) !== 'undefined' ? this.props.script : '', 51 | mode: 'javascript', 52 | theme: settings.console.theme, 53 | lineNumbers: true, 54 | lineWrapping: true, 55 | extraKeys: { 56 | 'Ctrl-Space': 'autocomplete' 57 | } 58 | }); 59 | 60 | if (typeof (this.props.onChange) === 'function') { 61 | this.editor.on ('change', editor => { 62 | this.props.onChange (editor.getValue ()); 63 | }); 64 | } 65 | } 66 | 67 | /** 68 | * Called before component is removed from DOM. 69 | */ 70 | componentWillUnmount () { 71 | this.editor = undefined; 72 | } 73 | 74 | /** 75 | * Render the component into html. 76 | */ 77 | render () { 78 | this.node = React.createRef (); 79 | 80 | return
; 81 | } 82 | 83 | /** 84 | * Change script of the editor. 85 | */ 86 | ChangeScript (value) { 87 | this.editor.getDoc ().setValue (value); 88 | } 89 | } 90 | 91 | export default CodeMirrorEditor; 92 | -------------------------------------------------------------------------------- /src/component/Form.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-whitespace-before-property */ 2 | import './form.css'; 3 | 4 | import React, {Component} from 'react'; 5 | import TextInput from './form/TextInput'; 6 | import EmailInput from './form/EmailInput'; 7 | import Textarea from './form/Textarea'; 8 | import Checkbox from './form/Checkbox'; 9 | import Select from './form/Select'; 10 | 11 | const uuid = window.require ('uuid'); 12 | const emailValidator = window.require ('email-validator'); 13 | 14 | class Form extends Component { 15 | /** 16 | * Form initialization. 17 | */ 18 | constructor (props) { 19 | super (props); 20 | 21 | let {id} = this.props; 22 | 23 | if (typeof (id) === 'undefined') { 24 | id = `form-${uuid.v4 ()}`; 25 | } 26 | 27 | this.state = { 28 | formData: this.props.inputs, 29 | id: id 30 | }; 31 | } 32 | 33 | /** 34 | * Render the component into html. 35 | */ 36 | render () { 37 | const {className} = this.props; 38 | const {id} = this.state; 39 | 40 | return
41 | {this.RenderInputs (id)} 42 |
; 43 | } 44 | 45 | /** 46 | * Render inputs based on parameters. 47 | */ 48 | RenderInputs (id) { 49 | const inputs = this.state.formData; 50 | let children = []; 51 | 52 | if (typeof (inputs) === 'object') { 53 | for (let index in inputs) { 54 | if (inputs.hasOwnProperty (index)) { 55 | const input = inputs [index]; 56 | 57 | input.name = index; 58 | input.onChange = this.ValueChanged.bind (this); 59 | 60 | switch (input.type) { 61 | case 'text': 62 | children.push (); 63 | break; 64 | case 'email': 65 | children.push (); 66 | break; 67 | case 'textarea': 68 | children.push (