├── .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 | [![WIP](https://img.shields.io/badge/work%20in%20progress-yes-green.svg)](https://github.com/neXenio/adb-util) [![GitHub license](https://img.shields.io/github/license/neXenio/adb-util.svg)](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 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
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 | 74 | 76 | 78 | 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 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {devices.map((device,index) => )} 47 | 48 |
SerialAndroid IDModelNameTypeState
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 |
17 |

ADB Util

18 | 19 |
20 |
21 | 24 | 27 | 30 |
31 | 32 | 36 | 37 | 40 |
41 |
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 | 4 | Copyright (C) 2015 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------