├── .babelrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── adb
├── adbkit-wrapper.js
├── device-manager.js
└── device.js
├── app
├── actions.js
├── app.jsx
├── devices-list.jsx
├── devices-table.jsx
├── footer.jsx
├── header.jsx
├── reducers
│ ├── devices.js
│ ├── index.js
│ └── navigation.js
├── sidebar.jsx
└── window.jsx
├── assets
├── icon.icns
├── icon.ico
├── icon.png
├── icon_background.png
├── icon_foreground.png
├── screenshot.png
├── screenshot_0.0.1.png
└── screenshot_0.0.2.png
├── background
├── index.html
└── index.js
├── config.js
├── dev.html
├── fonts
├── photon-entypo.eot
├── photon-entypo.svg
├── photon-entypo.ttf
└── photon-entypo.woff
├── index.html
├── index.js
├── index.scss
├── package.json
├── scripts
├── menu.js
└── watcher.js
├── starter.js
├── styles
├── photon.scss
└── theme.scss
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015"],
3 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,*.yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | *.swp
4 | app.js
5 | /out
6 | .DS_Store
7 | npm-debug.log
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@nexenio.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Please checkout the [open issues] and the [project] section. Looking forward to your pull requests!
2 |
3 | [open issues]: https://github.com/neXenio/adb-util/issues
4 | [project]: https://github.com/neXenio/adb-util/projects/1
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2017 neXenio GmbH
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/neXenio/adb-util) [](https://github.com/neXenio/adb-util/blob/master/LICENSE)
2 |
3 | # ADB Utility
4 |
5 | An [Electron] application for Android developers, providing a GUI for common [ADB] operations.
6 |
7 | ![screenshot]
8 |
9 | The tool offers some convenience utilities to speed up the development, especially when handling multiple devices. Features:
10 |
11 | - [x] Listing connected devices (`adb devices`)
12 | - [x] Discovering network devices with the ADB deamon (`nmap -p 5555`)
13 | - [ ] Connecting and disconnecting devices (`adb connect`, ...)
14 | - [ ] Toggling connections between USB and TCP (`adb usb`, ...)
15 | - [x] Executing shell commands (`adb shell`)
16 | - [x] Fetching features (`pm list features`)
17 | - [x] Fetching properties (`getprop`)
18 | - [x] Fetching settings (`settings get`)
19 | - [x] Fetching network configuration (`ip addr show wlan0`)
20 | - [ ] Starting and stopping applications (`am start`, ...)
21 | - [ ] Clearing application data (`pm clear`)
22 | - [ ] Grabbing screenshots (`screencap`)
23 | - [ ] Installing and uninstalling applications (`adb install`, ...)
24 | - [ ] Pushing and pulling files and folders
25 | - [ ] Viewing Logcat output (`adb logcat`)
26 |
27 | You can check the progress of new features in the [project] section.
28 |
29 | ## Requirements
30 |
31 | Make sure that `adb` is available in your `PATH`. You can check if that's the case by opening a terminal and executing `adb version`.
32 |
33 | ## Downloads
34 |
35 | If you don't want to build the project yourself, head over to the [releases] section and grab an executable from there.
36 |
37 | ## Development
38 |
39 | #### Deploying
40 |
41 | ```sh
42 | $ npm install # install dependencies
43 | $ npm start # support for reloading views, restarting electron
44 | ```
45 |
46 | #### Building Releases
47 |
48 | ```sh
49 | $ npm run build # all
50 | $ npm run build-osx # osx(64)
51 | $ npm run build-win # win(32, 64)
52 | ```
53 |
54 | #### Contributing
55 |
56 | Please checkout the [open issues] and the [project] section. Looking forward to your pull requests!
57 |
58 | ## Build With
59 |
60 | This tool is being built using [Electron], powered by [react] and [redux]. It heavily relies on the [adbkit] module.
61 |
62 | **Note** regarding code quality: This tool has been created by people that just got started with this technology stack. Although we try to follow best practices, you might not want to use this project as a reference.
63 |
64 | ## License
65 |
66 | [Apache License V2](LICENSE)
67 |
68 | Copyright © neXenio GmbH.
69 |
70 | [electron]: https://github.com/electron/electron
71 | [react]: https://github.com/facebook/react
72 | [redux]: https://github.com/reactjs/redux
73 | [adbkit]: https://github.com/openstf/adbkit
74 | [adb]: https://developer.android.com/studio/command-line/adb.html
75 | [screenshot]: assets/screenshot.png
76 | [project]: https://github.com/neXenio/adb-util/projects/1
77 | [open issues]: https://github.com/neXenio/adb-util/issues
78 | [releases]: https://github.com/neXenio/adb-util/releases
--------------------------------------------------------------------------------
/adb/adbkit-wrapper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const log = require('electron-log');
4 | const Promise = require('bluebird');
5 | const adb = require('adbkit');
6 | const client = adb.createClient();
7 | const toTitleCase = require('titlecase')
8 |
9 | const ipcMain = require('electron').ipcMain;
10 |
11 | ipcMain.on('adbkit-observe-devices', function(event, arg) {
12 | observeDevices(event, arg);
13 | });
14 |
15 | function observeDevices(event, arg) {
16 | log.debug(`Starting to observe device state changes`);
17 | client.trackDevices()
18 | .then(function(tracker) {
19 | tracker.on('add', function(device) {
20 | event.sender.send('adbkit-device-added', device);
21 | });
22 | tracker.on('remove', function(device) {
23 | event.sender.send('adbkit-device-removed', device);
24 | });
25 | tracker.on('end', function() {
26 | log.debug(`Device state observation stopped`);
27 | });
28 | })
29 | .catch(function(err) {
30 | log.warn('Unable to observe devices:', err.stack)
31 | });
32 | }
33 |
34 | ipcMain.on('adbkit-update-devices', function(event, arg) {
35 | updateDevices(event, arg);
36 | });
37 |
38 | function updateDevices(event, arg) {
39 | log.debug(`Updating device list`);
40 | client.listDevicesWithPaths()
41 | .then(function(devices) {
42 | event.sender.send('adbkit-devices-updated', devices);
43 | })
44 | .catch(function(err) {
45 | log.warn('Unable to get devices:', err.stack)
46 | event.sender.send('adbkit-devices-updated', []);
47 | });
48 | }
49 |
50 | ipcMain.on('adbkit-connect-device', function(event, arg) {
51 | connectDevice(event, arg);
52 | });
53 |
54 | function connectDevice(event, arg) {
55 | log.debug(`Connecting device: `, arg);
56 | client.connect(arg)
57 | .then(function(deviceId) {
58 | log.debug('Device connected:', deviceId)
59 | })
60 | .catch(function(err) {
61 | log.warn('Unable to connect device:', err.stack)
62 | });
63 | }
64 |
65 | ipcMain.on('adbkit-update-device', function(event, device) {
66 | updateDeviceFeatures(event, device);
67 | updateDeviceIpAddress(event, device);
68 | updateDeviceId(event, device);
69 | updateDeviceName(event, device);
70 | updateDeviceManufacturer(event, device);
71 | updateDeviceModel(event, device);
72 | });
73 |
74 | function updateDeviceFeatures(event, device) {
75 | log.debug(`Updating features of device: ${device.id}`);
76 | client.getFeatures(device.id)
77 | .then(function(features) {
78 | device.features = features;
79 | event.sender.send('adbkit-device-features-updated', device);
80 | })
81 | .catch(function(err) {
82 | log.warn('Unable to get device features:', err.stack)
83 | });
84 | }
85 |
86 | function updateDeviceIpAddress(event, device) {
87 | log.debug(`Updating IP address of device: ${device.id}`);
88 | runShellCommand(device, 'ip addr show wlan0 | grep \'inet \' | cut -d\' \' -f6|cut -d/ -f1')
89 | .then((output) => {
90 | device.ipAddress = output;
91 | event.sender.send('adbkit-device-updated', device);
92 | })
93 | .catch((err) => {
94 | log.warn('Unable to get device IP address:', err.stack)
95 | });
96 | }
97 |
98 | function updateDeviceId(event, device) {
99 | log.debug(`Updating Android ID of device: ${device.id}`);
100 | runShellCommand(device, createGetSettingCommand('secure', 'android_id'))
101 | .then((output) => {
102 | device.androidId = output;
103 | event.sender.send('adbkit-device-updated', device);
104 | })
105 | .catch((err) => {
106 | log.warn('Unable to get device ID:', err.stack)
107 | });
108 | }
109 |
110 | function updateDeviceName(event, device) {
111 | log.debug(`Updating name of device: ${device.id}`);
112 | runShellCommand(device, createGetSettingCommand('secure', 'bluetooth_name'))
113 | .then((output) => {
114 | device.bluetoothName = output;
115 | event.sender.send('adbkit-device-updated', device);
116 | })
117 | .catch((err) => {
118 | log.warn('Unable to get device name:', err.stack)
119 | });
120 | }
121 |
122 | function updateDeviceManufacturer(event, device) {
123 | log.debug(`Updating manufacturer of device: ${device.id}`);
124 | runShellCommand(device, createGetPropCommand('ro.product.manufacturer'))
125 | .then((output) => {
126 | device.manufacturer = toTitleCase(output.toLowerCase());
127 | event.sender.send('adbkit-device-updated', device);
128 | })
129 | .catch((err) => {
130 | log.warn('Unable to get device name:', err.stack)
131 | });
132 | }
133 |
134 | function updateDeviceModel(event, device) {
135 | log.debug(`Updating model of device: ${device.id}`);
136 | runShellCommand(device, createGetPropCommand('ro.product.model'))
137 | .then((output) => {
138 | device.model = toTitleCase(output.toLowerCase());
139 | event.sender.send('adbkit-device-updated', device);
140 | })
141 | .catch((err) => {
142 | log.warn('Unable to get device model:', err.stack)
143 | });
144 | }
145 |
146 | function runShellCommand(device, command) {
147 | log.debug(`Executing shell command on ${device.id}: ${command}`);
148 | return new Promise((resolve, reject) => {
149 | client.shell(device.id, command)
150 | .then(function(outputStream) {
151 | adb.util.readAll(outputStream)
152 | .then(function(outputBuffer) {
153 | let output = outputBuffer.toString('utf-8');
154 | // remove linebreaks from start and end
155 | output = output.replace(/^\s+|\s+$/g, '');
156 | resolve(output);
157 | });
158 | })
159 | .catch(reject);
160 | });
161 | }
162 |
163 | function createGetPropCommand(propertyId) {
164 | return `getprop | grep "${propertyId}" | cut -d ":" -f2 | cut -d "[" -f2 | cut -d "]" -f1`;
165 | }
166 |
167 | function createGetSettingCommand(namespace, settingId) {
168 | return `settings get ${namespace} "${settingId}"`;
169 | }
170 |
171 | module.exports = {
172 | updateDevices: updateDevices
173 | };
--------------------------------------------------------------------------------
/adb/device-manager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const electron = require('electron');
4 | const ipcRenderer = electron.ipcRenderer;
5 | const log = require('electron-log');
6 | const portscanner = require('portscanner');
7 | const ip = require('ip');
8 | const actions = require('../app/actions');
9 | const {Device} = require('./device');
10 |
11 | let instance = null;
12 |
13 | export class DeviceManager {
14 |
15 | constructor(dispatch) {
16 | if (!instance){
17 | this.deviceMap = new Map();
18 | this.adbKitListenersRegistered = false;
19 | instance = this;
20 | }
21 | instance.dispatch = dispatch;
22 |
23 | if (!instance.adbKitListenersRegistered) {
24 | instance.registerAdbKitListeners();
25 | }
26 | return instance;
27 | }
28 |
29 | registerAdbKitListeners() {
30 | log.debug("Registering ADB Kit IPC listeners");
31 |
32 | ipcRenderer.on('adbkit-device-added', (event, device) => {
33 | this.onDeviceAdded(device);
34 | });
35 |
36 | ipcRenderer.on('adbkit-device-removed', (event, device) => {
37 | this.onDeviceRemoved(device);
38 | });
39 |
40 | ipcRenderer.on('adbkit-device-updated', (event, device) => {
41 | this.onDeviceUpdated(device);
42 | });
43 |
44 | ipcRenderer.on('adbkit-devices-updated', (event, devices) => {
45 | devices.forEach((device) => this.onDeviceUpdated(device));
46 | });
47 |
48 | ipcRenderer.on('adbkit-device-features-updated', (event, device) => {
49 | this.onDeviceFeaturesUpdated(device);
50 | });
51 |
52 | ipcRenderer.on('portscan-requested', (event, args = {}) => {
53 | let ipAddress = args.ip || ip.address();
54 | let port = args.port || '5555';
55 | this.onPortscanRequested(ipAddress, port);
56 | });
57 |
58 | // tell the adbkit that we care about device updates
59 | ipcRenderer.send('adbkit-observe-devices');
60 | ipcRenderer.send('adbkit-update-devices');
61 |
62 | this.adbKitListenersRegistered = true;
63 | }
64 |
65 | getDevice(adbKitDevice) {
66 | let device = this.deviceMap.get(adbKitDevice.id);
67 | if (!device) {
68 | device = Device.fromAdbKitDevice(adbKitDevice);
69 | this.deviceMap.set(adbKitDevice.id, device);
70 | }
71 | return device;
72 | }
73 |
74 | onDeviceAdded(adbKitDevice) {
75 | let device = this.getDevice(adbKitDevice);
76 | Object.assign(device, adbKitDevice);
77 | device.onConnect();
78 | log.debug('Device added: ' + device.id);
79 | this.dispatch(actions.deviceAdded(device));
80 | //ipcRenderer.send('adbkit-update-device-features', device);
81 | ipcRenderer.send('adbkit-update-device', device);
82 | }
83 |
84 | onDeviceRemoved(adbKitDevice) {
85 | let device = this.getDevice(adbKitDevice);
86 | Object.assign(device, adbKitDevice);
87 | device.onDisconnect();
88 | log.debug('Device removed: ' + device.id);
89 | this.dispatch(actions.deviceRemoved(device));
90 | }
91 |
92 | onDeviceUpdated(adbKitDevice) {
93 | let device = this.getDevice(adbKitDevice);
94 | Object.assign(device, adbKitDevice);
95 | device.onPropertiesUpdated();
96 | log.debug('Device updated: ' + device.id);
97 | this.dispatch(actions.deviceUpdated(device));
98 | }
99 |
100 | onDeviceFeaturesUpdated(adbKitDevice) {
101 | let device = this.getDevice(adbKitDevice);
102 | Object.assign(device, adbKitDevice);
103 | device.onFeaturesUpdated(device.features);
104 | device.onPropertiesUpdated();
105 | log.debug('Device features updated: ' + device.id);
106 | this.dispatch(actions.deviceUpdated(device));
107 | }
108 |
109 | onPortscanRequested(ipAddress, port) {
110 | log.debug('Starting portscan');
111 | const baseIp = ipAddress.substring(0, ipAddress.lastIndexOf('.') + 1);
112 | for (let i = 0; i <= 255; i++) {
113 | const scannedIp = baseIp + i;
114 | portscanner.checkPortStatus(port, scannedIp)
115 | .then((status) => {
116 | if (status === 'open') {
117 | log.debug('Found open port at:', scannedIp);
118 | let device = Device.fromPortScan(scannedIp, port);
119 | this.onNetworkDeviceDiscovered(device);
120 | }
121 | }).catch((err) => {
122 | log.error('Unable to scan port:', err);
123 | });
124 | }
125 | }
126 |
127 | onNetworkDeviceDiscovered(device) {
128 | // check if we already know this device
129 | let existingDevice = this.deviceMap.get(device.id);
130 | if (!existingDevice) {
131 | // attempt to connect
132 | ipcRenderer.send('adbkit-connect-device', device.ipAddress);
133 | }
134 | }
135 |
136 | }
--------------------------------------------------------------------------------
/adb/device.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const log = require('electron-log');
4 |
5 | export class Device {
6 |
7 | constructor() {
8 | this.creationDate = new Date();
9 | }
10 |
11 | static fromPortScan(ipAddress, port) {
12 | let device = new Device();
13 | device.id = ipAddress + ':' + port;
14 | device.ipAddress = ipAddress;
15 | device.port = port;
16 | device.androidId = 'Unknown';
17 | device.hardwareType = device.getHardwareType();
18 | device.isConnected = false;
19 | device.readableState = device.getReadableState();
20 | return device;
21 | }
22 |
23 | static fromAdbKitDevice(adbKitDevice) {
24 | let device = new Device();
25 | device.id = adbKitDevice.id;
26 | device.androidId = 'Unknown';
27 | device.hardwareType = device.getHardwareType();
28 | device.path = adbKitDevice.path;
29 | device.onConnect();
30 | return device;
31 | }
32 |
33 | onConnect() {
34 | this.isConnected = true;
35 | this.readableState = this.getReadableState();
36 | this.connectionDate = new Date();
37 | }
38 |
39 | onDisconnect() {
40 | this.isConnected = false;
41 | this.readableState = this.getReadableState();
42 | this.disconnectionDate = new Date();
43 | // should we reset the path here?
44 | }
45 |
46 | onFeaturesUpdated(features) {
47 | this.features = features;
48 | this.hardwareType = this.getHardwareType();
49 | }
50 |
51 | onPropertiesUpdated() {
52 | log.debug(`${this.id} properties updated`);
53 | this.lastUpdateDate = new Date();
54 | this.hardwareType = this.getHardwareType();
55 | this.readableModel = this.getReadableModel();
56 | this.readableName = this.getReadableName();
57 | this.readableState = this.getReadableState();
58 | }
59 |
60 | getReadableName() {
61 | if (this.bluetoothName) {
62 | return this.bluetoothName;
63 | } else {
64 | return 'Unknown';
65 | }
66 | }
67 |
68 | getReadableState() {
69 | let readableState = '';
70 | if (this.isConnected) {
71 | readableState = 'Connected';
72 | if (this.path) {
73 | readableState += ' via ';
74 | if (this.path.indexOf('usb') != -1) {
75 | readableState += 'USB';
76 | } else {
77 | readableState += 'TCP';
78 | }
79 | }
80 | } else {
81 | readableState = 'Disconnected';
82 | }
83 | return readableState;
84 | }
85 |
86 | getReadableModel() {
87 | let readableModel = '';
88 | if (this.manufacturer && this.model) {
89 | // check if model name already contains the manufacturer name
90 | // e.g. when model is "Huawai Watch" and manufacturer is "Huawai"
91 | if (this.model.toLowerCase().indexOf(this.manufacturer.toLowerCase()) != -1) {
92 | readableModel = this.model
93 | } else {
94 | readableModel = `${this.manufacturer} ${this.model}`;
95 | }
96 | } else {
97 | if (this.model) {
98 | readableModel = this.model;
99 | }
100 | }
101 | if (readableModel.length == 0) {
102 | readableModel = 'Unknown';
103 | }
104 | return readableModel;
105 | }
106 |
107 | getHardwareType() {
108 | if (!this.features) {
109 | //log.debug('Unable to get hardware type, no features available');
110 | return 'Unknown';
111 | } else if (this.isPhone()) {
112 | return 'Phone';
113 | } else if (this.isTablet()) {
114 | return 'Tablet';
115 | } else if (this.isWatch()) {
116 | return 'Watch';
117 | } else if (this.isTelevision()) {
118 | return 'TV';
119 | } else if (this.isEmbedded()) {
120 | return 'Embedded';
121 | } else {
122 | return 'Unknown'
123 | }
124 | }
125 |
126 | isPhone() {
127 | return this.hasFeature('android.hardware.telephony');
128 | }
129 |
130 | isTablet() {
131 | // TODO: find a better way to detect tablets
132 | return !(this.isPhone() || this.isWatch() || this.isTelevision() || this.isEmbedded());
133 | }
134 |
135 | isWatch() {
136 | return this.hasFeature('android.hardware.type.watch');
137 | }
138 |
139 | isTelevision() {
140 | return this.hasFeature('android.hardware.type.television');
141 | }
142 |
143 | isEmbedded() {
144 | return this.hasFeature('android.hardware.type.embedded');
145 | }
146 |
147 | hasFeature(featureId) {
148 | if (!this.features) {
149 | return false;
150 | }
151 | return this.features[featureId];
152 | }
153 |
154 | }
--------------------------------------------------------------------------------
/app/actions.js:
--------------------------------------------------------------------------------
1 | export const deviceAdded = (device) => ({
2 | type: 'DEVICE_ADDED',
3 | payload: device
4 | });
5 |
6 | export const deviceRemoved = (device) => ({
7 | type: 'DEVICE_REMOVED',
8 | payload: device
9 | });
10 |
11 | export const deviceUpdated = (device) => ({
12 | type: 'DEVICE_UPDATED',
13 | payload: device
14 | });
15 |
16 | export const sidebarSelectionChanged = (item) => ({
17 | type: 'SIDEBAR_SELECTION_CHANGED',
18 | payload: item
19 | });
--------------------------------------------------------------------------------
/app/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactDom from "react-dom";
3 | import { Route } from 'react-router-dom';
4 | import { matchPath, withRouter } from 'react-router';
5 | import { Window, TitleBar, NavPane, NavPaneItem, Text } from 'react-desktop/macOs';
6 | import Header from "./header.jsx"
7 | import Footer from "./footer.jsx"
8 | import Sidebar from "./sidebar.jsx"
9 | import DevicesList from "./devices-list.jsx"
10 | import DevicesTable from "./devices-table.jsx"
11 |
12 | require('../index.scss');
13 |
14 | const routes = [{
15 | path: '/',
16 | exact: true,
17 | title: 'Home',
18 | //icon: Icons.welcomeIcon,
19 | component: DevicesList,
20 | }];
21 |
22 | class App extends Component {
23 |
24 | render() {
25 | return (
26 |
36 | );
37 | }
38 | }
39 |
40 | export default withRouter(App);
--------------------------------------------------------------------------------
/app/devices-list.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const electron = require('electron');
4 | const ipcRenderer = electron.ipcRenderer;
5 | import { connect } from 'react-redux'
6 | import {
7 | ListView,
8 | ListViewHeader,
9 | ListViewFooter,
10 | ListViewSection,
11 | ListViewSectionHeader,
12 | ListViewRow,
13 | ListViewSeparator,
14 | Text
15 | } from 'react-desktop/macOs';
16 |
17 | class DevicesList extends React.Component {
18 |
19 | constructor(props) {
20 | super(props);
21 | this.state = { selected: null };
22 | }
23 |
24 | componentDidMount() {
25 | //ipcRenderer.send('adbkit-update-devices');
26 | }
27 |
28 | render() {
29 | return (
30 |
31 |
32 |
33 | Order by name
34 |
35 |
36 | {this.props.devices.filter(
37 | (device) => device.isConnected
38 | ).map(
39 | (device) => this.renderDevice(device)
40 | )}
41 |
42 |
43 |
44 | {this.props.devices.filter(
45 | (device) => !device.isConnected
46 | ).map(
47 | (device) => this.renderDevice(device)
48 | )}
49 |
50 |
51 | Status
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | renderSectionHeader(title) {
59 | return (
60 |
61 | {title}
62 |
63 | );
64 | }
65 |
66 | renderDevice(device) {
67 | return (
68 | this.setState({ selected: device.id })}
71 | background={this.state.selected === device.id ? '#d8dadc' : null}
72 | >
73 |
79 | {device.name} ({device.id})
80 |
81 | );
82 | }
83 |
84 | }
85 |
86 | const mapStateToProps = (state) => {
87 | return {
88 | devices: Object.keys(state.devices).map(key => state.devices[key])
89 | };
90 | }
91 |
92 | const mapDispatchToProps = ({
93 |
94 | })
95 |
96 | export default connect(
97 | mapStateToProps,
98 | mapDispatchToProps
99 | )(DevicesList);
100 |
--------------------------------------------------------------------------------
/app/devices-table.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const log = require('electron-log');
3 | const electron = require('electron');
4 | const ipcRenderer = electron.ipcRenderer;
5 | import { connect } from 'react-redux'
6 |
7 | class DevicesTable extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | }
12 |
13 | componentDidMount() {
14 | window.setTimeout(() => {
15 | //this.requestDevicesUpdate();
16 | }, 1000);
17 | }
18 |
19 | render() {
20 | const selectedSidebarItem = this.props.navigation.selectedSidebarItem;
21 | const devices = this.props.devices.filter((device) => {
22 | switch (selectedSidebarItem.id) {
23 | case 'overview': return true;
24 | case 'phones': return device.isPhone();
25 | case 'wearables': return device.isWatch();
26 | case 'embedded': return device.isEmbedded();
27 | case 'televisions': return device.isTelevision();
28 | default: return false;
29 | }
30 | });
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | Serial |
38 | Android ID |
39 | Model |
40 | Name |
41 | Type |
42 | State |
43 |
44 |
45 |
46 | {devices.map((device,index) => )}
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | requestDevicesUpdate() {
54 | log.debug('Requesting devices update');
55 | ipcRenderer.send('adbkit-update-devices');
56 | }
57 |
58 | }
59 |
60 | class DevicesTableRow extends React.Component {
61 | render() {
62 | const device = this.props.device;
63 | return (
64 |
65 | {device.id} |
66 | {device.androidId} |
67 | {device.readableModel} |
68 | {device.readableName} |
69 | {device.hardwareType} |
70 | {device.readableState} |
71 |
72 | );
73 | }
74 | }
75 |
76 | const mapStateToProps = (state) => {
77 | return {
78 | devices: Object.keys(state.devices).map(key => state.devices[key]),
79 | navigation: state.navigation
80 | };
81 | }
82 |
83 | const mapDispatchToProps = ({
84 |
85 | })
86 |
87 | export default connect(
88 | mapStateToProps,
89 | mapDispatchToProps
90 | )(DevicesTable);
91 |
--------------------------------------------------------------------------------
/app/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | class Footer extends React.Component {
4 | render() {
5 | return (
6 |
9 | );
10 | }
11 | }
12 |
13 | export default Footer;
14 |
--------------------------------------------------------------------------------
/app/header.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const log = require('electron-log');
3 | const electron = require('electron');
4 | const ipcRenderer = electron.ipcRenderer;
5 | const shell = electron.shell;
6 | import { connect } from 'react-redux';
7 |
8 | class Header extends React.Component {
9 |
10 | constructor(props) {
11 | super(props);
12 | }
13 |
14 | render() {
15 | return (
16 |
42 | );
43 | }
44 |
45 | requestPortscan() {
46 | log.debug('Requesting portscan');
47 | electron.remote.getCurrentWindow().webContents.send('portscan-requested');
48 | }
49 |
50 | }
51 |
52 | const mapStateToProps = (state) => {
53 | return {
54 | navigation: state.navigation
55 | };
56 | }
57 |
58 | export default connect(
59 | mapStateToProps
60 | )(Header);
61 |
--------------------------------------------------------------------------------
/app/reducers/devices.js:
--------------------------------------------------------------------------------
1 | const log = require('electron-log');
2 |
3 | const devices = (state = {}, action) => {
4 | //log.debug(`Received action:\n${JSON.stringify(action, null, 2)}`);
5 | switch (action.type) {
6 | case 'DEVICE_ADDED':
7 | case 'DEVICE_REMOVED':
8 | case 'DEVICE_UPDATED':
9 | return Object.assign({}, state, {
10 | [action.payload.id]: action.payload
11 | });
12 | default:
13 | return state;
14 | }
15 | };
16 |
17 | export default devices;
--------------------------------------------------------------------------------
/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import devices from './devices.js'
3 | import navigation from './navigation.js'
4 |
5 | const app = combineReducers({
6 | devices, navigation
7 | })
8 |
9 | export default app
--------------------------------------------------------------------------------
/app/reducers/navigation.js:
--------------------------------------------------------------------------------
1 | const log = require('electron-log');
2 |
3 | const navigation = (state = {}, action) => {
4 | //log.debug(`Received navigation action:\n${JSON.stringify(action, null, 2)}`);
5 | switch (action.type) {
6 | case 'SIDEBAR_SELECTION_CHANGED':
7 | return Object.assign({}, state, {
8 | selectedSidebarItem: action.payload
9 | });
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | export default navigation;
--------------------------------------------------------------------------------
/app/sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from 'react-redux'
3 | const log = require('electron-log');
4 | const actions = require('./actions');
5 |
6 | class Sidebar extends React.Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | }
11 |
12 | render() {
13 | const selectedSidebarItem = this.props.navigation.selectedSidebarItem;
14 | return (
15 |
16 |
24 |
25 | );
26 | }
27 | }
28 |
29 | class SidebarItem extends React.Component {
30 | render() {
31 | const isSelected = this.props.id === this.props.selectedSidebarItem.id;
32 | return (
33 | this.props.onItemClick({ id: this.props.id, text: this.props.text})}>
34 |
35 | {this.props.text}
36 |
37 | );
38 | }
39 | }
40 |
41 | const mapStateToProps = (state, ownProps) => {
42 | return {
43 | navigation: state.navigation
44 | };
45 | }
46 |
47 | const mapDispatchToProps = (dispatch, ownProps) => {
48 | return {
49 | onItemClick: (item) => {
50 | log.debug(`Selected sidebar item: ${item.id}`);
51 | dispatch(actions.sidebarSelectionChanged(item))
52 | }
53 | }
54 | }
55 |
56 | export default connect(
57 | mapStateToProps,
58 | mapDispatchToProps
59 | )(Sidebar);
--------------------------------------------------------------------------------
/app/window.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { HashRouter as Router } from 'react-router-dom';
4 |
5 | import App from './app.jsx';
6 |
7 | import { createStore } from 'redux'
8 | import { Provider } from 'react-redux'
9 | import reducer from './reducers'
10 |
11 | const { DeviceManager } = require('../adb/device-manager');
12 |
13 |
14 | const initialState = {
15 | navigation: {
16 | selectedSidebarItem: {
17 | id: 'overview', name: 'Overview'
18 | }
19 | },
20 | devices: []
21 | };
22 | const store = createStore(reducer, initialState);
23 | const deviceManager = new DeviceManager(store.dispatch);
24 |
25 | ReactDOM.render(
26 |
27 |
28 |
29 |
30 |
31 | , document.querySelector("#main"));
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/icon_background.png
--------------------------------------------------------------------------------
/assets/icon_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/icon_foreground.png
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/screenshot.png
--------------------------------------------------------------------------------
/assets/screenshot_0.0.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/screenshot_0.0.1.png
--------------------------------------------------------------------------------
/assets/screenshot_0.0.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/assets/screenshot_0.0.2.png
--------------------------------------------------------------------------------
/background/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/background/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { ipcRenderer } = require('electron');
4 | const log = require('electron-log');
5 |
6 | window.onload = function () {
7 | log.debug("Background process started");
8 | ipcRenderer.on('background-start', (startTime) => {
9 |
10 | });
11 | };
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | 'port': 8576
5 | };
6 |
--------------------------------------------------------------------------------
/dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ADB Util
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/fonts/photon-entypo.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/fonts/photon-entypo.eot
--------------------------------------------------------------------------------
/fonts/photon-entypo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/fonts/photon-entypo.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/fonts/photon-entypo.ttf
--------------------------------------------------------------------------------
/fonts/photon-entypo.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neXenio/adb-util/df18d6c708e8e1397e3c64a7d11775ae128f662a/fonts/photon-entypo.woff
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ADB Util
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const electron = require('electron');
4 | const app = electron.app;
5 | const BrowserWindow = electron.BrowserWindow;
6 | const Menu = electron.Menu;
7 | const log = require('electron-log');
8 | const template = require('./scripts/menu');
9 | const adbkitWrapper = require('./adb/adbkit-wrapper');
10 |
11 | // adds debug features like hotkeys for triggering dev tools and reload
12 | require('electron-debug')();
13 |
14 |
15 |
16 | // prevent window being garbage collected
17 | let mainWindow;
18 | let backgroundWindow;
19 |
20 | var menu = Menu.buildFromTemplate(template);
21 |
22 | function createMainWindow() {
23 | log.debug('Creating main window');
24 | var opts = {
25 | width: 1000,
26 | height: 400,
27 | minWidth: 800,
28 | minHeight: 400,
29 | acceptFirstMouse: true,
30 | titleBarStyle: 'hidden',
31 | frame: false,
32 | icon: __dirname + '/assets/icon.ico'
33 | };
34 |
35 | if (process.env.DEV) {
36 | const is2nd = process.argv.indexOf('--2nd') >= 0;
37 | if (is2nd) {
38 | setOptsForDualScreen(opts);
39 | }
40 | }
41 |
42 | const win = new BrowserWindow(opts);
43 | if (process.env.DEV) {
44 | win.loadURL('http://localhost:8000/dev.html');
45 | win.openDevTools();
46 | } else {
47 | win.loadURL(`file://${__dirname}/index.html`);
48 | }
49 | win.on('closed', () => {
50 | mainWindow = null
51 | });
52 |
53 | if (menu) {
54 | Menu.setApplicationMenu(menu);
55 | menu = null;
56 | }
57 |
58 | return win;
59 | }
60 |
61 | function createBackgroundWindow() {
62 | log.debug('Creating background window');
63 | const win = new BrowserWindow({
64 | show: false
65 | });
66 | win.loadURL(`file://${__dirname}/background/index.html`);
67 | return win;
68 | }
69 |
70 | function setOptsForDualScreen(opts) {
71 | var atomScreen = electron.screen;
72 | var displays = atomScreen.getAllDisplays();
73 | var d2 = displays.length > 1 ? displays[1] : null;
74 | if (d2) {
75 | opts.x = d2.bounds.x + (d2.size.width - opts.width) / 2;
76 | opts.y = d2.bounds.y + (d2.size.height - opts.height) / 2;
77 | }
78 | }
79 |
80 | app.on('window-all-closed', () => {
81 | log.debug('All windows closed, quitting app');
82 | app.quit();
83 | });
84 |
85 | app.on('activate-with-no-open-windows', () => {
86 | if (!mainWindow) {
87 | mainWindow = createMainWindow();
88 | }
89 | });
90 |
91 | app.on('ready', () => {
92 | mainWindow = createMainWindow();
93 | backgroundWindow = createBackgroundWindow();
94 | //adb.updateDevices(mainWindow);
95 |
96 | if (process.env.DEV) {
97 | const watcher = require('./scripts/watcher.js');
98 | watcher.watch(app, ['./index.js', './scripts']);
99 | }
100 | });
101 |
--------------------------------------------------------------------------------
/index.scss:
--------------------------------------------------------------------------------
1 | @import 'styles/theme.scss';
2 | @import 'styles/photon.scss';
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adb-util",
3 | "productName": "ADB Util",
4 | "version": "0.0.3",
5 | "description": "An Electron application providing a GUI for common ADB operations",
6 | "license": "Apache-2.0",
7 | "repository": "nexenio/adb-util",
8 | "author": {
9 | "name": "Stephan Schultz",
10 | "email": "stephan.schultz@nexenio.com",
11 | "url": "https://nexenio.com"
12 | },
13 | "engines": {
14 | "node": ">=4"
15 | },
16 | "electronVersion": "1.7.8",
17 | "scripts": {
18 | "test": "xo",
19 | "start": "node starter.js --2nd",
20 | "start-1st": "node starter.js",
21 | "build": "npm run build-osx && npm run build-win",
22 | "build-osx": "NODE_ENV=production webpack -p --config webpack.config.js && electron-packager . $npm_package_productName --overwrite --out=dist --ignore='^/dist$' --prune --asar --platform=darwin --arch=x64 --version=$npm_package_electronVersion --icon=assets/icon.icns",
23 | "build-win": "NODE_ENV=production webpack -p --config webpack.config.js && electron-packager . $npm_package_productName --overwrite --out=dist --ignore='^/dist$' --prune --asar --platform=win32 --arch=all --version=$npm_package_electronVersion --icon=assets/icon.ico"
24 | },
25 | "keywords": [
26 | "adb",
27 | "android",
28 | "electron",
29 | "webpack",
30 | "react",
31 | "electron-app"
32 | ],
33 | "dependencies": {
34 | "adbkit": "^2.11.0",
35 | "electron-debug": "^1.4.0",
36 | "electron-log": "^2.2.9",
37 | "history": "^4.7.2",
38 | "ip": "^1.1.5",
39 | "object-assign": "^4.0.1",
40 | "portscanner": "^2.1.1",
41 | "react": "^15.6.1",
42 | "react-desktop": "^0.3.1",
43 | "react-dom": "^15.6.1",
44 | "react-redux": "^5.0.6",
45 | "react-router": "^4.2.0",
46 | "react-router-dom": "^4.2.2",
47 | "redux": "^3.7.2",
48 | "titlecase": "^1.1.2"
49 | },
50 | "devDependencies": {
51 | "babel-core": "^6.4.0",
52 | "babel-loader": "^7.1.2",
53 | "babel-preset-es2015": "^6.3.13",
54 | "babel-preset-react": "^6.3.13",
55 | "babel-preset-stage-1": "^6.24.1",
56 | "cross-spawn": "^5.1.0",
57 | "css-loader": "^0.28.7",
58 | "electron": "^1.7.8",
59 | "electron-packager": "^9.1.0",
60 | "electron-prebuilt": "^1.4.13",
61 | "file-loader": "^0.11.2",
62 | "json-loader": "^0.5.4",
63 | "node-sass": "^4.5.3",
64 | "sass-loader": "^6.0.6",
65 | "sleep": "^5.1.1",
66 | "style-loader": "^0.17.0",
67 | "url-loader": "^0.5.7",
68 | "webpack": "^3.6.0",
69 | "webpack-dev-server": "^2.8.2",
70 | "xo": "^0.18.2"
71 | },
72 | "xo": {
73 | "esnext": true,
74 | "envs": [
75 | "node",
76 | "browser"
77 | ]
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/scripts/menu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const electron = require('electron');
4 | const app = electron.app;
5 | const path = require('path');
6 | const dialog = electron.dialog;
7 | const NativeImage = electron.nativeImage;
8 |
9 | const appName = app.getName();
10 |
11 | const template = [{
12 | label: 'Edit',
13 | submenu: [{
14 | label: 'Undo',
15 | accelerator: 'CmdOrCtrl+Z',
16 | role: 'undo'
17 | }, {
18 | label: 'Redo',
19 | accelerator: 'Shift+CmdOrCtrl+Z',
20 | role: 'redo'
21 | }, {
22 | type: 'separator'
23 | }, {
24 | label: 'Cut',
25 | accelerator: 'CmdOrCtrl+X',
26 | role: 'cut'
27 | }, {
28 | label: 'Copy',
29 | accelerator: 'CmdOrCtrl+C',
30 | role: 'copy'
31 | }, {
32 | label: 'Paste',
33 | accelerator: 'CmdOrCtrl+V',
34 | role: 'paste'
35 | }, {
36 | label: 'Select All',
37 | accelerator: 'CmdOrCtrl+A',
38 | role: 'selectall'
39 | },
40 | ]}, {
41 | label: 'View',
42 | submenu: [{
43 | label: 'Reload',
44 | accelerator: 'CmdOrCtrl+R',
45 | click: function(item, focusedWindow) {
46 | if (focusedWindow)
47 | focusedWindow.reload();
48 | }
49 | }, {
50 | label: 'Toggle Full Screen',
51 | accelerator: (function() {
52 | if (process.platform == 'darwin')
53 | return 'Ctrl+Command+F';
54 | else
55 | return 'F11';
56 | })(),
57 | click: function(item, focusedWindow) {
58 | if (focusedWindow)
59 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
60 | }
61 | }, {
62 | label: 'Toggle Developer Tools',
63 | accelerator: (function() {
64 | if (process.platform == 'darwin')
65 | return 'Alt+Command+I';
66 | else
67 | return 'Ctrl+Shift+I';
68 | })(),
69 | click: function(item, focusedWindow) {
70 | if (focusedWindow)
71 | focusedWindow.toggleDevTools();
72 | }
73 | },
74 | ]}, {
75 | label: 'Window',
76 | role: 'window',
77 | submenu: [{
78 | label: 'Minimize',
79 | accelerator: 'CmdOrCtrl+M',
80 | role: 'minimize'
81 | }, {
82 | label: 'Close',
83 | accelerator: 'CmdOrCtrl+W',
84 | role: 'close'
85 | },
86 | ]}, {
87 | label: 'Help',
88 | role: 'help',
89 | submenu: [{
90 | label: 'Learn More',
91 | click: function() {
92 | require('shell').openExternal('https://github.com/importre/epp')
93 | }
94 | },
95 | ]}];
96 |
97 | var darwinMenu = [{
98 | label: appName,
99 | submenu: [{
100 | label: 'About ' + app.getName(),
101 | click: function(item, focusedWindow) {
102 | var file = path.resolve(__dirname, 'assets/epp.png');
103 | var appIcon = NativeImage.createFromPath(file);
104 | dialog.showMessageBox(focusedWindow, {
105 | 'type': 'info',
106 | 'title': app.getName(),
107 | 'message': app.getName() + ' ' + app.getVersion(),
108 | 'icon': appIcon,
109 | 'buttons': ['ok']
110 | });
111 | }
112 | }, {
113 | type: 'separator'
114 | }, {
115 | label: 'Hide',
116 | accelerator: 'Esc',
117 | selector: 'hide:'
118 | }, {
119 | type: 'separator'
120 | }, {
121 | label: 'Quit',
122 | accelerator: 'Cmd+Q',
123 | click() {
124 | app.quit();
125 | }
126 | }]
127 | }]
128 |
129 | var menu = template;
130 | if (process.platform == 'darwin') {
131 | menu = darwinMenu.concat(template)
132 | }
133 |
134 | module.exports = menu;
135 |
--------------------------------------------------------------------------------
/scripts/watcher.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const cp = require('child_process');
5 | const net = require('net');
6 | const config = require('../config');
7 |
8 | var client = null;
9 |
10 | function connect() {
11 | return net.connect({port: config.port}, function () {
12 | if (client !== null) {
13 | client.write('hello');
14 | }
15 | });
16 | }
17 |
18 | function start(app) {
19 | if (client === null) {
20 | client = connect();
21 | client.on('data', function (data) {
22 | app.quit();
23 | client.end();
24 | client = null;
25 | });
26 | }
27 | }
28 |
29 | function watch(app, files) {
30 | const opts = { persistent: true, recursive: true };
31 | files.forEach(file => {
32 | fs.watch(file, opts, function (event, filename) {
33 | start(app);
34 | });
35 | });
36 | }
37 |
38 | module.exports = {
39 | 'watch': watch
40 | };
41 |
--------------------------------------------------------------------------------
/starter.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const net = require('net');
6 | const path = require('path');
7 | //const spawn = require('child_process').spawn;
8 | const spawn = require('cross-spawn');
9 | const sleep = require('sleep').sleep;
10 | const config = require('./config');
11 | const win32 = process.platform === 'win32';
12 |
13 | var child = null;
14 |
15 | function getElectronArgs() {
16 | var args = ['.'];
17 | if (process.argv.indexOf('--2nd') != -1) {
18 | args.push('--2nd');
19 | }
20 | return args
21 | }
22 |
23 | const server = net.createServer(function (c) {
24 | if (child) {
25 | if (win32) {
26 | spawn("taskkill", ["/pid", child.pid, '/f', '/t']);
27 | } else {
28 | child.kill();
29 | }
30 |
31 | delete process.env.DEV;
32 | }
33 |
34 | process.env.DEV = 1
35 | var cmd = win32 ? 'electron.cmd' : 'electron';
36 | child = spawn(cmd, getElectronArgs());
37 | child.stdout.on('data', function (data) {
38 | console.log(data + '');
39 | });
40 |
41 | child.stderr.on('data', function (data) {
42 | console.log(data + '');
43 | });
44 |
45 | child.on('close', function (code) {
46 | console.log('child process exited with code ' + code);
47 | });
48 |
49 | c.write('world');
50 | });
51 |
52 | server.listen(config.port, function (e) {
53 | const client = net.connect({port: config.port}, function () {
54 | const args = ['--config', 'webpack.config.js', '--hot',
55 | '--port', 8000, '--inline'];
56 | spawn('webpack-dev-server', args, {
57 | stdio: 'inherit'
58 | });
59 | sleep(1);
60 | client.write('hello');
61 | });
62 |
63 | client.on('data', function (data) {
64 | client.end();
65 | });
66 | });
67 |
68 | server.on('error', function (e) {
69 | });
70 |
--------------------------------------------------------------------------------
/styles/photon.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * =====================================================
3 | * Photon v0.1.2
4 | * Copyright 2016 Connor Sears
5 | * Licensed under MIT (https://github.com/connors/proton/blob/master/LICENSE)
6 | *
7 | * v0.1.2 designed by @connors.
8 | * =====================================================
9 | */
10 |
11 | @charset "UTF-8";
12 | audio,
13 | canvas,
14 | progress,
15 | video {
16 | vertical-align: baseline;
17 | }
18 |
19 | audio:not([controls]) {
20 | display: none;
21 | }
22 |
23 | a:active,
24 | a:hover {
25 | outline: 0;
26 | }
27 |
28 | abbr[title] {
29 | border-bottom: 1px dotted;
30 | }
31 |
32 | b,
33 | strong {
34 | font-weight: bold;
35 | }
36 |
37 | dfn {
38 | font-style: italic;
39 | }
40 |
41 | h1 {
42 | font-size: 2em;
43 | margin: 0.67em 0;
44 | }
45 |
46 | small {
47 | font-size: 80%;
48 | }
49 |
50 | sub,
51 | sup {
52 | font-size: 75%;
53 | line-height: 0;
54 | position: relative;
55 | vertical-align: baseline;
56 | }
57 |
58 | sup {
59 | top: -0.5em;
60 | }
61 |
62 | sub {
63 | bottom: -0.25em;
64 | }
65 |
66 | pre {
67 | overflow: auto;
68 | }
69 |
70 | code,
71 | kbd,
72 | pre,
73 | samp {
74 | font-family: monospace, monospace;
75 | font-size: 1em;
76 | }
77 |
78 | button,
79 | input,
80 | optgroup,
81 | select,
82 | textarea {
83 | color: inherit;
84 | font: inherit;
85 | margin: 0;
86 | }
87 |
88 | input[type="number"]::-webkit-inner-spin-button,
89 | input[type="number"]::-webkit-outer-spin-button {
90 | height: auto;
91 | }
92 |
93 | input[type="search"] {
94 | -webkit-appearance: textfield;
95 | box-sizing: content-box;
96 | }
97 |
98 | input[type="search"]::-webkit-search-cancel-button,
99 | input[type="search"]::-webkit-search-decoration {
100 | -webkit-appearance: none;
101 | }
102 |
103 | fieldset {
104 | border: 1px solid #c0c0c0;
105 | margin: 0 2px;
106 | padding: 0.35em 0.625em 0.75em;
107 | }
108 |
109 | legend {
110 | border: 0;
111 | padding: 0;
112 | }
113 |
114 | table {
115 | border-collapse: collapse;
116 | border-spacing: 0;
117 | }
118 |
119 | td,
120 | th {
121 | padding: 0;
122 | }
123 |
124 | * {
125 | cursor: default;
126 | -webkit-user-select: none;
127 | }
128 |
129 | input,
130 | textarea {
131 | -webkit-user-select: text;
132 | }
133 |
134 | form,
135 | input,
136 | optgroup,
137 | select,
138 | textarea {
139 | -webkit-user-select: text;
140 | -webkit-app-region: no-drag;
141 | }
142 |
143 | * {
144 | -webkit-box-sizing: border-box;
145 | box-sizing: border-box;
146 | }
147 |
148 | html {
149 | height: 100%;
150 | width: 100%;
151 | overflow: hidden;
152 | }
153 |
154 | body {
155 | height: 100%;
156 | padding: 0;
157 | margin: 0;
158 | font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
159 | font-size: 13px;
160 | line-height: 1.6;
161 | color: #333;
162 | background-color: transparent;
163 | }
164 |
165 | hr {
166 | margin: 15px 0;
167 | overflow: hidden;
168 | background: transparent;
169 | border: 0;
170 | border-bottom: 1px solid #ddd;
171 | }
172 |
173 | h1, h2, h3, h4, h5, h6 {
174 | margin-top: 20px;
175 | margin-bottom: 10px;
176 | font-weight: 500;
177 | white-space: nowrap;
178 | overflow: hidden;
179 | text-overflow: ellipsis;
180 | }
181 |
182 | h1 {
183 | font-size: 36px;
184 | }
185 |
186 | h2 {
187 | font-size: 30px;
188 | }
189 |
190 | h3 {
191 | font-size: 24px;
192 | }
193 |
194 | h4 {
195 | font-size: 18px;
196 | }
197 |
198 | h5 {
199 | font-size: 14px;
200 | }
201 |
202 | h6 {
203 | font-size: 12px;
204 | }
205 |
206 | .window {
207 | position: absolute;
208 | top: 0;
209 | right: 0;
210 | bottom: 0;
211 | left: 0;
212 | display: flex;
213 | flex-direction: column;
214 | background-color: #fff;
215 | }
216 |
217 | .window-content {
218 | position: relative;
219 | overflow-y: auto;
220 | display: flex;
221 | flex: 1;
222 | }
223 |
224 | .selectable-text {
225 | cursor: text;
226 | -webkit-user-select: text;
227 | }
228 |
229 | .text-center {
230 | text-align: center;
231 | }
232 |
233 | .text-right {
234 | text-align: right;
235 | }
236 |
237 | .text-left {
238 | text-align: left;
239 | }
240 |
241 | .pull-left {
242 | float: left;
243 | }
244 |
245 | .pull-right {
246 | float: right;
247 | }
248 |
249 | .padded {
250 | padding: 10px;
251 | }
252 |
253 | .padded-less {
254 | padding: 5px;
255 | }
256 |
257 | .padded-more {
258 | padding: 20px;
259 | }
260 |
261 | .padded-vertically {
262 | padding-top: 10px;
263 | padding-bottom: 10px;
264 | }
265 |
266 | .padded-vertically-less {
267 | padding-top: 5px;
268 | padding-bottom: 5px;
269 | }
270 |
271 | .padded-vertically-more {
272 | padding-top: 20px;
273 | padding-bottom: 20px;
274 | }
275 |
276 | .padded-horizontally {
277 | padding-right: 10px;
278 | padding-left: 10px;
279 | }
280 |
281 | .padded-horizontally-less {
282 | padding-right: 5px;
283 | padding-left: 5px;
284 | }
285 |
286 | .padded-horizontally-more {
287 | padding-right: 20px;
288 | padding-left: 20px;
289 | }
290 |
291 | .padded-top {
292 | padding-top: 10px;
293 | }
294 |
295 | .padded-top-less {
296 | padding-top: 5px;
297 | }
298 |
299 | .padded-top-more {
300 | padding-top: 20px;
301 | }
302 |
303 | .padded-bottom {
304 | padding-bottom: 10px;
305 | }
306 |
307 | .padded-bottom-less {
308 | padding-bottom: 5px;
309 | }
310 |
311 | .padded-bottom-more {
312 | padding-bottom: 20px;
313 | }
314 |
315 | .sidebar {
316 | background-color: #f5f5f4;
317 | }
318 |
319 | .draggable {
320 | -webkit-app-region: drag;
321 | }
322 |
323 | .not-draggable {
324 | -webkit-app-region: no-drag;
325 | }
326 |
327 | .clearfix:before, .clearfix:after {
328 | display: table;
329 | content: " ";
330 | }
331 | .clearfix:after {
332 | clear: both;
333 | }
334 |
335 | .btn {
336 | display: inline-block;
337 | padding: 3px 8px;
338 | margin-bottom: 0;
339 | font-size: 12px;
340 | line-height: 1.4;
341 | text-align: center;
342 | white-space: nowrap;
343 | vertical-align: middle;
344 | cursor: default;
345 | background-image: none;
346 | border: 1px solid transparent;
347 | border-radius: 4px;
348 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
349 | -webkit-app-region: no-drag;
350 | }
351 | .btn:focus {
352 | outline: none;
353 | box-shadow: none;
354 | }
355 |
356 | .btn-mini {
357 | padding: 2px 6px;
358 | }
359 |
360 | .btn-large {
361 | padding: 6px 12px;
362 | }
363 |
364 | .btn-form {
365 | padding-right: 20px;
366 | padding-left: 20px;
367 | }
368 |
369 | .btn-default {
370 | color: #333;
371 | border-top-color: #c2c0c2;
372 | border-right-color: #c2c0c2;
373 | border-bottom-color: #a19fa1;
374 | border-left-color: #c2c0c2;
375 | background-color: #fcfcfc;
376 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fcfcfc), color-stop(100%, #f1f1f1));
377 | background-image: -webkit-linear-gradient(top, #fcfcfc 0%, #f1f1f1 100%);
378 | background-image: linear-gradient(to bottom, #fcfcfc 0%, #f1f1f1 100%);
379 | }
380 | .btn-default:active {
381 | background-color: #ddd;
382 | background-image: none;
383 | }
384 |
385 | .btn-primary,
386 | .btn-positive,
387 | .btn-negative,
388 | .btn-warning {
389 | color: #fff;
390 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
391 | }
392 |
393 | .btn-primary {
394 | border-color: #388df8;
395 | border-bottom-color: #0866dc;
396 | background-color: #6eb4f7;
397 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #6eb4f7), color-stop(100%, #1a82fb));
398 | background-image: -webkit-linear-gradient(top, #6eb4f7 0%, #1a82fb 100%);
399 | background-image: linear-gradient(to bottom, #6eb4f7 0%, #1a82fb 100%);
400 | }
401 | .btn-primary:active {
402 | background-color: #3e9bf4;
403 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3e9bf4), color-stop(100%, #0469de));
404 | background-image: -webkit-linear-gradient(top, #3e9bf4 0%, #0469de 100%);
405 | background-image: linear-gradient(to bottom, #3e9bf4 0%, #0469de 100%);
406 | }
407 |
408 | .btn-positive {
409 | border-color: #29a03b;
410 | border-bottom-color: #248b34;
411 | background-color: #5bd46d;
412 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bd46d), color-stop(100%, #29a03b));
413 | background-image: -webkit-linear-gradient(top, #5bd46d 0%, #29a03b 100%);
414 | background-image: linear-gradient(to bottom, #5bd46d 0%, #29a03b 100%);
415 | }
416 | .btn-positive:active {
417 | background-color: #34c84a;
418 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #34c84a), color-stop(100%, #248b34));
419 | background-image: -webkit-linear-gradient(top, #34c84a 0%, #248b34 100%);
420 | background-image: linear-gradient(to bottom, #34c84a 0%, #248b34 100%);
421 | }
422 |
423 | .btn-negative {
424 | border-color: #fb2f29;
425 | border-bottom-color: #fb1710;
426 | background-color: #fd918d;
427 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fd918d), color-stop(100%, #fb2f29));
428 | background-image: -webkit-linear-gradient(top, #fd918d 0%, #fb2f29 100%);
429 | background-image: linear-gradient(to bottom, #fd918d 0%, #fb2f29 100%);
430 | }
431 | .btn-negative:active {
432 | background-color: #fc605b;
433 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fc605b), color-stop(100%, #fb1710));
434 | background-image: -webkit-linear-gradient(top, #fc605b 0%, #fb1710 100%);
435 | background-image: linear-gradient(to bottom, #fc605b 0%, #fb1710 100%);
436 | }
437 |
438 | .btn-warning {
439 | border-color: #fcaa0e;
440 | border-bottom-color: #ee9d02;
441 | background-color: #fece72;
442 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fece72), color-stop(100%, #fcaa0e));
443 | background-image: -webkit-linear-gradient(top, #fece72 0%, #fcaa0e 100%);
444 | background-image: linear-gradient(to bottom, #fece72 0%, #fcaa0e 100%);
445 | }
446 | .btn-warning:active {
447 | background-color: #fdbc40;
448 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fdbc40), color-stop(100%, #ee9d02));
449 | background-image: -webkit-linear-gradient(top, #fdbc40 0%, #ee9d02 100%);
450 | background-image: linear-gradient(to bottom, #fdbc40 0%, #ee9d02 100%);
451 | }
452 |
453 | .btn .icon {
454 | float: left;
455 | width: 14px;
456 | height: 14px;
457 | margin-top: 1px;
458 | margin-bottom: 1px;
459 | color: #737475;
460 | font-size: 14px;
461 | line-height: 1;
462 | }
463 |
464 | .btn .icon-text {
465 | margin-right: 5px;
466 | }
467 |
468 | .btn-dropdown:after {
469 | font-family: "photon-entypo";
470 | margin-left: 5px;
471 | content: '\e873';
472 | }
473 |
474 | .btn-group {
475 | position: relative;
476 | display: inline-block;
477 | vertical-align: middle;
478 | -webkit-app-region: no-drag;
479 | }
480 | .btn-group .btn {
481 | position: relative;
482 | float: left;
483 | }
484 | .btn-group .btn:focus, .btn-group .btn:active {
485 | z-index: 2;
486 | }
487 | .btn-group .btn.active {
488 | z-index: 3;
489 | }
490 |
491 | .btn-group .btn + .btn,
492 | .btn-group .btn + .btn-group,
493 | .btn-group .btn-group + .btn,
494 | .btn-group .btn-group + .btn-group {
495 | margin-left: -1px;
496 | }
497 | .btn-group > .btn:first-child {
498 | border-top-right-radius: 0;
499 | border-bottom-right-radius: 0;
500 | }
501 | .btn-group > .btn:last-child {
502 | border-top-left-radius: 0;
503 | border-bottom-left-radius: 0;
504 | }
505 | .btn-group > .btn:not(:first-child):not(:last-child) {
506 | border-radius: 0;
507 | }
508 | .btn-group .btn + .btn {
509 | border-left: 1px solid #c2c0c2;
510 | }
511 | .btn-group .btn + .btn.active {
512 | border-left: 0;
513 | }
514 | .btn-group .active {
515 | color: #fff;
516 | border: 1px solid transparent;
517 | background-color: #6d6c6d;
518 | background-image: none;
519 | }
520 | .btn-group .active .icon {
521 | color: #fff;
522 | }
523 |
524 | .toolbar {
525 | min-height: 22px;
526 | box-shadow: inset 0 1px 0 #f5f4f5;
527 | background-color: #e8e6e8;
528 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #e8e6e8), color-stop(100%, #d1cfd1));
529 | background-image: -webkit-linear-gradient(top, #e8e6e8 0%, #d1cfd1 100%);
530 | background-image: linear-gradient(to bottom, #e8e6e8 0%, #d1cfd1 100%);
531 | }
532 | .toolbar:before, .toolbar:after {
533 | display: table;
534 | content: " ";
535 | }
536 | .toolbar:after {
537 | clear: both;
538 | }
539 |
540 | .toolbar-header {
541 | border-bottom: 1px solid #c2c0c2;
542 | }
543 | .toolbar-header .title {
544 | margin-top: 1px;
545 | }
546 |
547 | .toolbar-footer {
548 | border-top: 1px solid #c2c0c2;
549 | -webkit-app-region: drag;
550 | }
551 |
552 | .title {
553 | margin: 0;
554 | font-size: 12px;
555 | font-weight: 400;
556 | text-align: center;
557 | color: #555;
558 | cursor: default;
559 | }
560 |
561 | .toolbar-borderless {
562 | border-top: 0;
563 | border-bottom: 0;
564 | }
565 |
566 | .toolbar-actions {
567 | margin-top: 4px;
568 | margin-bottom: 3px;
569 | padding-right: 3px;
570 | padding-left: 3px;
571 | padding-bottom: 3px;
572 | -webkit-app-region: drag;
573 | }
574 | .toolbar-actions:before, .toolbar-actions:after {
575 | display: table;
576 | content: " ";
577 | }
578 | .toolbar-actions:after {
579 | clear: both;
580 | }
581 | .toolbar-actions > .btn,
582 | .toolbar-actions > .btn-group {
583 | margin-left: 4px;
584 | margin-right: 4px;
585 | }
586 |
587 | label {
588 | display: inline-block;
589 | font-size: 13px;
590 | margin-bottom: 5px;
591 | white-space: nowrap;
592 | overflow: hidden;
593 | text-overflow: ellipsis;
594 | }
595 |
596 | input[type="search"] {
597 | box-sizing: border-box;
598 | }
599 |
600 | input[type="radio"],
601 | input[type="checkbox"] {
602 | margin: 4px 0 0;
603 | line-height: normal;
604 | }
605 |
606 | .form-control {
607 | display: inline-block;
608 | width: 100%;
609 | min-height: 25px;
610 | padding: 5px 10px;
611 | font-size: 13px;
612 | line-height: 1.6;
613 | background-color: #fff;
614 | border: 1px solid #ddd;
615 | border-radius: 4px;
616 | outline: none;
617 | }
618 | .form-control:focus {
619 | border-color: #6db3fd;
620 | box-shadow: 0 0 0 3px #6db3fd;
621 | }
622 |
623 | textarea {
624 | height: auto;
625 | }
626 |
627 | .form-group {
628 | margin-bottom: 10px;
629 | }
630 |
631 | .radio,
632 | .checkbox {
633 | position: relative;
634 | display: block;
635 | margin-top: 10px;
636 | margin-bottom: 10px;
637 | }
638 | .radio label,
639 | .checkbox label {
640 | padding-left: 20px;
641 | margin-bottom: 0;
642 | font-weight: normal;
643 | }
644 |
645 | .radio input[type="radio"],
646 | .radio-inline input[type="radio"],
647 | .checkbox input[type="checkbox"],
648 | .checkbox-inline input[type="checkbox"] {
649 | position: absolute;
650 | margin-left: -20px;
651 | margin-top: 4px;
652 | }
653 |
654 | .form-actions .btn {
655 | margin-right: 10px;
656 | }
657 | .form-actions .btn:last-child {
658 | margin-right: 0;
659 | }
660 |
661 | .pane-group {
662 | position: absolute;
663 | top: 0;
664 | right: 0;
665 | bottom: 0;
666 | left: 0;
667 | display: flex;
668 | }
669 |
670 | .pane {
671 | position: relative;
672 | overflow-y: auto;
673 | flex: 1;
674 | border-left: 1px solid #ddd;
675 | }
676 | .pane:first-child {
677 | border-left: 0;
678 | }
679 |
680 | .pane-sm {
681 | max-width: 220px;
682 | min-width: 150px;
683 | }
684 |
685 | .pane-mini {
686 | width: 80px;
687 | flex: none;
688 | }
689 |
690 | .pane-one-fourth {
691 | width: 25%;
692 | flex: none;
693 | }
694 |
695 | .pane-one-third {
696 | width: 33.3%;
697 | flex: none;
698 | }
699 |
700 | img {
701 | -webkit-user-drag: text;
702 | }
703 |
704 | .img-circle {
705 | border-radius: 50%;
706 | }
707 |
708 | .img-rounded {
709 | border-radius: 4px;
710 | }
711 |
712 | .list-group {
713 | width: 100%;
714 | list-style: none;
715 | margin: 0;
716 | padding: 0;
717 | }
718 | .list-group * {
719 | margin: 0;
720 | white-space: nowrap;
721 | overflow: hidden;
722 | text-overflow: ellipsis;
723 | }
724 |
725 | .list-group-item {
726 | padding: 10px;
727 | font-size: 12px;
728 | color: #414142;
729 | border-top: 1px solid #ddd;
730 | }
731 | .list-group-item:first-child {
732 | border-top: 0;
733 | }
734 | .list-group-item.active, .list-group-item.selected {
735 | color: #fff;
736 | background-color: #116cd6;
737 | }
738 |
739 | .list-group-header {
740 | padding: 10px;
741 | }
742 |
743 | .media-object {
744 | margin-top: 3px;
745 | }
746 |
747 | .media-object.pull-left {
748 | margin-right: 10px;
749 | }
750 |
751 | .media-object.pull-right {
752 | margin-left: 10px;
753 | }
754 |
755 | .media-body {
756 | overflow: hidden;
757 | }
758 |
759 | .nav-group {
760 | font-size: 14px;
761 | }
762 |
763 | .nav-group-item {
764 | padding: 2px 10px 2px 25px;
765 | display: block;
766 | color: #333;
767 | text-decoration: none;
768 | white-space: nowrap;
769 | overflow: hidden;
770 | text-overflow: ellipsis;
771 | }
772 | .nav-group-item:active, .nav-group-item.active {
773 | background-color: #dcdfe1;
774 | }
775 | .nav-group-item .icon {
776 | width: 19px;
777 | height: 18px;
778 | float: left;
779 | color: #737475;
780 | margin-top: -3px;
781 | margin-right: 7px;
782 | font-size: 18px;
783 | text-align: center;
784 | }
785 |
786 | .nav-group-title {
787 | margin: 0;
788 | padding: 10px 10px 2px;
789 | font-size: 12px;
790 | font-weight: 500;
791 | color: #666666;
792 | }
793 |
794 | @font-face {
795 | font-family: "photon-entypo";
796 | src: url("fonts/photon-entypo.eot");
797 | src: url("fonts/photon-entypo.eot?#iefix") format("eot"), url("fonts/photon-entypo.woff") format("woff"), url("fonts/photon-entypo.ttf") format("truetype");
798 | font-weight: normal;
799 | font-style: normal;
800 | }
801 | .icon:before {
802 | position: relative;
803 | display: inline-block;
804 | font-family: "photon-entypo";
805 | speak: none;
806 | font-size: 100%;
807 | font-style: normal;
808 | font-weight: normal;
809 | font-variant: normal;
810 | text-transform: none;
811 | line-height: 1;
812 | -webkit-font-smoothing: antialiased;
813 | -moz-osx-font-smoothing: grayscale;
814 | }
815 |
816 | .icon-note:before {
817 | content: '\e800';
818 | }
819 |
820 | /* '' */
821 | .icon-note-beamed:before {
822 | content: '\e801';
823 | }
824 |
825 | /* '' */
826 | .icon-music:before {
827 | content: '\e802';
828 | }
829 |
830 | /* '' */
831 | .icon-search:before {
832 | content: '\e803';
833 | }
834 |
835 | /* '' */
836 | .icon-flashlight:before {
837 | content: '\e804';
838 | }
839 |
840 | /* '' */
841 | .icon-mail:before {
842 | content: '\e805';
843 | }
844 |
845 | /* '' */
846 | .icon-heart:before {
847 | content: '\e806';
848 | }
849 |
850 | /* '' */
851 | .icon-heart-empty:before {
852 | content: '\e807';
853 | }
854 |
855 | /* '' */
856 | .icon-star:before {
857 | content: '\e808';
858 | }
859 |
860 | /* '' */
861 | .icon-star-empty:before {
862 | content: '\e809';
863 | }
864 |
865 | /* '' */
866 | .icon-user:before {
867 | content: '\e80a';
868 | }
869 |
870 | /* '' */
871 | .icon-users:before {
872 | content: '\e80b';
873 | }
874 |
875 | /* '' */
876 | .icon-user-add:before {
877 | content: '\e80c';
878 | }
879 |
880 | /* '' */
881 | .icon-video:before {
882 | content: '\e80d';
883 | }
884 |
885 | /* '' */
886 | .icon-picture:before {
887 | content: '\e80e';
888 | }
889 |
890 | /* '' */
891 | .icon-camera:before {
892 | content: '\e80f';
893 | }
894 |
895 | /* '' */
896 | .icon-layout:before {
897 | content: '\e810';
898 | }
899 |
900 | /* '' */
901 | .icon-menu:before {
902 | content: '\e811';
903 | }
904 |
905 | /* '' */
906 | .icon-check:before {
907 | content: '\e812';
908 | }
909 |
910 | /* '' */
911 | .icon-cancel:before {
912 | content: '\e813';
913 | }
914 |
915 | /* '' */
916 | .icon-cancel-circled:before {
917 | content: '\e814';
918 | }
919 |
920 | /* '' */
921 | .icon-cancel-squared:before {
922 | content: '\e815';
923 | }
924 |
925 | /* '' */
926 | .icon-plus:before {
927 | content: '\e816';
928 | }
929 |
930 | /* '' */
931 | .icon-plus-circled:before {
932 | content: '\e817';
933 | }
934 |
935 | /* '' */
936 | .icon-plus-squared:before {
937 | content: '\e818';
938 | }
939 |
940 | /* '' */
941 | .icon-minus:before {
942 | content: '\e819';
943 | }
944 |
945 | /* '' */
946 | .icon-minus-circled:before {
947 | content: '\e81a';
948 | }
949 |
950 | /* '' */
951 | .icon-minus-squared:before {
952 | content: '\e81b';
953 | }
954 |
955 | /* '' */
956 | .icon-help:before {
957 | content: '\e81c';
958 | }
959 |
960 | /* '' */
961 | .icon-help-circled:before {
962 | content: '\e81d';
963 | }
964 |
965 | /* '' */
966 | .icon-info:before {
967 | content: '\e81e';
968 | }
969 |
970 | /* '' */
971 | .icon-info-circled:before {
972 | content: '\e81f';
973 | }
974 |
975 | /* '' */
976 | .icon-back:before {
977 | content: '\e820';
978 | }
979 |
980 | /* '' */
981 | .icon-home:before {
982 | content: '\e821';
983 | }
984 |
985 | /* '' */
986 | .icon-link:before {
987 | content: '\e822';
988 | }
989 |
990 | /* '' */
991 | .icon-attach:before {
992 | content: '\e823';
993 | }
994 |
995 | /* '' */
996 | .icon-lock:before {
997 | content: '\e824';
998 | }
999 |
1000 | /* '' */
1001 | .icon-lock-open:before {
1002 | content: '\e825';
1003 | }
1004 |
1005 | /* '' */
1006 | .icon-eye:before {
1007 | content: '\e826';
1008 | }
1009 |
1010 | /* '' */
1011 | .icon-tag:before {
1012 | content: '\e827';
1013 | }
1014 |
1015 | /* '' */
1016 | .icon-bookmark:before {
1017 | content: '\e828';
1018 | }
1019 |
1020 | /* '' */
1021 | .icon-bookmarks:before {
1022 | content: '\e829';
1023 | }
1024 |
1025 | /* '' */
1026 | .icon-flag:before {
1027 | content: '\e82a';
1028 | }
1029 |
1030 | /* '' */
1031 | .icon-thumbs-up:before {
1032 | content: '\e82b';
1033 | }
1034 |
1035 | /* '' */
1036 | .icon-thumbs-down:before {
1037 | content: '\e82c';
1038 | }
1039 |
1040 | /* '' */
1041 | .icon-download:before {
1042 | content: '\e82d';
1043 | }
1044 |
1045 | /* '' */
1046 | .icon-upload:before {
1047 | content: '\e82e';
1048 | }
1049 |
1050 | /* '' */
1051 | .icon-upload-cloud:before {
1052 | content: '\e82f';
1053 | }
1054 |
1055 | /* '' */
1056 | .icon-reply:before {
1057 | content: '\e830';
1058 | }
1059 |
1060 | /* '' */
1061 | .icon-reply-all:before {
1062 | content: '\e831';
1063 | }
1064 |
1065 | /* '' */
1066 | .icon-forward:before {
1067 | content: '\e832';
1068 | }
1069 |
1070 | /* '' */
1071 | .icon-quote:before {
1072 | content: '\e833';
1073 | }
1074 |
1075 | /* '' */
1076 | .icon-code:before {
1077 | content: '\e834';
1078 | }
1079 |
1080 | /* '' */
1081 | .icon-export:before {
1082 | content: '\e835';
1083 | }
1084 |
1085 | /* '' */
1086 | .icon-pencil:before {
1087 | content: '\e836';
1088 | }
1089 |
1090 | /* '' */
1091 | .icon-feather:before {
1092 | content: '\e837';
1093 | }
1094 |
1095 | /* '' */
1096 | .icon-print:before {
1097 | content: '\e838';
1098 | }
1099 |
1100 | /* '' */
1101 | .icon-retweet:before {
1102 | content: '\e839';
1103 | }
1104 |
1105 | /* '' */
1106 | .icon-keyboard:before {
1107 | content: '\e83a';
1108 | }
1109 |
1110 | /* '' */
1111 | .icon-comment:before {
1112 | content: '\e83b';
1113 | }
1114 |
1115 | /* '' */
1116 | .icon-chat:before {
1117 | content: '\e83c';
1118 | }
1119 |
1120 | /* '' */
1121 | .icon-bell:before {
1122 | content: '\e83d';
1123 | }
1124 |
1125 | /* '' */
1126 | .icon-attention:before {
1127 | content: '\e83e';
1128 | }
1129 |
1130 | /* '' */
1131 | .icon-alert:before {
1132 | content: '\e83f';
1133 | }
1134 |
1135 | /* '' */
1136 | .icon-vcard:before {
1137 | content: '\e840';
1138 | }
1139 |
1140 | /* '' */
1141 | .icon-address:before {
1142 | content: '\e841';
1143 | }
1144 |
1145 | /* '' */
1146 | .icon-location:before {
1147 | content: '\e842';
1148 | }
1149 |
1150 | /* '' */
1151 | .icon-map:before {
1152 | content: '\e843';
1153 | }
1154 |
1155 | /* '' */
1156 | .icon-direction:before {
1157 | content: '\e844';
1158 | }
1159 |
1160 | /* '' */
1161 | .icon-compass:before {
1162 | content: '\e845';
1163 | }
1164 |
1165 | /* '' */
1166 | .icon-cup:before {
1167 | content: '\e846';
1168 | }
1169 |
1170 | /* '' */
1171 | .icon-trash:before {
1172 | content: '\e847';
1173 | }
1174 |
1175 | /* '' */
1176 | .icon-doc:before {
1177 | content: '\e848';
1178 | }
1179 |
1180 | /* '' */
1181 | .icon-docs:before {
1182 | content: '\e849';
1183 | }
1184 |
1185 | /* '' */
1186 | .icon-doc-landscape:before {
1187 | content: '\e84a';
1188 | }
1189 |
1190 | /* '' */
1191 | .icon-doc-text:before {
1192 | content: '\e84b';
1193 | }
1194 |
1195 | /* '' */
1196 | .icon-doc-text-inv:before {
1197 | content: '\e84c';
1198 | }
1199 |
1200 | /* '' */
1201 | .icon-newspaper:before {
1202 | content: '\e84d';
1203 | }
1204 |
1205 | /* '' */
1206 | .icon-book-open:before {
1207 | content: '\e84e';
1208 | }
1209 |
1210 | /* '' */
1211 | .icon-book:before {
1212 | content: '\e84f';
1213 | }
1214 |
1215 | /* '' */
1216 | .icon-folder:before {
1217 | content: '\e850';
1218 | }
1219 |
1220 | /* '' */
1221 | .icon-archive:before {
1222 | content: '\e851';
1223 | }
1224 |
1225 | /* '' */
1226 | .icon-box:before {
1227 | content: '\e852';
1228 | }
1229 |
1230 | /* '' */
1231 | .icon-rss:before {
1232 | content: '\e853';
1233 | }
1234 |
1235 | /* '' */
1236 | .icon-phone:before {
1237 | content: '\e854';
1238 | }
1239 |
1240 | /* '' */
1241 | .icon-cog:before {
1242 | content: '\e855';
1243 | }
1244 |
1245 | /* '' */
1246 | .icon-tools:before {
1247 | content: '\e856';
1248 | }
1249 |
1250 | /* '' */
1251 | .icon-share:before {
1252 | content: '\e857';
1253 | }
1254 |
1255 | /* '' */
1256 | .icon-shareable:before {
1257 | content: '\e858';
1258 | }
1259 |
1260 | /* '' */
1261 | .icon-basket:before {
1262 | content: '\e859';
1263 | }
1264 |
1265 | /* '' */
1266 | .icon-bag:before {
1267 | content: '\e85a';
1268 | }
1269 |
1270 | /* '' */
1271 | .icon-calendar:before {
1272 | content: '\e85b';
1273 | }
1274 |
1275 | /* '' */
1276 | .icon-login:before {
1277 | content: '\e85c';
1278 | }
1279 |
1280 | /* '' */
1281 | .icon-logout:before {
1282 | content: '\e85d';
1283 | }
1284 |
1285 | /* '' */
1286 | .icon-mic:before {
1287 | content: '\e85e';
1288 | }
1289 |
1290 | /* '' */
1291 | .icon-mute:before {
1292 | content: '\e85f';
1293 | }
1294 |
1295 | /* '' */
1296 | .icon-sound:before {
1297 | content: '\e860';
1298 | }
1299 |
1300 | /* '' */
1301 | .icon-volume:before {
1302 | content: '\e861';
1303 | }
1304 |
1305 | /* '' */
1306 | .icon-clock:before {
1307 | content: '\e862';
1308 | }
1309 |
1310 | /* '' */
1311 | .icon-hourglass:before {
1312 | content: '\e863';
1313 | }
1314 |
1315 | /* '' */
1316 | .icon-lamp:before {
1317 | content: '\e864';
1318 | }
1319 |
1320 | /* '' */
1321 | .icon-light-down:before {
1322 | content: '\e865';
1323 | }
1324 |
1325 | /* '' */
1326 | .icon-light-up:before {
1327 | content: '\e866';
1328 | }
1329 |
1330 | /* '' */
1331 | .icon-adjust:before {
1332 | content: '\e867';
1333 | }
1334 |
1335 | /* '' */
1336 | .icon-block:before {
1337 | content: '\e868';
1338 | }
1339 |
1340 | /* '' */
1341 | .icon-resize-full:before {
1342 | content: '\e869';
1343 | }
1344 |
1345 | /* '' */
1346 | .icon-resize-small:before {
1347 | content: '\e86a';
1348 | }
1349 |
1350 | /* '' */
1351 | .icon-popup:before {
1352 | content: '\e86b';
1353 | }
1354 |
1355 | /* '' */
1356 | .icon-publish:before {
1357 | content: '\e86c';
1358 | }
1359 |
1360 | /* '' */
1361 | .icon-window:before {
1362 | content: '\e86d';
1363 | }
1364 |
1365 | /* '' */
1366 | .icon-arrow-combo:before {
1367 | content: '\e86e';
1368 | }
1369 |
1370 | /* '' */
1371 | .icon-down-circled:before {
1372 | content: '\e86f';
1373 | }
1374 |
1375 | /* '' */
1376 | .icon-left-circled:before {
1377 | content: '\e870';
1378 | }
1379 |
1380 | /* '' */
1381 | .icon-right-circled:before {
1382 | content: '\e871';
1383 | }
1384 |
1385 | /* '' */
1386 | .icon-up-circled:before {
1387 | content: '\e872';
1388 | }
1389 |
1390 | /* '' */
1391 | .icon-down-open:before {
1392 | content: '\e873';
1393 | }
1394 |
1395 | /* '' */
1396 | .icon-left-open:before {
1397 | content: '\e874';
1398 | }
1399 |
1400 | /* '' */
1401 | .icon-right-open:before {
1402 | content: '\e875';
1403 | }
1404 |
1405 | /* '' */
1406 | .icon-up-open:before {
1407 | content: '\e876';
1408 | }
1409 |
1410 | /* '' */
1411 | .icon-down-open-mini:before {
1412 | content: '\e877';
1413 | }
1414 |
1415 | /* '' */
1416 | .icon-left-open-mini:before {
1417 | content: '\e878';
1418 | }
1419 |
1420 | /* '' */
1421 | .icon-right-open-mini:before {
1422 | content: '\e879';
1423 | }
1424 |
1425 | /* '' */
1426 | .icon-up-open-mini:before {
1427 | content: '\e87a';
1428 | }
1429 |
1430 | /* '' */
1431 | .icon-down-open-big:before {
1432 | content: '\e87b';
1433 | }
1434 |
1435 | /* '' */
1436 | .icon-left-open-big:before {
1437 | content: '\e87c';
1438 | }
1439 |
1440 | /* '' */
1441 | .icon-right-open-big:before {
1442 | content: '\e87d';
1443 | }
1444 |
1445 | /* '' */
1446 | .icon-up-open-big:before {
1447 | content: '\e87e';
1448 | }
1449 |
1450 | /* '' */
1451 | .icon-down:before {
1452 | content: '\e87f';
1453 | }
1454 |
1455 | /* '' */
1456 | .icon-left:before {
1457 | content: '\e880';
1458 | }
1459 |
1460 | /* '' */
1461 | .icon-right:before {
1462 | content: '\e881';
1463 | }
1464 |
1465 | /* '' */
1466 | .icon-up:before {
1467 | content: '\e882';
1468 | }
1469 |
1470 | /* '' */
1471 | .icon-down-dir:before {
1472 | content: '\e883';
1473 | }
1474 |
1475 | /* '' */
1476 | .icon-left-dir:before {
1477 | content: '\e884';
1478 | }
1479 |
1480 | /* '' */
1481 | .icon-right-dir:before {
1482 | content: '\e885';
1483 | }
1484 |
1485 | /* '' */
1486 | .icon-up-dir:before {
1487 | content: '\e886';
1488 | }
1489 |
1490 | /* '' */
1491 | .icon-down-bold:before {
1492 | content: '\e887';
1493 | }
1494 |
1495 | /* '' */
1496 | .icon-left-bold:before {
1497 | content: '\e888';
1498 | }
1499 |
1500 | /* '' */
1501 | .icon-right-bold:before {
1502 | content: '\e889';
1503 | }
1504 |
1505 | /* '' */
1506 | .icon-up-bold:before {
1507 | content: '\e88a';
1508 | }
1509 |
1510 | /* '' */
1511 | .icon-down-thin:before {
1512 | content: '\e88b';
1513 | }
1514 |
1515 | /* '' */
1516 | .icon-left-thin:before {
1517 | content: '\e88c';
1518 | }
1519 |
1520 | /* '' */
1521 | .icon-right-thin:before {
1522 | content: '\e88d';
1523 | }
1524 |
1525 | /* '' */
1526 | .icon-up-thin:before {
1527 | content: '\e88e';
1528 | }
1529 |
1530 | /* '' */
1531 | .icon-ccw:before {
1532 | content: '\e88f';
1533 | }
1534 |
1535 | /* '' */
1536 | .icon-cw:before {
1537 | content: '\e890';
1538 | }
1539 |
1540 | /* '' */
1541 | .icon-arrows-ccw:before {
1542 | content: '\e891';
1543 | }
1544 |
1545 | /* '' */
1546 | .icon-level-down:before {
1547 | content: '\e892';
1548 | }
1549 |
1550 | /* '' */
1551 | .icon-level-up:before {
1552 | content: '\e893';
1553 | }
1554 |
1555 | /* '' */
1556 | .icon-shuffle:before {
1557 | content: '\e894';
1558 | }
1559 |
1560 | /* '' */
1561 | .icon-loop:before {
1562 | content: '\e895';
1563 | }
1564 |
1565 | /* '' */
1566 | .icon-switch:before {
1567 | content: '\e896';
1568 | }
1569 |
1570 | /* '' */
1571 | .icon-play:before {
1572 | content: '\e897';
1573 | }
1574 |
1575 | /* '' */
1576 | .icon-stop:before {
1577 | content: '\e898';
1578 | }
1579 |
1580 | /* '' */
1581 | .icon-pause:before {
1582 | content: '\e899';
1583 | }
1584 |
1585 | /* '' */
1586 | .icon-record:before {
1587 | content: '\e89a';
1588 | }
1589 |
1590 | /* '' */
1591 | .icon-to-end:before {
1592 | content: '\e89b';
1593 | }
1594 |
1595 | /* '' */
1596 | .icon-to-start:before {
1597 | content: '\e89c';
1598 | }
1599 |
1600 | /* '' */
1601 | .icon-fast-forward:before {
1602 | content: '\e89d';
1603 | }
1604 |
1605 | /* '' */
1606 | .icon-fast-backward:before {
1607 | content: '\e89e';
1608 | }
1609 |
1610 | /* '' */
1611 | .icon-progress-0:before {
1612 | content: '\e89f';
1613 | }
1614 |
1615 | /* '' */
1616 | .icon-progress-1:before {
1617 | content: '\e8a0';
1618 | }
1619 |
1620 | /* '' */
1621 | .icon-progress-2:before {
1622 | content: '\e8a1';
1623 | }
1624 |
1625 | /* '' */
1626 | .icon-progress-3:before {
1627 | content: '\e8a2';
1628 | }
1629 |
1630 | /* '' */
1631 | .icon-target:before {
1632 | content: '\e8a3';
1633 | }
1634 |
1635 | /* '' */
1636 | .icon-palette:before {
1637 | content: '\e8a4';
1638 | }
1639 |
1640 | /* '' */
1641 | .icon-list:before {
1642 | content: '\e8a5';
1643 | }
1644 |
1645 | /* '' */
1646 | .icon-list-add:before {
1647 | content: '\e8a6';
1648 | }
1649 |
1650 | /* '' */
1651 | .icon-signal:before {
1652 | content: '\e8a7';
1653 | }
1654 |
1655 | /* '' */
1656 | .icon-trophy:before {
1657 | content: '\e8a8';
1658 | }
1659 |
1660 | /* '' */
1661 | .icon-battery:before {
1662 | content: '\e8a9';
1663 | }
1664 |
1665 | /* '' */
1666 | .icon-back-in-time:before {
1667 | content: '\e8aa';
1668 | }
1669 |
1670 | /* '' */
1671 | .icon-monitor:before {
1672 | content: '\e8ab';
1673 | }
1674 |
1675 | /* '' */
1676 | .icon-mobile:before {
1677 | content: '\e8ac';
1678 | }
1679 |
1680 | /* '' */
1681 | .icon-network:before {
1682 | content: '\e8ad';
1683 | }
1684 |
1685 | /* '' */
1686 | .icon-cd:before {
1687 | content: '\e8ae';
1688 | }
1689 |
1690 | /* '' */
1691 | .icon-inbox:before {
1692 | content: '\e8af';
1693 | }
1694 |
1695 | /* '' */
1696 | .icon-install:before {
1697 | content: '\e8b0';
1698 | }
1699 |
1700 | /* '' */
1701 | .icon-globe:before {
1702 | content: '\e8b1';
1703 | }
1704 |
1705 | /* '' */
1706 | .icon-cloud:before {
1707 | content: '\e8b2';
1708 | }
1709 |
1710 | /* '' */
1711 | .icon-cloud-thunder:before {
1712 | content: '\e8b3';
1713 | }
1714 |
1715 | /* '' */
1716 | .icon-flash:before {
1717 | content: '\e8b4';
1718 | }
1719 |
1720 | /* '' */
1721 | .icon-moon:before {
1722 | content: '\e8b5';
1723 | }
1724 |
1725 | /* '' */
1726 | .icon-flight:before {
1727 | content: '\e8b6';
1728 | }
1729 |
1730 | /* '' */
1731 | .icon-paper-plane:before {
1732 | content: '\e8b7';
1733 | }
1734 |
1735 | /* '' */
1736 | .icon-leaf:before {
1737 | content: '\e8b8';
1738 | }
1739 |
1740 | /* '' */
1741 | .icon-lifebuoy:before {
1742 | content: '\e8b9';
1743 | }
1744 |
1745 | /* '' */
1746 | .icon-mouse:before {
1747 | content: '\e8ba';
1748 | }
1749 |
1750 | /* '' */
1751 | .icon-briefcase:before {
1752 | content: '\e8bb';
1753 | }
1754 |
1755 | /* '' */
1756 | .icon-suitcase:before {
1757 | content: '\e8bc';
1758 | }
1759 |
1760 | /* '' */
1761 | .icon-dot:before {
1762 | content: '\e8bd';
1763 | }
1764 |
1765 | /* '' */
1766 | .icon-dot-2:before {
1767 | content: '\e8be';
1768 | }
1769 |
1770 | /* '' */
1771 | .icon-dot-3:before {
1772 | content: '\e8bf';
1773 | }
1774 |
1775 | /* '' */
1776 | .icon-brush:before {
1777 | content: '\e8c0';
1778 | }
1779 |
1780 | /* '' */
1781 | .icon-magnet:before {
1782 | content: '\e8c1';
1783 | }
1784 |
1785 | /* '' */
1786 | .icon-infinity:before {
1787 | content: '\e8c2';
1788 | }
1789 |
1790 | /* '' */
1791 | .icon-erase:before {
1792 | content: '\e8c3';
1793 | }
1794 |
1795 | /* '' */
1796 | .icon-chart-pie:before {
1797 | content: '\e8c4';
1798 | }
1799 |
1800 | /* '' */
1801 | .icon-chart-line:before {
1802 | content: '\e8c5';
1803 | }
1804 |
1805 | /* '' */
1806 | .icon-chart-bar:before {
1807 | content: '\e8c6';
1808 | }
1809 |
1810 | /* '' */
1811 | .icon-chart-area:before {
1812 | content: '\e8c7';
1813 | }
1814 |
1815 | /* '' */
1816 | .icon-tape:before {
1817 | content: '\e8c8';
1818 | }
1819 |
1820 | /* '' */
1821 | .icon-graduation-cap:before {
1822 | content: '\e8c9';
1823 | }
1824 |
1825 | /* '' */
1826 | .icon-language:before {
1827 | content: '\e8ca';
1828 | }
1829 |
1830 | /* '' */
1831 | .icon-ticket:before {
1832 | content: '\e8cb';
1833 | }
1834 |
1835 | /* '' */
1836 | .icon-water:before {
1837 | content: '\e8cc';
1838 | }
1839 |
1840 | /* '' */
1841 | .icon-droplet:before {
1842 | content: '\e8cd';
1843 | }
1844 |
1845 | /* '' */
1846 | .icon-air:before {
1847 | content: '\e8ce';
1848 | }
1849 |
1850 | /* '' */
1851 | .icon-credit-card:before {
1852 | content: '\e8cf';
1853 | }
1854 |
1855 | /* '' */
1856 | .icon-floppy:before {
1857 | content: '\e8d0';
1858 | }
1859 |
1860 | /* '' */
1861 | .icon-clipboard:before {
1862 | content: '\e8d1';
1863 | }
1864 |
1865 | /* '' */
1866 | .icon-megaphone:before {
1867 | content: '\e8d2';
1868 | }
1869 |
1870 | /* '' */
1871 | .icon-database:before {
1872 | content: '\e8d3';
1873 | }
1874 |
1875 | /* '' */
1876 | .icon-drive:before {
1877 | content: '\e8d4';
1878 | }
1879 |
1880 | /* '' */
1881 | .icon-bucket:before {
1882 | content: '\e8d5';
1883 | }
1884 |
1885 | /* '' */
1886 | .icon-thermometer:before {
1887 | content: '\e8d6';
1888 | }
1889 |
1890 | /* '' */
1891 | .icon-key:before {
1892 | content: '\e8d7';
1893 | }
1894 |
1895 | /* '' */
1896 | .icon-flow-cascade:before {
1897 | content: '\e8d8';
1898 | }
1899 |
1900 | /* '' */
1901 | .icon-flow-branch:before {
1902 | content: '\e8d9';
1903 | }
1904 |
1905 | /* '' */
1906 | .icon-flow-tree:before {
1907 | content: '\e8da';
1908 | }
1909 |
1910 | /* '' */
1911 | .icon-flow-line:before {
1912 | content: '\e8db';
1913 | }
1914 |
1915 | /* '' */
1916 | .icon-flow-parallel:before {
1917 | content: '\e8dc';
1918 | }
1919 |
1920 | /* '' */
1921 | .icon-rocket:before {
1922 | content: '\e8dd';
1923 | }
1924 |
1925 | /* '' */
1926 | .icon-gauge:before {
1927 | content: '\e8de';
1928 | }
1929 |
1930 | /* '' */
1931 | .icon-traffic-cone:before {
1932 | content: '\e8df';
1933 | }
1934 |
1935 | /* '' */
1936 | .icon-cc:before {
1937 | content: '\e8e0';
1938 | }
1939 |
1940 | /* '' */
1941 | .icon-cc-by:before {
1942 | content: '\e8e1';
1943 | }
1944 |
1945 | /* '' */
1946 | .icon-cc-nc:before {
1947 | content: '\e8e2';
1948 | }
1949 |
1950 | /* '' */
1951 | .icon-cc-nc-eu:before {
1952 | content: '\e8e3';
1953 | }
1954 |
1955 | /* '' */
1956 | .icon-cc-nc-jp:before {
1957 | content: '\e8e4';
1958 | }
1959 |
1960 | /* '' */
1961 | .icon-cc-sa:before {
1962 | content: '\e8e5';
1963 | }
1964 |
1965 | /* '' */
1966 | .icon-cc-nd:before {
1967 | content: '\e8e6';
1968 | }
1969 |
1970 | /* '' */
1971 | .icon-cc-pd:before {
1972 | content: '\e8e7';
1973 | }
1974 |
1975 | /* '' */
1976 | .icon-cc-zero:before {
1977 | content: '\e8e8';
1978 | }
1979 |
1980 | /* '' */
1981 | .icon-cc-share:before {
1982 | content: '\e8e9';
1983 | }
1984 |
1985 | /* '' */
1986 | .icon-cc-remix:before {
1987 | content: '\e8ea';
1988 | }
1989 |
1990 | /* '' */
1991 | .icon-github:before {
1992 | content: '\e8eb';
1993 | }
1994 |
1995 | /* '' */
1996 | .icon-github-circled:before {
1997 | content: '\e8ec';
1998 | }
1999 |
2000 | /* '' */
2001 | .icon-flickr:before {
2002 | content: '\e8ed';
2003 | }
2004 |
2005 | /* '' */
2006 | .icon-flickr-circled:before {
2007 | content: '\e8ee';
2008 | }
2009 |
2010 | /* '' */
2011 | .icon-vimeo:before {
2012 | content: '\e8ef';
2013 | }
2014 |
2015 | /* '' */
2016 | .icon-vimeo-circled:before {
2017 | content: '\e8f0';
2018 | }
2019 |
2020 | /* '' */
2021 | .icon-twitter:before {
2022 | content: '\e8f1';
2023 | }
2024 |
2025 | /* '' */
2026 | .icon-twitter-circled:before {
2027 | content: '\e8f2';
2028 | }
2029 |
2030 | /* '' */
2031 | .icon-facebook:before {
2032 | content: '\e8f3';
2033 | }
2034 |
2035 | /* '' */
2036 | .icon-facebook-circled:before {
2037 | content: '\e8f4';
2038 | }
2039 |
2040 | /* '' */
2041 | .icon-facebook-squared:before {
2042 | content: '\e8f5';
2043 | }
2044 |
2045 | /* '' */
2046 | .icon-gplus:before {
2047 | content: '\e8f6';
2048 | }
2049 |
2050 | /* '' */
2051 | .icon-gplus-circled:before {
2052 | content: '\e8f7';
2053 | }
2054 |
2055 | /* '' */
2056 | .icon-pinterest:before {
2057 | content: '\e8f8';
2058 | }
2059 |
2060 | /* '' */
2061 | .icon-pinterest-circled:before {
2062 | content: '\e8f9';
2063 | }
2064 |
2065 | /* '' */
2066 | .icon-tumblr:before {
2067 | content: '\e8fa';
2068 | }
2069 |
2070 | /* '' */
2071 | .icon-tumblr-circled:before {
2072 | content: '\e8fb';
2073 | }
2074 |
2075 | /* '' */
2076 | .icon-linkedin:before {
2077 | content: '\e8fc';
2078 | }
2079 |
2080 | /* '' */
2081 | .icon-linkedin-circled:before {
2082 | content: '\e8fd';
2083 | }
2084 |
2085 | /* '' */
2086 | .icon-dribbble:before {
2087 | content: '\e8fe';
2088 | }
2089 |
2090 | /* '' */
2091 | .icon-dribbble-circled:before {
2092 | content: '\e8ff';
2093 | }
2094 |
2095 | /* '' */
2096 | .icon-stumbleupon:before {
2097 | content: '\e900';
2098 | }
2099 |
2100 | /* '' */
2101 | .icon-stumbleupon-circled:before {
2102 | content: '\e901';
2103 | }
2104 |
2105 | /* '' */
2106 | .icon-lastfm:before {
2107 | content: '\e902';
2108 | }
2109 |
2110 | /* '' */
2111 | .icon-lastfm-circled:before {
2112 | content: '\e903';
2113 | }
2114 |
2115 | /* '' */
2116 | .icon-rdio:before {
2117 | content: '\e904';
2118 | }
2119 |
2120 | /* '' */
2121 | .icon-rdio-circled:before {
2122 | content: '\e905';
2123 | }
2124 |
2125 | /* '' */
2126 | .icon-spotify:before {
2127 | content: '\e906';
2128 | }
2129 |
2130 | /* '' */
2131 | .icon-spotify-circled:before {
2132 | content: '\e907';
2133 | }
2134 |
2135 | /* '' */
2136 | .icon-qq:before {
2137 | content: '\e908';
2138 | }
2139 |
2140 | /* '' */
2141 | .icon-instagram:before {
2142 | content: '\e909';
2143 | }
2144 |
2145 | /* '' */
2146 | .icon-dropbox:before {
2147 | content: '\e90a';
2148 | }
2149 |
2150 | /* '' */
2151 | .icon-evernote:before {
2152 | content: '\e90b';
2153 | }
2154 |
2155 | /* '' */
2156 | .icon-flattr:before {
2157 | content: '\e90c';
2158 | }
2159 |
2160 | /* '' */
2161 | .icon-skype:before {
2162 | content: '\e90d';
2163 | }
2164 |
2165 | /* '' */
2166 | .icon-skype-circled:before {
2167 | content: '\e90e';
2168 | }
2169 |
2170 | /* '' */
2171 | .icon-renren:before {
2172 | content: '\e90f';
2173 | }
2174 |
2175 | /* '' */
2176 | .icon-sina-weibo:before {
2177 | content: '\e910';
2178 | }
2179 |
2180 | /* '' */
2181 | .icon-paypal:before {
2182 | content: '\e911';
2183 | }
2184 |
2185 | /* '' */
2186 | .icon-picasa:before {
2187 | content: '\e912';
2188 | }
2189 |
2190 | /* '' */
2191 | .icon-soundcloud:before {
2192 | content: '\e913';
2193 | }
2194 |
2195 | /* '' */
2196 | .icon-mixi:before {
2197 | content: '\e914';
2198 | }
2199 |
2200 | /* '' */
2201 | .icon-behance:before {
2202 | content: '\e915';
2203 | }
2204 |
2205 | /* '' */
2206 | .icon-google-circles:before {
2207 | content: '\e916';
2208 | }
2209 |
2210 | /* '' */
2211 | .icon-vkontakte:before {
2212 | content: '\e917';
2213 | }
2214 |
2215 | /* '' */
2216 | .icon-smashing:before {
2217 | content: '\e918';
2218 | }
2219 |
2220 | /* '' */
2221 | .icon-sweden:before {
2222 | content: '\e919';
2223 | }
2224 |
2225 | /* '' */
2226 | .icon-db-shape:before {
2227 | content: '\e91a';
2228 | }
2229 |
2230 | /* '' */
2231 | .icon-logo-db:before {
2232 | content: '\e91b';
2233 | }
2234 |
2235 | /* '' */
2236 | table {
2237 | width: 100%;
2238 | border: 0;
2239 | border-collapse: separate;
2240 | font-size: 12px;
2241 | text-align: left;
2242 | }
2243 |
2244 | thead {
2245 | background-color: #f5f5f4;
2246 | }
2247 |
2248 | tbody {
2249 | background-color: #fff;
2250 | }
2251 |
2252 | .table-striped tr:nth-child(even) {
2253 | background-color: #f5f5f4;
2254 | }
2255 |
2256 | tr:active,
2257 | .table-striped tr:active:nth-child(even) {
2258 | color: #fff;
2259 | background-color: #116cd6;
2260 | }
2261 |
2262 | thead tr:active {
2263 | color: #333;
2264 | background-color: #f5f5f4;
2265 | }
2266 |
2267 | th {
2268 | font-weight: normal;
2269 | border-right: 1px solid #ddd;
2270 | border-bottom: 1px solid #ddd;
2271 | }
2272 |
2273 | th,
2274 | td {
2275 | padding: 2px 15px;
2276 | white-space: nowrap;
2277 | overflow: hidden;
2278 | text-overflow: ellipsis;
2279 | }
2280 | th:last-child,
2281 | td:last-child {
2282 | border-right: 0;
2283 | }
2284 |
2285 | .tab-group {
2286 | margin-top: -1px;
2287 | display: flex;
2288 | border-top: 1px solid #989698;
2289 | border-bottom: 1px solid #989698;
2290 | }
2291 |
2292 | .tab-item {
2293 | position: relative;
2294 | flex: 1;
2295 | padding: 3px;
2296 | font-size: 12px;
2297 | text-align: center;
2298 | border-left: 1px solid #989698;
2299 | background-color: #b8b6b8;
2300 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #b8b6b8), color-stop(100%, #b0aeb0));
2301 | background-image: -webkit-linear-gradient(top, #b8b6b8 0%, #b0aeb0 100%);
2302 | background-image: linear-gradient(to bottom, #b8b6b8 0%, #b0aeb0 100%);
2303 | }
2304 | .tab-item:first-child {
2305 | border-left: 0;
2306 | }
2307 | .tab-item.active {
2308 | background-color: #d4d2d4;
2309 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #d4d2d4), color-stop(100%, #cccacc));
2310 | background-image: -webkit-linear-gradient(top, #d4d2d4 0%, #cccacc 100%);
2311 | background-image: linear-gradient(to bottom, #d4d2d4 0%, #cccacc 100%);
2312 | }
2313 | .tab-item .icon-close-tab {
2314 | position: absolute;
2315 | top: 50%;
2316 | left: 5px;
2317 | width: 15px;
2318 | height: 15px;
2319 | font-size: 15px;
2320 | line-height: 15px;
2321 | text-align: center;
2322 | color: #666;
2323 | opacity: 0;
2324 | transition: opacity .1s linear, background-color .1s linear;
2325 | border-radius: 3px;
2326 | transform: translateY(-50%);
2327 | z-index: 10;
2328 | }
2329 | .tab-item:after {
2330 | position: absolute;
2331 | top: 0;
2332 | right: 0;
2333 | bottom: 0;
2334 | left: 0;
2335 | content: "";
2336 | background-color: rgba(0, 0, 0, 0.08);
2337 | opacity: 0;
2338 | transition: opacity .1s linear;
2339 | z-index: 1;
2340 | }
2341 | .tab-item:hover:not(.active):after {
2342 | opacity: 1;
2343 | }
2344 | .tab-item:hover .icon-close-tab {
2345 | opacity: 1;
2346 | }
2347 | .tab-item .icon-close-tab:hover {
2348 | background-color: rgba(0, 0, 0, 0.08);
2349 | }
2350 |
2351 | .tab-item-fixed {
2352 | flex: none;
2353 | padding: 3px 10px;
2354 | }
2355 |
--------------------------------------------------------------------------------
/styles/theme.scss:
--------------------------------------------------------------------------------
1 | body, html {
2 | margin: 0px;
3 | padding: 0px;
4 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | entry: path.resolve(__dirname, 'app/window.jsx'),
5 | output: {
6 | path: path.resolve(__dirname, 'out'),
7 | publicPath: 'out/',
8 | filename: 'app.js'
9 | },
10 | target: 'electron',
11 | module: {
12 | loaders: [
13 | { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel-loader'] },
14 | { test: /\.css$/, loader: 'style-loader!css-loader' },
15 | { test: /\.scss$/, loaders: ['style-loader', 'css-loader', 'sass-loader']},
16 |
17 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
18 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
19 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream' },
20 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' },
21 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' },
22 |
23 | { test: /\.json$/, loader: 'json-loader' }
24 | ]
25 | }
26 | };
27 |
--------------------------------------------------------------------------------