├── .gitattributes
├── .gitignore
├── screenshot.png
├── images
├── icons
│ ├── tray.png
│ ├── tray@2x.png
│ └── tray@3x.png
├── star.svg
├── search.svg
└── npm.svg
├── .editorconfig
├── readme.md
├── index.html
├── license
├── package.json
├── index.css
├── browser.js
└── index.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/screenshot.png
--------------------------------------------------------------------------------
/images/icons/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/images/icons/tray.png
--------------------------------------------------------------------------------
/images/icons/tray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/images/icons/tray@2x.png
--------------------------------------------------------------------------------
/images/icons/tray@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadimdemedes/npm-search/HEAD/images/icons/tray@3x.png
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # npm-search
2 |
3 | > Search npm via node-modules.com
4 |
5 | 
6 |
7 |
8 | ## Dev
9 |
10 | ```
11 | $ npm install
12 | ```
13 |
14 | ### Run
15 |
16 | ```
17 | $ npm start
18 | ```
19 |
20 | ### Build
21 |
22 | ```
23 | $ npm run build
24 | ```
25 |
26 | Builds the app for OS X, Linux, and Windows, using [electron-packager](https://github.com/electron-userland/electron-packager).
27 |
28 |
29 | ## License
30 |
31 | MIT © [vdemedes](https://github.com/vdemedes)
32 |
--------------------------------------------------------------------------------
/images/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Electron boilerplate
6 |
7 |
8 |
9 |
10 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/images/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) vdemedes (https://github.com/vdemedes)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "npm-search",
3 | "productName": "NpmSearch",
4 | "version": "1.0.0",
5 | "description": "My excellent app",
6 | "license": "MIT",
7 | "repository": "vdemedes/npm-search",
8 | "author": {
9 | "name": "vdemedes",
10 | "email": "vdemedes@gmail.com",
11 | "url": "github.com/vdemedes"
12 | },
13 | "scripts": {
14 | "test": "xo",
15 | "start": "electron .",
16 | "postinstall": "install-app-deps",
17 | "pack": "build --dir",
18 | "build": "build"
19 | },
20 | "build": {
21 | "appId": "com.vdemedes.npm-search",
22 | "app-category-type": "public.app-category.developer-tools"
23 | },
24 | "files": [
25 | "index.js",
26 | "index.html",
27 | "index.css",
28 | "images"
29 | ],
30 | "keywords": [
31 | "electron-app",
32 | "electron"
33 | ],
34 | "dependencies": {
35 | "auto-launch": "^2.0.1",
36 | "electron-debug": "^1.0.0",
37 | "electron-positioner": "^3.0.0",
38 | "titlebar": "^1.4.0"
39 | },
40 | "devDependencies": {
41 | "devtron": "^1.1.0",
42 | "electron-builder": "^5.6.2",
43 | "electron-prebuilt": "^1.0.1",
44 | "eslint-config-vdemedes": "^1.0.2",
45 | "xo": "^0.15.1"
46 | },
47 | "xo": {
48 | "extends": "vdemedes",
49 | "esnext": true,
50 | "envs": [
51 | "node",
52 | "browser"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/images/npm.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | * {
8 | box-sizing: border-box;
9 | }
10 |
11 | body {
12 | font-family: -apple-system, 'Helvetica Neue', Helvetica, sans-serif;
13 | padding-top: 24px;
14 | }
15 |
16 | h3 {
17 | margin: 0;
18 | }
19 |
20 | .fixed-container {
21 | position: fixed;
22 | top: 0;
23 | width: 100%;
24 | background: #fff;
25 | }
26 |
27 | .titlebar {
28 | background-color: transparent;
29 | position: absolute;
30 | top: 0;
31 | width: 100%;
32 | }
33 |
34 | .titlebar-minimize,
35 | .titlebar-fullscreen {
36 | display: none;
37 | }
38 |
39 | .titlebar-close svg {
40 | margin-top: 1px;
41 | margin-left: 1px;
42 | }
43 |
44 | .title {
45 | position: absolute;
46 | text-align: center;
47 | width: 38px;
48 | left: calc(50% - 19px);
49 | top: 4px;
50 | }
51 |
52 | .npm-logo {
53 | width: 38px;
54 | }
55 |
56 | .search-form {
57 | padding: 0 8px 8px;
58 | margin-top: 24px;
59 | }
60 |
61 | .container {
62 | margin-top: 44px;
63 | padding: 0 8px 8px;
64 | display: flex;
65 | flex-direction: column;
66 | height: 332px;
67 | overflow: scroll;
68 | }
69 |
70 | .no-results {
71 | flex: 1 1 auto;
72 | display: flex;
73 | align-items: center;
74 | justify-content: center;
75 | }
76 |
77 | .search-input {
78 | background: url('images/search.svg') no-repeat center right 8px;
79 | background-size: 16px;
80 | border-radius: 3px;
81 | border: 1px solid #ddd;
82 | width: 100%;
83 | padding: 8px 32px 8px 8px;
84 | font-size: 16px;
85 | outline: none;
86 | }
87 |
88 | .list-item {
89 | border-radius: 3px;
90 | padding: 0 10px 8px;
91 | margin-bottom: 8px;
92 | }
93 |
94 | .list-item:hover {
95 | background: #eee;
96 | }
97 |
98 | .list-item:active {
99 | background: #ddd;
100 | }
101 |
102 | .list-item-header {
103 | display: flex;
104 | justify-content: space-between;
105 | align-items: center;
106 | }
107 |
108 | .list-item-name {
109 | color: #444F5A;
110 | font-weight: 500;
111 | }
112 |
113 | .list-item-description {
114 | margin: 4px 0;
115 | font-size: 12px;
116 | color: #ccc;
117 | font-weight: 300;
118 | }
119 |
120 | .list-item-stars {
121 | color: #ccc;
122 | font-weight: 300;
123 | }
124 |
125 | .star-icon {
126 | width: 12px;
127 | margin-right: 4px;
128 | }
129 |
--------------------------------------------------------------------------------
/browser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const titlebar = require('titlebar');
3 | const ipc = require('electron').ipcRenderer;
4 |
5 | const titleBar = titlebar();
6 | titleBar.appendTo(document.querySelector('.js-header'));
7 |
8 | const closeButton = document.querySelector('.titlebar-close');
9 | closeButton.addEventListener('click', () => {
10 | ipc.send('close');
11 | });
12 |
13 | const searchInput = document.querySelector('.js-search-input');
14 | const searchForm = document.querySelector('.js-search-form');
15 | searchForm.addEventListener('submit', e => {
16 | e.preventDefault();
17 |
18 | const query = searchInput.value;
19 | search(query)
20 | .then(modules => {
21 | if (modules.length > 0) {
22 | renderModules(modules);
23 | } else {
24 | renderEmptyResult();
25 | }
26 |
27 | ipc.send('resize');
28 | });
29 | });
30 |
31 | function search (query) {
32 | return fetch('http://node-modules.com/search.json?q=' + query)
33 | .then(res => res.json());
34 | }
35 |
36 | const container = document.querySelector('.js-container');
37 | container.addEventListener('click', e => {
38 | let target = e.target;
39 |
40 | if (!target.matches('.js-list-item')) {
41 | target = target.closest('.js-list-item');
42 | }
43 |
44 | if (!target) {
45 | return;
46 | }
47 |
48 | let url = target.dataset.url;
49 |
50 | if (e.altKey) {
51 | url = 'https://npmjs.org/package/' + target.dataset.name;
52 | }
53 |
54 | ipc.send('open', url, target.dataset.name);
55 | });
56 |
57 | function renderEmptyResult () {
58 | container.innerHTML = '';
59 |
60 | const wrapper = document.createElement('div');
61 | wrapper.className = 'no-results';
62 |
63 | const label = document.createElement('h3');
64 | label.textContent = 'No modules found.';
65 |
66 | wrapper.appendChild(label);
67 | container.appendChild(wrapper);
68 | }
69 |
70 | function renderModules (modules) {
71 | container.innerHTML = '';
72 |
73 | const fragment = document.createDocumentFragment();
74 |
75 | modules.forEach(module => {
76 | const listItem = document.createElement('div');
77 | listItem.className = 'list-item js-list-item';
78 | listItem.dataset.url = module.url;
79 | listItem.dataset.name = module.name;
80 |
81 | const header = document.createElement('header');
82 | header.className = 'list-item-header';
83 |
84 | const name = document.createElement('h3');
85 | name.className = 'list-item-name';
86 | name.textContent = module.name;
87 |
88 | const stars = document.createElement('span');
89 | stars.className = 'list-item-stars';
90 | stars.textContent = module.stars;
91 |
92 | const starIcon = document.createElement('img');
93 | starIcon.className = 'star-icon';
94 | starIcon.src = 'images/star.svg';
95 |
96 | const description = document.createElement('p');
97 | description.className = 'list-item-description';
98 | description.textContent = module.description;
99 |
100 | stars.insertBefore(starIcon, stars.firstChild);
101 |
102 | header.appendChild(name);
103 | header.appendChild(stars);
104 |
105 | listItem.appendChild(header);
106 | listItem.appendChild(description);
107 |
108 | fragment.appendChild(listItem);
109 | });
110 |
111 | container.appendChild(fragment);
112 | }
113 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Positioner = require('electron-positioner');
3 | const AutoLaunch = require('auto-launch');
4 | const electron = require('electron');
5 | const shell = electron.shell;
6 | const ipc = electron.ipcMain;
7 | const app = electron.app;
8 |
9 | app.dock.hide();
10 |
11 | // launch at login
12 | const appLauncher = new AutoLaunch({
13 | name: 'npm-search'
14 | });
15 |
16 | // adds debug features like hotkeys for triggering dev tools and reload
17 | require('electron-debug')();
18 |
19 | // prevent window and tray from being garbage collected
20 | let mainWindow;
21 | let tray;
22 | let trayMenu;
23 |
24 | function createMainWindow() {
25 | const win = new electron.BrowserWindow({
26 | width: 320,
27 | height: 68,
28 | frame: false,
29 | closeable: false,
30 | resizable: false
31 | });
32 |
33 | const positioner = new Positioner(win);
34 | positioner.move('center');
35 |
36 | win.loadURL(`file://${__dirname}/index.html`);
37 | win.on('close', e => {
38 | e.preventDefault();
39 | closeMainWindow();
40 | });
41 |
42 | return win;
43 | }
44 |
45 | function closeMainWindow () {
46 | mainWindow.hide();
47 | mainWindow.setSize(320, 68, false);
48 | mainWindow.reload();
49 | }
50 |
51 | function createTray () {
52 | const tray = new electron.Tray(`${__dirname}/images/icons/tray.png`);
53 | tray.setToolTip('Search npm');
54 |
55 | tray.on('click', () => {
56 | mainWindow.show();
57 | mainWindow.focus();
58 | });
59 |
60 | tray.on('right-click', () => {
61 | tray.popUpContextMenu(trayMenu);
62 | });
63 |
64 | return tray;
65 | }
66 |
67 | // list of last opened modules
68 | const modules = [];
69 |
70 | // handler for last opened module menu item click event
71 | function onModuleClick (menuItem) {
72 | const module = modules.find(module => module.name === menuItem.label);
73 | shell.openExternal(module.url);
74 | }
75 |
76 | function createTrayMenu () {
77 | const lastOpened = new electron.MenuItem({
78 | label: 'Last opened',
79 | enabled: false
80 | });
81 |
82 | const preferences = new electron.MenuItem({
83 | label: 'Preferences',
84 | enabled: false
85 | });
86 |
87 | const openAtLogin = new electron.MenuItem({
88 | label: 'Open at login',
89 | type: 'checkbox',
90 | click: menuItem => {
91 | appLauncher.isEnabled()
92 | .then(isEnabled => {
93 | menuItem.checked = !isEnabled;
94 |
95 | if (isEnabled) {
96 | appLauncher.disable();
97 | } else {
98 | appLauncher.enable();
99 | }
100 | });
101 | }
102 | });
103 |
104 | const quit = new electron.MenuItem({
105 | label: process.platform === 'darwin' ? `Quit ${app.getName()}` : 'Exit',
106 | click() {
107 | app.exit(0);
108 | }
109 | });
110 |
111 | const separator = new electron.MenuItem({
112 | type: 'separator'
113 | });
114 |
115 | const moduleItems = modules.map(module => {
116 | return new electron.MenuItem({
117 | label: module.name,
118 | click: onModuleClick
119 | });
120 | });
121 |
122 | const menu = new electron.Menu();
123 | menu.append(lastOpened);
124 |
125 | moduleItems.forEach(moduleItem => {
126 | menu.append(moduleItem);
127 | });
128 |
129 | menu.append(separator);
130 | menu.append(preferences);
131 | menu.append(openAtLogin);
132 | menu.append(separator);
133 | menu.append(quit);
134 |
135 | appLauncher.isEnabled()
136 | .then(isEnabled => {
137 | openAtLogin.checked = isEnabled;
138 | });
139 |
140 | return menu;
141 | }
142 |
143 | ipc.on('resize', () => {
144 | mainWindow.setSize(320, 400, true);
145 | });
146 |
147 | ipc.on('close', () => {
148 | closeMainWindow();
149 | });
150 |
151 | ipc.on('open', (e, url, name) => {
152 | if (modules.length === 5) {
153 | modules.pop();
154 | }
155 |
156 | modules.unshift({ url, name });
157 |
158 | shell.openExternal(url);
159 | closeMainWindow();
160 |
161 | trayMenu = createTrayMenu();
162 | });
163 |
164 | app.on('ready', () => {
165 | mainWindow = createMainWindow();
166 | tray = createTray();
167 | trayMenu = createTrayMenu();
168 | });
169 |
--------------------------------------------------------------------------------