);
123 | } else {
124 | let openDirectory = undefined;
125 | if (rowData.isDirectory) {
126 | openDirectory =
;
127 | }
128 |
129 | let execute = undefined;
130 | if (!rowData.isDirectory) {
131 | execute =
;
132 | }
133 |
134 | row.push (
135 | {openDirectory}
136 | {execute}
137 |
138 |
);
139 | //
//TODO
140 | }
141 | }
142 |
143 | return
144 | {row}
145 |
;
146 | }
147 |
148 | /**
149 | * Output formatted file size.
150 | */
151 | FileSize (size) {
152 | if (typeof (size) !== 'undefined') {
153 | let bytes = parseInt (size);
154 |
155 | if (bytes >= (1024 * 1024 * 1024)) {
156 | return `${Math.round (bytes / (1024 * 1024 * 1024))} GB`;
157 | } else if (bytes >= (1024 * 1024)) {
158 | return `${Math.round (bytes / (1024 * 1024))} MB`;
159 | } else if (bytes >= 1024) {
160 | return `${Math.round (bytes / 1024)} KB`;
161 | } else {
162 | return `${bytes} B`;
163 | }
164 | }
165 |
166 | return '';
167 | }
168 |
169 | /**
170 | * Handle actions on files.
171 | */
172 | FileAction (e) {
173 | const {contents} = this.state;
174 |
175 | if (typeof (this.props.onFileAction) !== 'undefined') {
176 | let target = e.target;
177 |
178 | if (target.tagName !== 'BUTTON') {
179 | target = window.TCH.Main.Utils.FindNearestParent (target, undefined, 'BUTTON');
180 | }
181 |
182 | if (target === null) {
183 | target = window.TCH.Main.Utils.FindNearestParent (e.target, 'tch-grid-action-row');
184 | }
185 |
186 | if (target !== null) {
187 | let params = undefined;
188 |
189 | for (let index in contents) {
190 | if (contents.hasOwnProperty (index)) {
191 | const rowData = contents [index];
192 |
193 | if (rowData.reactId === target.dataset.reactid) {
194 | params = rowData;
195 | break;
196 | }
197 | }
198 | }
199 |
200 | this.props.onFileAction (target.dataset.action, params);
201 | }
202 | }
203 | }
204 | }
205 |
206 | export default Files;
207 |
--------------------------------------------------------------------------------
/src/component/form.css:
--------------------------------------------------------------------------------
1 | /*** General form styles ***/
2 | .general-form label { min-height: 1px; margin-bottom: 8px; }
3 | .general-form-description { margin-top: -8px; margin-bottom: 16px; font-style: italic; }
4 | .general-form-error { /*display: block;*/ margin-top: -16px; margin-bottom: 16px; color: red; }
5 |
--------------------------------------------------------------------------------
/src/component/form/Checkbox.js:
--------------------------------------------------------------------------------
1 | import './checkbox.css';
2 | import {ReactComponent as Check} from '../../icon/check.svg';
3 |
4 | import React, {Component} from 'react';
5 | import Button from '../Button';
6 | import CSSTransition from 'react-transition-group/CSSTransition';
7 |
8 | class Checkbox extends Component {
9 | /**
10 | * Render the component into html.
11 | */
12 | render () {
13 | const {className, name, label, value, description, error, errorMessage} = this.props;
14 | let {id} = this.props;
15 |
16 | if (typeof (id) === 'undefined') {
17 | id = name;
18 | }
19 |
20 | const descriptionJsx = description ? (
21 |
22 | {description}
23 |
24 | ) : null;
25 |
26 | return
27 |
28 |
29 |
30 |
{label}
31 |
32 | {descriptionJsx}
33 |
34 |
35 | {errorMessage}
36 |
37 |
;
38 | }
39 |
40 | /**
41 | * Toggle checked value of formData.
42 | */
43 | Toggle () {
44 | this.props.onChange (this.props.name, !this.props.value);
45 | }
46 | }
47 |
48 | export default Checkbox;
49 |
--------------------------------------------------------------------------------
/src/component/form/EmailInput.js:
--------------------------------------------------------------------------------
1 | import TextInput from './TextInput';
2 |
3 | class EmailInput extends TextInput {
4 |
5 | }
6 |
7 | export default EmailInput;
8 |
--------------------------------------------------------------------------------
/src/component/form/Select.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-whitespace-before-property */
2 | import './select.css';
3 |
4 | import React, {Component} from 'react';
5 | import CSSTransition from 'react-transition-group/CSSTransition';
6 | import ButtonSelect from '../ButtonSelect';
7 |
8 | class Select extends Component {
9 | /**
10 | * Render the component into html.
11 | */
12 | render () {
13 | const {className, name, label, value, options, error, errorMessage} = this.props;
14 | let {id} = this.props;
15 |
16 | if (typeof (id) === 'undefined') {
17 | id = name;
18 | }
19 |
20 | const listOptions = options.map (element => {return {label: element.label, value: element.value, id: element.value};});
21 |
22 | const valueLabel = options.filter (element => element.value === value);
23 |
24 | return
25 |
{label}
26 |
27 |
28 |
29 | {errorMessage}
30 |
31 |
;
32 | }
33 |
34 | /**
35 | * OnSelectItem update value of formData.
36 | */
37 | OnSelectItem (e) {
38 | this.props.onChange (this.props.name, e.target.dataset.value);
39 | }
40 | }
41 |
42 | export default Select;
43 |
--------------------------------------------------------------------------------
/src/component/form/TextInput.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import CSSTransition from 'react-transition-group/CSSTransition';
3 |
4 | class TextInput extends Component {
5 | /**
6 | * Render the component into html.
7 | */
8 | render () {
9 | const {className, name, label, value, description, error, errorMessage} = this.props;
10 | let {id} = this.props;
11 |
12 | if (typeof (id) === 'undefined') {
13 | id = name;
14 | }
15 |
16 | const descriptionJsx = description ? (
17 |
18 | {description}
19 |
20 | ) : null;
21 |
22 | return
23 |
{label}
24 |
25 |
26 | {descriptionJsx}
27 |
28 |
29 | {errorMessage}
30 |
31 |
;
32 | }
33 |
34 | /**
35 | * OnChange update value of formData.
36 | */
37 | OnChange (e) {
38 | this.props.onChange (this.props.name, e.target.value);
39 | }
40 | }
41 |
42 | export default TextInput;
43 |
--------------------------------------------------------------------------------
/src/component/form/Textarea.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import CSSTransition from 'react-transition-group/CSSTransition';
3 |
4 | class Textarea extends Component {
5 | /**
6 | * Render the component into html.
7 | */
8 | render () {
9 | const {className, name, label, value, error, errorMessage} = this.props;
10 | let {id} = this.props;
11 |
12 | if (typeof (id) === 'undefined') {
13 | id = name;
14 | }
15 |
16 | return
17 |
{label}
18 |
19 |
20 |
21 | {errorMessage}
22 |
23 |
;
24 | }
25 |
26 | /**
27 | * OnChange update value of formData.
28 | */
29 | OnChange (e) {
30 | this.props.onChange (this.props.name, e.target.value);
31 | }
32 | }
33 |
34 | export default Textarea;
35 |
--------------------------------------------------------------------------------
/src/component/form/checkbox.css:
--------------------------------------------------------------------------------
1 | /*** Checkbox styles ***/
2 | .general-form-checkbox { display: flex; flex-wrap: wrap; flex-direction: row; margin-bottom: 16px; }
3 | .general-form-checkbox .button { width: 30px; height: 30px; margin-bottom: 0; margin-right: 16px; }
4 | .general-form-checkbox .button svg { display: none; }
5 | .general-form-checkbox.checked .button svg { display: block; }
6 | .general-form-checkbox label { flex-basis: calc(100% - 46px); margin-bottom: 0; }
7 | .general-form-checkbox .general-form-description { margin-top: 8px; margin-bottom: 0; }
8 | .general-form-checkbox .general-form-error { flex-basis: 100%; max-width: 100%; margin-top: 0; }
9 |
--------------------------------------------------------------------------------
/src/component/form/select.css:
--------------------------------------------------------------------------------
1 | /*** Form Select styles ***/
2 | .general-form-select button { width: 100%; min-height: 40px; text-align: left; }
3 |
--------------------------------------------------------------------------------
/src/component/link.css:
--------------------------------------------------------------------------------
1 | /*** Link styles ***/
2 | .link { display: inline-block; border-bottom: 2px solid transparent; margin-bottom: -2px; color: #e6c300; text-decoration: none; transition: border 0.4s linear, color 0.4s linear, background 0.4s linear; cursor: pointer; }
3 | .link:hover,
4 | .link:focus { border-bottom: 2px solid #e6c300; color: #e6c300; text-decoration: none; }
5 |
--------------------------------------------------------------------------------
/src/component/navigation.css:
--------------------------------------------------------------------------------
1 | /*** Header styles ***/
2 | #navigation-container { flex: 0 0 52px; max-height: 52px; z-index: 1; }
3 | .header-panel { width: calc(100% + 32px); border-width: 0; border-bottom-width: 2px; border-radius: 0; margin: 0 -16px 16px -16px; }
4 | .header-panel nav { display: flex; }
5 | .header-panel nav a.button { flex-grow: 1; border-width: 0; border-right-width: 2px; border-radius: 0; margin: 0; text-align: center; }
6 | .header-panel nav a.button:last-of-type { border-right-width: 0; }
7 | .header-panel nav .dummy { display: none; flex-grow: 1; }
8 |
9 | /*** Responsive styles ***/
10 | @media (min-width: 992px) {
11 | .header-panel nav a.button { flex-grow: 0; min-width: 250px; }
12 | .header-panel nav a.button:first-of-type { border-left-width: 2px; }
13 | .header-panel nav a.button:last-of-type { border-right-width: 2px; }
14 | .header-panel nav .dummy { display: block; }
15 | }
16 |
--------------------------------------------------------------------------------
/src/component/popup.css:
--------------------------------------------------------------------------------
1 | /*** General popup styles ***/
2 | .general-popup-container { /*display: none;*/ position: fixed; width: 100%; height: 100%; top: 0; left: 0; z-index: 100; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.6); }
3 | /*.general-popup-container.visible,
4 | .general-popup-container.animating { display: flex; }*/
5 | .general-popup { display: flex; overflow: hidden; width: 90%; max-width: 480px; height: calc(90% - 32px); max-height: 320px; flex-direction: column; padding: 0; border: 2px solid black; border-radius: 8px; background: white; }
6 | .general-popup.auto,
7 | .auto .general-popup { height: auto; }
8 | .red .general-popup { border-color: red; }
9 | .general-popup-header { display: block; width: 100%; height: auto; padding: 8px 16px; margin: 0; text-align: center; background: black; }
10 | .red .general-popup-header { background: red; }
11 | .general-popup-header h2 { margin: 0; color: white; text-transform: uppercase; }
12 | .general-popup-content { display: block; flex-grow: 1; width: 100%; height: auto; overflow: hidden; overflow-y: auto; padding: 16px 16px 0 16px; }
13 | .general-popup-content p { text-align: justify; }
14 | .general-popup-footer { display: block; width: 100%; height: auto; padding: 8px 16px; border-top: 2px solid black; margin: 0; background: transparent; }
15 | .red .general-popup-footer { border-top-color: red; }
16 | .general-popup-footer button { margin-bottom: 0; }
17 | .general-popup-footer button svg { width: 20px; height: 20px; }
18 |
19 | /*** Dark Mode styles ***/
20 | .dark-mode .general-popup { background: #1a1a1a; }
21 | .dark-mode .general-popup-header { background: #666666; }
22 | .dark-mode .red .general-popup-header { background: red; }
23 | .dark-mode .general-popup-header h2 { color: #e6e6e6; }
24 | .dark-mode .general-popup-footer { border-top-color: #666666; }
25 |
--------------------------------------------------------------------------------
/src/component/tabs.css:
--------------------------------------------------------------------------------
1 | /*** Tabs styles ***/
2 | .tch-tabs-navigation { display: flex; overflow: hidden; }
3 | .tch-tabs-navigation-items { display: flex; }
4 | .tch-tabs-navigation .button { flex: 0 0 auto; border-width: 0; border-right-width: 2px; border-radius: 0; margin: 0; vertical-align: middle; text-align: center; }
5 | /*.tch-tabs-navigation .button:last-child { border-right-width: 0; }*/
6 | .tch-tabs-navigation .button svg { display: inline-block; border: 2px solid black; border-radius: 50%; vertical-align: middle; transition: border 0.4s linear; }
7 | .tch-tabs-navigation .button:hover svg,
8 | .tch-tabs-navigation .button:focus svg { border-color: white; }
9 | .tch-tabs-navigation .button svg:hover,
10 | .tch-tabs-navigation .button svg:focus { border-color: red; color: red; }
11 | .tch-tabs-navigation .button.icon { border-width: 2px; border-radius: 50%; margin: 2px; }
12 | .tch-tabs-navigation .button.icon svg { display: block; border: 0; border-radius: 0; }
13 | .tch-tabs-navigation .button.icon svg:hover,
14 | .tch-tabs-navigation .button.icon svg:focus { color: white; }
15 | .tch-tabs-navigation.fill .tch-tabs-navigation-item { flex-grow: 1; }
16 | .tch-tabs-navigation.fill .tch-tabs-navigation-items { flex-grow: 1; }
17 |
18 | .tch-tabs-navigation .general-fade { transform: translateY(-100%); transition: border 0.4s linear, color 0.4s linear, background 0.4s linear, opacity 0.4s linear, transform 0.4s linear; }
19 | .tch-tabs-navigation .general-fade.button-select-container { display: none; }
20 | .tch-tabs-navigation .general-fade-enter,
21 | .tch-tabs-navigation .general-fade-appear { transform: translateY(-100%); }
22 | .tch-tabs-navigation .general-fade-enter-active,
23 | .tch-tabs-navigation .general-fade-enter-done,
24 | .tch-tabs-navigation .general-fade-appear-active { transform: translateY(0); }
25 | .tch-tabs-navigation .general-fade-exit,
26 | .tch-tabs-navigation .general-fade-exit-active { transform: translateY(-100%); }
27 | .tch-tabs-navigation .general-fade-enter.button-select-container,
28 | .tch-tabs-navigation .general-fade-appear.button-select-container,
29 | .tch-tabs-navigation .general-fade-enter-active.button-select-container,
30 | .tch-tabs-navigation .general-fade-enter-done.button-select-container,
31 | .tch-tabs-navigation .general-fade-appear-active.button-select-container,
32 | .tch-tabs-navigation .general-fade-exit.button-select-container,
33 | .tch-tabs-navigation .general-fade-exit-active.button-select-container { display: block; }
34 |
35 | /*** Dark Mode styles ***/
36 | .dark-mode .tch-tabs-navigation .button svg { border-color: #999999; }
37 | .dark-mode .tch-tabs-navigation .button:hover svg,
38 | .dark-mode .tch-tabs-navigation .button:focus svg { border-color: #e6e6e6; color: #e6e6e6; }
39 | .dark-mode .tch-tabs-navigation .button svg:hover,
40 | .dark-mode .tch-tabs-navigation .button svg:focus { border-color: red; color: red; }
41 | .dark-mode .tch-tabs-navigation .button.icon svg:hover,
42 | .dark-mode .tch-tabs-navigation .button.icon svg:focus { color: #e6e6e6; }
43 |
--------------------------------------------------------------------------------
/src/component/tabs/Content.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Content extends Component {
4 | /**
5 | * Content the component into html.
6 | */
7 | render () {
8 | return
9 | {this.props.params.content (this.props.params)}
10 |
;
11 | }
12 | }
13 |
14 | export default Content;
15 |
--------------------------------------------------------------------------------
/src/component/tabs/Navigation.js:
--------------------------------------------------------------------------------
1 | import { ReactComponent as Close } from '../../icon/close.svg';
2 |
3 | import React, { Component } from 'react';
4 | import Button from '../Button';
5 |
6 | class Navigation extends Component {
7 | /**
8 | * Render the component into html.
9 | */
10 | render () {
11 | return
{this.props.params.title} ;
12 | }
13 | }
14 |
15 | export default Navigation;
16 |
--------------------------------------------------------------------------------
/src/component/titlebar.css:
--------------------------------------------------------------------------------
1 | /*** Frame titlebar styles ***/
2 | #titlebar { display: flex; overflow: hidden; flex: 0 0 32px; max-height: 32px; background: black; -webkit-user-select: none; -webkit-app-region: drag; }
3 | .darwin #titlebar { border-top-left-radius: 4px; border-top-right-radius: 4px; }
4 | #title { display: inline-block; flex-grow: 1; padding: 8px 8px 8px 138px; color: white; font-size: 14px; text-align: center; }
5 | #titlebar.main-visible #title,
6 | #titlebar.reset-visible #title { padding-left: 92px; }
7 | #titlebar.main-visible.reset-visible #title { padding-left: 46px; }
8 | .darwin #title { padding: 8px 138px 8px 8px; }
9 | .darwin #titlebar.main-visible #title,
10 | .darwin #titlebar.reset-visible #title { padding-left: 8px; padding-right: 92px; }
11 | .darwin #titlebar.main-visible.reset-visible #title { padding-left: 8px; padding-right: 46px; }
12 | #titlebar-actions,
13 | #titlebar-actions-other { display: flex; }
14 | #titlebar-actions button,
15 | #titlebar-actions-other button { display: block; width: 46px; border: 0; border-bottom: 2px solid black; border-radius: 0; margin: 0; color: white; font-size: 14px; background: transparent; -webkit-app-region: no-drag; }
16 | #titlebar-actions button:hover,
17 | #titlebar-actions button:focus,
18 | #titlebar-actions-other button:hover,
19 | #titlebar-actions-other button:focus { color: black; background: white; }
20 | #titlebar-actions button svg,
21 | #titlebar-actions-other button svg { display: block; margin: 0 auto; }
22 | #titlebar-maximize .clone { display: none; }
23 | #titlebar-maximize.minimize .square { display: none; }
24 | #titlebar-maximize.minimize .clone { display: block; }
25 | #titlebar-actions #titlebar-main,
26 | #titlebar-actions-other #titlebar-main { display: none; }
27 | #titlebar.main-visible #titlebar-actions #titlebar-main,
28 | #titlebar.main-visible #titlebar-actions-other #titlebar-main { display: block; }
29 | #titlebar-actions #titlebar-reset,
30 | #titlebar-actions-other #titlebar-reset { display: none; }
31 | #titlebar.reset-visible #titlebar-actions #titlebar-reset,
32 | #titlebar.reset-visible #titlebar-actions-other #titlebar-reset { display: block; }
33 |
--------------------------------------------------------------------------------
/src/font/CaveHand.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/src/font/CaveHand.ttf
--------------------------------------------------------------------------------
/src/icon/caret-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/chevron-circle-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/chevron-circle-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/chevron-left-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/chevron-right-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/cog.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/ellipsis-v.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/expand-arrows-alt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/expand-arrows.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/folder-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/hdd.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/level-up-alt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/linkedin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/maximize-clone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/maximize-square.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/minimize.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/npm.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/question.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/save.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/stack-overflow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/trash-alt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icon/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/image/tomas-chylyV2-sign.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomaschyly/FileCtor/5529f1851a200cc7854864fc0b77e397d1bbef6f/src/image/tomas-chylyV2-sign.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | /*** Fonts ***/
2 | @font-face {
3 | font-family: CaveHand;
4 | src: url("./font/CaveHand.ttf");
5 | }
6 |
7 | /*** Animations ***/
8 | @keyframes spin {
9 | 0% {
10 | transform:rotate(0deg)
11 | }
12 | to {
13 | transform:rotate(1turn)
14 | }
15 | }
16 | .spin { animation:spin 2s infinite linear; }
17 |
18 | .general-fade { display: none; opacity: 0; transition: opacity 0.4s linear; }
19 | .general-fade-enter,
20 | .general-fade-appear { display: block; opacity: 0.01; }
21 | .general-fade-enter-active,
22 | .general-fade-enter-done,
23 | .general-fade-appear-active { display: block; opacity: 1; }
24 | .general-fade-exit,
25 | .general-fade-exit-active { display: block; opacity: 0.01; }
26 |
27 | .general-flex-fade { display: none; opacity: 0; transition: opacity 0.4s linear; }
28 | .general-flex-fade-enter,
29 | .general-flex-fade-appear { display: flex; opacity: 0.01; }
30 | .general-flex-fade-enter-active,
31 | .general-flex-fade-enter-done,
32 | .general-flex-fade-appear-active { display: flex; opacity: 1; }
33 | .general-flex-fade-exit,
34 | .general-flex-fade-exit-active { display: flex; opacity: 0.01; }
35 |
36 | /*** Global ***/
37 | * { box-sizing: border-box; }
38 |
39 | html { width: 100%; height: 100%; }
40 | body { width: 100%; height: 100%; overflow-x: hidden; border: 2px solid black; color: black; font-size: 16px; line-height: 1.2; font-family: CaveHand; background: #f5f5f5; }
41 | .fancy-font-disabled { font-family: Consolas, 'Courier New', monospace; }
42 | .darwin body { border-radius: 4px; }
43 | #root { width: 100%; height: 100%; }
44 | #app { display: flex; flex-direction: column; width: 100%; height: 100%; }
45 | #content { flex: 1 0 auto; max-height: calc(100% - 84px); }
46 | .navigation-disabled #content { height: calc(100% - 32px); max-height: calc(100% - 32px); }
47 |
48 | .gold { color: #ffd700; }
49 | .silver { color: #DDDDDD; }
50 |
51 | h1,
52 | h2,
53 | h3,
54 | h4 { margin: 0 0 16px 0; }
55 | h1 { font-size: 24px; }
56 | h2 { font-size: 20px; }
57 | h3 { font-size: 16px; }
58 | p { margin: 0 0 16px 0; line-height: 1.5; }
59 |
60 | .container { width: 100%; padding-left: 16px; padding-right: 16px; margin-left: auto; margin-right: auto; }
61 | .row { display: flex; flex-wrap: wrap; justify-content: center; margin-left: -16px; margin-right: -16px; }
62 | .col-1,
63 | .col-2,
64 | .col-3,
65 | .col-4,
66 | .col-5,
67 | .col-6,
68 | .col-7,
69 | .col-8,
70 | .col-9,
71 | .col-10 { position: relative; width: 100%; min-height: 1px; padding-left: 16px; padding-right: 16px; }
72 | .col-1 { flex: 0 0 10%; max-width: 10%; }
73 | .col-2 { flex: 0 0 20%; max-width: 20%; }
74 | .col-3 { flex: 0 0 30%; max-width: 30%; }
75 | .col-4 { flex: 0 0 40%; max-width: 40%; }
76 | .col-5 { flex: 0 0 50%; max-width: 50%; }
77 | .col-6 { flex: 0 0 60%; max-width: 60%; }
78 | .col-7 { flex: 0 0 70%; max-width: 70%; }
79 | .col-8 { flex: 0 0 80%; max-width: 80%; }
80 | .col-9 { flex: 0 0 90%; max-width: 90%; }
81 | .col-10 { flex: 0 0 100%; max-width: 100%; }
82 | .f-right { float: right; }
83 | .hidden { display: none; }
84 |
85 | label { display: inline-flex; align-items: center; width: 100%; min-height: 40px; margin-bottom: 16px; text-align: unset; cursor: pointer; }
86 | label.smaller { min-height: 1px; margin-bottom: 8px; }
87 | input { display: inline-block; width: 100%; height: 40px; padding: 0 0 0 8px; border: 2px solid black; border-radius: 8px; outline: none; margin-bottom: 16px; vertical-align: middle; background: transparent; transition: background 0.4s linear; }
88 | input:hover,
89 | input:focus { background: white; }
90 | textarea { display: inline-block; width: 100%; min-height: 120px; padding: 8px; border: 2px solid black; border-radius: 8px; resize: none; outline: none; margin-bottom: 16px; background: transparent; transition: background 0.4s linear; }
91 | textarea:hover,
92 | textarea:focus { background: white; }
93 |
94 | a { display: inline-block; border-bottom: 2px solid transparent; margin-bottom: -2px; color: #ffd700; text-decoration: none; transition: border 0.4s linear, color 0.4s linear, background 0.4s linear; }
95 | a:hover,
96 | a:focus { border-bottom: 2px solid #ffd700; color: #ffd700; text-decoration: none; }
97 | a.button,
98 | button,
99 | input[type=submit] { display: inline-block; width: auto; height: auto; padding: 6px 8px; border: 2px solid black; border-radius: 8px; outline: 0; margin: 0 0 16px 0; color: black; font-size: 20px; font-weight: bold; background: transparent; cursor: pointer; transition: border 0.4s linear, color 0.4s linear, background 0.4s linear; }
100 | a.button.active,
101 | button.active,
102 | input.button.active { background: #666666; }
103 | a.button:hover,
104 | button:hover,
105 | input[type=submit]:hover,
106 | a.button:focus,
107 | button:focus,
108 | input[type=submit]:focus { color: white; background: black; }
109 | .button-red { border-color: red; }
110 | .button-red.active,
111 | .button-red:hover,
112 | .button-red:focus { color: white; background: red; }
113 | button.icon { padding: 4px; }
114 | button.icon-larger { padding: 2px; }
115 | button svg { display: block; width: 18px; height: 18px; padding: 2px; }
116 | button.icon-larger svg { width: 28px; height: 28px; }
117 |
118 | img { max-width: 100%; }
119 |
120 | .panel { clear: both; overflow: hidden; width: 100%; padding: 16px 16px 0 16px; border: 2px solid black; border-radius: 8px; margin-bottom: 16px; }
121 | .panel.thin { padding: 8px 16px; }
122 | .panel.no-white { padding: 0; }
123 | .panel.empty { display: none; }
124 | .navigation .item { margin-right: 16px; vertical-align: middle; }
125 | .navigation .item:last-child { margin-right: 0; }
126 |
127 | .toggle-buttons { display: flex; justify-content: center; }
128 | .toggle-buttons.left { justify-content: start; }
129 | .toggle-buttons button { min-width: 90px; border-right: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; }
130 | .toggle-buttons button:last-child { border-right: 2px solid black; border-radius: 8px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
131 | .toggle-buttons button:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
132 |
133 | .flashes { font-weight: bold; text-align: center; }
134 | .flashes .flash { margin-bottom: 8px; }
135 | .flashes .flash:last-child { margin-bottom: 0; }
136 | .flashes .flash.info { color: orange; }
137 | .flashes .flash.error { color: red; }
138 | .flashes .flash.success { color: green; }
139 |
140 | .progress { display: block; width: 40px; height: 40px; margin-bottom: 16px; }
141 |
142 | /*** Grid styles ***/
143 | .tch-grid { clear: both; overflow: hidden; width: 100%; border: 2px solid black; border-radius: 8px; margin-bottom: 16px; }
144 | .tch-grid-header { border-bottom: 2px solid black; }
145 | .tch-grid-body { overflow: hidden; border-bottom: 2px solid black; }
146 | .tch-grid.empty .tch-grid-body { border-bottom: 0; }
147 | .tch-grid-row { display: flex; flex-wrap: nowrap; min-height: 40px; }
148 | .tch-grid-body .tch-grid-row { display: flex; border-top: 2px solid transparent; border-bottom: 2px solid transparent; transition: border 0.4s linear, opacity 0.4s linear, transform 0.4s linear; }
149 | .tch-grid-body .tch-grid-row.active { background: #666666; }
150 | .tch-grid-body .tch-grid-row:hover,
151 | .tch-grid-body .tch-grid-row:focus { border-top-color: black; border-bottom-color: black; }
152 | .tch-grid-body .tch-grid-row:first-of-type { border-top-color: transparent; }
153 | .tch-grid-body .tch-grid-row:last-of-type { border-bottom-color: transparent; }
154 | .tch-grid-col { display: flex; flex-wrap: wrap; flex-basis: 0; flex-grow: 1; flex-shrink: 1; align-items: center; padding: 4px; word-break: break-all; }
155 | .tch-grid-col.no-grow,
156 | .tch-grid-col.actions { flex-grow: 0; flex-shrink: 0; }
157 | .tch-grid-col.center { justify-content: center; }
158 | .tch-grid-col.right { justify-content: flex-end; }
159 | .tch-grid-col.loading { justify-content: center; }
160 | .tch-grid-col.loading svg { display: block; width: 28px; height: 28px; }
161 | .tch-grid-header .tch-grid-col { font-size: 20px; }
162 | .tch-grid button { margin: 0; vertical-align: middle; }
163 | .tch-grid-header button { margin-left: 4px; }
164 | .tch-grid .tch-grid-action { margin-right: 4px; }
165 | .tch-grid .tch-grid-action:last-of-type { margin-right: 0; }
166 | .tch-grid label { margin: 0; }
167 | .tch-grid-filter-search { margin: 0; }
168 | .tch-grid-page { display: flex; align-items: center; padding: 0 8px; }
169 | .tch-grid .tch-grid-page-prev { margin-right: 4px; }
170 | .tch-grid .tch-grid-page-next { margin-left: 4px; }
171 | .tch-grid-pagesize { display: flex; align-items: center; padding: 0 8px; }
172 | .tch-grid-page span,
173 | .tch-grid-pagesize span { padding-right: 8px; }
174 | .tch-grid-page .button-select-list,
175 | .tch-grid-pagesize .button-select-list { min-width: 60px; }
176 | .tch-grid .button-select-container { margin-bottom: 0; }
177 |
178 | /*** Dark Mode styles ***/
179 | body.dark-mode { color: #e6e6e6; background: #1a1a1a; }
180 |
181 | .dark-mode input { border-color: #666666; color: #e6e6e6; }
182 | .dark-mode input:hover,
183 | .dark-mode input:focus { background: #666666; }
184 | .dark-mode textarea { border-color: #666666; color: #e6e6e6; }
185 | .dark-mode textarea:hover,
186 | .dark-mode textarea:focus { background: #666666; }
187 |
188 | .dark-mode a.button,
189 | .dark-mode button,
190 | .dark-mode input[type=submit] { border-color: #666666; color: #999999; }
191 | .dark-mode a.button.active,
192 | .dark-mode button.active,
193 | .dark-mode input.button.active { color: #e6e6e6; background: #333333; }
194 | .dark-mode a.button:hover,
195 | .dark-mode button:hover,
196 | .dark-mode input[type=submit]:hover,
197 | .dark-mode a.button:focus,
198 | .dark-mode button:focus,
199 | .dark-mode input[type=submit]:focus { color: #e6e6e6; background: #666666; }
200 | .dark-mode .button-red { border-color: red; }
201 | .dark-mode .button-red.active,
202 | .dark-mode .button-red:hover,
203 | .dark-mode .button-red:focus { background: red; }
204 |
205 | .dark-mode .panel { border-color: #666666; }
206 |
207 | .dark-mode .tch-grid { border-color: #666666; }
208 | .dark-mode .tch-grid-header { border-bottom-color: #666666; }
209 | .dark-mode .tch-grid-body { border-bottom-color: #666666; }
210 | .dark-mode .tch-grid-body .tch-grid-row:hover,
211 | .dark-mode .tch-grid-body .tch-grid-row:focus { border-top-color: #666666; border-bottom-color: #666666; }
212 | .dark-mode .tch-grid-body .tch-grid-row:first-of-type { border-top-color: transparent; }
213 | .dark-mode .tch-grid-body .tch-grid-row:last-of-type { border-bottom-color: transparent; }
214 |
215 | /*** Responsive styles ***/
216 | @media (min-width: 576px) {
217 | .col-sm-1 { flex: 0 0 10%; max-width: 10%; }
218 | .col-sm-2 { flex: 0 0 20%; max-width: 20%; }
219 | .col-sm-3 { flex: 0 0 30%; max-width: 30%; }
220 | .col-sm-4 { flex: 0 0 40%; max-width: 40%; }
221 | .col-sm-5 { flex: 0 0 50%; max-width: 50%; }
222 | .col-sm-6 { flex: 0 0 60%; max-width: 60%; }
223 | .col-sm-7 { flex: 0 0 70%; max-width: 70%; }
224 | .col-sm-8 { flex: 0 0 80%; max-width: 80%; }
225 | .col-sm-9 { flex: 0 0 90%; max-width: 90%; }
226 | .col-sm-10 { flex: 0 0 100%; max-width: 100%; }
227 | }
228 |
229 | @media (min-width: 768px) {
230 | .col-md-1 { flex: 0 0 10%; max-width: 10%; }
231 | .col-md-2 { flex: 0 0 20%; max-width: 20%; }
232 | .col-md-3 { flex: 0 0 30%; max-width: 30%; }
233 | .col-md-4 { flex: 0 0 40%; max-width: 40%; }
234 | .col-md-5 { flex: 0 0 50%; max-width: 50%; }
235 | .col-md-6 { flex: 0 0 60%; max-width: 60%; }
236 | .col-md-7 { flex: 0 0 70%; max-width: 70%; }
237 | .col-md-8 { flex: 0 0 80%; max-width: 80%; }
238 | .col-md-9 { flex: 0 0 90%; max-width: 90%; }
239 | .col-md-10 { flex: 0 0 100%; max-width: 100%; }
240 | }
241 |
242 | @media (min-width: 992px) {
243 | .col-lg-1 { flex: 0 0 10%; max-width: 10%; }
244 | .col-lg-2 { flex: 0 0 20%; max-width: 20%; }
245 | .col-lg-3 { flex: 0 0 30%; max-width: 30%; }
246 | .col-lg-4 { flex: 0 0 40%; max-width: 40%; }
247 | .col-lg-5 { flex: 0 0 50%; max-width: 50%; }
248 | .col-lg-6 { flex: 0 0 60%; max-width: 60%; }
249 | .col-lg-7 { flex: 0 0 70%; max-width: 70%; }
250 | .col-lg-8 { flex: 0 0 80%; max-width: 80%; }
251 | .col-lg-9 { flex: 0 0 90%; max-width: 90%; }
252 | .col-lg-10 { flex: 0 0 100%; max-width: 100%; }
253 | }
254 |
255 | @media (min-width: 1200px) {
256 | /*.container { width: 1200px; }*/
257 | .col-xl-1 { flex: 0 0 10%; max-width: 10%; }
258 | .col-xl-2 { flex: 0 0 20%; max-width: 20%; }
259 | .col-xl-3 { flex: 0 0 30%; max-width: 30%; }
260 | .col-xl-4 { flex: 0 0 40%; max-width: 40%; }
261 | .col-xl-5 { flex: 0 0 50%; max-width: 50%; }
262 | .col-xl-6 { flex: 0 0 60%; max-width: 60%; }
263 | .col-xl-7 { flex: 0 0 70%; max-width: 70%; }
264 | .col-xl-8 { flex: 0 0 80%; max-width: 80%; }
265 | .col-xl-9 { flex: 0 0 90%; max-width: 90%; }
266 | .col-xl-10 { flex: 0 0 100%; max-width: 100%; }
267 | }
268 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './index.css';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import App from './App';
6 |
7 | ReactDOM.render (
8 |
,
9 | document.getElementById ('root')
10 | );
11 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const {app, BrowserWindow} = require ('electron');
3 | const path = require ('path');
4 | const Api = require ('./main/Api');
5 | const Config = require ('./main/Config');
6 | const installSnippet = require ('./main/install/snippet');
7 | const uuid = require ('uuid');
8 |
9 | const singleAppLock = app.requestSingleInstanceLock ();
10 |
11 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
12 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
13 | }
14 |
15 | const Main = {
16 | IDENTIFIER: 'main',
17 |
18 | default: {
19 | width: 800,
20 | height: 600
21 | },
22 | window: null,
23 | uuid: null,
24 | port: null,
25 | config: null,
26 |
27 | /**
28 | * Create main app window.
29 | */
30 | async CreateWindow () {
31 | if (this.port === null) {
32 | this.port = process.env.FILECTOR_PORT;
33 | }
34 |
35 | this.config = new Config ();
36 | await this.config.Load ();
37 |
38 | await installSnippet (this.config);
39 |
40 | Api.Init (this);
41 |
42 | let windowParameters = this.LoadWindow (Main.IDENTIFIER);
43 | const width = windowParameters !== null && typeof (windowParameters.size) !== 'undefined' ? windowParameters.size.width : this.default.width;
44 | const height = windowParameters !== null && typeof (windowParameters.size) !== 'undefined' ? windowParameters.size.height : this.default.height;
45 |
46 | this.uuid = uuid.v4 ();
47 |
48 | windowParameters = {
49 | width: width,
50 | minWidth: 640,
51 | height: height,
52 | minHeight: 480,
53 | frame: false,
54 | center: true,
55 | show: false,
56 | webPreferences: {
57 | nodeIntegration: true
58 | },
59 | uuid: this.uuid
60 | };
61 |
62 | switch (process.platform) {
63 | case 'linux':
64 | windowParameters.icon = path.join (__dirname, '..', 'icon.png');
65 | break;
66 | case 'darwin':
67 | //windowParameters.icon = path.join (__dirname, '..', 'icon.icns');
68 | windowParameters.icon = path.join (__dirname, '..', 'icon.png');
69 | break;
70 | default:
71 | windowParameters.icon = path.join (__dirname, '..', 'icon.ico');
72 | }
73 |
74 | this.window = new BrowserWindow (windowParameters);
75 | this.window.uuid = this.uuid;
76 |
77 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
78 | this.window.loadURL (`http://127.0.0.1:${this.port}/`);
79 |
80 | //const {default: installExtension, REACT_DEVELOPER_TOOLS} = require ('electron-devtools-installer'); // does not work with current Electron versions after v5
81 |
82 | //await installExtension (REACT_DEVELOPER_TOOLS);
83 | } else {
84 | this.window.loadURL (`file://${path.join (__dirname, '../build/index.html')}`);
85 | }
86 |
87 | this.window.once ('ready-to-show', () => {
88 | this.window.setMenu (null);
89 |
90 | if (windowParameters !== null && typeof (windowParameters.maximized) !== 'undefined' && windowParameters.maximized) {
91 | this.window.setBounds ({width: this.default.width, height: this.default.height});
92 | this.window.center ();
93 | this.window.maximize ();
94 | }
95 |
96 | this.window.show ();
97 |
98 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
99 | this.window.webContents.openDevTools ();
100 | }
101 |
102 | this.ShouldShowReset (this.window);
103 | });
104 |
105 | this.window.on ('closed', () => {
106 | this.window = null;
107 |
108 | Api.ClosedMain ();
109 | });
110 |
111 | this.window.on ('maximize', () => {
112 | this.SaveWindow (Main.IDENTIFIER, 'maximized', true);
113 | });
114 | this.window.on ('unmaximize', () => {
115 | this.SaveWindow (Main.IDENTIFIER, 'maximized', false);
116 | });
117 |
118 | this.window.on ('resize', () => {
119 | let size = this.window.getSize ();
120 |
121 | this.SaveWindow (Main.IDENTIFIER, 'size', {
122 | width: size [0],
123 | height: size [1]
124 | });
125 |
126 | this.ShouldShowReset (this.window);
127 | });
128 | },
129 |
130 | /**
131 | * Load BrowserWindow parameters.
132 | * @param {string} which Identifier for window
133 | * @return {Object|null}
134 | */
135 | LoadWindow (which) {
136 | let windows = this.config.Get ('windows');
137 | if (windows !== null && typeof (windows [which]) !== 'undefined') {
138 | return windows [which];
139 | }
140 |
141 | return null;
142 | },
143 |
144 | /**
145 | * Save BrowserWindow parameters.
146 | */
147 | SaveWindow (which, key, value) {
148 | let windows = this.config.Get ('windows');
149 | if (windows === null) {
150 | windows = {};
151 | }
152 |
153 | if (typeof (windows [which]) === 'undefined') {
154 | windows [which] = {};
155 | }
156 |
157 | windows [which] [key] = value;
158 |
159 | this.config.Set ('windows', windows);
160 | },
161 |
162 | /**
163 | * Check if window should show reset and notify.
164 | */
165 | ShouldShowReset (window) {
166 | const size = window.getSize ();
167 |
168 | if (Math.abs (size [0] - this.default.width) > 4 || Math.abs (size [1] - this.default.height) > 4) {
169 | window.send ('reset-show', {window: Main.IDENTIFIER});
170 | } else {
171 | window.send ('reset-hide');
172 | }
173 | }
174 | };
175 |
176 | if (singleAppLock) {
177 | app.on ('ready', () => {
178 | Main.CreateWindow ();
179 | });
180 |
181 | app.on ('window-all-closed', () => {
182 | if (process.platform !== 'darwin') {
183 | app.quit ();
184 | }
185 | });
186 |
187 | app.on ('activate', () => {
188 | if (Main.window === null) {
189 | Main.CreateWindow ();
190 | }
191 | });
192 |
193 | app.on ('second-instance', () => {
194 | if (Main.window !== null) {
195 | if (Main.window.isMinimized ()) {
196 | Main.window.restore ();
197 | }
198 |
199 | Main.window.focus ();
200 | }
201 | });
202 | } else {
203 | app.quit ();
204 | }
205 |
206 | //TODo remove this testing
207 | // setTimeout (async () => {
208 | // const RxSnippet = require ('./main/model/RxSnippet');
209 | //
210 | // const model = new RxSnippet ();
211 | // /*await model.Load ('d8ef8a4a-e1fd-47fc-b6e6-fbc226239e55');
212 | // await model.Delete ();
213 | // await model.Load ('514a14c4-1184-4d9c-921d-99208d125fbb');
214 | // await model.Delete ();*/
215 | //
216 | // model.data = {
217 | // name: 'Delete test'
218 | // };
219 | // await model.Save ();
220 | //
221 | // /*model.data.description = 'Lorem ipsum dolor sit amet';
222 | // await model.Save ();*/
223 | //
224 | // //console.log (model.id, model.data);
225 | //
226 | // // console.log (await model.List ({
227 | // // sort: 'created',
228 | // // sortBy: 1,
229 | // // //limit: 1,
230 | // // //page: 1
231 | // // /*where: {
232 | // // name: {
233 | // // comparison: 'regex',
234 | // // value: new RegExp ('s', 'i')
235 | // // }
236 | // // }*/
237 | // // }));
238 | // console.log (await model.Count ());
239 | // }, 2000);
240 |
--------------------------------------------------------------------------------
/src/main/Config.js:
--------------------------------------------------------------------------------
1 | const electron = require ('electron');
2 | const path = require ('path');
3 | const fse = require ('fs-extra');
4 | const writeFileAtomic = require ('write-file-atomic');
5 |
6 | class Config {
7 | /**
8 | * Config initialization.
9 | */
10 | constructor () {
11 | this.fileDir = path.join ((electron.app || electron.remote.app).getPath ('userData'), 'var', 'config');
12 | this.filePath = path.join ((electron.app || electron.remote.app).getPath ('userData'), 'var', 'config', 'default.json');
13 | this.data = {};
14 | }
15 |
16 | /**
17 | * Load saved config from file.
18 | */
19 | async Load () {
20 | try {
21 | let fileData = await fse.readFile (this.filePath);
22 |
23 | this.data = JSON.parse (fileData);
24 | } catch (error) {
25 | console.error ('TCH_e Config - Load - ' + error.message);
26 |
27 | this.data = {};
28 |
29 | try {
30 | await fse.ensureDir (this.fileDir);
31 | } catch (error2) {
32 | console.error ('TCH_e Config - Load.2 - ' + error.message);
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Save config data to file.
39 | */
40 | Save () {
41 | try {
42 | writeFileAtomic (this.filePath, JSON.stringify (this.data), error => {
43 | if (error) {
44 | console.error ('TCH_e Config - Save - ' + error.message);
45 | }
46 | });
47 | } catch (error) {
48 | console.error ('TCH_e Config - Save - ' + error.message);
49 | }
50 | }
51 |
52 | /**
53 | * Get value or all data from config.
54 | */
55 | Get (key = null) {
56 | if (key !== null && typeof (this.data [key]) !== 'undefined') {
57 | return this.data [key];
58 | } else if (key === null) {
59 | return this.data;
60 | }
61 |
62 | return null;
63 | }
64 |
65 | /**
66 | * Set value to config and save.
67 | */
68 | Set (key, value) {
69 | this.data [key] = value;
70 |
71 | this.Save ();
72 | }
73 |
74 | /**
75 | * Delete value from config and save.
76 | */
77 | Delete (key) {
78 | if (typeof (this.data [key]) !== 'undefined') {
79 | delete this.data [key];
80 |
81 | this.Save ();
82 | }
83 | }
84 |
85 | /**
86 | * Reset the config.
87 | */
88 | async Reset () {
89 | return new Promise ((resolve, reject) => {
90 | try {
91 | this.data = {};
92 |
93 | writeFileAtomic (this.filePath, JSON.stringify (this.data), error => {
94 | if (error) {
95 | reject (error);
96 | } else {
97 | resolve ();
98 | }
99 | });
100 | } catch (error) {
101 | reject (error);
102 | }
103 | });
104 | }
105 | }
106 |
107 | module.exports = Config;
108 |
--------------------------------------------------------------------------------
/src/main/Console.js:
--------------------------------------------------------------------------------
1 | const {BrowserWindow} = require ('electron');
2 | const path = require ('path');
3 | const uuid = require ('uuid');
4 |
5 | const Console_static = {
6 | IDENTIFIER: 'console',
7 |
8 | default: {
9 | width: 640,
10 | height: 480
11 | },
12 | window: null,
13 | uuid: null,
14 | port: null,
15 | main: undefined
16 | };
17 |
18 | class Console {
19 | /**
20 | * Create console window.
21 | */
22 | static CreateWindow () {
23 | if (Console_static.port === null) {
24 | Console_static.port = process.env.FILECTOR_PORT;
25 | }
26 |
27 | let windowParameters = Console_static.main.LoadWindow (Console_static.IDENTIFIER);
28 | const width = windowParameters !== null && typeof (windowParameters.size) !== 'undefined' ? windowParameters.size.width : Console_static.default.width;
29 | const height = windowParameters !== null && typeof (windowParameters.size) !== 'undefined' ? windowParameters.size.height : Console_static.default.height;
30 |
31 | Console_static.uuid = uuid.v4 ();
32 |
33 | windowParameters = {
34 | width: width,
35 | minWidth: 640,
36 | height: height,
37 | minHeight: 480,
38 | frame: false,
39 | center: true,
40 | show: false,
41 | webPreferences: {
42 | nodeIntegration: true
43 | },
44 | uuid: Console_static.uuid
45 | };
46 |
47 | switch (process.platform) {
48 | case 'linux':
49 | windowParameters.icon = path.join (__dirname, '..', '..', 'icon.png');
50 | break;
51 | case 'darwin':
52 | //windowParameters.icon = path.join (__dirname, '..', '..', 'icon.icns');
53 | windowParameters.icon = path.join (__dirname, '..', '..', 'icon.png');
54 | break;
55 | default:
56 | windowParameters.icon = path.join (__dirname, '..', '..', 'icon.ico');
57 | }
58 |
59 | Console_static.window = new BrowserWindow (windowParameters);
60 | Console_static.window.uuid = Console_static.uuid;
61 |
62 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
63 | Console_static.window.loadURL (`http://127.0.0.1:${Console_static.port}/#/console`);
64 | } else {
65 | Console_static.window.loadURL (`file://${path.join (__dirname, '../../build/index.html')}#/console`);
66 | }
67 |
68 | Console_static.window.once ('ready-to-show', () => {
69 | Console_static.window.setMenu (null);
70 |
71 | if (windowParameters !== null && typeof (windowParameters.maximized) !== 'undefined' && windowParameters.maximized) {
72 | Console_static.window.setBounds ({width: Console_static.default.width, height: Console_static.default.height});
73 | Console_static.window.center ();
74 | Console_static.window.maximize ();
75 | }
76 |
77 | Console_static.window.show ();
78 |
79 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
80 | Console_static.window.webContents.openDevTools ();
81 | }
82 |
83 | Console.ShouldShowReset (Console_static.window);
84 | });
85 |
86 | Console_static.window.on ('closed', () => {
87 | Console_static.window = null;
88 | });
89 |
90 | Console_static.window.on ('maximize', () => {
91 | Console_static.main.SaveWindow (Console_static.IDENTIFIER, 'maximized', true);
92 | });
93 | Console_static.window.on ('unmaximize', () => {
94 | Console_static.main.SaveWindow (Console_static.IDENTIFIER, 'maximized', false);
95 | });
96 |
97 | Console_static.window.on ('resize', () => {
98 | let size = Console_static.window.getSize ();
99 |
100 | Console_static.main.SaveWindow (Console_static.IDENTIFIER, 'size', {
101 | width: size [0],
102 | height: size [1]
103 | });
104 |
105 | Console.ShouldShowReset (Console_static.window);
106 | });
107 | }
108 |
109 | /**
110 | * Open console, create if not exists.
111 | */
112 | static Open (main, lastPayloadCallback) {
113 | Console_static.main = main;
114 |
115 | let alreadyInitialized = true;
116 | if (Console_static.window === null) {
117 | Console.CreateWindow ();
118 |
119 | alreadyInitialized = false;
120 | }
121 |
122 | Console_static.window.focus ();
123 |
124 | if (alreadyInitialized) {
125 | lastPayloadCallback (undefined, Console_static.window);
126 | }
127 | }
128 |
129 | /**
130 | * Check if window should show reset and notify.
131 | */
132 | static ShouldShowReset (window) {
133 | const size = window.getSize ();
134 |
135 | if (Math.abs (size [0] - Console_static.default.width) > 4 || Math.abs (size [1] - Console_static.default.height) > 4) {
136 | window.send ('reset-show', {window: Console_static.IDENTIFIER});
137 | } else {
138 | window.send ('reset-hide');
139 | }
140 | }
141 | }
142 |
143 | module.exports = {
144 | Console,
145 | Console_static
146 | };
147 |
--------------------------------------------------------------------------------
/src/main/Reference.js:
--------------------------------------------------------------------------------
1 | const {BrowserWindow} = require ('electron');
2 | const path = require ('path');
3 | const uuid = require ('uuid');
4 |
5 | const Reference_static = {
6 | IDENTIFIER: 'reference',
7 |
8 | default: {
9 | width: 640,
10 | height: 480
11 | },
12 | window: null,
13 | uuid: null,
14 | port: null,
15 | main: undefined
16 | };
17 |
18 | class Reference {
19 | /**
20 | * Create reference window.
21 | */
22 | static CreateWindow () {
23 | if (Reference_static.port === null) {
24 | Reference_static.port = process.env.FILECTOR_PORT;
25 | }
26 |
27 | let windowParameters = Reference_static.main.LoadWindow (Reference_static.IDENTIFIER);
28 | const width = windowParameters !== null && typeof (windowParameters.size) !== 'undefined' ? windowParameters.size.width : Reference_static.default.width;
29 | const height = windowParameters !== null && typeof (windowParameters.size) !== 'undefined' ? windowParameters.size.height : Reference_static.default.height;
30 |
31 | Reference_static.uuid = uuid.v4 ();
32 |
33 | windowParameters = {
34 | width: width,
35 | minWidth: 640,
36 | height: height,
37 | minHeight: 480,
38 | frame: false,
39 | center: true,
40 | show: false,
41 | webPreferences: {
42 | nodeIntegration: true
43 | },
44 | uuid: Reference_static.uuid
45 | };
46 |
47 | switch (process.platform) {
48 | case 'linux':
49 | windowParameters.icon = path.join (__dirname, '..', '..', 'icon.png');
50 | break;
51 | case 'darwin':
52 | //windowParameters.icon = path.join (__dirname, '..', '..', 'icon.icns');
53 | windowParameters.icon = path.join (__dirname, '..', '..', 'icon.png');
54 | break;
55 | default:
56 | windowParameters.icon = path.join (__dirname, '..', '..', 'icon.ico');
57 | }
58 |
59 | Reference_static.window = new BrowserWindow (windowParameters);
60 | Reference_static.window.uuid = Reference_static.uuid;
61 |
62 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
63 | Reference_static.window.loadURL (`http://127.0.0.1:${Reference_static.port}/#/reference`);
64 | } else {
65 | Reference_static.window.loadURL (`file://${path.join (__dirname, '../../build/index.html')}#/reference`);
66 | }
67 |
68 | Reference_static.window.once ('ready-to-show', () => {
69 | Reference_static.window.setMenu (null);
70 |
71 | if (windowParameters !== null && typeof (windowParameters.maximized) !== 'undefined' && windowParameters.maximized) {
72 | Reference_static.window.setBounds ({width: Reference_static.default.width, height: Reference_static.default.height});
73 | Reference_static.window.center ();
74 | Reference_static.window.maximize ();
75 | }
76 |
77 | Reference_static.window.show ();
78 |
79 | if (typeof (process.env.FILECTOR_DEV) !== 'undefined' && process.env.FILECTOR_DEV === 'true') {
80 | Reference_static.window.webContents.openDevTools ();
81 | }
82 |
83 | Reference.ShouldShowReset (Reference_static.window);
84 | });
85 |
86 | Reference_static.window.on ('closed', () => {
87 | Reference_static.window = null;
88 | });
89 |
90 | Reference_static.window.on ('maximize', () => {
91 | Reference_static.main.SaveWindow (Reference_static.IDENTIFIER, 'maximized', true);
92 | });
93 | Reference_static.window.on ('unmaximize', () => {
94 | Reference_static.main.SaveWindow (Reference_static.IDENTIFIER, 'maximized', false);
95 | });
96 |
97 | Reference_static.window.on ('resize', () => {
98 | let size = Reference_static.window.getSize ();
99 |
100 | Reference_static.main.SaveWindow (Reference_static.IDENTIFIER, 'size', {
101 | width: size [0],
102 | height: size [1]
103 | });
104 |
105 | Reference.ShouldShowReset (Reference_static.window);
106 | });
107 | }
108 |
109 | /**
110 | * Open reference, create if not exists.
111 | */
112 | static Open (main) {
113 | Reference_static.main = main;
114 |
115 | if (Reference_static.window === null) {
116 | Reference.CreateWindow ();
117 | }
118 |
119 | Reference_static.window.focus ();
120 | }
121 |
122 | /**
123 | * Check if window should show reset and notify.
124 | */
125 | static ShouldShowReset (window) {
126 | const size = window.getSize ();
127 |
128 | if (Math.abs (size [0] - Reference_static.default.width) > 4 || Math.abs (size [1] - Reference_static.default.height) > 4) {
129 | window.send ('reset-show', {window: Reference_static.IDENTIFIER});
130 | } else {
131 | window.send ('reset-hide');
132 | }
133 | }
134 | }
135 |
136 | module.exports = {
137 | Reference,
138 | Reference_static
139 | };
140 |
--------------------------------------------------------------------------------
/src/main/api/Console.js:
--------------------------------------------------------------------------------
1 | const { ipcMain } = require ('electron');
2 | const vm = require ('vm');
3 | const path = require ('path');
4 | const {promisify} = require ('util');
5 | const fs = require ('fs');
6 | const readline = require ('readline');
7 | const extend = require ('extend');
8 | const ConsoleWindow = require ('../Console').Console;
9 | const ReferenceWindow = require ('../Reference').Reference;
10 | const RxSnippet = require ('../model/RxSnippet');
11 | const tinify = require ('tinify');
12 | const axios = require ('axios');
13 | const sanitizeHtml = require ('sanitize-html');
14 |
15 | const Console_static = {
16 | main: undefined,
17 | lastPayload: undefined
18 | };
19 |
20 | class Console {
21 | /**
22 | * Console Api initialization.
23 | */
24 | static Init (main) {
25 | Console_static.main = main;
26 |
27 | ipcMain.on ('console-show', Console.OpenConsole);
28 |
29 | ipcMain.on ('payload-last', Console.LastPayload);
30 |
31 | ipcMain.on ('script-execute', Console.ExecuteScript);
32 |
33 | ipcMain.on ('snippet-load', Console.LoadSnippet);
34 |
35 | ipcMain.on ('script-save', Console.SaveScriptSnippet);
36 |
37 | ipcMain.on ('snippet-delete', Console.DeleteSnippet);
38 |
39 | ipcMain.on ('snippet-list-name', Console.SnippetsByName);
40 |
41 | ipcMain.on ('console-reference', Console.OpenConsoleReference);
42 | }
43 |
44 | /**
45 | * Open console window and pass parameters.
46 | */
47 | static OpenConsole (event, message) {
48 | Console_static.lastPayload = message;
49 |
50 | ConsoleWindow.Open (Console_static.main, Console.LastPayload);
51 | }
52 |
53 | /**
54 | * Get last payload from Main.
55 | */
56 | static LastPayload (event, window = null) {
57 | let sender = window !== null ? window : event.sender;
58 |
59 | sender.send ('payload-last', Console_static.lastPayload);
60 | }
61 |
62 | /**
63 | * Execute script inside VM with custom sandbox.
64 | */
65 | static async ExecuteScript (event, message) {
66 | let settings = Console_static.main.config.Get ('app-settings');
67 | tinify.key = settings !== null ? settings.console.tinypngApiKey : null;
68 | const proMode = settings !== null ? (settings.console.pro || false) : false;
69 |
70 | let sandbox = {
71 | fs: {
72 | createReadStream: fs.createReadStream,
73 | createWriteStream: fs.createWriteStream
74 | },
75 | log: '',
76 | _log: [],
77 | path: {
78 | extname: path.extname,
79 | join: path.join
80 | },
81 | readDirPromise: promisify (fs.readdir),
82 | statPromise: promisify (fs.stat),
83 | readline: {
84 | createInterface: readline.createInterface
85 | },
86 | renameFilePromise: promisify (fs.rename),
87 | readFilePromise: promisify (fs.readFile),
88 | writeFileAtomic: require ('write-file-atomic'),
89 | tinify: tinify,
90 | axios: {
91 | get: axios.get,
92 | post: axios.post
93 | },
94 | sanitizeHtml: sanitizeHtml,
95 | setTimeout: setTimeout,
96 | setInterval: setInterval
97 | };
98 |
99 | if (proMode) {
100 | sandbox.require = require;
101 | }
102 |
103 | sandbox = extend (sandbox, message.parameters);
104 |
105 | vm.createContext (sandbox);
106 |
107 | let error = undefined;
108 | try {
109 | const functions = require ('./functions');
110 | let functionsScript = '/*** VM API FUNCTIONS ***/\n\n';
111 | for (let index in functions) {
112 | if (functions.hasOwnProperty (index)) {
113 | functionsScript += `${functions [index].toString ()}\n\n`;
114 | }
115 | }
116 | functionsScript += 'Init ();\n\n';
117 | functionsScript += '/*** VM API FUNCTIONS END ***/\n\n';
118 |
119 | await vm.runInContext (`${functionsScript}(async function () {
120 | ${message.script}
121 | })();`, sandbox);
122 | } catch (e) {
123 | console.error (e);
124 | error = `${e.name}: ${e.message}`;
125 | }
126 |
127 | const response = {
128 | /*log: sandbox.log,*/
129 | log: sandbox._log.join ('\n'),
130 | error: error,
131 | result: sanitizeHtml (sandbox.result, {allowedTags: []})
132 | };
133 |
134 | event.sender.send ('script-execute', response);
135 | }
136 |
137 | /**
138 | * Load Snippet for id.
139 | */
140 | static async LoadSnippet (event, message) {
141 | const snippet = await new RxSnippet ().Load (message.id);
142 |
143 | if (snippet.id) {
144 | event.sender.send ('snippet-load', snippet.data);
145 | } else {
146 | event.sender.send ('snippet-load', {
147 | error: true,
148 | message: 'Snippet does not exist.'
149 | });
150 | }
151 | }
152 |
153 | /**
154 | * Save current script as Snippet.
155 | */
156 | static async SaveScriptSnippet (event, message) {
157 | const snippet = new RxSnippet ().LoadFromData ({
158 | id: message.id,
159 | name: message.name,
160 | description: message.description,
161 | script: message.script
162 | });
163 |
164 | await snippet.Save ();
165 |
166 | if (Console_static.main.window !== null) {
167 | Console_static.main.window.send ('snippet-saved');
168 | }
169 | }
170 |
171 | /**
172 | * Delete Snippet by id.
173 | */
174 | static async DeleteSnippet (event, message) {
175 | const snippet = await new RxSnippet ().Load (message.id);
176 |
177 | if (snippet.id) {
178 | const deleted = await snippet.Delete ();
179 |
180 | event.sender.send ('snippet-delete', {
181 | deleted: deleted
182 | });
183 | } else {
184 | event.sender.send ('snippet-delete', {
185 | error: true,
186 | message: 'Snippet does not exist.'
187 | });
188 | }
189 | }
190 |
191 | /**
192 | * Load list of Snippets by name.
193 | */
194 | static async SnippetsByName (event, message) {
195 | const parameters = {
196 | where: {
197 | name: {
198 | comparison: 'regex',
199 | value: new RegExp (`${message.name}`, 'i')
200 | }
201 | },
202 | limit: message.limit,
203 | sort: 'name',
204 | sortBy: 1
205 | };
206 |
207 | const list = await new RxSnippet ().List (parameters);
208 | event.sender.send ('snippet-list-name', {
209 | list: list
210 | });
211 | }
212 |
213 | /**
214 | * Open console reference window.
215 | */
216 | static OpenConsoleReference () {
217 | ReferenceWindow.Open (Console_static.main);
218 | }
219 | }
220 |
221 | module.exports = Console;
222 |
--------------------------------------------------------------------------------
/src/main/api/Grid.js:
--------------------------------------------------------------------------------
1 | const { ipcMain } = require ('electron');
2 |
3 | class Grid {
4 | /**
5 | * Grid API initialization.
6 | */
7 | static Init () {
8 | ipcMain.on ('grid-update', this.UpdateData);
9 | }
10 |
11 | /**
12 | * Update grid data based on received parameters.
13 | */
14 | static async UpdateData (event, message) {
15 | let model = require (`../model/${message.modelName}`);
16 | model = new model ();
17 |
18 | for (const filter in message.parameters.where) {
19 | if (message.parameters.where.hasOwnProperty (filter)) {
20 | message.parameters.where [filter].value = new RegExp (`${message.parameters.where [filter].value}`, 'i');
21 | }
22 | }
23 |
24 | const response = {};
25 | response.count = await model.Count (message.parameters);
26 | response.pages = Math.ceil (response.count / message.parameters.limit);
27 |
28 | response.items = await model.List (message.parameters);
29 |
30 | event.sender.send ('grid-update', response);
31 | }
32 | }
33 |
34 | module.exports = Grid;
35 |
--------------------------------------------------------------------------------
/src/main/api/functions.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | function Init () {
3 | console = {
4 | log: function (string) {
5 | if (arguments.length > 0) {
6 | let output = [];
7 | for (let i = 0; i < arguments.length; i++) {
8 | output.push (sanitizeHtml (arguments [i].toString (), {
9 | allowedTags: [],
10 | }));
11 | }
12 |
13 | _log.push (output.join (', '));
14 | }
15 | }
16 | };
17 | }
18 |
19 | async function ReadDirectory (directory, filter = null) {
20 | let files = await readDirPromise (directory);
21 |
22 | if (filter !== null) {
23 | files = files.filter (file => {
24 | return filter.test (file);
25 | });
26 | }
27 |
28 | return files;
29 | }
30 |
31 | async function ReadDirectoryRecursive (directory, params = {}) {
32 | let files = [];
33 |
34 | for (let file of await readDirPromise (directory)) {
35 | const filePath = path.join (directory, file);
36 | let stat = await statPromise (filePath);
37 |
38 | if (stat.isDirectory ()) {
39 | files.push (...await ReadDirectoryRecursive (filePath, params));
40 | } else {
41 | if (params.fullFilePath) {
42 | files.push (filePath);
43 | } else {
44 | files.push (file);
45 | }
46 | }
47 | }
48 |
49 | if (params.filterFile) {
50 | files = files.filter (file => {
51 | return params.filterFile.test (file);
52 | });
53 | }
54 |
55 | if (params.fullFilePath && params.filterFileContents) {
56 | const filtered = [];
57 |
58 | for (let file of files) {
59 | const contents = await ReadFileContents (file);
60 |
61 | if (params.filterFileContents.test (contents)) {
62 | filtered.push (file);
63 | }
64 | }
65 |
66 | files = filtered;
67 | }
68 |
69 | return files;
70 | }
71 |
72 | async function RenameFiles (directory, files, newName) {
73 | for (let i = 0; i < files.length; i++) {
74 | let extension = files [i].split ('.');
75 | extension = extension.length > 1 ? `.${extension.pop ()}` : '';
76 |
77 | let fileNewName = `${newName}${i > 0 ? i : ''}${extension}`;
78 |
79 | await renameFilePromise (path.join (directory, files [i]), path.join (directory, fileNewName));
80 | }
81 |
82 | return files.length;
83 | }
84 |
85 | async function RenameFilesPart (directory, files, removePart, newPart) {
86 | for (let i = 0; i < files.length; i++) {
87 | let extension = files [i].split ('.');
88 | extension = extension.length > 1 ? `.${extension.pop ()}` : '';
89 |
90 | let newName = files [i].replace (extension, '');
91 | let fileNewName = `${newName.replace (new RegExp (removePart, 'i'), newPart)}${extension}`;
92 |
93 | await renameFilePromise (path.join (directory, files [i]), path.join (directory, fileNewName));
94 | }
95 |
96 | return files.length;
97 | }
98 |
99 | async function ReadFileContents (filePath) {
100 | return await readFilePromise (filePath, 'utf8');
101 | }
102 |
103 | async function WriteFileContents (filePath, contents) {
104 | await writeFileAtomic (filePath, contents);
105 | }
106 |
107 | async function TinyPNGCompressFile (file) {
108 | let extension = file.split ('.');
109 | extension = extension.length > 1 ? `.${extension.pop ()}` : '';
110 |
111 | let newName = file.replace (extension, `.compressed${extension}`);
112 |
113 | await tinify.fromFile (file).preserve ('copyright', 'creation').toFile (newName);
114 | }
115 |
116 | async function TinyPNGResizeCropFile (file, params) {
117 | let extension = file.split ('.');
118 | extension = extension.length > 1 ? `.${extension.pop ()}` : '';
119 |
120 | let newName = file.replace (extension, `.processed${extension}`);
121 |
122 | await tinify.fromFile (file).preserve ('copyright', 'creation').resize (params).toFile (newName);
123 | }
124 |
125 | async function Fetch (url, type = 'GET', params = null, skipSanitize = false) {
126 | let response = null;
127 |
128 | switch (type) {
129 | case 'GET':
130 | response = await axios.get (url);
131 | break;
132 | case 'POST':
133 | response = await axios.post (url, params || {});
134 | break;
135 | }
136 |
137 | if (response) {
138 | response = !skipSanitize ? sanitizeHtml (response.data) : response.data;
139 | }
140 |
141 | return response;
142 | }
143 |
144 | async function Sleep (ms) {
145 | return new Promise (resolve => setTimeout (resolve, ms));
146 | }
147 |
148 | module.exports = {
149 | Init,
150 | ReadDirectory,
151 | ReadDirectoryRecursive,
152 | RenameFiles,
153 | RenameFilesPart,
154 | ReadFileContents,
155 | WriteFileContents,
156 | TinyPNGCompressFile,
157 | TinyPNGResizeCropFile,
158 | Fetch,
159 | Sleep
160 | };
161 |
--------------------------------------------------------------------------------
/src/main/install/snippet.js:
--------------------------------------------------------------------------------
1 | const Snippet = require ('../model/Snippet');
2 | const RxSnippet = require ('../model/RxSnippet');
3 |
4 | module.exports = async function (config) {
5 | let version = config.Get ('install-snippet-version');
6 | if (version === null) {
7 | version = 0;
8 | }
9 | version = parseInt (version);
10 |
11 | if (version < 3) {
12 | version = 4;
13 | }
14 |
15 | for (let index in updates) {
16 | if (updates.hasOwnProperty (index)) {
17 | if (index > version) {
18 | if (await updates [index] (config)) {
19 | config.Set ('install-snippet-version', index);
20 | } else {
21 | version = config.Get ('install-snippet-version');
22 | version = parseInt (version);
23 | }
24 | }
25 | }
26 | }
27 | };
28 |
29 | const updates = {
30 | 4: async function (config) {
31 | const existing = await new Snippet ().Collection ();
32 |
33 | for (const snippet of existing) {
34 | snippet.id = undefined;
35 |
36 | await new RxSnippet ().LoadFromData (snippet).Save ();
37 | }
38 |
39 | config.Set ('install-snippet-version', 5);
40 | return false;
41 | },
42 | 5: async function (config) {
43 | const simpleExampleSnippet = {
44 | name: 'Simple Example',
45 | description: 'This is just a basic script example that does almost nothing, but demonstrate that execution works.',
46 | script: `result = 'this will be script result output';
47 |
48 | let variable = 2 * 3;
49 | console.log ('2 * 3 equals ' + variable);`
50 | };
51 |
52 | await new RxSnippet ().LoadFromData (simpleExampleSnippet).Save ();
53 |
54 | const renameExampleSnippet = {
55 | name: 'Rename Files',
56 | description: 'Rename files to a new name and append with number if there are more than one.',
57 | script: `const newName = NEW_NAME;
58 | const files = await ReadDirectory (directory);
59 | result = '# files renamed ' + await RenameFiles (directory, files, newName);`
60 | };
61 |
62 | await new RxSnippet ().LoadFromData (renameExampleSnippet).Save ();
63 |
64 | const renameHostSql = {
65 | name: 'Rename Host Sql',
66 | description: '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.',
67 | script: `const currentHost = CURRENT_HOSTNAME;
68 | const newHost = NEW_HOSTNAME;
69 |
70 | if (files.length !== 1) {
71 | throw Error ('Make sure to open the console by selecting the Sql file you want to edit');
72 | }
73 |
74 | const fileExtension = path.extname (files [0]);
75 | if (fileExtension !== '.sql') {
76 | throw Error ('Make sure the selected file is Sql file you want to edit');
77 | }
78 |
79 | const source = path.join (directory, files [0]);
80 | const destination = path.join (directory, files [0].replace (fileExtension, '.edited.sql'));
81 |
82 | const sourceStream = fs.createReadStream (source);
83 | const destinationStream = fs.createWriteStream (destination, {flags: 'a'});
84 | const readInterface = readline.createInterface ({
85 | input: sourceStream,
86 | crlfDelay: Infinity
87 | });
88 |
89 | async function ProcessLineByLine () {
90 | return new Promise ((resolve, reject) => {
91 | readInterface.on ('line', line => {
92 | try {
93 | const newLine = line.replace (new RegExp (currentHost, 'g'), newHost);
94 |
95 | destinationStream.write (newLine + '\\n');
96 | } catch (error) {
97 | reject (error);
98 | }
99 | });
100 |
101 | readInterface.on ('close', () => {
102 | resolve ();
103 | });
104 | });
105 | }
106 |
107 | await ProcessLineByLine ();
108 |
109 | result = 'Saved new file to: ' + destination;`
110 | };
111 |
112 | await new RxSnippet ().LoadFromData (renameHostSql).Save ();
113 |
114 | const renameFilesPart = {
115 | name: 'Rename Files (part of name)',
116 | description: 'Rename files to a new name by changing part of name with provided new part.',
117 | script: `const removePart = REMOVE_PART;
118 | const newPart = NEW_PART;
119 | const files = await ReadDirectory (directory);
120 | result = '# files renamed ' + await RenameFilesPart (directory, files, removePart, newPart);`
121 | };
122 |
123 | await new RxSnippet ().LoadFromData (renameFilesPart).Save ();
124 |
125 | const compressImages = {
126 | name: 'Compress Images (TinyPNG)',
127 | description: 'Compress PNG & JPG images using TinyPNG API. You have to have TinyPNG API key set in settings.',
128 | script: `const files = await ReadDirectory (directory, /(.png|.jpg)$/);
129 |
130 | if (files.length < 1) {
131 | throw Error ('The directory does not contain any images');
132 | }
133 |
134 | for (let i = 0; i < files.length; i++) {
135 | await TinyPNGCompressFile (path.join (directory, files [i]));
136 | }
137 |
138 | result = '# compressed images ' + files.length;`
139 | };
140 |
141 | await new RxSnippet ().LoadFromData (compressImages).Save ();
142 |
143 | const resizeCropImages = {
144 | name: 'Resize/Crop Images (TinyPNG)',
145 | description: 'Resize or crop PNG & JPG images using TinyPNG API. You have to have TinyPNG API key set in settings.',
146 | script: `const method = 'fit'; //"fit" => resize, "cover" => intelligently crop
147 | const width = WIDTH;
148 | const height = HEIGHT;
149 |
150 | const files = await ReadDirectory (directory, /(.png|.jpg)$/);
151 |
152 | if (files.length < 1) {
153 | throw Error ('The directory does not contain any images');
154 | }
155 |
156 | for (let i = 0; i < files.length; i++) {
157 | await TinyPNGResizeCropFile (path.join (directory, files [i]), {method: method, width: width, height: height});
158 | }
159 |
160 | result = '# resized/cropped images ' + files.length;`
161 | };
162 |
163 | await new RxSnippet ().LoadFromData (resizeCropImages).Save ();
164 |
165 | return true;
166 | },
167 | 6: async function (config) {
168 | const filterFilesWriteResultSnippet = {
169 | name: 'Filter Files & Write Result',
170 | description: 'Filter files in directory by name and contests, write result to file.',
171 | script: `const filterFileName = REGEXP_FILTER_FILENAME;
172 | const filterFileContents = REGEXP_FILTER_FILE_CONTENTS;
173 |
174 | const files = await ReadDirectoryRecursive (directory, {fullFilePath: true, filterFile: filterFileName, filterFileContents: filterFileContents});
175 |
176 | await WriteFileContents (path.join (directory, 'result.txt'), files.join ('\\n'));
177 |
178 | result = 'Result contains ' + files.length + ' files';`
179 | };
180 |
181 | await new RxSnippet ().LoadFromData (filterFilesWriteResultSnippet).Save ();
182 |
183 | return true;
184 | }
185 | };
186 |
--------------------------------------------------------------------------------
/src/main/install/snippet_backup.js:
--------------------------------------------------------------------------------
1 | const Snippet = require ('../model/Snippet');
2 |
3 | module.exports = async function (config) {
4 | const installed = config.Get ('install-snippet');
5 |
6 | if (installed === null || !installed) {
7 | const simpleExampleSnippet = {
8 | name: 'Simple Example',
9 | description: 'This is just a basic script example that does almost nothing, but demonstrate that execution works.',
10 | script: `result = 'this will be script result output';
11 |
12 | let variable = 2 * 3;
13 | console.log ('2 * 3 equals ' + variable);`
14 | };
15 |
16 | await new Snippet ().LoadFromData (simpleExampleSnippet).Save ();
17 |
18 | const renameExampleSnippet = {
19 | name: 'Rename Files',
20 | description: 'Rename files to a new name and append with number if there are more than one.',
21 | script: `const newName = NEW_NAME;
22 | const files = await ReadDirectory (directory);
23 | result = '# files renamed ' + await RenameFiles (directory, files, newName);`
24 | };
25 |
26 | await new Snippet ().LoadFromData (renameExampleSnippet).Save ();
27 |
28 | config.Set ('install-snippet', true);
29 | }
30 |
31 | let version = config.Get ('install-snippet-version');
32 | if (version === null) {
33 | version = 0;
34 | }
35 | version = parseInt (version);
36 |
37 | for (let index in updates) {
38 | if (updates.hasOwnProperty (index)) {
39 | if (index > version) {
40 | await updates [index] ();
41 |
42 | config.Set ('install-snippet-version', index);
43 | }
44 | }
45 | }
46 | };
47 |
48 | const updates = {
49 | 1: async function () {
50 | const renameHostSql = {
51 | name: 'Rename Host Sql',
52 | description: '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.',
53 | script: `const currentHost = CURRENT_HOSTNAME;
54 | const newHost = NEW_HOSTNAME;
55 |
56 | if (files.length !== 1) {
57 | throw Error ('Make sure to open the console by selecting the Sql file you want to edit');
58 | }
59 |
60 | const fileExtension = path.extname (files [0]);
61 | if (fileExtension !== '.sql') {
62 | throw Error ('Make sure the selected file is Sql file you want to edit');
63 | }
64 |
65 | const source = path.join (directory, files [0]);
66 | const destination = path.join (directory, files [0].replace (fileExtension, '.edited.sql'));
67 |
68 | const sourceStream = fs.createReadStream (source);
69 | const destinationStream = fs.createWriteStream (destination, {flags: 'a'});
70 | const readInterface = readline.createInterface ({
71 | input: sourceStream,
72 | crlfDelay: Infinity
73 | });
74 |
75 | async function ProcessLineByLine () {
76 | return new Promise ((resolve, reject) => {
77 | readInterface.on ('line', line => {
78 | try {
79 | const newLine = line.replace (new RegExp (currentHost, 'g'), newHost);
80 |
81 | destinationStream.write (newLine + '\\n');
82 | } catch (error) {
83 | reject (error);
84 | }
85 | });
86 |
87 | readInterface.on ('close', () => {
88 | resolve ();
89 | });
90 | });
91 | }
92 |
93 | await ProcessLineByLine ();
94 |
95 | result = 'Saved new file to: ' + destination;`
96 | };
97 |
98 | await new Snippet ().LoadFromData (renameHostSql).Save ();
99 | },
100 | 2: async function () {
101 | const renameFilesPart = {
102 | name: 'Rename Files (part of name)',
103 | description: 'Rename files to a new name by changing part of name with provided new part.',
104 | script: `const removePart = REMOVE_PART;
105 | const newPart = NEW_PART;
106 | const files = await ReadDirectory (directory);
107 | result = '# files renamed ' + await RenameFilesPart (directory, files, removePart, newPart);`
108 | };
109 |
110 | await new Snippet ().LoadFromData (renameFilesPart).Save ();
111 | },
112 | 3: async function () {
113 | const compressImages = {
114 | name: 'Compress Images (TinyPNG)',
115 | description: 'Compress PNG & JPG images using TinyPNG API. You have to have TinyPNG API key set in settings.',
116 | script: `const files = await ReadDirectory (directory, /(.png|.jpg)$/);
117 |
118 | if (files.length < 1) {
119 | throw Error ('The directory does not contain any images');
120 | }
121 |
122 | for (let i = 0; i < files.length; i++) {
123 | await TinyPNGCompressFile (path.join (directory, files [i]));
124 | }
125 |
126 | result = '# compressed images ' + files.length;`
127 | };
128 |
129 | await new Snippet ().LoadFromData (compressImages).Save ();
130 |
131 | const resizeCropImages = {
132 | name: 'Resize/Crop Images (TinyPNG)',
133 | description: 'Resize or crop PNG & JPG images using TinyPNG API. You have to have TinyPNG API key set in settings.',
134 | script: `const method = 'fit'; //"fit" => resize, "cover" => intelligently crop
135 | const width = WIDTH;
136 | const height = HEIGHT;
137 |
138 | const files = await ReadDirectory (directory, /(.png|.jpg)$/);
139 |
140 | if (files.length < 1) {
141 | throw Error ('The directory does not contain any images');
142 | }
143 |
144 | for (let i = 0; i < files.length; i++) {
145 | await TinyPNGResizeCropFile (path.join (directory, files [i]), {method: method, width: width, height: height});
146 | }
147 |
148 | result = '# resized/cropped images ' + files.length;`
149 | };
150 |
151 | await new Snippet ().LoadFromData (resizeCropImages).Save ();
152 | }
153 | };
154 |
--------------------------------------------------------------------------------
/src/main/model/Base.js:
--------------------------------------------------------------------------------
1 | const RxDB = require ('rxdb');
2 | RxDB.addRxPlugin (require ('pouchdb-adapter-node-websql'));
3 | const uuid = require ('uuid');
4 | const extend = require ('extend');
5 |
6 | const Base_static = {
7 | rxDB: undefined,
8 | collections: {}
9 | };
10 |
11 | class Base {
12 | /**
13 | * Base initialization.
14 | */
15 | constructor (config) {
16 | this.config = config;
17 |
18 | this.collectionName = '';
19 | this.collectionSchema = {};
20 |
21 | this.data = {};
22 | this.id = undefined;
23 | }
24 |
25 | /**
26 | * Reset data.
27 | */
28 | Reset () {
29 | this.data = [];
30 | this.id = undefined;
31 | }
32 |
33 | /**
34 | * Initialize RxDB collection if not yet for collectionName.
35 | */
36 | async InitCollection () {
37 | if (typeof Base_static.rxDB === 'undefined') {
38 | Base_static.rxDB = await RxDB.createRxDatabase ({
39 | name: this.config.rxDB.name,
40 | adapter: 'websql'
41 | });
42 | }
43 |
44 | if (typeof Base_static.collections [this.collectionName] === 'undefined') {
45 | Base_static.collections [this.collectionName] = await Base_static.rxDB.collection ({
46 | name: this.collectionName,
47 | schema: this.collectionSchema
48 | });
49 | }
50 |
51 | return Base_static.collections [this.collectionName];
52 | }
53 |
54 | /**
55 | * Load data by id from DB.
56 | */
57 | async Load (id) {
58 | this.Reset ();
59 |
60 | const collection = await this.InitCollection ();
61 |
62 | const record = await collection.findOne (id).exec ();
63 |
64 | if (record) {
65 | this.id = id;
66 | this.data = record.toJSON ();
67 | }
68 |
69 | return this;
70 | }
71 |
72 | /**
73 | * Load data from object.
74 | */
75 | LoadFromData (data) {
76 | this.data = data;
77 | this.id = data.id;
78 |
79 | return this;
80 | }
81 |
82 | /**
83 | * Save data to DB, insert/update is used depending on ID.
84 | */
85 | async Save () {
86 | const collection = await this.InitCollection ();
87 |
88 | if (typeof this.id === 'undefined') {
89 | this.id = uuid.v4 ();
90 | this.data.id = this.id;
91 |
92 | this.data.created = Base.NowTimestamp ();
93 | } else {
94 | const old = await collection.findOne (this.id).exec ();
95 |
96 | if (old) {
97 | this.data = extend (old.toJSON (), this.data);
98 | }
99 | }
100 |
101 | this.data.updated = Base.NowTimestamp ();
102 |
103 | await collection.upsert (this.data);
104 |
105 | return this;
106 | }
107 |
108 | /**
109 | * Delete data from DB.
110 | */
111 | async Delete () {
112 | const collection = await this.InitCollection ();
113 |
114 | if (typeof this.id !== 'undefined') {
115 | const record = await collection.findOne (this.id).exec ();
116 |
117 | if (record) {
118 | await record.remove ();
119 |
120 | this.Reset ();
121 |
122 | return true;
123 | }
124 | }
125 |
126 | return false;
127 | }
128 |
129 | /**
130 | * Process where selection parameters on query.
131 | */
132 | ProcessParametersWhere (query, parameters = {}) {
133 | if (typeof parameters.where === 'object') {
134 | const fields = Object.keys (parameters.where);
135 |
136 | for (let field of fields) {
137 | const comparison = parameters.where [field].comparison;
138 |
139 | switch (comparison) {
140 | case 'eq':
141 | query = query.where (field).eq (parameters.where [field].value);
142 | break;
143 | case 'and':
144 | query = query.where (field).and (parameters.where [field].value);
145 | break;
146 | case 'or':
147 | query = query.where (field).or (parameters.where [field].value);
148 | break;
149 | case 'in':
150 | query = query.where (field).in (parameters.where [field].value);
151 | break;
152 | case 'regex':
153 | query = query.where (field).regex (parameters.where [field].value);
154 | break;
155 | default:
156 | throw new Error ('Unsupported where condition comparison type');
157 | }
158 | }
159 | }
160 |
161 | return query;
162 | }
163 |
164 | /**
165 | * Get count of records in DB.
166 | */
167 | async Count (parameters = {}) {
168 | const collection = await this.InitCollection ();
169 |
170 | let list = collection.find ();
171 |
172 | list = this.ProcessParametersWhere (list, parameters);
173 |
174 | list = await list.exec ();
175 |
176 | return list.length;
177 | }
178 |
179 | /**
180 | * Get all records from DB.
181 | */
182 | async List (parameters = {}, asObject = undefined) {
183 | const collection = await this.InitCollection ();
184 |
185 | let list = collection.find ();
186 |
187 | list = this.ProcessParametersWhere (list, parameters);
188 |
189 | if (parameters.limit) {
190 | list = list.limit (parameters.limit);
191 |
192 | if (parameters.page) {
193 | list = list.skip (parameters.limit * parameters.page);
194 | }
195 | }
196 |
197 | if (parameters.sort) {
198 | const sortBy = parameters.sortBy || 1;
199 | const sort = {};
200 | sort [parameters.sort] = sortBy;
201 |
202 | list = list.sort (sort);
203 | }
204 |
205 | list = await list.exec ();
206 |
207 | list = list.map (element => {
208 | if (asObject) {
209 | return new asObject ().LoadFromData (element.toJSON ());
210 | } else {
211 | return element.toJSON ();
212 | }
213 | });
214 |
215 | return list;
216 | }
217 |
218 | /**
219 | * Return current time as timestamp.
220 | */
221 | static NowTimestamp () {
222 | return Math.round (new Date ().getTime () / 1000);
223 | }
224 | }
225 |
226 | module.exports = Base;
227 |
--------------------------------------------------------------------------------
/src/main/model/RxSnippet.js:
--------------------------------------------------------------------------------
1 | const Base = require ('./Base');
2 | const config = require ('../../../config');
3 |
4 | class RxSnippet extends Base {
5 | /**
6 | * RxSnippet initialization.
7 | */
8 | constructor () {
9 | super (config);
10 |
11 | this.collectionName = 'snippet';
12 | this.collectionSchema = {
13 | title: 'Snippet schema',
14 | version: 0,
15 | type: 'object',
16 | properties: {
17 | id: {
18 | type: 'string',
19 | primary: true
20 | },
21 | name: {
22 | type: 'string'
23 | },
24 | description: {
25 | type: 'string'
26 | },
27 | script: {
28 | type: 'string'
29 | },
30 | created: {
31 | type: 'number',
32 | final: true
33 | },
34 | updated: {
35 | type: 'number'
36 | }
37 | },
38 | indexes: [
39 | 'name',
40 | 'created'
41 | ]
42 | };
43 | }
44 | }
45 |
46 | module.exports = RxSnippet;
47 |
--------------------------------------------------------------------------------
/src/main/model/Snippet.js:
--------------------------------------------------------------------------------
1 | const Base = require ('tch-database/model/Base');
2 | const config = require ('../../../config');
3 |
4 | class Snippet extends Base {
5 | /**
6 | * Snippet initialization.
7 | */
8 | constructor () {
9 | super (config);
10 |
11 | this.table = 'snippet';
12 | }
13 |
14 | /**
15 | * Data defaults.
16 | */
17 | Defaults () {
18 | return {
19 | name: '',
20 | description: '',
21 | script: '',
22 | created: 0,
23 | updated: 0
24 | };
25 | }
26 | }
27 |
28 | module.exports = Snippet;
29 |
--------------------------------------------------------------------------------
/src/view/About.js:
--------------------------------------------------------------------------------
1 | import './about.css';
2 | import logo from '../image/tomas-chylyV2-sign.png';
3 | import {ReactComponent as Github} from '../icon/github.svg';
4 | import {ReactComponent as Npm} from '../icon/npm.svg';
5 | import {ReactComponent as StackOverflow} from '../icon/stack-overflow.svg';
6 | import {ReactComponent as Twitter} from '../icon/twitter.svg';
7 | import {ReactComponent as LinkedIn} from "../icon/linkedin.svg";
8 |
9 | import React, { Component } from 'react';
10 | import Button from '../component/Button';
11 | import Link from '../component/Link';
12 | import Popup from '../component/Popup';
13 | import Form from '../component/Form';
14 |
15 | const {ipcRenderer} = window.require ('electron');
16 |
17 | class About extends Component {
18 | /**
19 | * About initialization.
20 | */
21 | constructor (props) {
22 | super (props);
23 |
24 | this.contactForm = undefined;
25 |
26 | this.state = {
27 | contact: false,
28 | contactSending: false
29 | };
30 | }
31 |
32 | /**
33 | * First rendered to DOM.
34 | */
35 | componentDidMount () {
36 | window.TCH.Main.SetTitle ('About');
37 |
38 | this.onMessageResultListener = this.SendMessageResult.bind (this);
39 | ipcRenderer.on ('contact-message-send', this.onMessageResultListener);
40 | }
41 |
42 | /**
43 | * Called before component is removed from DOM.
44 | */
45 | componentWillUnmount () {
46 | this.contactForm = undefined;
47 |
48 | ipcRenderer.removeListener ('contact-message-send', this.onMessageResultListener);
49 | delete this.onMessageResultListener;
50 | }
51 |
52 | /**
53 | * Render the component into html.
54 | */
55 | render () {
56 | const {name, version} = window.TCH.mainParameters;
57 | const {contactSending} = this.state;
58 |
59 | this.contactForm = React.createRef ();
60 |
61 | return
62 |
63 |
64 |
65 |
66 |
{name}: {version}
67 |
68 |
This app is brought to you by Tomáš Chylý, Full Stack Web/Mobile/Desktop Developer. I started this app to get better with Electron and to learn React. I hope you like it!
69 |
If you have any issues with the app, UI or have ideas for improvement, please use the contact form below. Thank you!
70 |
71 |
72 | Website
73 | App Repository
74 | Contact
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Icons used in this app are from the great Font Awesome. The free version is used.
87 |
88 |
89 |
90 |
91 |
122 | } onClose={this.ContactClosed.bind (this)} acceptVisible={true} accept="Submit" onAccept={this.ContactAccepted.bind (this)} loading={contactSending}/>
123 |
;
124 | }
125 |
126 | /**
127 | * Open website in default browser.
128 | */
129 | Website () {
130 | ipcRenderer.send ('url-open', {url: 'https://tomas-chyly.com/en/project/filector/'});
131 | }
132 |
133 | /**
134 | * Open app code repository in default browser.
135 | */
136 | Repository () {
137 | ipcRenderer.send ('url-open', {url: 'https://github.com/tomaschyly/FileCtor'});
138 | }
139 |
140 | /**
141 | * Open contact form popup.
142 | */
143 | ContactOpen () {
144 | this.setState ({contact: true});
145 | }
146 |
147 | /**
148 | * Close contact form popup.
149 | */
150 | ContactClosed () {
151 | this.setState ({contact: false});
152 | }
153 |
154 | /**
155 | * Submit contact form, then send message to API.
156 | */
157 | ContactAccepted () {
158 | this.contactForm.current.Submit ();
159 | }
160 |
161 | /**
162 | * Send message to API and close contact form popup.
163 | */
164 | SendMessage (values) {
165 | this.setState ({contactSending: true});
166 |
167 | ipcRenderer.send ('contact-message-send', values);
168 | }
169 |
170 | /**
171 | * Send message result is error or success, show result and close popup.
172 | */
173 | SendMessageResult (event, message) {
174 | this.setState ({contactSending: false});
175 |
176 | if (typeof (message.success) !== 'undefined') {
177 | window.TCH.Main.Alert ('Message sent successfully, thank you!', 'Message Sent');
178 |
179 | this.setState ({contact: false});
180 | } else {
181 | window.TCH.Main.Alert ('I am sorry, but I have failed to send the message. Are you connected to the Internet? Do you want to try again?', 'Message Failed');
182 | }
183 | }
184 |
185 | /**
186 | * Open LinkedIn in default browser.
187 | */
188 | LinkedIn () {
189 | ipcRenderer.send ('url-open', {url: 'https://www.linkedin.com/in/tomas-chyly/'});
190 | }
191 |
192 | /**
193 | * Open github in default browser.
194 | */
195 | Github () {
196 | ipcRenderer.send ('url-open', {url: 'https://github.com/tomaschyly'});
197 | }
198 |
199 | /**
200 | * Open npm in default browser.
201 | */
202 | Npm () {
203 | ipcRenderer.send ('url-open', {url: 'https://www.npmjs.com/~tomaschyly'});
204 | }
205 |
206 | /**
207 | * Open stackoverflow in default browser.
208 | */
209 | StackOverflow () {
210 | ipcRenderer.send ('url-open', {url: 'https://stackoverflow.com/users/1979892/tom%C3%A1%C5%A1-chyl%C3%BD'});
211 | }
212 |
213 | /**
214 | * Open twitter in default browser.
215 | */
216 | Twitter () {
217 | ipcRenderer.send ('url-open', {url: 'https://twitter.com/TomasChyly'});
218 | }
219 | }
220 |
221 | export default About;
222 |
--------------------------------------------------------------------------------
/src/view/Reference.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-whitespace-before-property */
2 | import './reference.css';
3 |
4 | import React, { Component } from 'react';
5 | import TransitionGroup from 'react-transition-group/TransitionGroup';
6 | import CSSTransition from 'react-transition-group/CSSTransition';
7 |
8 | class Reference extends Component {
9 | /**
10 | * Reference initialization.
11 | */
12 | constructor (props) {
13 | super (props);
14 |
15 | this.content = {
16 | 'global-parameters': {
17 | label: 'Global Parameters',
18 | content: {
19 | log: {
20 | label: 'log',
21 | description: 'Initial value: empty string\nThis value holds all console.log output which will be shown after script is done executing.'
22 | },
23 | result: {
24 | label: 'result',
25 | description: 'Initial value: undefined\nValue used to hold arbitrary script execution result. Any value assigned will be converted to string and shown after log output.'
26 | },
27 | directory: {
28 | label: 'directory',
29 | description: 'Initial value: current selected directory\nThis value has assigned current selected directory of console and it should be used by script for execution.'
30 | },
31 | file: {
32 | label: 'file',
33 | description: 'Initial value: current primary file\nThis value contains the name of the file which was used to open the console.'
34 | },
35 | files: {
36 | label: 'files',
37 | description: 'Initial value: current selected files\nThis value contains array of currently selected files and should be used by script for execution.'
38 | }
39 | }
40 | },
41 | 'global-functions': {
42 | label: 'Functions',
43 | content: {
44 | 'console-log': {
45 | label: 'console.log ()',
46 | description: 'Parameters: any[, ...]\nUse this function like you would standard console.log (). Value/s will be shown as output after script is done executing.'
47 | },
48 | 'fetch': {
49 | label: 'Fetch ()',
50 | description: 'Parameters: string, GET|POST, object, boolean\nFirst parameter is valid url, second is either GET or POST request type, third is data as object if POST is used, last parameter is used to disable sanitization. Can be awaited. This function returns contents of called url.'
51 | },
52 | 'fs-createReadStream': {
53 | label: 'fs.createReadStream ()',
54 | description: 'Parameters: string, object\nUse this function like you would join Node\'s fs.createReadStream ().'
55 | },
56 | 'fs-createWriteStream': {
57 | label: 'fs.createWriteStream ()',
58 | description: 'Parameters: string, object\nUse this function like you would join Node\'s fs.createWriteStream ().'
59 | },
60 | 'path-extname': {
61 | label: 'path.extname ()',
62 | description: 'Parameter: string\nUse this function like you would join Node\'s path.extname ().'
63 | },
64 | 'path-join': {
65 | label: 'path.join ()',
66 | description: 'Parameters: string[, ...]\nUse this function like you would join Node\'s path.join ().'
67 | },
68 | 'ReadDirectory': {
69 | label: 'ReadDirectory ()',
70 | description: 'async\nParameters: string[, RegExp]\nFirst parameter is directory path, second is optional filter. Can be awaited. Lists files inside directory.'
71 | },
72 | 'ReadDirectoryRecursive': {
73 | label: 'ReadDirectoryRecursive ()',
74 | description: 'async\nParameters: string[, Object]\nFirst parameter is directory path.\nSecond parameter is optional Object of parameters: {fullFilePath: boolean, filterFile: RegExp, filterFileContents: RegExp}'
75 | },
76 | 'readline-createInterface': {
77 | label: 'readline.createInterface ()',
78 | description: 'Parameter: object\nUse this function like you would join Node\'s readline.createInterface ().'
79 | },
80 | 'RenameFiles': {
81 | label: 'RenameFiles ()',
82 | description: 'async\nParameters: string, array, string\nFirst parameter is directory containing the files, second is array of file names, third is new name. Can be awaited. Renames files to a new name with number if more than one file.'
83 | },
84 | 'RenameFilesPart': {
85 | label: 'RenameFilesPart ()',
86 | description: 'async\nParameters: string, array, string, string\nFirst parameter is directory containing the files, second is array of file names, third is part to be removed and fourth is new part instead of old one. Can be awaited. Rename files to a new name by changing part of name with provided new part.'
87 | },
88 | 'ReadFileContents': {
89 | label: 'ReadFileContents ()',
90 | description: 'async\nParameter: string\nParameter is file path for the file that you want to get contents of.\nContents is read using UTF-8 encoding.'
91 | },
92 | 'WriteFileContents': {
93 | label: 'WriteFileContents ()',
94 | description: 'async\nParameters: string, string\nFirst parameter is file path to save the file contents to. Second parameter is data to be saved.\nContents are written using UTF-8 encoding.'
95 | },
96 | 'TinyPNGCompressFile': {
97 | label: 'TinyPNGCompressFile ()',
98 | description: 'async\nParameter: string\nParameter is path to a file. Can be awaited. This function will use TinyPNG API to compress the file and then write it to the same directory with modified name.'
99 | },
100 | 'TinyPNGResizeCropFile': {
101 | label: 'TinyPNGResizeCropFile ()',
102 | description: 'async\nParameters: string, object\nFirst parameter is path to a file, second are parameters defining what to do with the file. Can be awaited. This function will resize or crop the file and then write it to the same directory with modified name.\nTo resize the file use: {method: "fit", width: 150, height: 150}.\nTo crop the file change method to "cover". This will not just crop the image, but it will intelligently determine area of interest.'
103 | }
104 | }
105 | }
106 | };
107 |
108 | this.state = {
109 | query: '',
110 | content: {}
111 | };
112 | }
113 |
114 | /**
115 | * First rendered to DOM.
116 | */
117 | componentDidMount () {
118 | window.TCH.Main.SetTitle ('Reference');
119 |
120 | window.TCH.Main.HideNavigation ();
121 |
122 | this.ContentBySearch ('');
123 | }
124 |
125 | /**
126 | * Render the component into html.
127 | */
128 | render () {
129 | const duration = 400;
130 |
131 | let content = [];
132 | for (let index in this.state.content) {
133 | if (this.state.content.hasOwnProperty (index)) {
134 | const category = this.state.content [index];
135 |
136 | let categoryContent = [];
137 | for (let index2 in category.content) {
138 | if (category.content.hasOwnProperty (index2)) {
139 | let elementDescription = category.content [index2].description.split ('\n');
140 | let description = [];
141 | for (let i = 0; i < elementDescription.length; i++) {
142 | description.push (
{elementDescription [i]}
);
143 | }
144 |
145 | categoryContent.push (
146 |
147 |
148 |
{category.content [index2].label}
149 |
150 |
151 |
152 | {description}
153 |
154 |
155 | );
156 | }
157 | }
158 |
159 | if (categoryContent.length === 0) {
160 | categoryContent.push (
161 |
162 |
163 |
Nothing found here.
164 |
165 |
166 | );
167 | }
168 |
169 | content.push (
170 |
{category.label}
171 |
172 |
173 | {categoryContent}
174 |
175 | );
176 | }
177 | }
178 |
179 | return
180 |
181 |
182 |
183 | this.ContentBySearch (e.target.value)}/>
184 |
185 |
186 |
187 | {content}
188 |
189 |
190 |
191 |
;
192 | }
193 |
194 | /**
195 | * Filter content by search in both label and description.
196 | */
197 | ContentBySearch (value) {
198 | let content = {};
199 |
200 | for (let index in this.content) {
201 | if (this.content.hasOwnProperty (index)) {
202 | const category = this.content [index];
203 |
204 | let categoryContent = {};
205 | for (let index2 in category.content) {
206 | if (category.content.hasOwnProperty (index2)) {
207 | if (category.content [index2].label.indexOf (value) >= 0 || category.content [index2].description.indexOf (value) >= 0) {
208 | categoryContent [index2] = category.content [index2];
209 | }
210 | }
211 | }
212 |
213 | content [index] = {
214 | label: this.content [index].label,
215 | content: categoryContent
216 | };
217 | }
218 | }
219 |
220 | this.setState ({
221 | query: value,
222 | content: content
223 | });
224 | }
225 | }
226 |
227 | export default Reference;
228 |
--------------------------------------------------------------------------------
/src/view/Settings.js:
--------------------------------------------------------------------------------
1 | import './settings.css';
2 |
3 | import React, {Component} from 'react';
4 | import Form from '../component/Form';
5 | import Button from '../component/Button';
6 | import Popup from '../component/Popup';
7 | import {THEMES_AS_OPTIONS} from '../component/CodeMirrorEditor';
8 |
9 | const {ipcRenderer} = window.require ('electron');
10 | const extend = window.require ('extend');
11 |
12 | const defaults = {
13 | controls: {
14 | execute: 'ctrl+r',
15 | snippetLoad: 'ctrl+l',
16 | snippetSave: 'ctrl+s'
17 | },
18 | console: {
19 | theme: 'default',
20 | executeConfirm: true,
21 | tinypngApiKey: '',
22 | pro: false
23 | },
24 | theme: {
25 | fancyFont: false,
26 | darkMode: null
27 | }
28 | };
29 |
30 | class Settings extends Component {
31 | /**
32 | * Settings initialization.
33 | */
34 | constructor (props) {
35 | super (props);
36 |
37 | this.formTheme = undefined;
38 | this.formConsole = undefined;
39 | this.formControls = undefined;
40 |
41 | this.formsValues = {};
42 |
43 | this.state = {
44 | current: window.TCH.mainParameters.settings !== null ? extend (true, {}, defaults, window.TCH.mainParameters.settings) : defaults,
45 | reset: false
46 | };
47 | }
48 |
49 | /**
50 | * First rendered to DOM.
51 | */
52 | componentDidMount () {
53 | window.TCH.Main.SetTitle ('Settings');
54 |
55 | document.querySelector ('#content .settings').addEventListener ('scroll', window.ButtonSelect_static.globalHideOptionsListener);
56 | }
57 |
58 | /**
59 | * Called before component is removed from DOM.
60 | */
61 | componentWillUnmount () {
62 | this.formTheme = undefined;
63 | this.formConsole = undefined;
64 | this.formControls = undefined;
65 | }
66 |
67 | /**
68 | * Render the component into html.
69 | */
70 | render () {
71 | const {current} = this.state;
72 |
73 | this.formTheme = React.createRef ();
74 | this.formConsole = React.createRef ();
75 | this.formControls = React.createRef ();
76 |
77 | return
78 |
79 |
80 |
81 | Save
82 |
83 |
84 |
85 |
86 |
Theme
87 |
88 |
101 |
102 |
103 |
104 |
105 |
Console
106 |
107 |
136 |
137 |
138 |
139 |
140 |
Controls
141 |
142 |
160 |
161 |
162 |
163 | Reset
164 | Save
165 |
166 |
167 |
168 |
169 |
171 | Are you sure you want to reset App config and data?
172 | This will delete all of your own saved Snippets, but it will reinstall App's included Snippets.
173 |
174 | } onClose={this.ResetAppClosed.bind (this)} acceptVisible={true} acceptClassName="button-red" accept="Confirm" onAccept={this.ResetAppAccepted.bind (this)}/>
175 |
;
176 | }
177 |
178 | /**
179 | * Get config defaults.
180 | */
181 | static Defaults () {
182 | return defaults;
183 | }
184 |
185 | /**
186 | * Save all forms to Config.
187 | */
188 | FormsSubmit () {
189 | this.formTheme.current.Submit ();
190 | this.formConsole.current.Submit ();
191 | this.formControls.current.Submit ();
192 |
193 | ipcRenderer.send ('app-settings-save', this.formsValues);
194 | }
195 |
196 | /**
197 | * Ask user to cofirm and then if yes, reset the app config and data.
198 | */
199 | ResetApp () {
200 | this.setState ({reset: true});
201 | }
202 |
203 | /**
204 | * Reset app popup closed.
205 | */
206 | ResetAppClosed () {
207 | this.setState ({reset: false});
208 | }
209 |
210 | /**
211 | * Reset app popup accepted, reset the app.
212 | */
213 | ResetAppAccepted () {
214 | ipcRenderer.send ('app-reset');
215 | }
216 | }
217 |
218 | export default Settings;
219 |
--------------------------------------------------------------------------------
/src/view/Snippet.js:
--------------------------------------------------------------------------------
1 | import './snippet.css';
2 |
3 | import React, { Component } from 'react';
4 | import Button from '../component/Button';
5 | import Grid from '../component/Grid';
6 |
7 | const {ipcRenderer} = window.require ('electron');
8 |
9 | class Snippet extends Component {
10 | /**
11 | * Snippet initialization.
12 | */
13 | constructor (props) {
14 | super (props);
15 |
16 | this.grid = undefined;
17 | }
18 |
19 | /**
20 | * First rendered to DOM.
21 | */
22 | componentDidMount () {
23 | window.TCH.Main.SetTitle ('Snippets');
24 |
25 | this.snippetLoadListener = this.EditSnippetLoaded.bind (this);
26 | ipcRenderer.on ('snippet-load', this.snippetLoadListener);
27 |
28 | this.snippetSavedListener = () => {
29 | this.grid.current.UpdateData ();
30 | };
31 | ipcRenderer.on ('snippet-saved', this.snippetSavedListener);
32 | ipcRenderer.on ('snippet-delete', this.snippetSavedListener);
33 | }
34 |
35 | /**
36 | * Called before component is removed from DOM.
37 | */
38 | componentWillUnmount () {
39 | this.grid = undefined;
40 |
41 | ipcRenderer.removeListener ('snippet-load', this.snippetLoadListener);
42 | delete this.snippetLoadListener;
43 |
44 | ipcRenderer.removeListener ('snippet-saved', this.snippetSavedListener);
45 | ipcRenderer.removeListener ('snippet-delete', this.snippetSavedListener);
46 | delete this.snippetSavedListener;
47 | }
48 |
49 | /**
50 | * Render the component into html.
51 | */
52 | render () {
53 | const descriptionRenderer = value => {
54 | if (typeof (value) === 'string' && value.length > 65) {
55 | return `${value.substring (0, 62)}...`;
56 | } else {
57 | return value;
58 | }
59 | };
60 | const dateRenderer = value => {
61 | let date = new Date (parseInt (value) * 1000);
62 | return date.toLocaleDateString ();
63 | };
64 |
65 | const columns = [
66 | {
67 | index: 'name',
68 | label: 'Name',
69 | sort: true,
70 | filter: 'search'
71 | },
72 | {
73 | index: 'description',
74 | label: 'Description',
75 | sort: false,
76 | filter: 'search',
77 | renderer: descriptionRenderer
78 | },
79 | {
80 | index: 'created',
81 | label: 'Created',
82 | sort: true,
83 | renderer: dateRenderer
84 | }
85 | ];
86 | const actions = {
87 | edit: {
88 | index: 'id',
89 | label: 'Edit Snippet',
90 | labelIndex: 'name',
91 | action: this.EditSnippet.bind (this),
92 | icon: 'edit'
93 | },
94 | delete: {
95 | index: 'id',
96 | label: 'Delete Snippet',
97 | labelIndex: 'name',
98 | action: this.DeleteSnippet.bind (this),
99 | icon: 'delete',
100 | confirm: true
101 | }
102 | };
103 |
104 | this.grid = React.createRef ();
105 |
106 | return