├── .gitignore
├── LICENSE
├── README.md
├── app
├── app.html
├── helpers
│ ├── context_menu.js
│ └── external_links.js
├── history.html
├── js
│ ├── const.js
│ ├── controllers
│ │ ├── historyController.js
│ │ ├── requestController.js
│ │ └── responseController.js
│ ├── directives
│ │ ├── fileModel.js
│ │ ├── modalShow.js
│ │ └── statusCode.js
│ ├── filters
│ │ └── objLength.js
│ ├── ling.js
│ └── services
│ │ ├── historyService.js
│ │ └── requestService.js
├── package.json
├── request.html
└── response.html
├── config
├── env_development.json
├── env_production.json
└── env_test.json
├── e2e
├── ling.e2e.js
└── utils.js
├── gulpfile.js
├── ling.png
├── ling.psd
├── package.json
├── resources
├── icons
│ └── 512x512.png
├── osx
│ ├── dmg-background.png
│ ├── dmg-background@2x.png
│ ├── dmg-icon.icns
│ └── icon.icns
└── windows
│ ├── icon.ico
│ └── setup-icon.ico
├── screenshot.png
├── src
├── app.js
├── background.js
├── env.js
├── helpers
│ └── window.js
├── menu
│ ├── dev_menu_template.js
│ └── edit_menu_template.js
└── stylesheets
│ ├── imports
│ ├── bootstrap-override.scss
│ ├── history.scss
│ ├── layout.scss
│ ├── mixins.scss
│ ├── request.scss
│ ├── response.scss
│ ├── utilities.scss
│ └── variables.scss
│ └── style.scss
└── tasks
├── build_app.js
├── build_tests.js
├── bundle.js
├── start.js
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | Thumbs.db
4 | *.log
5 | *.autogenerated
6 |
7 | # ignore everything in 'app' folder what had been generated from 'src' folder
8 | /app/stylesheets
9 | /app/app.js
10 | /app/background.js
11 | /app/env.json
12 | /app/**/*.map
13 | /app/node_modules
14 |
15 | /dist
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2016 Jakub Szwacz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ling
2 |
3 |
4 |
5 |
6 | > REST client built with [Electron](https://github.com/electron/electron) and AngularJS.
7 |
8 | ## Clone
9 |
10 | ```
11 | $ git clone https://github.com/talhasch/ling
12 | $ cd ling
13 | ```
14 |
15 | ## Install dependencies
16 |
17 | ```
18 | $ npm install
19 | ```
20 |
21 | ### Run
22 |
23 | ```
24 | $ npm start
25 | ```
26 |
27 | ### Package
28 |
29 | ```
30 | $ npm run release
31 | ```
32 |
33 | ### Screenshot
34 |
35 |
36 |
37 | ### Ling?
38 |
39 | [Ling](https://en.wikipedia.org/wiki/Ling_Ling_(giant_panda))
40 |
41 | ## License
42 |
43 | The MIT License (MIT) © Talha Buğra Bulut 2016
--------------------------------------------------------------------------------
/app/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Ling
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/helpers/context_menu.js:
--------------------------------------------------------------------------------
1 | // This gives you default context menu (cut, copy, paste)
2 | // in all input fields and textareas across your app.
3 |
4 | (function () {
5 | 'use strict';
6 |
7 | var remote = require('electron').remote;
8 | var Menu = remote.Menu;
9 | var MenuItem = remote.MenuItem;
10 |
11 | var isAnyTextSelected = function () {
12 | return window.getSelection().toString() !== '';
13 | };
14 |
15 | var cut = new MenuItem({
16 | label: "Cut",
17 | click: function () {
18 | document.execCommand("cut");
19 | }
20 | });
21 |
22 | var copy = new MenuItem({
23 | label: "Copy",
24 | click: function () {
25 | document.execCommand("copy");
26 | }
27 | });
28 |
29 | var paste = new MenuItem({
30 | label: "Paste",
31 | click: function () {
32 | document.execCommand("paste");
33 | }
34 | });
35 |
36 | var normalMenu = new Menu();
37 | normalMenu.append(copy);
38 |
39 | var textEditingMenu = new Menu();
40 | textEditingMenu.append(cut);
41 | textEditingMenu.append(copy);
42 | textEditingMenu.append(paste);
43 |
44 | document.addEventListener('contextmenu', function (e) {
45 | switch (e.target.nodeName) {
46 | case 'TEXTAREA':
47 | case 'INPUT':
48 | e.preventDefault();
49 | textEditingMenu.popup(remote.getCurrentWindow());
50 | break;
51 | default:
52 | if (isAnyTextSelected()) {
53 | e.preventDefault();
54 | normalMenu.popup(remote.getCurrentWindow());
55 | }
56 | }
57 | }, false);
58 |
59 | }());
60 |
--------------------------------------------------------------------------------
/app/helpers/external_links.js:
--------------------------------------------------------------------------------
1 | // Convenient way for opening links in external browser, not in the app.
2 | // Useful especially if you have a lot of links to deal with.
3 | //
4 | // Usage:
5 | //
6 | // Every link with class ".js-external-link" will be opened in external browser.
7 | // google
8 | //
9 | // The same behaviour for many links can be achieved by adding
10 | // this class to any parent tag of an anchor tag.
11 | //
12 | // google
13 | // bing
14 | //
15 |
16 | (function () {
17 | 'use strict';
18 |
19 | var shell = require('electron').shell;
20 |
21 | var supportExternalLinks = function (e) {
22 | var href;
23 | var isExternal = false;
24 |
25 | var checkDomElement = function (element) {
26 | if (element.nodeName === 'A') {
27 | href = element.getAttribute('href');
28 | }
29 | if (element.classList.contains('js-external-link')) {
30 | isExternal = true;
31 | }
32 | if (href && isExternal) {
33 | shell.openExternal(href);
34 | e.preventDefault();
35 | } else if (element.parentElement) {
36 | checkDomElement(element.parentElement);
37 | }
38 | };
39 |
40 | checkDomElement(e.target);
41 | };
42 |
43 | document.addEventListener('click', supportExternalLinks, false);
44 | }());
45 |
--------------------------------------------------------------------------------
/app/history.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 | method |
13 | url |
14 | duration |
15 | status |
16 | |
17 |
18 |
19 |
20 |
21 | empty |
22 |
23 |
24 | {{ row.method }} |
25 | {{ row.url | limitTo: 50 }}{{ row.url.length > 50 ? '...' : '' }} |
26 | {{ row.duration }} ms |
27 | {{ row.status }} |
28 | |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/js/const.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'LINK', 'UNLINK', 'OPTIONS'];
4 | const METHOD_GET = 'GET', METHOD_HEAD = 'HEAD', METHOD_POST = 'POST', METHOD_PUT = 'PUT', METHOD_DELETE = 'DELETE';
5 | const METHOD_LINK = 'LINK', METHOD_UNLINK = 'UNLINK', METHOD_OPTIONS = 'OPTIONS';
6 |
7 | const CONTENT_TYPES = [
8 | 'application/json', 'application/xml', 'application/atom+xml', 'multipart/form-data',
9 | 'multipart/alternative', 'multipart/mixed', 'application/x-www-form-urlencoded',
10 | 'application/base64', 'application/octet-stream', 'text/plain', 'text/css', 'text/html',
11 | 'application/javascript']
12 |
13 | const CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded';
14 | const CONTENT_TYPE_FILE = 'multipart/form-data';
--------------------------------------------------------------------------------
/app/js/controllers/historyController.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | window.app.controller('HistoryCtrl', ['$scope', '$rootScope', 'historyService', ($scope, $rootScope, historyService) => {
5 |
6 | const loadData = () => {
7 | historyService.getHistory((docs) => {
8 | $scope.data = docs;
9 | $scope.$apply();
10 | });
11 | }
12 |
13 | $scope.data = [];
14 | loadData();
15 |
16 | $scope.$on('history', (event) => {
17 | loadData();
18 | });
19 |
20 | $scope.clearHistory = () => {
21 | historyService.clearHistory((numRemoved) => {
22 | loadData();
23 | })
24 | }
25 |
26 | $scope.use = (historyItem) => {
27 | $rootScope.$broadcast('clear');
28 | $rootScope.$broadcast('useHistory', historyItem);
29 | }
30 | }]);
31 | })();
--------------------------------------------------------------------------------
/app/js/controllers/requestController.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict'
3 |
4 | window.app.controller('RequestCtrl', ['$scope', '$rootScope', '$timeout', 'requestService', 'historyService', ($scope, $rootScope, $timeout, requestService, historyService) => {
5 |
6 | const contentTypeSelectionShouldVisible = () => {
7 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) {
8 | return false;
9 | }
10 | return true;
11 | }
12 |
13 | const payloadFormShouldEnable = () => {
14 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) {
15 | return false;
16 | }
17 |
18 | return true;
19 | }
20 |
21 | const paramsFormShouldEnable = () => {
22 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) {
23 | return false;
24 | }
25 | let curContentTypeHeader = $scope.headers['Content-Type'];
26 | if (curContentTypeHeader == CONTENT_TYPE_FORM || curContentTypeHeader == CONTENT_TYPE_FILE) {
27 | return true;
28 | }
29 |
30 | return false;
31 | }
32 |
33 | const filesFormShouldEnable = () => {
34 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) {
35 | return false;
36 | }
37 | let curContentTypeHeader = $scope.headers['Content-Type'];
38 | if (curContentTypeHeader == CONTENT_TYPE_FILE) {
39 | return true;
40 | }
41 |
42 | return false;
43 | }
44 |
45 | const payloadFromParams = () => {
46 | let payload = '';
47 | for (let i = 0; i < $scope.params.length; i += 1) {
48 | let paramName = encodeURIComponent($scope.params[i]['name']);
49 | let paramValue = encodeURIComponent($scope.params[i]['value']);
50 | payload += paramName + '=' + paramValue + '&';
51 | }
52 | return payload.replace(/&+$/, ''); // remove last &
53 | }
54 |
55 | const paramsFromPayload = (payload) => {
56 | let params = [];
57 | let _payload = payload.split('&');
58 | for (let i = 0; i < _payload.length; i++) {
59 | let element = _payload[i];
60 | let _param = element.split('=');
61 | let paramName = decodeURIComponent(_param[0]);
62 | let paramValue = _param[1] ? decodeURIComponent(_param[1]) : '';
63 | params.push({ 'name': paramName, 'value': paramValue });
64 | }
65 |
66 | return params;
67 | }
68 |
69 | // URL
70 | $scope.url = '';
71 |
72 | // METHOD
73 | $scope.methods = METHODS;
74 | $scope.selectedMethod = METHOD_GET;
75 |
76 | // TABS
77 | $scope.selectedTab = 'headers';
78 | $scope.selectTab = (tab) => {
79 | $scope.selectedTab = tab;
80 |
81 | if (tab == 'params') {
82 | if ($scope.payload != '') {
83 | $scope.params = paramsFromPayload($scope.payload);
84 | }
85 |
86 | }
87 | }
88 |
89 | // HEADERS
90 | $scope.headers = {};
91 |
92 | $scope.newHeaderName = '';
93 | $scope.newHeaderValue = '';
94 |
95 | $scope.resetHeadersForm = () => {
96 | $scope.headersForm.$setPristine();
97 | $scope.newHeaderName = '';
98 | $scope.newHeaderValue = '';
99 | }
100 |
101 | $scope.submitHeadersForm = (isValid) => {
102 | if (isValid) {
103 | let headerName = $scope.newHeaderName;
104 | let headerValue = $scope.newHeaderValue;
105 |
106 | $scope.headers[headerName] = headerValue;
107 | $scope.resetHeadersForm();
108 | }
109 | }
110 |
111 | $scope.editHeader = (headerName, headerValue) => {
112 | $scope.resetHeadersForm();
113 |
114 | $scope.newHeaderName = headerName;
115 | $scope.newHeaderValue = headerValue;
116 |
117 | delete $scope.headers[headerName]
118 | }
119 |
120 | $scope.removeHeader = (headerName) => {
121 | delete $scope.headers[headerName];
122 |
123 | if (headerName == 'Authorization') {
124 | $scope.authUser = '';
125 | $scope.authPass = '';
126 | }
127 | }
128 |
129 | // CONTENT TYPE HEADER
130 | $scope.contentTypes = CONTENT_TYPES;
131 |
132 | $scope.contentTypeSelectionShouldVisible = () => {
133 | return contentTypeSelectionShouldVisible();
134 | }
135 |
136 | $scope.contentTypeSelectionChanged = () => {
137 | if (!$scope.headers['Content-Type']) {
138 | delete $scope.headers['Content-Type'];
139 | }
140 | }
141 |
142 | // PAYLOAD
143 | $scope.payload = '';
144 |
145 | $scope.payloadFormShouldEnable = () => {
146 | return payloadFormShouldEnable();
147 | }
148 |
149 | $scope.$watchCollection('params', () => {
150 | $scope.payload = payloadFromParams();
151 | });
152 |
153 | $scope.switchMethodForPayload = () => {
154 | $scope.selectedMethod = METHOD_POST;
155 | }
156 |
157 | // PARAMS (data)
158 | $scope.params = [];
159 |
160 | $scope.addParam = (paramName, paramValue) => {
161 | $scope.params.push({ 'name': paramName, 'value': paramValue });
162 | }
163 |
164 | $scope.removeParam = (param) => {
165 | let index = $scope.params.indexOf(param);
166 | $scope.params.splice(index, 1);
167 | }
168 |
169 | $scope.newParamName = '';
170 | $scope.newParamValue = '';
171 |
172 | $scope.resetParamsForm = () => {
173 | $scope.paramsForm.$setPristine();
174 | $scope.newParamName = '';
175 | $scope.newParamValue = '';
176 | }
177 |
178 | $scope.submitParamsForm = (isValid) => {
179 | if (isValid) {
180 | $scope.addParam($scope.newParamName, $scope.newParamValue)
181 | $scope.resetParamsForm();
182 | }
183 | }
184 |
185 | $scope.editParam = (param) => {
186 | $scope.resetParamsForm();
187 |
188 | $scope.newParamName = param.name;
189 | $scope.newParamValue = param.value;
190 |
191 | $scope.removeParam(param);
192 | }
193 |
194 | $scope.paramsFormShouldEnable = () => {
195 | return paramsFormShouldEnable();
196 | }
197 |
198 | $scope.switchContentTypeForParamsForm = () => {
199 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) {
200 | $scope.selectedMethod = METHOD_POST;
201 | }
202 |
203 | $scope.headers['Content-Type'] = CONTENT_TYPE_FORM;
204 | }
205 |
206 | // FILES
207 | $scope.files = [];
208 |
209 | $scope.addFile = (fileName, fileValue, fileLabel) => {
210 | $scope.files.push({ 'name': fileName, 'value': fileValue, 'label': fileLabel });
211 | }
212 |
213 | $scope.removeFile = (file) => {
214 | let index = $scope.files.indexOf(file);
215 | $scope.files.splice(index, 1);
216 | }
217 |
218 | $scope.newFileName = '';
219 | $scope.newFileValue = '';
220 |
221 | $scope.resetFilesForm = () => {
222 | $scope.filesForm.$setPristine();
223 | $scope.newFileName = '';
224 | $scope.newFileValue = '';
225 | }
226 |
227 | $scope.submitFilesForm = () => {
228 | $scope.filesForm.$setValidity('required', true);
229 | if (!$scope.newFileName || !$scope.newFileValue) {
230 | $scope.filesForm.$setValidity('required', false);
231 | return;
232 | }
233 |
234 | $scope.addFile($scope.newFileName, $scope.newFileValue[0], $scope.newFileValue[0].name);
235 | $scope.resetFilesForm();
236 | }
237 |
238 | $scope.filesFormShouldEnable = () => {
239 | return filesFormShouldEnable();
240 | }
241 |
242 | $scope.switchContentTypeForFilesForm = () => {
243 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) {
244 | $scope.selectedMethod = METHOD_POST;
245 | }
246 |
247 | $scope.headers['Content-Type'] = CONTENT_TYPE_FILE;
248 | }
249 |
250 | // AUTH
251 | $scope.authUser = '';
252 | $scope.authPass = '';
253 |
254 | const watchAuth = () => {
255 | if ($scope.authUser || $scope.authPass) {
256 | $scope.headers['Authorization'] = 'Basic ' + btoa($scope.authUser + ':' + $scope.authPass);
257 | } else {
258 | delete $scope.headers['Authorization'];
259 | }
260 | }
261 |
262 | let watchAuthT = null;
263 | $scope.$watchGroup(['authUser', 'authPass'], () => {
264 | if (watchAuthT != null) {
265 | $timeout.cancel(watchAuthT);
266 | watchAuthT = null;
267 | }
268 |
269 | watchAuthT = $timeout(() => { watchAuth() }, 300);
270 | });
271 |
272 | let request = null;
273 |
274 | $scope.showProgressDialog = false;
275 |
276 | $scope.run = (isValid) => {
277 |
278 | if (isValid) {
279 | let url = angular.copy($scope.url);
280 | let method = angular.copy($scope.selectedMethod);
281 | let headers = angular.copy($scope.headers);
282 | let _data = payloadFormShouldEnable() ? angular.copy($scope.payload) : '';
283 | let files = $scope.files;
284 |
285 | if (headers['Content-Type'] == CONTENT_TYPE_FILE) {
286 | headers['Content-Type'] = undefined;
287 |
288 | let params = paramsFromPayload(_data);
289 |
290 | _data = new FormData();
291 |
292 | for (let i = 0; i < params.length; i++) {
293 | let param = params[i];
294 | _data.append(param.name, param.value);
295 | }
296 |
297 | for (let i = 0; i < files.length; i++) {
298 | let file = files[i];
299 | _data.append(file.name, file.value);
300 | }
301 | }
302 |
303 | $scope.showProgressDialog = true;
304 |
305 | $rootScope.$broadcast('beforeRequest');
306 | let startDate = new Date();
307 | request = requestService.makeRequest(url, method, headers, _data, files);
308 | request.promise.then((response) => {
309 | httpCallback(response, startDate);
310 | }, (reason) => {
311 | $rootScope.$broadcast('requestError');
312 |
313 | if (reason instanceof Error) {
314 | alert(reason.message);
315 | }
316 |
317 | if (reason instanceof Object) {
318 | if (reason.status != undefined) {
319 | if (reason.status == -1) {
320 | // cancelled
321 | return;
322 | }
323 |
324 | httpCallback(reason, startDate);
325 | }
326 | }
327 |
328 | }).finally(() => {
329 | $scope.showProgressDialog = false;
330 | request = null;
331 | });
332 | }
333 | }
334 |
335 | const httpCallback = (responseObj, startDate) => {
336 | let endDate = new Date();
337 | let duration = endDate - startDate;
338 |
339 | let responseHeaders = responseObj.headers();
340 | let responseBody = responseObj.data;
341 | let responseStatusCode = responseObj.status;
342 | $rootScope.$broadcast('response', responseHeaders, responseBody, responseStatusCode);
343 |
344 | historyService.addToHistory(
345 | {
346 | 'url': $scope.url,
347 | 'method': $scope.selectedMethod,
348 | 'headers': $scope.headers,
349 | 'payload': $scope.payload,
350 | 'authUser': $scope.authUser,
351 | 'authPass': $scope.authPass,
352 | 'status': responseStatusCode,
353 | 'duration': duration,
354 | 'created': new Date()
355 | }, (doc) => {
356 | $rootScope.$broadcast('history');
357 | });
358 | }
359 |
360 | $scope.cancelRequest = () => {
361 | request.cancel('User cancelled');
362 | }
363 |
364 | $scope.clear = () => {
365 | $rootScope.$broadcast('clear');
366 | }
367 |
368 | $scope.$on('clear', (event) => {
369 |
370 | $scope.url = '';
371 | $scope.selectedMethod = METHOD_GET;
372 | $scope.selectedTab = 'headers';
373 | $scope.headers = {};
374 | $scope.payload = '';
375 | $scope.params = [];
376 | $scope.files = [];
377 | $scope.authUser = '';
378 | $scope.authPass = '';
379 |
380 | $scope.runForm.$setPristine();
381 | $scope.headersForm.$setPristine();
382 | $scope.paramsForm.$setPristine();
383 | });
384 |
385 | $scope.$on('useHistory', (event, historyItem) => {
386 | $scope.url = historyItem.url;
387 | $scope.selectedMethod = historyItem.method;
388 | $scope.headers = historyItem.headers;
389 | $scope.payload = historyItem.payload;
390 | if (paramsFormShouldEnable) {
391 | $scope.params = paramsFromPayload($scope.payload);
392 | }
393 | $scope.authUser = historyItem.authUser;
394 | $scope.authPass = historyItem.authPass;
395 | });
396 | }]);
397 | })();
--------------------------------------------------------------------------------
/app/js/controllers/responseController.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | window.app.controller('ResponseCtrl', ['$scope', ($scope) => {
5 |
6 | const isBinaryContentType = function (contentType) {
7 | let p = contentType.match(/([^\/]+)\/([^;]+)/),
8 | type = p && p[1],
9 | subtype = p && p[2];
10 |
11 | if (type === 'text') {
12 | return false;
13 | }
14 | else if (type === 'application' && (subtype == 'javascript' || subtype == 'json' || subtype == 'xml')) {
15 | return false;
16 | }
17 | return true;
18 | }
19 |
20 | const isJsonContentType = function (contentType) {
21 | let p = contentType.match(/([^\/]+)\/([^;]+)/),
22 | type = p && p[1],
23 | subtype = p && p[2];
24 |
25 | if (subtype == 'json') {
26 | return true;
27 | }
28 |
29 | return false;
30 | }
31 |
32 | $scope.selectedTab = 'headers';
33 | $scope.selectTab = (tab) => {
34 | $scope.selectedTab = tab;
35 | }
36 |
37 | $scope.reset = () => {
38 | $scope.headers = {};
39 | $scope.body = null;
40 | $scope.statusCode = null;
41 | $scope.isBinary = false;
42 | $scope.isJson = false;
43 | $scope.formatted = false;
44 | $scope.flag = false;
45 | }
46 |
47 | $scope.format = () => {
48 | try {
49 | let obj = JSON.parse($scope.body);
50 | $scope.body = JSON.stringify(obj, null, 4);
51 | $scope.formatted = true;
52 | } catch (e) {
53 | return;
54 | }
55 | }
56 |
57 | $scope.reset();
58 |
59 | $scope.$on("response", (event, headers, body, statusCode) => {
60 | $scope.headers = headers;
61 | $scope.statusCode = statusCode;
62 |
63 | let contentType = headers['content-type'];
64 |
65 | if(contentType!==undefined){
66 | $scope.isBinary = isBinaryContentType(headers['content-type']);
67 | } else {
68 | $scope.isBinary = false;
69 | }
70 |
71 | if (!$scope.isBinary) {
72 | $scope.body = body;
73 | }
74 |
75 | if(contentType !== undefined){
76 | $scope.isJson = isJsonContentType(headers['content-type']);
77 | } else {
78 | $scope.isJson = false;
79 | }
80 |
81 | $scope.flag = true;
82 | });
83 |
84 | $scope.$on('beforeRequest', (event) => {
85 | $scope.reset();
86 | });
87 |
88 | $scope.$on('requestError', (event) => {
89 | $scope.reset();
90 | });
91 |
92 | $scope.$on('clear', (event) => {
93 | $scope.reset();
94 | });
95 |
96 | $scope.$on('xhrDone', (event, xhr) => {
97 | $scope.responseUrl = xhr.responseURL;
98 | });
99 | }]);
100 | })();
--------------------------------------------------------------------------------
/app/js/directives/fileModel.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | window.app.directive('fileModel', [() => {
5 | return {
6 | scope: {
7 | fileModel: '='
8 | },
9 | link: (scope, element, attributes) => {
10 | element.bind('change', (changeEvent) => {
11 | scope.fileModel = changeEvent.target.files;
12 | });
13 | }
14 | }
15 | }]);
16 | })();
--------------------------------------------------------------------------------
/app/js/directives/modalShow.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | window.app.directive('modalShow', () => {
5 | return {
6 | restrict: 'A',
7 | scope: {
8 | modalVisible: '='
9 | },
10 | link: (scope, element, attrs) => {
11 |
12 | //Hide or show the modal
13 | scope.showModal = (visible) => {
14 | if (visible) {
15 | element.modal('show');
16 | }
17 | else {
18 | element.modal('hide');
19 | }
20 | }
21 |
22 | //Check to see if the modal-visible attribute exists
23 | if (!attrs.modalVisible) {
24 | //The attribute isn't defined, show the modal by default
25 | scope.showModal(true);
26 | }
27 | else {
28 | //Watch for changes to the modal-visible attribute
29 | scope.$watch('modalVisible', (newValue, oldValue) => {
30 | scope.showModal(newValue);
31 | });
32 |
33 | //Update the visible value when the dialog is closed through UI actions (Ok, cancel, etc.)
34 | element.bind('hide.bs.modal', () => {
35 | scope.modalVisible = false;
36 | if (!scope.$$phase && !scope.$root.$$phase)
37 | scope.$apply();
38 | });
39 | }
40 | }
41 | };
42 | });
43 | })();
--------------------------------------------------------------------------------
/app/js/directives/statusCode.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | // from https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
5 | const statusCodes = {
6 | // 1xx Informational
7 | '100': { 'msg': 'Continue', 'cls': 'info' },
8 | '101': { 'msg': 'Switching Protocols', 'cls': 'info' },
9 | '102': { 'msg': 'Processing', 'cls': 'info' },
10 |
11 | // 2xx Success
12 | '200': { 'msg': 'OK', 'cls': 'success' },
13 | '201': { 'msg': 'Created', 'cls': 'success' },
14 | '202': { 'msg': 'Accepted', 'cls': 'success' },
15 | '203': { 'msg': 'Non-Authoritative Information', 'cls': 'success' },
16 | '204': { 'msg': 'No Content', 'cls': 'success' },
17 | '205': { 'msg': 'Reset Content', 'cls': 'success' },
18 | '206': { 'msg': 'Partial Content', 'cls': 'success' },
19 | '207': { 'msg': 'Multi-Status', 'cls': 'success' },
20 | '208': { 'msg': 'Already Reported', 'cls': 'success' },
21 | '226': { 'msg': 'IM Used', 'cls': 'success' },
22 |
23 | // 3xx Redirection
24 | '300': { 'msg': 'Multiple Choices', 'cls': 'redir' },
25 | '301': { 'msg': 'Moved Permanently', 'cls': 'redir' },
26 | '302': { 'msg': 'Found', 'cls': 'redir' },
27 | '303': { 'msg': 'See Other', 'cls': 'redir' },
28 | '304': { 'msg': 'Not Modified', 'cls': 'redir' },
29 | '305': { 'msg': 'Use Proxy', 'cls': 'redir' },
30 | '306': { 'msg': 'Switch Proxy', 'cls': 'redir' },
31 | '307': { 'msg': 'Temporary Redirect', 'cls': 'redir' },
32 | '308': { 'msg': 'Permanent Redirect', 'cls': 'redir' },
33 |
34 | // 4xx Client Error
35 | '400': { 'msg': 'Bad Request', 'cls': 'client-err' },
36 | '401': { 'msg': 'Unauthorized', 'cls': 'client-err' },
37 | '402': { 'msg': 'Payment Required', 'cls': 'client-err' },
38 | '403': { 'msg': 'Forbidden', 'cls': 'client-err' },
39 | '404': { 'msg': 'Not Found', 'cls': 'client-err' },
40 | '405': { 'msg': 'Method Not Allowed', 'cls': 'client-err' },
41 | '406': { 'msg': 'Not Acceptable', 'cls': 'client-err' },
42 | '407': { 'msg': 'Proxy Authentication Required', 'cls': 'client-err' },
43 | '408': { 'msg': 'Request Timeout', 'cls': 'client-err' },
44 | '409': { 'msg': 'Conflict', 'cls': 'client-err' },
45 | '410': { 'msg': 'Gone', 'cls': 'client-err' },
46 | '411': { 'msg': 'Length Required', 'cls': 'client-err' },
47 | '412': { 'msg': 'Precondition Failed', 'cls': 'client-err' },
48 | '413': { 'msg': 'Payload Too Large', 'cls': 'client-err' },
49 | '414': { 'msg': 'URI Too Long', 'cls': 'client-err' },
50 | '415': { 'msg': 'Unsupported Media Type', 'cls': 'client-err' },
51 | '416': { 'msg': 'Range Not Satisfiable', 'cls': 'client-err' },
52 | '417': { 'msg': 'Expectation Failed', 'cls': 'client-err' },
53 | '418': { 'msg': "I'm a teapot", 'cls': 'client-err' },
54 | '421': { 'msg': 'Misdirected Request', 'cls': 'client-err' },
55 | '422': { 'msg': 'Unprocessable Entity', 'cls': 'client-err' },
56 | '423': { 'msg': 'Locked', 'cls': 'client-err' },
57 | '424': { 'msg': 'Failed Dependency', 'cls': 'client-err' },
58 | '426': { 'msg': 'Upgrade Required', 'cls': 'client-err' },
59 | '428': { 'msg': 'Precondition Required', 'cls': 'client-err' },
60 | '429': { 'msg': 'Too Many Requests', 'cls': 'client-err' },
61 | '431': { 'msg': 'Request Header Fields Too Large', 'cls': 'client-err' },
62 | '451': { 'msg': 'Unavailable For Legal Reasons', 'cls': 'client-err' },
63 |
64 | // 5xx Server Error
65 | '500': { 'msg': 'Internal Server Error', 'cls': 'server-err' },
66 | '501': { 'msg': 'Not Implemented', 'cls': 'server-err' },
67 | '502': { 'msg': 'Bad Gateway', 'cls': 'server-err' },
68 | '503': { 'msg': 'Service Unavailable', 'cls': 'server-err' },
69 | '504': { 'msg': 'Gateway Timeout', 'cls': 'server-err' },
70 | '505': { 'msg': 'HTTP Version Not Supported', 'cls': 'server-err' },
71 | '506': { 'msg': 'Variant Also Negotiates', 'cls': 'server-err' },
72 | '507': { 'msg': 'Insufficient Storage', 'cls': 'server-err' },
73 | '508': { 'msg': 'Loop Detected', 'cls': 'server-err' },
74 | '510': { 'msg': 'Not Extended', 'cls': 'server-err' },
75 | '511': { 'msg': 'Network Authentication Required', 'cls': 'server-err' },
76 |
77 | // Internet Information Services
78 | '440': { 'msg': 'Login Timeout', 'cls': 'iis' },
79 | '449': { 'msg': 'Retry With', 'cls': 'iis' },
80 |
81 | // nginx
82 | '444': { 'msg': 'No Response', 'cls': 'nginx' },
83 | '495': { 'msg': 'SSL Certificate Error', 'cls': 'nginx' },
84 | '496': { 'msg': 'SSL Certificate Required', 'cls': 'nginx' },
85 | '497': { 'msg': 'HTTP Request Sent to HTTPS Port', 'cls': 'nginx' },
86 | '499': { 'msg': 'Client Closed Request', 'cls': 'nginx' },
87 |
88 | // CloudFlare
89 | '520': { 'msg': 'Unknown Error', 'cls': 'cloudflare' },
90 | '521': { 'msg': 'Web Server Is Down', 'cls': 'cloudflare' },
91 | '522': { 'msg': 'Connection Timed Out', 'cls': 'cloudflare' },
92 | '523': { 'msg': 'Origin Is Unreachable', 'cls': 'cloudflare' },
93 | '524': { 'msg': 'A Timeout Occurred', 'cls': 'cloudflare' },
94 | '525': { 'msg': 'SSL Handshake Failed', 'cls': 'cloudflare' },
95 | '526': { 'msg': 'Invalid SSL Certificate', 'cls': 'cloudflare' }
96 | };
97 |
98 | app.directive("responseStatus", () => {
99 | return {
100 | scope: {
101 | code: '@'
102 | },
103 | link: (scope, element, attrs) => {
104 | scope.$watch('code', (val) => {
105 | if (val) {
106 | scope.text = '';
107 | scope.class = '';
108 | if (statusCodes[val] != undefined) {
109 | scope.text = statusCodes[val].msg;
110 | scope.class = statusCodes[val].cls;
111 | }
112 | }
113 | });
114 | },
115 | template: '{{code}} - {{text}}
'
116 | }
117 | })
118 | })();
119 |
120 |
121 |
--------------------------------------------------------------------------------
/app/js/filters/objLength.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | window.app.filter('objLength', () => {
5 | return (object) => {
6 | return Object.keys(object).length;
7 | }
8 | });
9 |
10 | })();
--------------------------------------------------------------------------------
/app/js/ling.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | var app = window.app = angular.module('LingApp', []);
5 |
6 | })();
--------------------------------------------------------------------------------
/app/js/services/historyService.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | const db = new Nedb({
5 | filename: 'lingHistory.db',
6 | autoload: true
7 | });
8 |
9 | window.app.factory('historyService', () => {
10 | const addToHistory = (doc, cb) => {
11 | db.insert(doc, (err, doc) => {
12 | if (typeof cb === 'function') {
13 | cb(doc);
14 | }
15 | });
16 | }
17 |
18 | const getHistory = (cb) => {
19 | db.find({}).sort({ created: -1 }).limit(50).exec((err, docs) => {
20 | if (typeof cb === 'function') {
21 | cb(docs);
22 | }
23 | });
24 | }
25 |
26 | const clearHistory = (cb) => {
27 | db.remove({}, { multi: true }, (err, numRemoved) => {
28 | if (typeof cb === 'function') {
29 | cb(numRemoved);
30 | }
31 | });
32 | }
33 |
34 | return {
35 | addToHistory: addToHistory,
36 | getHistory: getHistory,
37 | clearHistory: clearHistory
38 | };
39 | });
40 |
41 | })();
--------------------------------------------------------------------------------
/app/js/services/requestService.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | window.app.factory('$xhrFactory', ['$rootScope', ($rootScope) => {
5 | return function createXhr(method, url) {
6 | const xhr = new window.XMLHttpRequest({ mozSystem: true });
7 | xhr.onreadystatechange = () => {
8 | if (xhr.readyState === XMLHttpRequest.DONE) {
9 | if (xhr.responseURL.startsWith('file://')) {
10 | return;
11 | }
12 | $rootScope.$broadcast('xhrDone', xhr);
13 | }
14 | };
15 | return xhr;
16 | };
17 | }]);
18 |
19 | window.app.factory('requestService', ($http, $q) => {
20 | const makeRequest = (url, method, headers, data, files) => {
21 | let canceller = $q.defer();
22 |
23 | let cancel = (reason) => {
24 | canceller.resolve(reason);
25 | };
26 |
27 | let promise = $http({
28 | url: url,
29 | method: method,
30 | cache: false,
31 | headers: headers,
32 | data: data,
33 | timeout: canceller.promise,
34 | transformResponse: null
35 | });
36 |
37 | return {
38 | promise: promise,
39 | cancel: cancel
40 | };
41 | }
42 |
43 | return {
44 | makeRequest: makeRequest
45 | };
46 | });
47 |
48 | })();
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ling",
3 | "productName": "Ling",
4 | "description": "REST client",
5 | "version": "0.2.0",
6 | "author": "Talha Buğra Bulut ",
7 | "homepage": "https://github.com/talhasch/ling",
8 | "license": "MIT",
9 | "main": "background.js",
10 | "dependencies": {
11 | "fs-jetpack": "^0.9.0",
12 | "angular": "^1.5.8",
13 | "bootstrap": "^3.3.7",
14 | "font-awesome": "^4.6.3",
15 | "jquery": "^3.1.0",
16 | "nedb": "^1.8.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/request.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | Empty |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | |
110 |
111 |
112 |
113 |
114 |
115 |
124 | |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Empty |
138 |
139 |
140 |
141 |
146 | |
147 |
148 |
149 |
150 |
151 |
152 |
160 | |
161 |
162 |
163 |
164 |
165 |
166 |
174 |
175 |
176 |
177 |
178 |
179 |
182 |
183 |
Sending the request...
184 |
185 |
188 |
189 |
190 |
191 |
--------------------------------------------------------------------------------
/app/response.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
29 |
30 |
31 |
32 |
Unable to display binary data.
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/config/env_development.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "development"
3 | }
4 |
--------------------------------------------------------------------------------
/config/env_production.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "production"
3 | }
4 |
--------------------------------------------------------------------------------
/config/env_test.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test"
3 | }
4 |
--------------------------------------------------------------------------------
/e2e/ling.e2e.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import testUtils from './utils';
3 |
4 | describe('application launch', function () {
5 |
6 | beforeEach(testUtils.beforeEach);
7 | afterEach(testUtils.afterEach);
8 |
9 | it('Request section header', function () {
10 | return this.app.client.getText('.request .scope-header').then(function (text) {
11 | expect(text).to.equal('REQUEST');
12 | });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/utils.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | import { Application } from 'spectron';
3 |
4 | var beforeEach = function () {
5 | this.timeout(10000);
6 | this.app = new Application({
7 | path: electron,
8 | args: ['app'],
9 | startTimeout: 10000,
10 | waitTimeout: 10000,
11 | });
12 | return this.app.start();
13 | };
14 |
15 | var afterEach = function () {
16 | this.timeout(10000);
17 | if (this.app && this.app.isRunning()) {
18 | return this.app.stop();
19 | }
20 | };
21 |
22 | export default {
23 | beforeEach: beforeEach,
24 | afterEach: afterEach,
25 | };
26 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./tasks/build_app');
4 | require('./tasks/build_tests');
5 | require('./tasks/start');
6 |
--------------------------------------------------------------------------------
/ling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/ling.png
--------------------------------------------------------------------------------
/ling.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/ling.psd
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "win": {
4 | "target": [
5 | "nsis"
6 | ],
7 | "icon": "resources/windows/icon.ico"
8 | },
9 | "nsis": {
10 | "oneClick": true,
11 | "installerHeaderIcon": "resources/windows/setup-icon.ico"
12 | },
13 | "mac": {
14 | "icon": "resources/osx/icon.icns"
15 | },
16 | "dmg": {
17 | "icon": "resources/osx/dmg-icon.icns",
18 | "background": "resources/osx/dmg-background.png"
19 | }
20 | },
21 | "directories": {
22 | "buildResources": "resources"
23 | },
24 | "scripts": {
25 | "postinstall": "install-app-deps",
26 | "build": "gulp build",
27 | "prerelease": "gulp build --env=production",
28 | "release": "build --x64 --publish never",
29 | "start": "gulp start",
30 | "pretest": "gulp build-unit --env=test",
31 | "test": "electron-mocha app/specs.js.autogenerated --renderer --require source-map-support/register",
32 | "pree2e": "gulp build-e2e --env=test",
33 | "e2e": "mocha app/e2e.js.autogenerated --require source-map-support/register"
34 | },
35 | "devDependencies": {
36 | "chai": "^3.5.0",
37 | "electron": "1.8.4",
38 | "electron-builder": "^5.12.1",
39 | "electron-mocha": "^3.0.0",
40 | "fs-jetpack": "^0.9.0",
41 | "gulp": "^3.9.0",
42 | "gulp-batch": "^1.0.5",
43 | "gulp-plumber": "^1.1.0",
44 | "gulp-sass": "^2.3.2",
45 | "gulp-util": "^3.0.6",
46 | "gulp-watch": "^4.3.5",
47 | "mocha": "^3.0.2",
48 | "rollup": "^0.34.7",
49 | "source-map-support": "^0.4.2",
50 | "spectron": "^3.3.0",
51 | "yargs": "^4.2.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/resources/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/icons/512x512.png
--------------------------------------------------------------------------------
/resources/osx/dmg-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/dmg-background.png
--------------------------------------------------------------------------------
/resources/osx/dmg-background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/dmg-background@2x.png
--------------------------------------------------------------------------------
/resources/osx/dmg-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/dmg-icon.icns
--------------------------------------------------------------------------------
/resources/osx/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/icon.icns
--------------------------------------------------------------------------------
/resources/windows/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/windows/icon.ico
--------------------------------------------------------------------------------
/resources/windows/setup-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/windows/setup-icon.ico
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/screenshot.png
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | // Here is the starting point for your application code.
2 | // All stuff below is just to show you how it works. You can delete all of it.
3 |
4 | // Use new ES6 modules syntax for everything.
5 | import os from 'os'; // native node.js module
6 | import { remote } from 'electron'; // native electron module
7 | const {ipcRenderer} = require('electron');
8 | import jetpack from 'fs-jetpack'; // module loaded from npm
9 | import env from './env';
10 |
11 | console.log('Loaded environment variables:', env);
12 |
13 | var app = remote.app;
14 | var appDir = jetpack.cwd(app.getAppPath());
15 |
16 | // Holy crap! This is browser window with HTML and stuff, but I can read
17 | // here files like it is node.js! Welcome to Electron world :)
18 | console.log('The author of this app is:', appDir.read('package.json', 'json').author);
19 |
20 | document.addEventListener('DOMContentLoaded', function () {
21 | setTimeout(() => {
22 | document.body.style.visibility = 'visible';
23 | }, 300);
24 | });
25 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | // This is main process of Electron, started as first thing when your
2 | // app starts. This script is running through entire life of your application.
3 | // It doesn't have any windows which you can see on screen, but we can open
4 | // window from here.
5 |
6 | import { app, Menu } from 'electron';
7 | import { devMenuTemplate } from './menu/dev_menu_template';
8 | import { editMenuTemplate } from './menu/edit_menu_template';
9 | import createWindow from './helpers/window';
10 |
11 | // Special module holding environment variables which you declared
12 | // in config/env_xxx.json file.
13 | import env from './env';
14 |
15 | var mainWindow;
16 |
17 | var setApplicationMenu = function () {
18 | var menus = [editMenuTemplate];
19 | if (env.name !== 'production') {
20 | menus.push(devMenuTemplate);
21 | }
22 | Menu.setApplicationMenu(Menu.buildFromTemplate(menus));
23 | };
24 |
25 | // Save userData in separate folders for each environment.
26 | // Thanks to this you can use production and development versions of the app
27 | // on same machine like those are two separate apps.
28 | if (env.name !== 'production') {
29 | var userDataPath = app.getPath('userData');
30 | app.setPath('userData', userDataPath + ' (' + env.name + ')');
31 | }
32 |
33 | app.on('ready', function () {
34 | setApplicationMenu();
35 |
36 | var mainWindow = createWindow('main', {
37 | width: 1200,
38 | height: 700,
39 | minWidth: 1200,
40 | minHeight: 700
41 | });
42 |
43 | mainWindow.loadURL('file://' + __dirname + '/app.html');
44 |
45 | if (env.name === 'development') {
46 | mainWindow.openDevTools({detach: true, width: 200, height:200});
47 | }
48 | });
49 |
50 | app.on('window-all-closed', function () {
51 | app.quit();
52 | });
53 |
--------------------------------------------------------------------------------
/src/env.js:
--------------------------------------------------------------------------------
1 | // Simple wrapper exposing environment variables to rest of the code.
2 |
3 | import jetpack from 'fs-jetpack';
4 |
5 | // The variables have been written to `env.json` by the build process.
6 | var env = jetpack.cwd(__dirname).read('env.json', 'json');
7 |
8 | export default env;
9 |
--------------------------------------------------------------------------------
/src/helpers/window.js:
--------------------------------------------------------------------------------
1 | // This helper remembers the size and position of your windows (and restores
2 | // them in that place after app relaunch).
3 | // Can be used for more than one window, just construct many
4 | // instances of it and give each different name.
5 |
6 | import { app, BrowserWindow, screen } from 'electron';
7 | import jetpack from 'fs-jetpack';
8 |
9 | export default function (name, options) {
10 |
11 | var userDataDir = jetpack.cwd(app.getPath('userData'));
12 | var stateStoreFile = 'window-state-' + name +'.json';
13 | var defaultSize = {
14 | width: options.width,
15 | height: options.height
16 | };
17 | var state = {};
18 | var win;
19 |
20 | var restore = function () {
21 | var restoredState = {};
22 | try {
23 | restoredState = userDataDir.read(stateStoreFile, 'json');
24 | } catch (err) {
25 | // For some reason json can't be read (might be corrupted).
26 | // No worries, we have defaults.
27 | }
28 | return Object.assign({}, defaultSize, restoredState);
29 | };
30 |
31 | var getCurrentPosition = function () {
32 | var position = win.getPosition();
33 | var size = win.getSize();
34 | return {
35 | x: position[0],
36 | y: position[1],
37 | width: size[0],
38 | height: size[1]
39 | };
40 | };
41 |
42 | var windowWithinBounds = function (windowState, bounds) {
43 | return windowState.x >= bounds.x &&
44 | windowState.y >= bounds.y &&
45 | windowState.x + windowState.width <= bounds.x + bounds.width &&
46 | windowState.y + windowState.height <= bounds.y + bounds.height;
47 | };
48 |
49 | var resetToDefaults = function (windowState) {
50 | var bounds = screen.getPrimaryDisplay().bounds;
51 | return Object.assign({}, defaultSize, {
52 | x: (bounds.width - defaultSize.width) / 2,
53 | y: (bounds.height - defaultSize.height) / 2
54 | });
55 | };
56 |
57 | var ensureVisibleOnSomeDisplay = function (windowState) {
58 | var visible = screen.getAllDisplays().some(function (display) {
59 | return windowWithinBounds(windowState, display.bounds);
60 | });
61 | if (!visible) {
62 | // Window is partially or fully not visible now.
63 | // Reset it to safe defaults.
64 | return resetToDefaults(windowState);
65 | }
66 | return windowState;
67 | };
68 |
69 | var saveState = function () {
70 | if (!win.isMinimized() && !win.isMaximized()) {
71 | Object.assign(state, getCurrentPosition());
72 | }
73 | userDataDir.write(stateStoreFile, state, { atomic: true });
74 | };
75 |
76 | state = ensureVisibleOnSomeDisplay(restore());
77 |
78 | win = new BrowserWindow(Object.assign({}, options, state));
79 |
80 | win.on('close', saveState);
81 |
82 | return win;
83 | }
84 |
--------------------------------------------------------------------------------
/src/menu/dev_menu_template.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from 'electron';
2 |
3 | export var devMenuTemplate = {
4 | label: 'Development',
5 | submenu: [{
6 | label: 'Reload',
7 | accelerator: 'CmdOrCtrl+R',
8 | click: function () {
9 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache();
10 | }
11 | },{
12 | label: 'Toggle DevTools',
13 | accelerator: 'Alt+CmdOrCtrl+I',
14 | click: function () {
15 | BrowserWindow.getFocusedWindow().toggleDevTools();
16 | }
17 | },{
18 | label: 'Quit',
19 | accelerator: 'CmdOrCtrl+Q',
20 | click: function () {
21 | app.quit();
22 | }
23 | }]
24 | };
25 |
--------------------------------------------------------------------------------
/src/menu/edit_menu_template.js:
--------------------------------------------------------------------------------
1 | const {shell} = require('electron');
2 |
3 | export var editMenuTemplate = {
4 | label: 'Edit',
5 | submenu: [
6 | { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
7 | { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
8 | { type: "separator" },
9 | { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
10 | { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
11 | { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
12 | { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" },
13 | { type: "separator" },
14 | { label: "Github", click: () => { shell.openExternal('https://github.com/talhasch/ling') } }
15 | ]
16 | };
17 |
--------------------------------------------------------------------------------
/src/stylesheets/imports/bootstrap-override.scss:
--------------------------------------------------------------------------------
1 | .btn{
2 | &.btn-primary{
3 | background-color: #3498db;
4 | border-color: #3498db;
5 | }
6 | &.btn-success{
7 | background-color: #81b53e;
8 | border-color: #81b53e;
9 | }
10 | &.btn-warning{
11 | background-color: #f0ad4e;
12 | border-color: #f0ad4e;
13 | }
14 | &.btn-inverse{
15 | background-color: #3a444e;
16 | border-color: #3a444e;
17 | }
18 | &.btn-danger{
19 | background-color: #e74c3c;
20 | border-color: #e74c3c;
21 | }
22 | }
23 |
24 |
25 | textarea:hover,
26 | input:hover,
27 | textarea:active,
28 | input:active,
29 | textarea:focus,
30 | input:focus,
31 | button:focus,
32 | button:active,
33 | button:hover
34 | {
35 | outline:0px !important;
36 | -webkit-appearance:none;
37 | }
38 |
--------------------------------------------------------------------------------
/src/stylesheets/imports/history.scss:
--------------------------------------------------------------------------------
1 | .history{
2 | position: absolute;
3 | right:0;
4 | bottom:0;
5 | width: 50%;
6 | height: 30%;
7 | padding: 55px 10px 10px 10px;
8 | box-sizing: border-box;
9 |
10 | .history-container{
11 | height:100%;
12 |
13 | .history-table{
14 | height:100%;
15 | overflow: auto;
16 |
17 | table{
18 | tr{
19 | .apply{
20 | visibility: hidden;
21 | cursor: pointer;
22 | }
23 | th, td{
24 | padding: 3px;
25 | font-size: 12px;
26 | }
27 | }
28 |
29 | tr:hover{
30 | .apply{
31 | visibility: visible;
32 | }
33 | }
34 | }
35 | }
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/src/stylesheets/imports/layout.scss:
--------------------------------------------------------------------------------
1 | html,body{
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | body{
7 | background: #ecf0f1;
8 | }
9 |
10 | .wrapper{
11 | position: absolute;
12 | width: 100%;
13 | height: 100%;
14 | left: 0;
15 | top: 0;
16 | }
--------------------------------------------------------------------------------
/src/stylesheets/imports/mixins.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/src/stylesheets/imports/mixins.scss
--------------------------------------------------------------------------------
/src/stylesheets/imports/request.scss:
--------------------------------------------------------------------------------
1 |
2 | .request{
3 | position: absolute;
4 | left:0;
5 | top:0;
6 | width: 50%;
7 | height: 100%;
8 | padding: 55px 10px 10px 10px;
9 | box-sizing: border-box;
10 | background-color: #ecf0f5;
11 | border-right: 3px solid #d2d6de;
12 |
13 | .request-container{
14 | height:100%;
15 |
16 | /*
17 | row1 = url, run button
18 | row2 = method, content type
19 | row3 = tabs and tab contents (headers, raw payload, data, files, auth)
20 | */
21 |
22 | .row1, .row2{
23 | margin-bottom: 20px;
24 | }
25 |
26 | .row1{
27 | form{
28 | &.ng-submitted{
29 | input{
30 | &.ng-invalid{
31 | border-color: red;
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
38 | .row3{
39 | /* 130px = .row1's height + .row2's height (approximate) */
40 | height: calc(100% - 132px);
41 |
42 | .col{
43 | height: 100%;
44 | }
45 | }
46 |
47 | .tabs, .tab-content{
48 | width: 100%;
49 | margin-left: auto;
50 | margin-right: auto;
51 | }
52 |
53 | .tabs{
54 | height:41px;
55 |
56 | .btn {
57 | background-color: #2c3b41;
58 | color:#fdfdfd;
59 | border-width: 0;
60 | border-radius: 0;
61 | color:#839ca8;
62 | font-size:14px;
63 | border-bottom: 3px solid #1e282c;
64 |
65 | &.active{
66 | border-bottom: 3px solid #3c8dbc;
67 | color:#fff;
68 | }
69 |
70 | &:focus{
71 | outline: none;
72 | }
73 |
74 | .label{
75 | font-size: 12px;
76 | line-height: 100%;
77 | padding: 1px 2px;
78 | background-color: #2c3b41;
79 | color: #3c8dbc;
80 | visibility: hidden;
81 | margin-left: 2px;
82 | &.active{
83 | visibility: visible;
84 | }
85 | }
86 |
87 | &.active{
88 | .label{
89 | background-color: #fff;
90 | }
91 | }
92 | }
93 | }
94 |
95 | .tab-content{
96 | /* 38px = .tabs's height (approximate) */
97 | height: calc(100% - 41px);
98 | padding: 10px;
99 | overflow: auto;
100 | background-color: #58656b;
101 |
102 | textarea{
103 | height: 100%;
104 | resize: none;
105 | background: #f7f7f7;
106 | font-size: 16px;
107 | }
108 |
109 | .table{
110 |
111 | &> thead{
112 | &> tr{
113 | &> th{
114 | color: #ffffff;
115 | border-color: #374850;
116 | }
117 | }
118 | }
119 |
120 | &> tbody{
121 | &> tr{
122 | &> td{
123 | border-top: 1px solid #374850;
124 | color: #839ca8;
125 | word-wrap: break-word;
126 | overflow-wrap: break-word;
127 | }
128 | }
129 | }
130 |
131 | &> tfoot{
132 |
133 | &> tr{
134 | &> td{
135 | padding-top: 30px;
136 | border-top: 1px solid #374850;
137 | }
138 | }
139 | }
140 | }
141 |
142 | form{
143 |
144 | &.ng-submitted{
145 | input{
146 | &.ng-invalid{
147 | border-color: red;
148 | }
149 | }
150 | }
151 |
152 | .alert{
153 | padding:5px;
154 | margin: 4px 0;
155 | }
156 | }
157 | }
158 | }
159 | }
--------------------------------------------------------------------------------
/src/stylesheets/imports/response.scss:
--------------------------------------------------------------------------------
1 | .response{
2 | position: absolute;
3 | right:0;
4 | top:0;
5 | width: 50%;
6 | height: 70%;
7 | padding: 55px 10px 10px 10px;
8 | box-sizing: border-box;
9 | border-bottom: 3px solid #d2d6de;
10 |
11 | .response-container{
12 | height:100%;
13 |
14 | .nav-tabs{
15 | li{
16 | &.active{
17 | a{
18 | font-weight: 600;
19 | background-color: #ecf0f5;
20 | }
21 | }
22 | }
23 | }
24 |
25 | .tab-content{
26 | /* 42px = .nav-tabs's height */
27 | height: calc(100% - 42px);
28 | padding: 10px;
29 |
30 | box-sizing: border-box;
31 | background-color: #ecf0f5;
32 | border-left: 1px solid #ccc;
33 | border-right: 1px solid #ccc;
34 | border-bottom: 1px solid #ccc;
35 |
36 | &.tab-content-headers{
37 | .response-headers{
38 | height: 100%;
39 | overflow: auto;
40 |
41 | .status-text{
42 | font-size: 22px;
43 | margin-bottom: 10px;
44 | color: #333;
45 |
46 | &.success{
47 | color: green;
48 | }
49 |
50 | &.client-err{
51 | color: red;
52 | }
53 |
54 | &.server-err{
55 | color: red;
56 | }
57 | }
58 |
59 | .response-url{
60 | font-size: 16px;
61 | margin-bottom: 20px;
62 | max-width: 90%;
63 | white-space: nowrap;
64 | overflow: hidden;
65 | text-overflow: ellipsis;
66 | display: block;
67 | }
68 |
69 | .table{
70 | td{
71 | font-family: monospace
72 | }
73 | }
74 | }
75 | }
76 |
77 | &.tab-content-body{
78 | position: relative;
79 |
80 | .response-body{
81 | height: 100%;
82 |
83 | textarea{
84 | height: 100%;
85 | resize: none;
86 | background: #f7f7f7;
87 | font-size: 12px;
88 | }
89 |
90 | .btn-format{
91 | position: absolute;
92 | right: 10px;
93 | top: 10px;
94 | }
95 |
96 | }
97 |
98 | }
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/src/stylesheets/imports/utilities.scss:
--------------------------------------------------------------------------------
1 |
2 | .col-no-right-pad{
3 | padding-right: 0;
4 | }
5 |
6 | .col-no-left-pad{
7 | padding-left: 0;
8 | }
9 |
10 | .col-no-pad{
11 | padding-left: 0;
12 | padding-right: 0
13 | }
14 |
--------------------------------------------------------------------------------
/src/stylesheets/imports/variables.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/src/stylesheets/imports/variables.scss
--------------------------------------------------------------------------------
/src/stylesheets/style.scss:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,700&subset=latin,latin-ext);
2 |
3 | body {
4 | font-family: 'Roboto', sans-serif;
5 | }
6 |
7 |
8 | // Core variables and mixins
9 | @import "imports/variables";
10 | @import "imports/mixins";
11 |
12 | // Core layouts
13 | @import "imports/layout";
14 | @import "imports/utilities";
15 |
16 | @import "imports/bootstrap-override.scss";
17 |
18 | body{
19 | visibility: hidden;
20 | }
21 |
22 | .scope-header{
23 | position: absolute;
24 | left:0;
25 | top:0;
26 | right:0;
27 | background: #fff;
28 | color: #6e7882;
29 | box-sizing: border-box;
30 | height: 45px;
31 | padding: 10px 0 0 15px;
32 | font-size: 20px;
33 |
34 | &>.fa{
35 | margin-right: 10px;
36 | }
37 |
38 | .buttons{
39 | position: absolute;
40 | right: 10px;
41 | top: 5px;
42 | }
43 | }
44 |
45 | @import "imports/request.scss";
46 | @import "imports/response.scss";
47 | @import "imports/history.scss";
--------------------------------------------------------------------------------
/tasks/build_app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var sass = require('gulp-sass');
5 | var watch = require('gulp-watch');
6 | var batch = require('gulp-batch');
7 | var plumber = require('gulp-plumber');
8 | var jetpack = require('fs-jetpack');
9 | var bundle = require('./bundle');
10 | var utils = require('./utils');
11 |
12 | var projectDir = jetpack;
13 | var srcDir = jetpack.cwd('./src');
14 | var destDir = jetpack.cwd('./app');
15 |
16 | gulp.task('bundle', function () {
17 | return Promise.all([
18 | bundle(srcDir.path('background.js'), destDir.path('background.js')),
19 | bundle(srcDir.path('app.js'), destDir.path('app.js')),
20 | ]);
21 | });
22 |
23 | gulp.task('sass', function () {
24 | return gulp.src(srcDir.path('stylesheets/style.scss'))
25 | .pipe(plumber())
26 | .pipe(sass({outputStyle: 'compressed'}))
27 | .pipe(gulp.dest(destDir.path('stylesheets')));
28 | });
29 |
30 | gulp.task('environment', function () {
31 | var configFile = 'config/env_' + utils.getEnvName() + '.json';
32 | projectDir.copy(configFile, destDir.path('env.json'), { overwrite: true });
33 | });
34 |
35 | gulp.task('watch', function () {
36 | var beepOnError = function (done) {
37 | return function (err) {
38 | if (err) {
39 | utils.beepSound();
40 | }
41 | done(err);
42 | };
43 | };
44 |
45 | watch('src/**/*.js', batch(function (events, done) {
46 | gulp.start('bundle', beepOnError(done));
47 | }));
48 |
49 | watch('src/**/*.scss', batch(function (events, done) {
50 | gulp.start('sass', beepOnError(done));
51 | }));
52 | });
53 |
54 | gulp.task('build', ['bundle', 'sass', 'environment']);
55 |
--------------------------------------------------------------------------------
/tasks/build_tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var jetpack = require('fs-jetpack');
5 | var bundle = require('./bundle');
6 |
7 | // Spec files are scattered through the whole project. Here we're searching
8 | // for them and generate one entry file which will run all the tests.
9 | var generateEntryFile = function (dir, destFileName, filePattern) {
10 | var fileBanner = "// This file is generated automatically.\n"
11 | + "// All modifications will be lost.\n";
12 |
13 | return dir.findAsync('.', { matching: filePattern })
14 | .then(function (specPaths) {
15 | var fileContent = specPaths.map(function (path) {
16 | return 'import "./' + path.replace(/\\/g, '/') + '";';
17 | }).join('\n');
18 | return dir.writeAsync(destFileName, fileBanner + fileContent);
19 | })
20 | .then(function () {
21 | return dir.path(destFileName);
22 | });
23 | };
24 |
25 | gulp.task('build-unit', ['environment'], function () {
26 | var srcDir = jetpack.cwd('src');
27 | var destDir = jetpack.cwd('app');
28 |
29 | return generateEntryFile(srcDir, 'specs.js.autogenerated', '*.spec.js')
30 | .then(function (entryFilePath) {
31 | return bundle(entryFilePath, destDir.path('specs.js.autogenerated'));
32 | });
33 | });
34 |
35 | gulp.task('build-e2e', ['build'], function () {
36 | var srcDir = jetpack.cwd('e2e');
37 | var destDir = jetpack.cwd('app');
38 |
39 | return generateEntryFile(srcDir, 'e2e.js.autogenerated', '*.e2e.js')
40 | .then(function (entryFilePath) {
41 | return bundle(entryFilePath, destDir.path('e2e.js.autogenerated'));
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tasks/bundle.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var jetpack = require('fs-jetpack');
5 | var rollup = require('rollup').rollup;
6 |
7 | var nodeBuiltInModules = ['assert', 'buffer', 'child_process', 'cluster',
8 | 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events',
9 | 'fs', 'http', 'https', 'module', 'net', 'os', 'path', 'process', 'punycode',
10 | 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers',
11 | 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib'];
12 |
13 | var electronBuiltInModules = ['electron'];
14 |
15 | var npmModulesUsedInApp = function () {
16 | var appManifest = require('../app/package.json');
17 | return Object.keys(appManifest.dependencies);
18 | };
19 |
20 | var generateExternalModulesList = function () {
21 | return [].concat(nodeBuiltInModules, electronBuiltInModules, npmModulesUsedInApp());
22 | };
23 |
24 | var cached = {};
25 |
26 | module.exports = function (src, dest) {
27 | return rollup({
28 | entry: src,
29 | external: generateExternalModulesList(),
30 | cache: cached[src],
31 | })
32 | .then(function (bundle) {
33 | cached[src] = bundle;
34 |
35 | var jsFile = path.basename(dest);
36 | var result = bundle.generate({
37 | format: 'cjs',
38 | sourceMap: true,
39 | sourceMapFile: jsFile,
40 | });
41 | // Wrap code in self invoking function so the variables don't
42 | // pollute the global namespace.
43 | var isolatedCode = '(function () {' + result.code + '\n}());';
44 | return Promise.all([
45 | jetpack.writeAsync(dest, isolatedCode + '\n//# sourceMappingURL=' + jsFile + '.map'),
46 | jetpack.writeAsync(dest + '.map', result.map.toString()),
47 | ]);
48 | });
49 | };
50 |
--------------------------------------------------------------------------------
/tasks/start.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var childProcess = require('child_process');
4 | var electron = require('electron');
5 | var gulp = require('gulp');
6 |
7 | gulp.task('start', ['build', 'watch'], function () {
8 | childProcess.spawn(electron, ['./app'], {
9 | stdio: 'inherit'
10 | })
11 | .on('close', function () {
12 | // User closed the app. Kill the host process.
13 | process.exit();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/tasks/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var argv = require('yargs').argv;
4 |
5 | exports.getEnvName = function () {
6 | return argv.env || 'development';
7 | };
8 |
9 | exports.beepSound = function () {
10 | process.stdout.write('\u0007');
11 | };
12 |
--------------------------------------------------------------------------------