├── .babelrc
├── .bowerrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .yo-rc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app
├── _locales
│ ├── de
│ │ └── messages.json
│ └── en
│ │ └── messages.json
├── images
│ ├── icon-128.png
│ ├── icon-16.png
│ ├── icon-19.png
│ ├── icon-38.png
│ └── options-loader.gif
├── manifest.json
├── options.html
├── popup.html
├── scripts.babel
│ ├── background.js
│ ├── chromereload.js
│ ├── contentscript.js
│ ├── options.js
│ └── popup.js
└── styles
│ ├── bootstrap.css.map
│ └── main.css
├── bower.json
├── gulpfile.babel.js
├── package-lock.json
├── package.json
└── test
├── index.html
└── spec
└── test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "app/bower_components",
3 | "strict-ssl": false,
4 | "registry": {
5 | "search": [
6 | "https://registry.bower.io"
7 | ]
8 | },
9 | "timeout": 300000
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.json]
15 | indent_size = 2
16 |
17 | # We recommend you to keep these unchanged
18 | end_of_line = lf
19 | charset = utf-8
20 | trim_trailing_whitespace = true
21 | insert_final_newline = true
22 |
23 | [*.md]
24 | trim_trailing_whitespace = false
25 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | temp
3 | .tmp
4 | dist
5 | .sass-cache
6 | app/bower_components
7 | test/bower_components
8 | package
9 | app/scripts
10 |
11 | .idea/
12 |
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-mocha": {
3 | "ui": "bdd",
4 | "rjs": false
5 | }
6 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # 1.0.16 (2019-12-March)
3 |
4 | * New: add full screen maximized mode support.
5 |
6 |
7 | # 1.0.15 (2018-20-June)
8 |
9 | * Proper fix for maximized mode support.
10 | * Improve monitor detection overlay, its now in max mode. For Win10, due to DPI misaligment issue, its even better, as no misaligment exists in max mode.
11 |
12 |
13 | # 1.0.14 (2018-20-June)
14 |
15 | * Revert maximized mode.
16 |
17 |
18 | # 1.0.12 (2018-20-June)
19 |
20 | * New: custom positions can be set. Click the 'more options' to create additional custom positions, and select them if needed.
21 | * Fixed maximized mode.
22 |
23 |
24 | # 1.0.11 (2018-2-April)
25 |
26 | * Usability improvements
27 | * Easy help button
28 | * Quick info section
29 | * Help / Getting started section
30 | * Corporate branding
31 | * More error handling in internal code.
32 | * Use newer features of JavaScript ES5.
33 | * Upgrade internal libraries. That include webkit bugfixes.
34 | * jquery: ~3.1.0 -> ~3.3.1,
35 | * lodash: 4.16.4 -> 4.17.5,
36 | * angular: 1.5.8 -> ~1.6.9,
37 | * angular-resource: 1.5.8 -> ~1.6.9,
38 | * font-awesome: ~4.6.3 -> ~4.7.0,
39 | * angular-bootstrap: 2.2.0 -> ~2.5.0,
40 | * file-saver: 1.3.3 -> ~1.3.8,
41 | * angular-bootstrap-checkbox: 0.4.0 -> ~0.5.1
42 | * angular-intro.js: ~3.3.0
43 |
44 |
45 |
46 | # 1.0.10 (2016-13-December)
47 |
48 | * Add ability to do batch changes. For example, if you want to change the monitor or position to all rules.
49 | * Usability: Ability to close the monitor detection by pressing "Esc" key
50 |
51 |
52 | # 1.0.8 (2016-12-December)
53 |
54 | * Fixes Name of "Monitor" does not always reflect the number.
55 | * Usability: Reduce duration of monitor detection to 3 seconds.
56 | * Fixes enforce order of rules when import template enhancement.
57 | * Usability: Save-Button does not close window.
58 |
59 |
60 | # 1.0.6 (2016-01-December)
61 |
62 | * Usability: reorganize the toolbar, and include the undo button.
63 | * Fixed after template import and restarting the page, unexpected(default) rule templates are shown.
64 |
65 |
66 | # 1.0.4 (2016-01-December)
67 |
68 | * Usability: make more visible when changes are not saved.
69 | * Fixed when importing a template, unsaved changes hint was not being displayed.
70 | * Usability: Increased the delay when showing the monitor detection.
71 | * Usability: add ability to quickly change the main settings from an existent rule.
72 | * Fixed when in advanced more the options page would become wider, so that table fits.
73 | * Improve german translations. (thanks Pascal)
74 |
75 | # 1.0.2 (2016-30-November)
76 |
77 | * Smoother transition when the options page is loading
78 |
79 |
80 |
81 | # 1.0.1 (2016-17-November)
82 |
83 | Initial implementation that includes:
84 | * Flexible positioning options via Rules concept
85 | * Multi-monitor support
86 | * Validation of rules configuration against existent Monitor.
87 | * Monitor detection
88 | * Manual positioning detection that saves into rules.
89 | * Default monitor support.
90 | * Templates have unique code for any option in order to support template merging during import.
91 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 ControlExpert
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chrome MultiWindow Positioner
2 |
3 | Tool extension that enables effective window positioning/placement in multi-monitor setups.
4 |
5 | ## Features
6 | * Flexible positioning options via Rules concept
7 | * Multi-monitor support
8 | * Validation of rules configuration against existent Monitor.
9 | * Monitor detection
10 | * Configuration templates support. It enables user profiles and larger organization distributed environments.
11 | * Manual positioning detection that saves into rules.
12 | * Default monitor support.
13 |
14 | ## Installation
15 |
16 | 1. Under the following address (https://goo.gl/bxuw3E) you will find the *MultiWindow Positioner*
17 | 2. Click the **ADD TO CHROME** button and then the **ADD EXTENSION** button.
18 | 3. The extension installation will take some seconds and you need to configure it. The configuration is available under either:
19 | * the following address: *chrome-extension://hmgehpjpfhobbnhhelhlggjfcaollidl/options.html*
20 | * or goint to the chrome://extensions/ und opening the **Options** link.
21 | 4. You may, at first, import a rules template
22 | * Click the **IMPORT TEMPLATE** icon
23 | * Give the following URL: https://cdn.rawgit.com/ControlExpert/chrome-multiwindow-positioner/gh-pages/templates/default-template-options.json
24 | * Click **ADD** to complete the dialog.
25 | * Finally click **SAVE** to save all the changes permanently.
26 | 5. If you need to to use other monitors for specific rules/websites you may edit/add a rule respectively.
27 | 6. In the edit or create rule dialog you may:
28 | * *Template*: List all available rule-templates.
29 | * *Active*: Tells if the rule is enabled and active.
30 | * *Remember*: (experimental) When enabled, automatically saves the target monitor when a window that matches to rule was re-position manually by the user.
31 | * *Name*: The rule name
32 | * *URL*: The address that matches the rule. Its case sensitive. (should usually not change).
33 | * *Monitor*: Target-Monitor of the rule. It lists all the available monitors.
34 | * *Default Monitor*: Will pre-select the target-monitor when importing a template (in case no matching monitor was given from the Rules template.)
35 | * *Position*: The position within the *target-monitor* where the window will be placed.
36 | * *Popup*: Shows the web address
37 | 7. Click **UPDATE** to accept the dialog changes.
38 | 8. Finally, click **SAVE** to save the all changes.
39 |
40 | # Development
41 |
42 | ## Getting Started
43 |
44 | ```sh
45 | # Install dependencies
46 | npm install
47 | npm -g install bower
48 | bower install
49 |
50 | # Transform updated source written by ES2015 (default option)
51 | /
52 |
53 | # or Using watch to update source continuously
54 | gulp watch
55 |
56 | # Make a production version extension
57 | gulp build
58 | ```
59 |
60 | ## Test Chrome Extension
61 |
62 | To test, go to: chrome://extensions, enable Developer mode and load app as an unpacked extension.
63 |
64 | Need more information about Chrome Extension? Please visit [Google Chrome Extension Development](http://developer.chrome.com/extensions/devguide.html)
65 |
66 | ## gulp tasks
67 |
68 | ### Babel
69 |
70 | The generator supports ES 2015 syntax through babel transforming. You may have a source files in `script.babel` if your project has been generated without `--no-babel` options. While developing, When those of source has been changed, `gulp babel` should be run before test and run a extension on Chrome.
71 |
72 | ```sh
73 | gulp babel
74 | ```
75 |
76 | If you would like to have a continuous transforming by babel you can use `watch` task
77 |
78 | ### Watch
79 |
80 | Watch task helps you reduce your efforts during development extensions. If the task detects your changes of source files, re-compile your sources automatically or Livereload([chromereload.js](https://github.com/yeoman/generator-chrome-extension/blob/master/app/templates/scripts/chromereload.js)) reloads your extension. If you would like to know more about Live-reload and preview of Yeoman? Please see [Getting started with Yeoman and generator-webapp](http://youtu.be/zBt2g9ekiug?t=3m51s) for your understanding.
81 |
82 | ```bash
83 | gulp watch
84 | ```
85 |
86 | ### Build and Package
87 |
88 | It will build your app as a result you can have a distribution version of the app in `dist`. Run this command to build your Chrome Extension app.
89 |
90 | ```bash
91 | gulp build
92 | ```
93 |
94 | You can also distribute your project with compressed file using the Chrome Developer Dashboard at Chrome Web Store. This command will compress your app built by `gulp build` command.
95 |
96 | ```bash
97 | gulp package
98 | ```
99 |
100 | ### ES2015 and babel
101 |
102 | You can use es2015 now for developing the Chrome extension. However, at this moment, you need to execute `babel` task of gulp to compile to test and run your extension on Chrome, because [ES2015 is not full functionality on Chrome as yet](http://kangax.github.io/compat-table/es6/).
103 |
104 | The sources written by es2015 is located at `scripts.babel` and runnable sources are will be at `script` after compiling by `gulp babel`. May you don't want to use babel and ES2015 use `--no-babel` option when scaffolding a new project.
105 |
106 | ## Contribute
107 |
108 | See the [contributing docs](https://github.com/ControlExpert/chrome-multiwindow-positioner/blob/master/contributing.md)
109 |
110 | ## License
111 |
112 | [MIT license](https://github.com/ControlExpert/chrome-multiwindow-positioner/blob/master/LICENSE)
113 |
--------------------------------------------------------------------------------
/app/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "MultiWindow Positioner",
4 | "description": ""
5 | },
6 | "appDescription": {
7 | "message": "Tool-Erweiterung die eine effektive Fensterpositionierung / Platzierung in Multi-Monitor Umgebung ermöglicht.",
8 | "description": ""
9 | },
10 | "OPTIONS_TITLE": {
11 | "message": "MultiWindow Positioner - Optionen",
12 | "description": ""
13 | },
14 | "TAB_SETTINGS": {
15 | "message": "Regeln",
16 | "description": ""
17 | },
18 | "TAB_POSITIONS": {
19 | "message": "Benutzerdefinierte Positionen",
20 | "description": ""
21 | },
22 | "CUSTOM": {
23 | "message": "Benutzerdefinierte",
24 | "description": ""
25 | },
26 | "WIDTH": {
27 | "message": "Lange",
28 | "description": ""
29 | },
30 | "HEIGHT": {
31 | "message": "Hohe",
32 | "description": ""
33 | },
34 | "ACTIVE": {
35 | "message": "Aktiv",
36 | "description": ""
37 | },
38 | "NAME": {
39 | "message": "Name",
40 | "description": ""
41 | },
42 | "URL": {
43 | "message": "URL",
44 | "description": ""
45 | },
46 | "REMEMBER": {
47 | "message": "Merken",
48 | "description": ""
49 | },
50 | "MONITOR": {
51 | "message": "Monitor",
52 | "description": ""
53 | },
54 | "POSITION": {
55 | "message": "Position",
56 | "description": ""
57 | },
58 | "PLAIN": {
59 | "message": "Ebene",
60 | "description": ""
61 | },
62 | "EDIT_TAB_RULE": {
63 | "message": "Regel bearbeiten",
64 | "description": ""
65 | },
66 | "DELETE_TAB_RULE": {
67 | "message": "Regel löschen",
68 | "description": ""
69 | },
70 | "MOVE_UP": {
71 | "message": "Nach oben",
72 | "description": ""
73 | },
74 | "MOVE_DOWN": {
75 | "message": "Nach unten",
76 | "description": ""
77 | },
78 | "NEW_TAB_OPTION_TITLE": {
79 | "message": "Neuer Tab",
80 | "description": ""
81 | },
82 | "EDIT_TAB_OPTION_TITLE": {
83 | "message": "Tab-Regel bearbeiten",
84 | "description": ""
85 | },
86 | "TEMPLATE": {
87 | "message": "Vorlage",
88 | "description": ""
89 | },
90 | "PLAIN_WINDOW": {
91 | "message": "Ebene Fenster",
92 | "description": ""
93 | },
94 | "ADD": {
95 | "message": "Hinzufügen",
96 | "description": ""
97 | },
98 | "UPDATE": {
99 | "message": "Aktualisieren",
100 | "description": ""
101 | },
102 | "CANCEL": {
103 | "message": "Abbrechen",
104 | "description": ""
105 | },
106 | "IMPORT_TEMPLATE": {
107 | "message": "Vorlage importieren",
108 | "description": ""
109 | },
110 | "TEMPLATE_URL": {
111 | "message": "Vorlagen-URL",
112 | "description": ""
113 | },
114 | "REPLACE_ALL_TEMPLATES": {
115 | "message": "Ersetzen Sie alle Vorlage",
116 | "description": ""
117 | },
118 | "ADD_TAB_OPTION": {
119 | "message": "Regel hinzufügen",
120 | "description": ""
121 | },
122 | "SAVE": {
123 | "message": "Speichern",
124 | "description": ""
125 | },
126 | "UNDO": {
127 | "message": "Rücksetzen",
128 | "description": ""
129 | },
130 | "RELOAD": {
131 | "message": "Neu laden",
132 | "description": ""
133 | },
134 | "EXPORT_TEMPLATE": {
135 | "message": "Exportvorlage",
136 | "description": ""
137 | },
138 | "SHOW_MORE_OPTIONS": {
139 | "message": "Weitere Optionen anzeigen",
140 | "description": ""
141 | },
142 | "VALIDATE_RULES": {
143 | "message": "Überprüfen alle existierende Regel",
144 | "description": ""
145 | },
146 | "DETECT_MONITORS": {
147 | "message": "Monitore erkennen",
148 | "description": ""
149 | },
150 | "AUTO_REPAIR_RULES": {
151 | "message": "Reparieren",
152 | "description": ""
153 | },
154 | "MAXIMIZED": {
155 | "message": "Maximiert",
156 | "description": ""
157 | },
158 | "LEFT_HALF": {
159 | "message": "linke Hälfte",
160 | "description": ""
161 | },
162 | "RIGHT_HALF": {
163 | "message": "rechte Hälfte",
164 | "description": ""
165 | },
166 | "TOP_HALF": {
167 | "message": "obere Hälfte",
168 | "description": ""
169 | },
170 | "BOTTOM_HALF": {
171 | "message": "untere Hälfte",
172 | "description": ""
173 | },
174 | "FULLSCREEN": {
175 | "message": "Vollbild",
176 | "description": ""
177 | },
178 | "DEFAULT_MONITOR": {
179 | "message": "Standardmonitor",
180 | "description": ""
181 | },
182 | "MAIN_MONITOR": {
183 | "message": "Hauptmonitor",
184 | "description": ""
185 | },
186 | "NOT_MAIN_MONITOR": {
187 | "message": "Nicht Hauptmonitor",
188 | "description": ""
189 | },
190 | "BIGGEST_RESOLUTION": {
191 | "message": "Größte Auflösung",
192 | "description": ""
193 | },
194 | "BIGGEST_HEIGHT": {
195 | "message": "Größte Höhe",
196 | "description": ""
197 | },
198 | "BIGGEST_WIDTH": {
199 | "message": "Größte Breite",
200 | "description": ""
201 | },
202 | "SMALLEST_RESOLUTION": {
203 | "message": "Kleinste Auflösung",
204 | "description": ""
205 | },
206 | "SMALLEST_HEIGHT": {
207 | "message": "Kleinste Höhe",
208 | "description": ""
209 | },
210 | "SMALLEST_WIDTH": {
211 | "message": "Kleinste Breite",
212 | "description": ""
213 | },
214 | "RULE_NAME_PLACEHOLDER": {
215 | "message": "Geben Sie einen Regelnamen ein...",
216 | "description": ""
217 | },
218 | "DRAFT": {
219 | "message": "Entwurf",
220 | "description": ""
221 | },
222 | "ADD_POSITION": {
223 | "message": "Fügen Sie eine benutzerdefinierte Position hinzu",
224 | "description": ""
225 | },
226 | "REMOVE_POSITION": {
227 | "message": "Entfernen Sie die benutzerdefinierte Position",
228 | "description": ""
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/app/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "MultiWindow Positioner",
4 | "description": "The name of the application"
5 | },
6 | "appDescription": {
7 | "message": "Tool extension that enables effective window positioning/placement in multi-monitor setups",
8 | "description": "The description of the application"
9 | },
10 | "OPTIONS_TITLE": {
11 | "message": "MultiWindow Positioner - Options",
12 | "description": ""
13 | },
14 | "TAB_SETTINGS": {
15 | "message": "Rules",
16 | "description": ""
17 | },
18 | "TAB_POSITIONS": {
19 | "message": "Custom Positions",
20 | "description": ""
21 | },
22 | "CUSTOM": {
23 | "message": "Custom",
24 | "description": ""
25 | },
26 | "WIDTH": {
27 | "message": "Width",
28 | "description": ""
29 | },
30 | "HEIGHT": {
31 | "message": "Height",
32 | "description": ""
33 | },
34 | "ACTIVE": {
35 | "message": "Active",
36 | "description": ""
37 | },
38 | "NAME": {
39 | "message": "Name",
40 | "description": ""
41 | },
42 | "URL": {
43 | "message": "URL",
44 | "description": ""
45 | },
46 | "REMEMBER": {
47 | "message": "Remember",
48 | "description": ""
49 | },
50 | "MONITOR": {
51 | "message": "Monitor",
52 | "description": ""
53 | },
54 | "POSITION": {
55 | "message": "Position",
56 | "description": ""
57 | },
58 | "PLAIN": {
59 | "message": "Plain",
60 | "description": ""
61 | },
62 | "EDIT_TAB_RULE": {
63 | "message": "Edit tab rule",
64 | "description": ""
65 | },
66 | "DELETE_TAB_RULE": {
67 | "message": "Delete tab rule",
68 | "description": ""
69 | },
70 | "MOVE_UP": {
71 | "message": "Move up",
72 | "description": ""
73 | },
74 | "MOVE_DOWN": {
75 | "message": "Move down",
76 | "description": ""
77 | },
78 | "NEW_TAB_OPTION_TITLE": {
79 | "message": "New tab option",
80 | "description": ""
81 | },
82 | "EDIT_TAB_OPTION_TITLE": {
83 | "message": "Edit tab option",
84 | "description": ""
85 | },
86 | "TEMPLATE": {
87 | "message": "Template",
88 | "description": ""
89 | },
90 | "PLAIN_WINDOW": {
91 | "message": "Plain Window",
92 | "description": ""
93 | },
94 | "ADD": {
95 | "message": "ADD",
96 | "description": ""
97 | },
98 | "UPDATE": {
99 | "message": "UPDATE",
100 | "description": ""
101 | },
102 | "CANCEL": {
103 | "message": "Cancel",
104 | "description": ""
105 | },
106 | "IMPORT_TEMPLATE": {
107 | "message": "Import Template",
108 | "description": ""
109 | },
110 | "TEMPLATE_URL": {
111 | "message": "Template URL",
112 | "description": ""
113 | },
114 | "REPLACE_ALL_TEMPLATES": {
115 | "message": "Replace all templates",
116 | "description": ""
117 | },
118 | "ADD_TAB_OPTION": {
119 | "message": "Add Tab Option",
120 | "description": ""
121 | },
122 | "SAVE": {
123 | "message": "Save",
124 | "description": ""
125 | },
126 | "UNDO": {
127 | "message": "Undo",
128 | "description": ""
129 | },
130 | "RELOAD": {
131 | "message": "Re-load",
132 | "description": ""
133 | },
134 | "EXPORT_TEMPLATE": {
135 | "message": "Export template",
136 | "description": ""
137 | },
138 | "SHOW_MORE_OPTIONS": {
139 | "message": "Show more options",
140 | "description": ""
141 | },
142 | "VALIDATE_RULES": {
143 | "message": "Validate tab rules",
144 | "description": ""
145 | },
146 | "DETECT_MONITORS": {
147 | "message": "Detect Monitors",
148 | "description": ""
149 | },
150 | "AUTO_REPAIR_RULES": {
151 | "message": "Auto-repair rules",
152 | "description": ""
153 | },
154 | "MAXIMIZED": {
155 | "message": "Maximized",
156 | "description": ""
157 | },
158 | "LEFT_HALF": {
159 | "message": "Left-Half",
160 | "description": ""
161 | },
162 | "RIGHT_HALF": {
163 | "message": "Right-Half",
164 | "description": ""
165 | },
166 | "TOP_HALF": {
167 | "message": "Top-Half",
168 | "description": ""
169 | },
170 | "BOTTOM_HALF": {
171 | "message": "Bottom-Half",
172 | "description": ""
173 | },
174 | "FULLSCREEN": {
175 | "message": "Fullscreen",
176 | "description": ""
177 | },
178 | "DEFAULT_MONITOR": {
179 | "message": "Default Monitor",
180 | "description": ""
181 | },
182 | "MAIN_MONITOR": {
183 | "message": "Main monitor",
184 | "description": ""
185 | },
186 | "NOT_MAIN_MONITOR": {
187 | "message": "Not main monitor",
188 | "description": ""
189 | },
190 | "BIGGEST_RESOLUTION": {
191 | "message": "Biggest Resolution",
192 | "description": ""
193 | },
194 | "BIGGEST_HEIGHT": {
195 | "message": "Biggest Height",
196 | "description": ""
197 | },
198 | "BIGGEST_WIDTH": {
199 | "message": "Biggest Width",
200 | "description": ""
201 | },
202 | "SMALLEST_RESOLUTION": {
203 | "message": "Smallest Resolution",
204 | "description": ""
205 | },
206 | "SMALLEST_HEIGHT": {
207 | "message": "Smallest Height",
208 | "description": ""
209 | },
210 | "SMALLEST_WIDTH": {
211 | "message": "Smallest Width",
212 | "description": ""
213 | },
214 | "RULE_NAME_PLACEHOLDER": {
215 | "message": "Type a rule name...",
216 | "description": ""
217 | },
218 | "DRAFT": {
219 | "message": "Draft",
220 | "description": ""
221 | },
222 | "ADD_POSITION": {
223 | "message": "Add custom position",
224 | "description": ""
225 | },
226 | "REMOVE_POSITION": {
227 | "message": "Remove custom position",
228 | "description": ""
229 | }
230 |
231 |
232 | }
233 |
--------------------------------------------------------------------------------
/app/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-128.png
--------------------------------------------------------------------------------
/app/images/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-16.png
--------------------------------------------------------------------------------
/app/images/icon-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-19.png
--------------------------------------------------------------------------------
/app/images/icon-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-38.png
--------------------------------------------------------------------------------
/app/images/options-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/options-loader.gif
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_appName__",
4 | "short_name": "MWP",
5 | "version": "1.0.17",
6 |
7 | "default_locale": "en",
8 | "description": "__MSG_appDescription__",
9 | "author": "Igor Lino @ ControlExpert",
10 | "icons": {
11 | "16": "images/icon-16.png",
12 | "128": "images/icon-128.png"
13 | },
14 |
15 | "homepage_url": "https://github.com/ControlExpert/chrome-multiwindow-positioner",
16 | "background": {
17 | "scripts": [
18 | "scripts/chromereload.js",
19 | "scripts/background.js"
20 | ]
21 | },
22 | "permissions": [
23 | "system.display",
24 | "tabs"
25 | ],
26 | "options_ui": {
27 | "page": "options.html",
28 | "chrome_style": true,
29 | "open_in_tab": true
30 | },
31 | "content_scripts": [
32 | {
33 | "matches": [
34 | "http://*/*",
35 | "https://*/*"
36 | ],
37 | "js": [
38 | "scripts/contentscript.js"
39 | ],
40 | "run_at": "document_end",
41 | "all_frames": false
42 | }
43 | ],
44 | "externally_connectable": {
45 | "matches": ["*://igorlino.github.io/*"]
46 | },
47 | "web_accessible_resources": [
48 | "images/icon-48.png",
49 | "images/options-loader.gif"
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/app/options.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
36 |
37 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
{{::locale.TAB_SETTINGS}}
59 |
62 |
63 |
64 |
65 |
66 |
67 | {{::locale.ACTIVE}}
68 | {{::locale.NAME}}
69 | {{::locale.URL}}
70 | {{::locale.REMEMBER}}
71 | {{::locale.CUSTOM}}
72 | {{::locale.MONITOR}}
73 | {{::locale.DEFAULT_MONITOR}}
74 | {{::locale.POSITION}}
75 | {{::locale.PLAIN}}
76 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
91 |
92 |
93 | {{tabOption.name}}
94 | {{tabOption.url}}
95 |
96 |
99 |
100 |
104 |
105 |
106 |
109 | {{tabOption.custom}}
110 |
113 | select custom
114 | {{position.name}}
115 |
116 |
117 |
118 | {{tabOption.monitor.idx}} {{tabOption.monitor.name}}
119 |
123 |
124 | {{localizeDefaultMonitor(tabOption.defaultMonitor)}}
125 |
126 | {{localizePosition(tabOption.position)}}
127 |
130 | {{option.name}}
131 |
132 |
133 |
134 |
135 |
138 |
139 |
140 |
143 |
144 |
145 |
149 |
150 |
151 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
{{::locale.TAB_POSITIONS}}
172 |
175 |
176 |
177 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
240 |
241 |
242 | {{::locale.NEW_TAB_OPTION_TITLE}}
243 | {{::locale.EDIT_TAB_OPTION_TITLE}}
244 |
245 |
246 |
247 |
262 |
263 |
264 |
265 |
266 |
278 |
279 |
280 |
292 |
293 |
294 |
306 |
307 |
319 |
320 |
333 |
334 |
347 |
348 |
349 |
362 |
363 |
375 |
376 |
377 |
378 |
379 |
380 |
383 | {{::locale.ADD}}
384 |
385 |
388 | {{::locale.UPDATE}}
389 |
390 |
{{::locale.CANCEL}}
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 | {{::locale.IMPORT_TEMPLATE}}
407 |
408 |
437 |
438 |
439 |
440 |
444 | {{::locale.ADD}}
445 |
446 | {{::locale.CANCEL}}
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
463 |
466 |
469 |
473 |
474 |
475 |
476 |
480 |
483 |
488 |
489 |
490 |
491 |
495 |
496 |
497 |
502 |
503 |
504 |
508 |
509 |
510 |
511 |
512 |
517 |
518 |
519 | Toggle Dropdown
520 |
521 |
533 |
534 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
Quick Info
550 |
551 |
552 |
553 |
554 | Help and getting started click
555 |
559 |
560 |
561 |
562 |
563 | You are happy for the extension and would like to give a good review/feedback/rating, then click
564 |
565 | here
566 |
567 |
568 |
569 | Support questions may be raised
570 |
571 | here
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
590 |
591 |
592 |
593 |
Help - Getting Started
594 |
595 |
596 |
597 |
598 | You may, at first, import a rules template
599 |
600 |
601 | Click the IMPORT TEMPLATE icon
602 |
603 |
604 | Give the following URL: https://cdn.rawgit.com/ControlExpert/chrome-multiwindow-positioner/gh-pages/templates/default-template-options.json
605 |
606 |
607 | Click the button ADD to complete the dialog.
608 |
609 |
610 | Finally click SAVE to save all the changes permanently.
611 |
612 |
613 |
614 |
615 | If you need to to use other monitors for specific rules/websites you may edit/add a rule respectively.
616 |
617 |
618 | In the edit or create rule dialog you may:
619 |
620 | Template : List all available rule-templates.
621 | Active : Tells if the rule is enabled and active.
622 | Remember : (experimental) When enabled, automatically saves the target monitor when a window that matches to rule was re-position manually by the user.
623 | Name : The rule name
624 | URL : The address that matches the rule. Its case sensitive. (should usually not change).
625 | Monitor : Target-Monitor of the rule. It lists all the available monitors.
626 | Default Monitor : Will pre-select the target-monitor when importing a template (in case no matching monitor was given from the Rules template.)
627 | Position : The position within the target-monitor where the window will be placed.
628 | Popup : Shows the web address
629 |
630 |
631 |
632 | Click the button UPDATE to accept the dialog changes.
633 |
634 |
635 | Finally, click to save the all changes.
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
--------------------------------------------------------------------------------
/app/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | MultiWindow Positioner
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/scripts.babel/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | try {
4 | chrome.runtime.onInstalled.addListener(details => {
5 | console.log('previousVersion', details.previousVersion);
6 | });
7 | } catch(err) {
8 | (console.error || console.log).call(console, err.stack || err);
9 | }
10 |
11 | function showOptionsPage() {
12 | try {
13 | chrome.runtime.openOptionsPage();
14 | } catch(err) {
15 | (console.error || console.log).call(console, err.stack || err);
16 | }
17 | }
18 |
19 | //chrome.browserAction.setBadgeText({text: '\'Allo'});
20 | //chrome.browserAction.onClicked.addListener(showOptionsPage);
21 |
22 | //console.log('\'Allo \'Allo! Event Page for Browser Action');
23 |
24 | const OPTIONS_KEY = 'TAB_HELPER_OPTIONS';
25 |
26 | const POSITIONS = {
27 | CENTER: {id: 'center', name: 'center'},
28 | LEFT_HALF: {id: 'left-half', name: 'left-half'},
29 | RIGHT_HALF: {id: 'right-half', name: 'right-half'},
30 | TOP_HALF: {id: 'top-half', name: 'top-half'},
31 | BOTTOM_HALF: {id: 'bottom-half', name: 'bottom-half'},
32 | FULLSCREEN: { id: 'fullscreen', name: 'fullscreen' }
33 | };
34 |
35 | const WINDOW_ID_NONE = -1;
36 | const PIXEL_MONITOR_DETECTION_DELTA = 100;
37 | const WINDOW_CHANGE_DETECTION_INTERVAL = 1000;
38 | const MAX_MOVE_TRIES = 10;
39 |
40 | const WINDOW_CACHE_SIZE = 20;
41 | const windowCache = [];
42 |
43 | const WINDOW_STATES = {
44 | NORMAL: 'normal',
45 | MINIMIZED: 'minimized',
46 | MAXIMIZED: 'maximized',
47 | FULLSCREEN: 'fullscreen',
48 | DOCKED: 'docked'
49 | };
50 |
51 | const states = {
52 | lastWindowInFocus: WINDOW_ID_NONE,
53 | currentWindowInFocus: WINDOW_ID_NONE,
54 | currentWindowLocationHandler: null
55 | };
56 |
57 | let displayInfos = [];
58 |
59 | loadDisplayInfos();
60 |
61 | function loadDisplayInfos() {
62 | try {
63 | chrome.system.display.getInfo(function (displayInfosResult) {
64 | displayInfos = displayInfosResult;
65 | });
66 | } catch(err) {
67 | (console.error || console.log).call(console, err.stack || err);
68 | }
69 | }
70 |
71 |
72 | // chrome.windows.onRemoved.addListener(function callback(windowId) {
73 | // console.log('Window removed ' + windowId);
74 | // const indexToRemove = findCachedWindow(windowId);
75 | // if (indexToRemove !== -1) {
76 | // const window = windowCache[indexToRemove];
77 | // windowCache.splice(indexToRemove, 1);
78 | // updateTabRules(windowId, window);
79 | // }
80 | // });
81 |
82 | function findCachedWindow(windowId) {
83 | let found = -1;
84 | try {
85 | for (let idx = 0; idx < windowCache.length; idx++) {
86 | if (windowCache[idx].id === windowId) {
87 | found = idx;
88 | }
89 | }
90 | } catch(err) {
91 | (console.error || console.log).call(console, err.stack || err);
92 | }
93 | return found
94 | }
95 |
96 | function storeWindowIntoCache(window) {
97 | try {
98 | const idx = findCachedWindow(window.id);
99 | if (idx >= 0) {
100 | windowCache.splice(idx, 1);
101 | }
102 | if (windowCache.length >= WINDOW_CACHE_SIZE) {
103 | windowCache.shift();
104 | }
105 | console.log('Window cached ' + window.id);
106 | windowCache.push(window);
107 | } catch(err) {
108 | (console.error || console.log).call(console, err.stack || err);
109 | }
110 | }
111 |
112 | try {
113 | chrome.windows.onFocusChanged.addListener(onFocusChangeListener);
114 | } catch(err) {
115 | (console.error || console.log).call(console, err.stack || err);
116 | }
117 |
118 | function onFocusChangeListener(windowId) {
119 | try {
120 | console.log('Window Focused ' + windowId);
121 | const allIdentifiersMap = {};
122 | allIdentifiersMap['i' + states.lastWindowInFocus] = states.lastWindowInFocus;
123 | allIdentifiersMap['i' + states.currentWindowInFocus] = states.currentWindowInFocus;
124 | allIdentifiersMap['i' + windowId] = windowId;
125 |
126 | states.lastWindowInFocus = states.currentWindowInFocus;
127 | states.currentWindowInFocus = windowId;
128 | console.log('Window transition ' + states.lastWindowInFocus + ' to ' + states.currentWindowInFocus);
129 |
130 | for (const key in allIdentifiersMap) {
131 | if (allIdentifiersMap.hasOwnProperty(key)) {
132 | const windowId = allIdentifiersMap[key];
133 | if (windowId !== WINDOW_ID_NONE) {
134 | startUpdateTabRules(windowId);
135 | }
136 | }
137 | }
138 | } catch(err) {
139 | (console.error || console.log).call(console, err.stack || err);
140 | }
141 |
142 | function startUpdateTabRules(targetWindowId) {
143 | setTimeout(function () {
144 | updateTabRules(targetWindowId);
145 | setTimeout(function () {
146 | updateTabRules(targetWindowId);
147 | }, WINDOW_CHANGE_DETECTION_INTERVAL * 5);
148 | }, WINDOW_CHANGE_DETECTION_INTERVAL);
149 | }
150 |
151 | }
152 |
153 | function updateTabRules(windowId, cachedWindow) {
154 | try {
155 | if (cachedWindow) {
156 | doUpdateTabRules(cachedWindow);
157 | } else {
158 | chrome.windows.get(windowId, {
159 | populate: true
160 | }, function (window) {
161 | try {
162 | if (window) {
163 | storeWindowIntoCache(window);
164 | doUpdateTabRules(window);
165 | }
166 | } catch (e) {
167 | if (e.toString().indexOf('No window with id') >= 0) {
168 | }
169 | }
170 | });
171 | }
172 | } catch(err) {
173 | (console.error || console.log).call(console, err.stack || err);
174 | }
175 |
176 | function doUpdateTabRules(window) {
177 | if (window && window.tabs) {
178 | const tabRuleOptions = loadOptions();
179 | for (let idx = 0; idx < window.tabs.length; idx++) {
180 | const tab = window.tabs[idx];
181 | const tabRule = findTabRuleMatch(tabRuleOptions, tab);
182 | if (tabRule && tabRule.remember && !validateTabLocation(window, tab, tabRule)) {
183 | const monitor = findMonitorByWindow(window);
184 | if (monitor) {
185 | const position = determinePositionByCurrentLocation(monitor, window);
186 | if (position) {
187 | const changed = updateTabRuleByLocation(tabRule, monitor, position, windowId);
188 | if (changed) {
189 | saveOptions(tabRuleOptions);
190 | }
191 | }
192 | }
193 | }
194 | }
195 | }
196 | }
197 | }
198 |
199 | function determinePositionByCurrentLocation(monitor, window) {
200 | let position = POSITIONS.CENTER.id;
201 | try {
202 | if (window.state === WINDOW_STATES.MAXIMIZED) {
203 | position = POSITIONS.CENTER.id;
204 | } else if (window.state === WINDOW_STATES.FULLSCREEN) {
205 | position = POSITIONS.FULLSCREEN.id;
206 | } else {
207 | for (const key in POSITIONS) {
208 | if (POSITIONS.hasOwnProperty(key)) {
209 | const workArea = calculateWorkAreaByPosition(monitor.workArea, POSITIONS[key].id);
210 | if (matchesWorkArea(window, workArea, PIXEL_MONITOR_DETECTION_DELTA)) {
211 | position = POSITIONS[key].id;
212 | break;
213 | }
214 | }
215 | }
216 | }
217 | } catch(err) {
218 | (console.error || console.log).call(console, err.stack || err);
219 | }
220 | return position;
221 | }
222 |
223 | function matchesWorkArea(window, workArea, pixelErrorMargin) {
224 | let matches = false;
225 | try {
226 | const delta = pixelErrorMargin ? pixelErrorMargin : 0;
227 | matches = (
228 | window.top >= (workArea.top - delta) &&
229 | window.top <= (workArea.top + delta) &&
230 | window.top + window.height >= (workArea.top - delta) + workArea.height &&
231 | window.top + window.height <= (workArea.top + delta) + workArea.height &&
232 | window.left >= (workArea.left - delta) &&
233 | window.left <= (workArea.left + delta) &&
234 | window.left + window.width >= (workArea.left - delta) + workArea.width &&
235 | window.left + window.width <= (workArea.left + delta) + workArea.width
236 | );
237 | } catch(err) {
238 | (console.error || console.log).call(console, err.stack || err);
239 | }
240 | return matches;
241 | }
242 |
243 | function findMonitorByWindow(window) {
244 | let monitor = null;
245 | try {
246 | let highestIdx = -1;
247 | let highestArea = -1;
248 | for (let idx = 0; idx < displayInfos.length; idx++) {
249 | const display = displayInfos[idx];
250 | const displayWorkArea = display.workArea;
251 | const rightMostLeft = window.left > displayWorkArea.left ? window.left : displayWorkArea.left;
252 | const leftMostRight = window.left + window.width < displayWorkArea.left + displayWorkArea.width ?
253 | window.left + window.width : displayWorkArea.left + displayWorkArea.width;
254 | const bottomMostTop = window.top > displayWorkArea.top ? window.top : displayWorkArea.top;
255 | const topMostBottom = window.top + window.height < displayWorkArea.top + displayWorkArea.height ?
256 | window.top + window.height : displayWorkArea.top + displayWorkArea.height;
257 |
258 | const area = (leftMostRight - rightMostLeft) * (topMostBottom - bottomMostTop);
259 | if (area > highestArea) {
260 | highestArea = area;
261 | highestIdx = idx;
262 | }
263 | /*if (window.top >= displayWorkArea.top &&
264 | window.top <= displayWorkArea.top + displayWorkArea.height &&
265 | window.left >= displayWorkArea.left &&
266 | window.left <= displayWorkArea.left + displayWorkArea.width) {
267 | monitor = display;
268 | break;
269 | }*/
270 | }
271 | if (highestIdx !== -1) {
272 | monitor = displayInfos[highestIdx];
273 | }
274 | } catch(err) {
275 | (console.error || console.log).call(console, err.stack || err);
276 | }
277 | return monitor;
278 | }
279 |
280 | function updateTabRuleByLocation(tabRule, monitor, position, windowId) {
281 | let changed = false;
282 | try {
283 | if (tabRule.position !== position &&
284 | tabRule.monitor.id !== monitor.id) {
285 | console.log('TabRule Reposition Saved (triggered by window.id:' + windowId + ')');
286 | console.log(tabRule.position + ' -> ' + position);
287 | console.log(tabRule.monitor.workArea);
288 | console.log(monitor.workArea);
289 | tabRule.position = position;
290 | tabRule.monitor = monitor;
291 | changed = true;
292 | }
293 | } catch(err) {
294 | (console.error || console.log).call(console, err.stack || err);
295 | }
296 |
297 | return changed;
298 | }
299 |
300 | function validateTabLocation(window, tab, tabRule) {
301 | let valid = true;
302 | try {
303 | valid = (window.left === tabRule.monitor.workArea.left &&
304 | window.top === tabRule.monitor.workArea.top &&
305 | window.width === tabRule.monitor.workArea.width &&
306 | window.height === tabRule.monitor.workArea.height);
307 | } catch(err) {
308 | (console.error || console.log).call(console, err.stack || err);
309 | }
310 | return valid;
311 | }
312 |
313 | function findTabRuleMatch(tabRuleOptions, tab) {
314 | let match = null;
315 | try {
316 | if (tab) {
317 | for (let idx = 0; idx < tabRuleOptions.tabs.length; idx++) {
318 | const tabRule = tabRuleOptions.tabs[idx];
319 | if (tabRule.active && tab.url && tabRule.url && tab.url.indexOf(tabRule.url) >= 0) {
320 | match = tabRule;
321 | break;
322 | }
323 | }
324 | }
325 | } catch(err) {
326 | (console.error || console.log).call(console, err.stack || err);
327 | }
328 | return match;
329 | }
330 |
331 | function findCustomPositionMatch(tabRuleOptions, custom) {
332 | let match = null;
333 | try {
334 | if (custom) {
335 | for (let idx = 0; idx < tabRuleOptions.positions.length; idx++) {
336 | const customPosition = tabRuleOptions.positions[idx];
337 | if (customPosition.name && customPosition.name !== '' && customPosition.name === custom) {
338 | match = customPosition;
339 | break;
340 | }
341 | }
342 | }
343 | } catch(err) {
344 | (console.error || console.log).call(console, err.stack || err);
345 | }
346 | return match;
347 | }
348 |
349 | function calculateWorkAreaByPosition(monitorWorkArea, position) {
350 | const workarea = {
351 | left: monitorWorkArea.left,
352 | top: monitorWorkArea.top,
353 | width: monitorWorkArea.width,
354 | height: monitorWorkArea.height
355 | };
356 |
357 | if (position === POSITIONS.LEFT_HALF.id) {
358 | workarea.width = Math.floor(workarea.width / 2);
359 | }
360 | if (position === POSITIONS.RIGHT_HALF.id) {
361 | const halfWidth = Math.floor(workarea.width / 2);
362 | workarea.left += workarea.width - halfWidth;
363 | workarea.width = halfWidth;
364 | }
365 | if (position === POSITIONS.TOP_HALF.id) {
366 | workarea.height = Math.floor(workarea.height / 2);
367 | }
368 | if (position === POSITIONS.BOTTOM_HALF.id) {
369 | const halfHeight = Math.floor(workarea.height / 2);
370 | workarea.top += workarea.height - halfHeight;
371 | workarea.height = halfHeight;
372 | }
373 | return workarea;
374 | }
375 |
376 | function loadOptions() {
377 | let tabRuleOptions = localStorage[OPTIONS_KEY];
378 | try {
379 | tabRuleOptions = tabRuleOptions ? JSON.parse(tabRuleOptions) : {
380 | tabs: []
381 | };
382 | if (!tabRuleOptions.options) {
383 | tabRuleOptions.options = [];
384 | }
385 | } catch(err) {
386 | (console.error || console.log).call(console, err.stack || err);
387 | }
388 | return tabRuleOptions;
389 | }
390 |
391 | function saveOptions(tabRuleOptions) {
392 | localStorage[OPTIONS_KEY] = JSON.stringify(tabRuleOptions);
393 | }
394 |
395 | try {
396 | chrome.tabs.onCreated.addListener(onTabCreated);
397 | //chrome.tabs.onUpdated.addListener(onTabUpdate);
398 | } catch(err) {
399 | (console.error || console.log).call(console, err.stack || err);
400 | }
401 |
402 | // function onTabUpdate(tabId, changeInfo, tab) {
403 | // if (changeInfo.url && changeInfo.url !== '') {
404 | // console.log('Tab updated id:' + tab.id + ' url:' + changeInfo.url);
405 | // onTabCreated(tab, true);
406 | // }
407 | // }
408 |
409 | function getInt(value) {
410 | let intValue = 0;
411 | try {
412 | if (typeof(value) === 'string') {
413 | intValue = parseInt(value, 10);
414 | } else if (typeof(value) === 'number') {
415 | intValue = value;
416 | }
417 | } catch(err) {
418 | (console.error || console.log).call(console, err.stack || err);
419 | }
420 | return intValue;
421 | }
422 |
423 | function onTabCreated(tab, disableCreationMessage) {
424 | try {
425 | if (!disableCreationMessage) {
426 | console.log('Tab Created id:' + tab.id + ' url:' + tab.url);
427 | }
428 | moveTabIntoPositionedWindow(tab, 0);
429 | } catch(err) {
430 | (console.error || console.log).call(console, err.stack || err);
431 | }
432 |
433 | function moveTabIntoPositionedWindow(tab, count) {
434 | if (count > MAX_MOVE_TRIES) {
435 | console.log('Tab with empty url could not be resolved after ' + MAX_MOVE_TRIES + ' tries');
436 | }
437 | if (!tab.url || tab.url === '') {
438 | console.log('Tab with empty url, trying in 100ms');
439 | setTimeout(function () {
440 | chrome.tabs.get(tab.id, function (tab) {
441 | moveTabIntoPositionedWindow(tab, count + 1);
442 | });
443 | }, 100);
444 | } else {
445 |
446 | const tabRuleOptions = loadOptions();
447 | const tabRule = findTabRuleMatch(tabRuleOptions, tab);
448 | let isCustomPosition = false;
449 | if (tabRule) {
450 | console.log('Tab matched ' + tab.id + ' moving tab with url:' + tab.url);
451 | let createData = calculateWorkAreaByPosition(tabRule.monitor.workArea, tabRule.position);
452 |
453 | if (tabRule.custom && tabRuleOptions.positions && tabRuleOptions.positions.length > 0) {
454 | const customPosition = findCustomPositionMatch(tabRuleOptions, tabRule.custom);
455 | if (customPosition) {
456 | createData = {
457 | left: getInt(customPosition.x),
458 | top: getInt(customPosition.y),
459 | width: getInt(customPosition.width),
460 | height: getInt(customPosition.height)
461 | };
462 | isCustomPosition = true;
463 | }
464 | }
465 |
466 | createData.tabId = tab.id;
467 | if (tabRule.popup) {
468 | createData.type = 'popup';
469 | }
470 | chrome.windows.create(createData, function onCreated(window) {
471 | if (!isCustomPosition && tabRule.position === POSITIONS.CENTER.id) {
472 | console.log('Maximizing tab matched ' + tab.id + ' moving tab with url:' + tab.url);
473 | // maximized mode, should only be set after the tab has moved to the right monitor.
474 | chrome.windows.update(window.id, {state:'maximized'}, function onUpdated() {
475 | console.log('Maximized');
476 | });
477 | } else if (!isCustomPosition && tabRule.position === POSITIONS.FULLSCREEN.id) {
478 | console.log('Fullscreen tab matched ' + tab.id + ' moving tab with url:' + tab.url);
479 | // maximized mode, should only be set after the tab has moved to the right monitor.
480 | chrome.windows.update(window.id, {state:'fullscreen'}, function onUpdated() {
481 | console.log('Fullscreen');
482 | });
483 | }
484 | });
485 | }
486 | }
487 | }
488 | }
489 |
490 |
--------------------------------------------------------------------------------
/app/scripts.babel/chromereload.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Reload client for Chrome Apps & Extensions.
4 | // The reload client has a compatibility with livereload.
5 | // WARNING: only supports reload command.
6 |
7 | try {
8 | const LIVERELOAD_HOST = 'localhost:';
9 | const LIVERELOAD_PORT = 35729;
10 | const connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload');
11 |
12 | connection.onerror = error => {
13 | console.log('reload connection got error:', error);
14 | };
15 |
16 | connection.onmessage = e => {
17 | try {
18 | if (e.data) {
19 | const data = JSON.parse(e.data);
20 | if (data && data.command === 'reload') {
21 | chrome.runtime.reload();
22 | }
23 | }
24 | } catch(err) {
25 | (console.error || console.log).call(console, err.stack || err);
26 | }
27 | };
28 |
29 | } catch(err) {
30 | (console.error || console.log).call(console, err.stack || err);
31 | }
--------------------------------------------------------------------------------
/app/scripts.babel/contentscript.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //console.log('\'Allo \'Allo! Content script');
4 |
--------------------------------------------------------------------------------
/app/scripts.babel/options.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('multiWindowPositioner', ['ngFileUpload', 'ui.checkbox', 'uuid4']).controller('PositionerOptionsController',
4 | ['$scope', '$timeout', 'Upload', '$http', 'uuid4', function ($scope, $timeout, Upload, $http, uuid4) {
5 | const vm = $scope;
6 |
7 | const OPTIONS_KEY = 'TAB_HELPER_OPTIONS';
8 | const TAB_HELPER_TEMPLATE_URL = 'TAB_HELPER_TEMPLATE_URL';
9 |
10 | const PAGE_LOADING_OFFSET = 1100;
11 | const PAGE_DETECTION_DISPLAY_INTERVAL = 3;//3 seconds
12 | const MONITORS = {};
13 |
14 | vm.locale = prepareLocale();
15 |
16 | const POSITIONS = {
17 | CENTER: {id: 'center', name: vm.locale.MAXIMIZED},
18 | LEFT_HALF: {id: 'left-half', name: vm.locale.LEFT_HALF},
19 | RIGHT_HALF: {id: 'right-half', name: vm.locale.RIGHT_HALF},
20 | TOP_HALF: {id: 'top-half', name: vm.locale.TOP_HALF},
21 | BOTTOM_HALF: {id: 'bottom-half', name: vm.locale.BOTTOM_HALF},
22 | FULLSCREEN: { id: 'fullscreen', name: vm.locale.FULLSCREEN }
23 | };
24 |
25 | const DEFAULT_MONITORS = {
26 | MAIN_MONITOR: {id: 'main-monitor', name: vm.locale.MAIN_MONITOR},
27 | NOT_MAIN_MONITOR: {id: 'not-main-monitor', name: vm.locale.NOT_MAIN_MONITOR},
28 | BIGGEST_RESOLUTION: {id: 'biggest-area', name: vm.locale.BIGGEST_RESOLUTION},
29 | BIGGEST_HEIGHT: {id: 'biggest-height', name: vm.locale.BIGGEST_HEIGHT},
30 | BIGGEST_WIDTH: {id: 'biggest-width', name: vm.locale.BIGGEST_WIDTH},
31 | SMALLEST_RESOLUTION: {id: 'smallest-area', name: vm.locale.SMALLEST_RESOLUTION},
32 | SMALLEST_HEIGHT: {id: 'smallest-height', name: vm.locale.SMALLEST_HEIGHT},
33 | SMALLEST_WIDTH: {id: 'smallest-width', name: vm.locale.SMALLEST_WIDTH}
34 | };
35 |
36 | vm.POSITIONS = POSITIONS;
37 | vm.MONITORS = MONITORS;
38 | vm.DEFAULT_MONITORS = DEFAULT_MONITORS;
39 |
40 |
41 | vm.windowHandlers = {};
42 | vm.options = null;
43 |
44 | vm.showNewTabOption = false;
45 | vm.showEditTabOption = false;
46 |
47 | vm.showImportTemplateDialog = false;
48 | vm.inconsistentOptions = false;
49 | vm.dirty = false;
50 | vm.showExtraOptions = false;
51 | vm.showsHelp = false;
52 | vm.isopen = true;
53 | vm.showImportTemplateDialog = false;
54 | vm.templateUrl = '';
55 | vm.replaceAllTemplates = true;
56 |
57 | vm.localizeDefaultMonitor = localizeDefaultMonitor;
58 | vm.localizePosition = localizePosition;
59 | vm.markAsDirty = markAsDirk;
60 | vm.saveOptions = saveOptions;
61 | vm.loadOptions = loadOptions;
62 | vm.undoOptions = loadOptions;
63 | vm.reloadOptions = reloadOptions;
64 | vm.detectMonitors = detectMonitors;
65 | vm.showAdvancedOptions = showAdvancedOptions;
66 | vm.addTabOption = addTabOption;
67 | vm.saveTabOption = saveTabOption;
68 | vm.updateTabOption = updateTabOption;
69 | vm.editTabOption = editTabOption;
70 | vm.useTemplateAsOption = useTemplateAsOption;
71 |
72 | vm.addPosition = addPosition;
73 | vm.setCustomPositionAsMonitor = setCustomPositionAsMonitor;
74 |
75 | vm.applyPositionToAll = applyPositionToAll;
76 | vm.applyMonitorToAll = applyMonitorToAll;
77 |
78 | vm.autofixOptions = autofixOptions;
79 | vm.validateOptions = validateOptions;
80 |
81 | vm.moveOptionUp = moveOptionUp;
82 | vm.moveOptionDown = moveOptionDown;
83 |
84 | vm.importTemplate = importTemplate;
85 | vm.openImportTemplateMenu = openImportTemplateMenu;
86 | vm.acceptImportTemplateMenu = acceptImportTemplateMenu;
87 | vm.cancelImportTemplateMenu = cancelImportTemplateMenu;
88 | vm.exportTemplate = exportTemplate;
89 |
90 | vm.cancelTabOption = cancelTabOption;
91 |
92 | vm.deleteTabOption = deleteTabOption;
93 |
94 | vm.deletePositionOption = deletePositionOption;
95 |
96 | vm.toggleHelp = toggleHelp;
97 |
98 | activate();
99 |
100 | //////////////////////////////////////////////////////////////////
101 |
102 |
103 | function activate() {
104 | try {
105 | loadOptions();
106 | loadDisplayInfos();
107 | registerPostMessageListener();
108 | doScrollToElement('top-section');
109 | } catch(err) {
110 | (console.error || console.log).call(console, err.stack || err);
111 | }
112 | }
113 |
114 | function applyPositionToAll(positionId) {
115 | _.forEach(vm.options.tabs, function (tab, idx) {
116 | tab.position = positionId;
117 | });
118 | markAsDirk();
119 | }
120 |
121 | function applyMonitorToAll(displayId) {
122 | const monitor = getDisplayById(displayId);
123 | _.forEach(vm.options.tabs, function (tab, idx) {
124 | tab.monitor = monitor;
125 | });
126 | markAsDirk();
127 | }
128 |
129 | function registerPostMessageListener() {
130 | try {
131 | chrome.runtime.onMessageExternal.addListener(onMessageExternalListener);
132 | } catch(err) {
133 | (console.error || console.log).call(console, err.stack || err);
134 | }
135 | function onMessageExternalListener(request, sender, sendResponse) {
136 | try {
137 | if (request.closePageGenerator) {
138 | const groupId = request.closePageGenerator;
139 | for (const key in vm.windowHandlers) {
140 | if (vm.windowHandlers.hasOwnProperty(key)) {
141 | const windowHandler = vm.windowHandlers[key];
142 | if (windowHandler.groupId === groupId) {
143 | closeWindowByHandler(windowHandler);
144 | }
145 | }
146 | }
147 | }
148 | } catch(err) {
149 | (console.error || console.log).call(console, err.stack || err);
150 | }
151 | }
152 | }
153 |
154 | function closeWindowByHandler(windowHandler) {
155 | try {
156 | if (vm.windowHandlers[windowHandler.uuid]) {
157 | chrome.windows.remove(windowHandler.id, function () {
158 | delete vm.windowHandlers[windowHandler.uuid];
159 | console.log('Removed window ' + windowHandler.id);
160 | });
161 | }
162 | } catch(err) {
163 | (console.error || console.log).call(console, err.stack || err);
164 | }
165 | }
166 |
167 | function showAdvancedOptions() {
168 | vm.showExtraOptions = !vm.showExtraOptions;
169 | }
170 |
171 | function loadDisplayInfos() {
172 | chrome.system.display.getInfo(function (displayInfos) {
173 | try {
174 | vm.displayInfos = angular.copy(displayInfos);
175 | console.table(displayInfos);
176 | _.forEach(vm.displayInfos, function (display, idx) {
177 | display.idx = idx + 1;
178 | const monitor = {
179 | id: display.id,
180 | idx: display.idx,
181 | name: display.name, //display.idx + ' ' + display.name,
182 | workArea: display.workArea
183 | };
184 | MONITORS[monitor.id] = monitor;
185 | });
186 | $timeout(function () {
187 | validateOptions();
188 | });
189 | } catch(err) {
190 | (console.error || console.log).call(console, err.stack || err);
191 | }
192 | });
193 | }
194 |
195 | function detectMonitors() {
196 | try {
197 | const groupId = uuid4.generate();
198 |
199 | _.forEach(vm.displayInfos, function (display, idx) {
200 | const detectionUrl =
201 | 'https://igorlino.github.io/page-generator/? ' +
202 | 'title=Monitor%20' + (idx + 1) +
203 | '&type=monitor&id=' + (idx + 1) +
204 | '&groupid=' + groupId +
205 | '&extid=' + chrome.runtime.id +
206 | '&delay=' + PAGE_DETECTION_DISPLAY_INTERVAL;
207 | const createData = {
208 | url: detectionUrl,
209 | left: display.workArea.left,
210 | top: display.workArea.top,
211 | width: display.workArea.width,
212 | height: display.workArea.height,
213 | type: 'popup'
214 | };
215 | const windowHandler = {
216 | groupId: groupId,
217 | uuid: uuid4.generate(),
218 | handler: null
219 | };
220 | //close-page-generator'
221 | windowHandler.handler = chrome.windows.create(createData, function onWindowsCreated(window) {
222 | windowHandler.id = window.id;
223 | vm.windowHandlers[windowHandler.uuid] = windowHandler;
224 | console.log('Window ' + window.id + ' created.');
225 | chrome.windows.update(window.id, {state:'maximized'}, function onUpdated() {
226 | console.log('Maximized detection window');
227 | });
228 | setTimeout(function () {
229 | console.log('Removing window ' + window.id);
230 | closeWindowByHandler(windowHandler);
231 | }, (PAGE_DETECTION_DISPLAY_INTERVAL * 1000) + PAGE_LOADING_OFFSET); //+800ms to offset detect page loading
232 | });
233 | });
234 | } catch(err) {
235 | (console.error || console.log).call(console, err.stack || err);
236 | }
237 | }
238 |
239 | function getPrimaryDisplay() {
240 | let found = null;
241 | _.forEach(vm.displayInfos, function (display, idx) {
242 | if (display.isPrimary) {
243 | found = display;
244 | return false;
245 | }
246 | });
247 | return found;
248 | }
249 |
250 | function getDisplayById(id) {
251 | let found = null;
252 | _.forEach(vm.displayInfos, function (display, idx) {
253 | if (display.id === id) {
254 | found = display;
255 | return false;
256 | }
257 | });
258 | return found;
259 | }
260 |
261 | function deleteTabOption(tabOption) {
262 | _.remove(vm.options.tabs, function (option) {
263 | return option.timestamp === tabOption.timestamp;
264 | });
265 | markAsDirk();
266 | }
267 |
268 | function deletePositionOption(positionToDelete) {
269 | _.remove(vm.options.positions, function (position) {
270 | return positionToDelete.name === position.name;
271 | });
272 | markAsDirk();
273 | }
274 |
275 | function addTabOption() {
276 | vm.showNewTabOption = true;
277 | vm.newTabOption = createNewOption();
278 | vm.newTabOption.template = null;
279 | }
280 |
281 | function setCustomPositionAsMonitor(position, monitor) {
282 | position.x = monitor.workArea.left;
283 | position.y = monitor.workArea.top;
284 | position.width = monitor.workArea.width;
285 | position.height = monitor.workArea.height;
286 | }
287 |
288 | function addPosition() {
289 | vm.options.positions.push({
290 | name: 'CustomPosition' + vm.options.positions.length,
291 | x: 0,
292 | y: 0,
293 | height: 10,
294 | width: 10
295 | });
296 | markAsDirk();
297 | }
298 |
299 | function editTabOption(tabOption) {
300 | vm.showEditTabOption = true;
301 | vm.newTabOption = angular.copy(tabOption);
302 | vm.newTabOption.position = findPositionById(tabOption.position);
303 | vm.newTabOption.defaultMonitor = findDefaultMonitorById(tabOption.defaultMonitor);
304 | vm.newTabOption.monitor = getDisplayById(tabOption.monitor.id);
305 | vm.editTabOptionIdx = _.findIndex(vm.options.tabs, tabOption);
306 | vm.newTabOption.template = null;
307 | }
308 |
309 | function useTemplateAsOption() {
310 | if (vm.newTabOption.template) {
311 | vm.newTabOption.name = vm.newTabOption.template.name;
312 | vm.newTabOption.url = vm.newTabOption.template.url;
313 | vm.newTabOption.code = vm.newTabOption.template.code;
314 | vm.newTabOption.active = vm.newTabOption.template.active;
315 | vm.newTabOption.remember = vm.newTabOption.template.remember;
316 | if (vm.newTabOption.template.defaultMonitor) {
317 | vm.newTabOption.defaultMonitor = findDefaultMonitorById(vm.newTabOption.template.defaultMonitor);
318 | }
319 | if (vm.newTabOption.template.position) {
320 | vm.newTabOption.position = findPositionById(vm.newTabOption.template.position);
321 | }
322 | }
323 | }
324 |
325 | function findDefaultMonitorById(defaultMonitorId) {
326 | const key = _.findKey(DEFAULT_MONITORS, function (defaultMonitor) {
327 | return defaultMonitor.id === defaultMonitorId;
328 | });
329 | return key ? DEFAULT_MONITORS[key] : null;
330 | }
331 |
332 | function findPositionById(positionId) {
333 | const positionKey = _.findKey(POSITIONS, function (position) {
334 | return position.id === positionId;
335 | });
336 | return positionKey ? POSITIONS[positionKey] : null;
337 | }
338 |
339 | function cancelTabOption() {
340 | vm.showNewTabOption = false;
341 | vm.showEditTabOption = false;
342 | }
343 |
344 | function updateTabOption() {
345 | vm.showEditTabOption = false;
346 | vm.options.tabs[vm.editTabOptionIdx] = {
347 | active: vm.newTabOption.active,
348 | code: vm.newTabOption.code,
349 | remember: vm.newTabOption.remember,
350 | url: vm.newTabOption.url,
351 | name: vm.newTabOption.name,
352 | monitor: vm.newTabOption.monitor,
353 | fullScreen: vm.newTabOption.fullScreen,
354 | popup: vm.newTabOption.popup,
355 | position: vm.newTabOption.position ? vm.newTabOption.position.id : MONITORS.CENTER.id,
356 | defaultMonitor: vm.newTabOption.defaultMonitor ? vm.newTabOption.defaultMonitor.id : DEFAULT_MONITORS.MAIN_MONITOR.id,
357 | timestamp: new Date().toISOString()
358 | };
359 | vm.editTabOptionIdx = -1;
360 | validateOptions();
361 | markAsDirk();
362 | }
363 |
364 | function saveTabOption() {
365 | try {
366 | vm.options.tabs.push({
367 | active: vm.newTabOption.active,
368 | code: vm.newTabOption.code,
369 | remember: vm.newTabOption.remember,
370 | url: vm.newTabOption.url,
371 | name: vm.newTabOption.name,
372 | monitor: vm.newTabOption.monitor,
373 | fullScreen: vm.newTabOption.fullScreen,
374 | popup: vm.newTabOption.popup,
375 | position: vm.newTabOption.position ? vm.newTabOption.position.id : MONITORS.CENTER.id,
376 | defaultMonitor: vm.newTabOption.defaultMonitor ? vm.newTabOption.defaultMonitor.id : DEFAULT_MONITORS.MAIN_MONITOR.id,
377 | timestamp: new Date().toISOString()
378 | });
379 | vm.showNewTabOption = false;
380 | validateOptions();
381 | markAsDirk();
382 | } catch(err) {
383 | (console.error || console.log).call(console, err.stack || err);
384 | }
385 | }
386 |
387 | function saveOptions() {
388 | localStorage[OPTIONS_KEY] = JSON.stringify(vm.options);
389 | markAsPristine();
390 | //closeCurrentWindow();
391 | }
392 |
393 | function closeCurrentWindow() {
394 | chrome.tabs.getCurrent(function (tab) {
395 | chrome.tabs.remove(tab.id, function () {
396 | });
397 | });
398 | }
399 |
400 | function markAsDirk() {
401 | vm.dirty = true;
402 | }
403 |
404 | function markAsPristine() {
405 | vm.dirty = false;
406 | }
407 |
408 | function loadOptions() {
409 | try {
410 | const tabRuleOptions = localStorage[OPTIONS_KEY];
411 | if (tabRuleOptions) {
412 | vm.options = JSON.parse(tabRuleOptions);
413 | if (!vm.options.tabs) {
414 | vm.options.tabs = [];
415 | }
416 | if (!vm.options.positions) {
417 | vm.options.positions = [];
418 | }
419 | markAsPristine();
420 | } else {
421 | vm.options = {
422 | tabs: [],
423 | positions: []
424 | };
425 | markAsDirk();
426 | }
427 | if (!vm.options.templates || vm.options.templates.length <= 0) {
428 | vm.options.templates = getDefaultTemplates();
429 | }
430 | } catch(err) {
431 | (console.error || console.log).call(console, err.stack || err);
432 | }
433 | //show help by default if no rule has yet been set
434 | if (!vm.showsHelp && vm.options.tabs.length === 0) {
435 | vm.showsHelp = true;
436 | }
437 | return vm.options;
438 | }
439 |
440 | function reloadOptions() {
441 | loadOptions();
442 | validateOptions();
443 | }
444 |
445 | function getMappedDefaultMonitorById(defaultMonitors, defaultMonitorId) {
446 | let found = null;
447 | try {
448 | if (defaultMonitorId) {
449 | for (const key in defaultMonitors) {
450 | if (defaultMonitors.hasOwnProperty(key)) {
451 | const defaultMonitor = defaultMonitors[key];
452 | if (defaultMonitorId === defaultMonitor.id) {
453 | found = defaultMonitor.monitor;
454 | break;
455 | }
456 | }
457 | }
458 | }
459 | } catch(err) {
460 | (console.error || console.log).call(console, err.stack || err);
461 | }
462 | return found;
463 | }
464 |
465 | function getDefaultMonitorsMapping() {
466 | const defaultMonitors = angular.copy(DEFAULT_MONITORS);
467 | try {
468 | _.forEach(vm.displayInfos, function (display, idx) {
469 | if (display.isEnabled) {
470 | const displayWorkArea = display.workArea;
471 | const area = displayWorkArea.height * displayWorkArea.width;
472 |
473 | if (display.isPrimary) {
474 | defaultMonitors.MAIN_MONITOR.monitor = display;
475 | }
476 | if (!display.isPrimary) {
477 | if (!defaultMonitors.NOT_MAIN_MONITOR.monitor) {
478 | defaultMonitors.NOT_MAIN_MONITOR.monitor = display;
479 | } else {
480 | const notMainResolution = defaultMonitors.NOT_MAIN_MONITOR.monitor.workArea.height *
481 | defaultMonitors.NOT_MAIN_MONITOR.monitor.workArea.width;
482 | if (area > notMainResolution) {
483 | defaultMonitors.NOT_MAIN_MONITOR.monitor = display;
484 | }
485 | }
486 | }
487 | if (!defaultMonitors.BIGGEST_RESOLUTION.monitor) {
488 | defaultMonitors.BIGGEST_RESOLUTION.monitor = display;
489 | } else {
490 | const biggestResolution = defaultMonitors.BIGGEST_RESOLUTION.monitor.workArea.height *
491 | defaultMonitors.BIGGEST_RESOLUTION.monitor.workArea.width;
492 | if (area > biggestResolution) {
493 | defaultMonitors.BIGGEST_RESOLUTION.monitor = display;
494 | }
495 | }
496 | if (!defaultMonitors.BIGGEST_HEIGHT.monitor ||
497 | displayWorkArea.height > defaultMonitors.BIGGEST_HEIGHT.monitor.workArea.height) {
498 | defaultMonitors.BIGGEST_HEIGHT.monitor = display;
499 | }
500 | if (!defaultMonitors.BIGGEST_WIDTH.monitor ||
501 | displayWorkArea.width > defaultMonitors.BIGGEST_WIDTH.monitor.workArea.width) {
502 | defaultMonitors.BIGGEST_WIDTH.monitor = display;
503 | }
504 | if (!defaultMonitors.SMALLEST_RESOLUTION.monitor) {
505 | defaultMonitors.SMALLEST_RESOLUTION.monitor = display;
506 | } else {
507 | const biggestResolution = defaultMonitors.SMALLEST_RESOLUTION.monitor.workArea.height *
508 | defaultMonitors.SMALLEST_RESOLUTION.monitor.workArea.width;
509 | if (area < biggestResolution) {
510 | defaultMonitors.SMALLEST_RESOLUTION.monitor = display;
511 | }
512 | }
513 | if (!defaultMonitors.SMALLEST_HEIGHT.monitor ||
514 | displayWorkArea.height < defaultMonitors.SMALLEST_HEIGHT.monitor.workArea.height) {
515 | defaultMonitors.SMALLEST_HEIGHT.monitor = display;
516 | }
517 | if (!defaultMonitors.SMALLEST_WIDTH.monitor ||
518 | displayWorkArea.width < defaultMonitors.SMALLEST_WIDTH.monitor.workArea.width) {
519 | defaultMonitors.SMALLEST_WIDTH.monitor = display;
520 | }
521 | }
522 | });
523 | } catch(err) {
524 | (console.error || console.log).call(console, err.stack || err);
525 | }
526 | return defaultMonitors;
527 | }
528 |
529 | function validateOptions(useDefaultMonitor) {
530 | try {
531 | const defaultMonitors = getDefaultMonitorsMapping();
532 | let missing = false;
533 | _.forEach(vm.options.tabs, function (tab, idx) {
534 | //verify custom
535 | tab.inconsistentCustom = false;
536 | if (tab.custom && tab.custom !== '') {
537 | let customMatch = false;
538 | _.forEach(vm.options.positions, function (customPosition, idx) {
539 | if (customPosition.name === tab.custom) {
540 | tab.inconsistentCustom = true;
541 | customMatch = true;
542 | return false;
543 | }
544 | });
545 | if (!customMatch) {
546 | missing = true;
547 | }
548 | }
549 |
550 | //verify display
551 | let displayMatch = false;
552 | _.forEach(vm.displayInfos, function (display, idx) {
553 | if (display.isEnabled && tab.monitor.id === display.id) {
554 | displayMatch = true;
555 | return false;
556 | }
557 | });
558 | if (!displayMatch) {
559 | let defaultMonitor = null;
560 | if (useDefaultMonitor) {
561 | defaultMonitor = getMappedDefaultMonitorById(defaultMonitors, tab.defaultMonitor);
562 | if (defaultMonitor) {
563 | tab.monitor = angular.copy(defaultMonitor);
564 | }
565 | }
566 | if (!useDefaultMonitor || !defaultMonitor) {
567 | tab.inconsistentMonitor = true;
568 | missing = true;
569 | }
570 | } else {
571 | tab.inconsistentMonitor = false;
572 | }
573 | });
574 | vm.inconsistentOptions = missing;
575 | } catch(err) {
576 | (console.error || console.log).call(console, err.stack || err);
577 | }
578 | }
579 |
580 | function autofixOptions() {
581 | try {
582 | const primaryDisplay = getPrimaryDisplay();
583 | _.forEach(vm.options.tabs, function (tab, idx) {
584 | let found = false;
585 | let closestMatch = null;
586 | _.forEach(vm.displayInfos, function (display, idx) {
587 | if (display.isEnabled && display.workArea &&
588 | display.workArea.height === tab.monitor.workArea.height &&
589 | display.workArea.width === tab.monitor.workArea.width) {
590 | closestMatch = display;
591 | }
592 | if (tab.monitor.id === display.id) {
593 | found = true;
594 | return false;
595 | }
596 | });
597 | if (!found) {
598 | if (closestMatch) {
599 | replaceMonitor(tab.monitor, closestMatch);
600 | } else if (primaryDisplay) {
601 | replaceMonitor(tab.monitor, primaryDisplay);
602 | }
603 | //monitor: vm.newTabOption.monitor,
604 | //position: vm.newTabOption.position.id,
605 | }
606 | });
607 |
608 | validateOptions();
609 | } catch(err) {
610 | (console.error || console.log).call(console, err.stack || err);
611 | }
612 |
613 | function replaceMonitor(target, sourceDisplay) {
614 | target.workArea = angular.copy(sourceDisplay.workArea);
615 | target.id = sourceDisplay.id;
616 | const idx = _.findIndex(vm.displayInfos, sourceDisplay);
617 | target.name = sourceDisplay.name;//(idx + 1) + ' ' + sourceDisplay.name;
618 | target.idx = idx + 1;
619 | }
620 | }
621 |
622 | function createNewOption() {
623 | return {
624 | active: true,
625 | remember: false,
626 | code: 'custom',
627 | name: vm.locale.RULE_NAME_PLACEHOLDER,
628 | url: 'http://any.url/',
629 | monitor: getPrimaryDisplay(),
630 | defaultMonitor: DEFAULT_MONITORS.MAIN_MONITOR,
631 | fullScreen: false,
632 | popup: true,
633 | position: POSITIONS.CENTER
634 | };
635 | }
636 |
637 | function openImportTemplateMenu() {
638 | const templateUrl = localStorage[TAB_HELPER_TEMPLATE_URL];
639 | if (templateUrl) {
640 | vm.templateUrl = templateUrl;
641 | }
642 | vm.showImportTemplateDialog = true;
643 | }
644 |
645 | function acceptImportTemplateMenu(templateUrl) {
646 | try {
647 | vm.showImportTemplateDialog = false;
648 | if (templateUrl && templateUrl !== '') {
649 | localStorage[TAB_HELPER_TEMPLATE_URL] = templateUrl;
650 | vm.templateUrl = templateUrl;
651 |
652 | callHttpByGet(templateUrl, function onResponse(response) {
653 | if (response.success && response.data) {
654 | if (response.data.tabs) {
655 | mergeRules(vm.options.tabs, response.data.tabs);
656 | }
657 | if (response.data.templates) {
658 | if (vm.replaceAllTemplates) {
659 | vm.options.templates = response.data.templates;
660 | } else {
661 | mergeTemplates(vm.options.templates, response.data.templates);
662 | }
663 | }
664 | validateOptions(true);
665 | markAsDirk();
666 | }
667 | });
668 | }
669 | } catch(err) {
670 | (console.error || console.log).call(console, err.stack || err);
671 | }
672 | }
673 |
674 | function mergeRules(existentRules, templateRules) {
675 | try {
676 | cleanOrder(existentRules);
677 |
678 | for (let i = 0; i < existentRules.length; i++) {
679 | const rule = existentRules[i];
680 | for (let k = 0; k < templateRules.length; k++) {
681 | const template = templateRules[k];
682 | template.order = k + 1;
683 | if (rule.code === template.code) {
684 | //mark as merged
685 | template.merged = true;
686 | //rule.active = template.active;
687 | //rule.code = template.code;
688 | //rule.remember = template.remember;
689 | rule.url = template.url;
690 | rule.name = template.name;
691 | //rule.monitor = template.monitor;
692 | rule.defaultMonitor = template.defaultMonitor;
693 | //rule.fullScreen = template.fullScreen;
694 | //rule.popup = template.popup;
695 | //rule.position = template.position ? template.position.id : 'center';
696 |
697 | rule.order = template.order;
698 | }
699 | }
700 | }
701 |
702 | //add new templates
703 | for (let k = 0; k < templateRules.length; k++) {
704 | const template = templateRules[k];
705 | if (!template.merged) {
706 | existentRules.push(template);
707 | }
708 | }
709 |
710 | sortByOrder(existentRules);
711 | } catch(err) {
712 | (console.error || console.log).call(console, err.stack || err);
713 | }
714 | }
715 |
716 | //clean any order value.
717 | function cleanOrder(list) {
718 | for (let i = 0; i < list.length; i++) {
719 | delete list[i].order;
720 | }
721 | }
722 |
723 | function sortByOrder(list) {
724 | //sort items by order
725 | for (let i = 0; i < list.length; i++) {
726 | for (let k = 0; k < list.length - 1 - i; k++) {
727 | const item1 = list[k];
728 | const item2 = list[k + 1];
729 |
730 | if ((item1.order && item2.order && item1.order > item2.order) ||
731 | (item1.order && !item2.order)) {
732 | list[k] = item2;
733 | list[k + 1] = item1;
734 | }
735 | }
736 | }
737 | }
738 |
739 | function mergeTemplates(currentTemplates, newTemplates) {
740 | try {
741 | cleanOrder(currentTemplates);
742 |
743 | for (let i = 0; i < currentTemplates.length; i++) {
744 | const existingTemplate = currentTemplates[i];
745 | for (let k = 0; k < newTemplates.length; k++) {
746 | const newTemplate = newTemplates[k];
747 | newTemplate.order = k + 1;
748 | if (existingTemplate.code === newTemplate.code) {
749 | //mark as merged
750 | newTemplate.merged = true;
751 | existingTemplate.active = newTemplate.active;
752 | existingTemplate.code = newTemplate.code;
753 | existingTemplate.remember = newTemplate.remember;
754 | existingTemplate.url = newTemplate.url;
755 | existingTemplate.name = newTemplate.name;
756 | existingTemplate.defaultMonitor = newTemplate.defaultMonitor;
757 | existingTemplate.position = newTemplate.position;
758 |
759 | existingTemplate.order = newTemplate.order;
760 | }
761 | }
762 | }
763 |
764 | //add new templates
765 | for (let k = 0; k < newTemplates.length; k++) {
766 | const template = newTemplates[k];
767 | if (!newTemplate.merged) {
768 | currentTemplates.push(template);
769 | }
770 | }
771 |
772 | //sort templates by order
773 | sortByOrder(currentTemplates);
774 | } catch(err) {
775 | (console.error || console.log).call(console, err.stack || err);
776 | }
777 | }
778 |
779 | function cancelImportTemplateMenu() {
780 | vm.showImportTemplateDialog = false;
781 | vm.templateUrl = '';
782 | }
783 |
784 | function importTemplate(file) {
785 | Upload.upload({
786 | url: 'upload/url',
787 | data: {file: file, 'username': $scope.username}
788 | }).then(function (resp) {
789 | console.log('Success ' + resp.config.data.file.name + 'uploaded. Response: ' + resp.data);
790 | }, function (resp) {
791 | console.log('Error status: ' + resp.status);
792 | }, function (evt) {
793 | const progressPercentage = parseInt(100.0 * evt.loaded / evt.total);
794 | if (evt.config.data.file.name) {
795 | console.log('progress: ' + progressPercentage + '% ' + evt.config.data.file.name);
796 | } else {
797 | console.log('progress: ' + progressPercentage + '% ' + evt.config.data.file);
798 | }
799 | });
800 | }
801 |
802 | function exportTemplate() {
803 | const optionsAsJson = angular.toJson(vm.options, 3);
804 | const blob = new Blob([optionsAsJson], {type: 'application/json'});
805 | const saveAs = window.saveAs;
806 | saveAs(blob, 'multiwindow-positioner-rule-export.json');
807 | }
808 |
809 | function getDefaultTemplates() {
810 | return [
811 | {
812 | active: true,
813 | remember: false,
814 | name: 'Google Search',
815 | url: 'https://www.google.com/',
816 | code: 'google-search',
817 | defaultMonitor: 'main-monitor'
818 | },
819 | {
820 | active: true,
821 | remember: false,
822 | name: 'Facebook',
823 | url: 'https://www.facebook.com',
824 | code: 'facebook',
825 | defaultMonitor: 'main-monitor'
826 | },
827 | {
828 | active: true,
829 | remember: false,
830 | name: 'YouTube',
831 | url: 'https://www.youtube.com/',
832 | code: 'google-youtube',
833 | defaultMonitor: 'main-monitor'
834 | },
835 | {
836 | active: true,
837 | remember: false,
838 | name: 'Wikipedia',
839 | url: 'https://www.wikipedia.org/',
840 | code: 'wikipedia',
841 | defaultMonitor: 'main-monitor'
842 | },
843 | {
844 | active: true,
845 | remember: false,
846 | name: 'Amazon',
847 | url: 'https://www.amazon.com/',
848 | code: 'amazon-global',
849 | defaultMonitor: 'main-monitor'
850 | },
851 | {
852 | active: true,
853 | remember: false,
854 | name: 'Ebay',
855 | url: 'http://www.ebay.com/',
856 | code: 'ebay-global',
857 | defaultMonitor: 'main-monitor'
858 | }
859 | ]
860 | }
861 |
862 | function moveOptionUp(first, index) {
863 | if (!first) {
864 | swap(vm.options.tabs, index, index - 1);
865 | markAsDirk();
866 | }
867 | }
868 |
869 | function moveOptionDown(last, index) {
870 | if (!last) {
871 | swap(vm.options.tabs, index, index + 1);
872 | markAsDirk();
873 | }
874 | }
875 |
876 | function swap(list, idx1, idx2) {
877 | const tmp = list[idx1];
878 | list[idx1] = list[idx2];
879 | list[idx2] = tmp;
880 | }
881 |
882 | function callHttpByGet(callPath, callback) {
883 | $http({
884 | method: 'get',
885 | url: callPath,
886 | headers: {'Cache-Control': 'no-cache'}
887 | }).then(onSuccess, onError);
888 |
889 | function onError(response) {
890 | const result = handleHttpError(response.data, callPath);
891 | callback(result);
892 | }
893 |
894 | function onSuccess(response) {
895 | const result =
896 | {
897 | success: true,
898 | data: response.data
899 | };
900 |
901 | callback(result);
902 | }
903 | }
904 |
905 | function handleHttpError(data, callPath) {
906 | const response = {success: false};
907 | if (data) {
908 | response.error = data;
909 | }
910 | else {
911 | response.error = dateNow() + ' - Request failed: ' + callPath;
912 | }
913 | return response;
914 | }
915 |
916 | function dateNow() {
917 | const d = new Date();
918 | return d.toLocaleString();
919 | }
920 |
921 | function prepareLocale() {
922 | return {
923 | OPTIONS_TITLE: chrome.i18n.getMessage('OPTIONS_TITLE'),
924 | TAB_SETTINGS: chrome.i18n.getMessage('TAB_SETTINGS'),
925 |
926 | //custom positions
927 | TAB_POSITIONS: chrome.i18n.getMessage('TAB_POSITIONS'),
928 | CUSTOM: chrome.i18n.getMessage('CUSTOM'),
929 | WIDTH: chrome.i18n.getMessage('WIDTH'),
930 | HEIGHT: chrome.i18n.getMessage('HEIGHT'),
931 | ADD_POSITION: chrome.i18n.getMessage('ADD_POSITION'),
932 | REMOVE_POSITION: chrome.i18n.getMessage('REMOVE_POSITION'),
933 |
934 | //table columns
935 | ACTIVE: chrome.i18n.getMessage('ACTIVE'),
936 | NAME: chrome.i18n.getMessage('NAME'),
937 | URL: chrome.i18n.getMessage('URL'),
938 | REMEMBER: chrome.i18n.getMessage('REMEMBER'),
939 | MONITOR: chrome.i18n.getMessage('MONITOR'),
940 | POSITION: chrome.i18n.getMessage('POSITION'),
941 | PLAIN: chrome.i18n.getMessage('PLAIN'),
942 |
943 | //table actions
944 | EDIT_TAB_RULE: chrome.i18n.getMessage('EDIT_TAB_RULE'),
945 | DELETE_TAB_RULE: chrome.i18n.getMessage('DELETE_TAB_RULE'),
946 | MOVE_UP: chrome.i18n.getMessage('MOVE_UP'),
947 | MOVE_DOWN: chrome.i18n.getMessage('MOVE_DOWN'),
948 |
949 | //dialogs
950 | NEW_TAB_OPTION_TITLE: chrome.i18n.getMessage('NEW_TAB_OPTION_TITLE'),
951 | EDIT_TAB_OPTION_TITLE: chrome.i18n.getMessage('EDIT_TAB_OPTION_TITLE'),
952 | TEMPLATE: chrome.i18n.getMessage('TEMPLATE'),
953 | PLAIN_WINDOW: chrome.i18n.getMessage('PLAIN_WINDOW'),
954 | ADD: chrome.i18n.getMessage('ADD'),
955 | UPDATE: chrome.i18n.getMessage('UPDATE'),
956 | CANCEL: chrome.i18n.getMessage('CANCEL'),
957 | TEMPLATE_URL: chrome.i18n.getMessage('TEMPLATE_URL'),
958 | REPLACE_ALL_TEMPLATES: chrome.i18n.getMessage('REPLACE_ALL_TEMPLATES'),
959 |
960 | //actions
961 | ADD_TAB_OPTION: chrome.i18n.getMessage('ADD_TAB_OPTION'),
962 | SAVE: chrome.i18n.getMessage('SAVE'),
963 | UNDO: chrome.i18n.getMessage('UNDO'),
964 | RELOAD: chrome.i18n.getMessage('RELOAD'),
965 | IMPORT_TEMPLATE: chrome.i18n.getMessage('IMPORT_TEMPLATE'),
966 | EXPORT_TEMPLATE: chrome.i18n.getMessage('EXPORT_TEMPLATE'),
967 | SHOW_MORE_OPTIONS: chrome.i18n.getMessage('SHOW_MORE_OPTIONS'),
968 | VALIDATE_RULES: chrome.i18n.getMessage('VALIDATE_RULES'),
969 | DETECT_MONITORS: chrome.i18n.getMessage('DETECT_MONITORS'),
970 | AUTO_REPAIR_RULES: chrome.i18n.getMessage('AUTO_REPAIR_RULES'),
971 |
972 | //window positions
973 | MAXIMIZED: chrome.i18n.getMessage('MAXIMIZED'),
974 | LEFT_HALF: chrome.i18n.getMessage('LEFT_HALF'),
975 | RIGHT_HALF: chrome.i18n.getMessage('RIGHT_HALF'),
976 | TOP_HALF: chrome.i18n.getMessage('TOP_HALF'),
977 | BOTTOM_HALF: chrome.i18n.getMessage('BOTTOM_HALF'),
978 | FULLSCREEN: chrome.i18n.getMessage('FULLSCREEN'),
979 |
980 | //default monitors
981 | DEFAULT_MONITOR: chrome.i18n.getMessage('DEFAULT_MONITOR'),
982 | MAIN_MONITOR: chrome.i18n.getMessage('MAIN_MONITOR'),
983 | NOT_MAIN_MONITOR: chrome.i18n.getMessage('NOT_MAIN_MONITOR'),
984 | BIGGEST_RESOLUTION: chrome.i18n.getMessage('BIGGEST_RESOLUTION'),
985 | BIGGEST_HEIGHT: chrome.i18n.getMessage('BIGGEST_HEIGHT'),
986 | BIGGEST_WIDTH: chrome.i18n.getMessage('BIGGEST_WIDTH'),
987 | SMALLEST_RESOLUTION: chrome.i18n.getMessage('SMALLEST_RESOLUTION'),
988 | SMALLEST_HEIGHT: chrome.i18n.getMessage('SMALLEST_HEIGHT'),
989 | SMALLEST_WIDTH: chrome.i18n.getMessage('SMALLEST_WIDTH'),
990 |
991 | RULE_NAME_PLACEHOLDER: chrome.i18n.getMessage('RULE_NAME_PLACEHOLDER'),
992 |
993 | DRAFT: chrome.i18n.getMessage('DRAFT')
994 | };
995 | }
996 |
997 | function localizeDefaultMonitor(defaultMonitor) {
998 | let localizedDefaultMonitor = defaultMonitor;
999 | if (defaultMonitor === DEFAULT_MONITORS.MAIN_MONITOR.id) {
1000 | localizedDefaultMonitor = vm.locale.MAIN_MONITOR;
1001 | } else if (defaultMonitor === DEFAULT_MONITORS.NOT_MAIN_MONITOR.id) {
1002 | localizedDefaultMonitor = vm.locale.NOT_MAIN_MONITOR;
1003 | } else if (defaultMonitor === DEFAULT_MONITORS.BIGGEST_RESOLUTION.id) {
1004 | localizedDefaultMonitor = vm.locale.BIGGEST_RESOLUTION;
1005 | } else if (defaultMonitor === DEFAULT_MONITORS.BIGGEST_HEIGHT.id) {
1006 | localizedDefaultMonitor = vm.locale.BIGGEST_HEIGHT;
1007 | } else if (defaultMonitor === DEFAULT_MONITORS.BIGGEST_WIDTH.id) {
1008 | localizedDefaultMonitor = vm.locale.BIGGEST_WIDTH;
1009 | } else if (defaultMonitor === DEFAULT_MONITORS.SMALLEST_RESOLUTION.id) {
1010 | localizedDefaultMonitor = vm.locale.SMALLEST_RESOLUTION;
1011 | } else if (defaultMonitor === DEFAULT_MONITORS.SMALLEST_HEIGHT.id) {
1012 | localizedDefaultMonitor = vm.locale.SMALLEST_HEIGHT;
1013 | } else if (defaultMonitor === DEFAULT_MONITORS.SMALLEST_WIDTH.id) {
1014 | localizedDefaultMonitor = vm.locale.SMALLEST_WIDTH;
1015 | }
1016 |
1017 | return localizedDefaultMonitor
1018 | }
1019 |
1020 | function localizePosition(position) {
1021 | let localizedPosition = position;
1022 | if (position === POSITIONS.CENTER.id) {
1023 | localizedPosition = vm.locale.MAXIMIZED;
1024 | } else if (position === POSITIONS.LEFT_HALF.id) {
1025 | localizedPosition = vm.locale.LEFT_HALF;
1026 | } else if (position === POSITIONS.RIGHT_HALF.id) {
1027 | localizedPosition = vm.locale.RIGHT_HALF;
1028 | } else if (position === POSITIONS.TOP_HALF.id) {
1029 | localizedPosition = vm.locale.TOP_HALF;
1030 | } else if (position === POSITIONS.BOTTOM_HALF.id) {
1031 | localizedPosition = vm.locale.BOTTOM_HALF;
1032 | } else if (position === POSITIONS.FULLSCREEN.id) {
1033 | localizedPosition = vm.locale.FULLSCREEN;
1034 | }
1035 |
1036 | return localizedPosition
1037 | }
1038 |
1039 | function toggleHelp() {
1040 | vm.showsHelp = !vm.showsHelp;
1041 | if (vm.showsHelp) {
1042 | doScrollToElement('quick-info-section');
1043 | } else {
1044 | doScrollToElement('top-section');
1045 | }
1046 | }
1047 |
1048 | function doScrollToElement(elementId, notifyScroll, duration, offset) {
1049 | var defaultDuration = angular.isDefined(duration) ? duration : 0; //milliseconds
1050 | var defaultOffset = angular.isDefined(offset) ? offset : 0; //pixels; adjust for floating menu, context etc
1051 | //Scroll to #some-id with 30 px "padding"
1052 | //Note: Use this in a directive, not with document.getElementById
1053 |
1054 | var container = jQuery('html, body');
1055 | container.stop();
1056 |
1057 | var elementToScroll = jQuery('#' + elementId);
1058 | if (elementToScroll && elementToScroll.length > 0) {
1059 | $timeout(function () {
1060 | container.animate({
1061 | scrollTop: jQuery('#' + elementId).offset().top + defaultOffset
1062 | }, 800);
1063 | }, 100, false);
1064 | }
1065 | }
1066 |
1067 | }]);
1068 |
--------------------------------------------------------------------------------
/app/scripts.babel/popup.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //console.log('\'Allo \'Allo! Popup');
4 |
--------------------------------------------------------------------------------
/app/styles/bootstrap.css.map:
--------------------------------------------------------------------------------
1 | {
2 | version:"1.0"
3 | }
--------------------------------------------------------------------------------
/app/styles/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 20px;
3 | }
4 |
5 | .centered {
6 | position: absolute;
7 | top: 50%;
8 | left: 50%;
9 | transform: translate(-50%, -50%) scale(2);
10 | }
11 |
12 | .options-loader.centered {
13 | }
14 |
15 | .options-loader {
16 | background: url(/images/options-loader.gif) no-repeat center center;
17 | width: 200px;
18 | height: 200px;
19 | }
20 |
21 | .wait-in-hidden {
22 | opacity: 0;
23 | transition: opacity 1s;
24 | }
25 |
26 | .wait-in-hidden-done {
27 | opacity: 1;
28 | }
29 |
30 | .wait-in-show {
31 | opacity: 1;
32 | transition: opacity 1s;
33 | }
34 | .wait-in-show-done {
35 | opacity: 0;
36 | }
37 |
38 | .tab-helper-options {
39 | width : 1280px;
40 | margin-left: auto;
41 | margin-right: auto;
42 | }
43 | .tab-helper-options.wider {
44 | width : 1680px;
45 | }
46 |
47 | .missing-monitor {
48 | color: red;
49 | }
50 |
51 | .tab-action-bar .btn {
52 | border: none;
53 | color: lightgray;
54 | }
55 |
56 | tr:hover .tab-action-bar .btn {
57 | color: black;
58 | }
59 |
60 | .hidden-only {
61 | visibility: hidden;
62 | }
63 |
64 | .fix-checkbox {
65 | min-width: inherit !important
66 | }
67 |
68 | .fix-checkbox-mark {
69 | width: 10px;
70 | left: -1px;
71 | padding-left: 5px;
72 | }
73 |
74 | .rule-dialog .form-group {
75 |
76 | }
77 |
78 | .margin-left-lg {
79 | margin-left: 30px !important;
80 | }
81 |
82 | .margin-top-md {
83 | margin-top: 8px !important;
84 | }
85 |
86 | .margin-md {
87 | margin: 8px !important;
88 | }
89 |
90 | .hoverable-section {
91 |
92 | }
93 |
94 | .hoverable-section .show-when-hover {
95 | display: none;
96 | }
97 |
98 | .hoverable-section:hover .show-when-hover {
99 | display: block;
100 | }
101 |
102 | .hoverable-section .hide-when-hover {
103 | display: block;
104 | }
105 |
106 | .hoverable-section:hover .hide-when-hover {
107 | display: none;
108 | }
109 |
110 | .options-rule-row td,
111 | .options-position-row td {
112 | line-height: 34px !important
113 | }
114 |
115 | .custom-select {
116 | max-width: 250px !important;
117 | min-width: 250px !important;
118 | }
119 |
120 | .monitor-select {
121 | max-width: 250px !important;
122 | min-width: 250px !important;
123 | }
124 |
125 | .position-select {
126 | max-width: 120px !important;
127 | min-width: 120px !important;
128 | }
129 |
130 | .checkbox-cell {
131 | padding-top: 14px !important;
132 | }
133 |
134 | .dropdown-header {
135 | margin-bottom:15px;
136 | }
137 |
138 | .help-section {
139 | padding: 8px !important;
140 | border: 9px #2e6da4 solid;
141 | display: inline-block;
142 | width: 100%;
143 | position: relative;
144 | }
145 |
146 | .quick-info-section {
147 | padding: 8px !important;
148 | border: 9px darkgrey solid;
149 | display: inline-block;
150 | width: 100%;
151 | position: relative;
152 | }
153 |
154 | .logo-wrapper {
155 | float: left;
156 | width: 289px;
157 | }
158 | .logo-wrapper > img {
159 | max-width: 274px;
160 | float: left;
161 | margin-right: 12px;
162 | margin-top: -2px;
163 | }
164 |
165 | .help-button-container {
166 | position: fixed;
167 | right: 18px;
168 | top: 40px;
169 | }
170 |
171 | .help-section-close {
172 | position: absolute;
173 | right: 8px;
174 | top: 8px;
175 | color: darkgrey;
176 | cursor:pointer;
177 | z-index: 10;
178 | }
179 |
180 | .help-section-close:hover {
181 | color: inherit;
182 | }
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chrome-multiwindow-positioner",
3 | "private": true,
4 | "version": "1.0.15",
5 | "description": "Tool extension that enables seamless window placement/positioning in multi-monitor setups.",
6 | "authors": [
7 | "Control Expert"
8 | ],
9 | "license": "MIT",
10 | "dependencies": {
11 | "jquery": "~3.3.1",
12 | "lodash": "4.17.5",
13 | "bootstrap": "3.3.7",
14 | "angular": "~1.6.9",
15 | "angular-resource": "~1.6.9",
16 | "font-awesome": "~4.7.0",
17 | "select2-bootstrap-css": "1.4.6",
18 | "angular-bootstrap": "~2.5.0",
19 | "file-saver": "~1.3.8",
20 | "ng-file-upload": "~12.2.12",
21 | "angular-uuid4": "0.3.1",
22 | "angular-bootstrap-checkbox": "~0.5.1",
23 | "angular-intro.js": "~3.3.0"
24 | },
25 | "devDependencies": {},
26 | "overrides": {
27 | "bootstrap": {
28 | "main": [
29 | "dist/css/bootstrap.css",
30 | "dist/js/bootstrap.js"
31 | ]
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | // generated on 2016-10-04 using generator-chrome-extension 0.6.1
2 | import gulp from 'gulp';
3 | import gulpLoadPlugins from 'gulp-load-plugins';
4 | import del from 'del';
5 | import runSequence from 'run-sequence';
6 | import {stream as wiredep} from 'wiredep';
7 |
8 | const $ = gulpLoadPlugins();
9 |
10 | gulp.task('extras', () => {
11 | return gulp.src([
12 | 'app/*.*',
13 | 'app/_locales/**',
14 | '!app/scripts.babel',
15 | '!app/*.json',
16 | '!app/*.html',
17 | ], {
18 | base: 'app',
19 | dot: true
20 | }).pipe(gulp.dest('dist'));
21 | });
22 |
23 | function lint(files, options) {
24 | return () => {
25 | return gulp.src(files)
26 | .pipe($.eslint(options))
27 | .pipe($.eslint.format());
28 | };
29 | }
30 |
31 | gulp.task('lint', lint('app/scripts.babel/**/*.js', {
32 | env: {
33 | es6: true
34 | }
35 | }));
36 |
37 | gulp.task('images', () => {
38 | return gulp.src('app/images/**/*')
39 | .pipe($.if($.if.isFile, $.cache($.imagemin({
40 | progressive: true,
41 | interlaced: true,
42 | // don't remove IDs from SVGs, they are often used
43 | // as hooks for embedding and styling
44 | svgoPlugins: [{cleanupIDs: false}]
45 | }))
46 | .on('error', function (err) {
47 | console.log(err);
48 | this.end();
49 | })))
50 | .pipe(gulp.dest('dist/images'));
51 | });
52 |
53 | gulp.task('html', () => {
54 | return gulp.src('app/*.html')
55 | .pipe($.useref({searchPath: ['.tmp', 'app', '.']}))
56 | .pipe($.sourcemaps.init())
57 | .pipe($.if('*.js', $.uglify()))
58 | //TODO has problems with bootstrap sourcemaps
59 | //.pipe($.if('*.css', $.cleanCss({compatibility: '*'})))
60 | .pipe($.sourcemaps.write())
61 | .pipe($.if('*.html', $.htmlmin({removeComments: true, collapseWhitespace: true})))
62 | .pipe(gulp.dest('dist'));
63 | });
64 |
65 | gulp.task('chromeManifest', () => {
66 | return gulp.src('app/manifest.json')
67 | .pipe($.chromeManifest({
68 | buildnumber: true,
69 | background: {
70 | target: 'scripts/background.js',
71 | exclude: [
72 | 'scripts/chromereload.js'
73 | ]
74 | }
75 | }))
76 | .pipe($.if('*.css', $.cleanCss({compatibility: '*'})))
77 | .pipe($.if('*.js', $.sourcemaps.init()))
78 | .pipe($.if('*.js', $.uglify()))
79 | .pipe($.if('*.js', $.sourcemaps.write('.')))
80 | .pipe(gulp.dest('dist'));
81 | });
82 |
83 | gulp.task('babel', () => {
84 | return gulp.src('app/scripts.babel/**/*.js')
85 | .pipe($.babel({
86 | presets: ['es2015']
87 | }))
88 | .pipe(gulp.dest('app/scripts'));
89 | });
90 |
91 | gulp.task('clean', del.bind(null, ['.tmp', 'dist']));
92 |
93 | gulp.task('watch', ['lint', 'babel'], () => {
94 | $.livereload.listen();
95 |
96 | gulp.watch([
97 | 'app/*.html',
98 | 'app/scripts/**/*.js',
99 | 'app/images/**/*',
100 | 'app/styles/**/*',
101 | 'app/_locales/**/*.json'
102 | ]).on('change', $.livereload.reload);
103 |
104 | gulp.watch('app/scripts.babel/**/*.js', ['lint', 'babel']);
105 | gulp.watch('bower.json', ['wiredep']);
106 | });
107 |
108 | gulp.task('size', () => {
109 | return gulp.src('dist/**/*').pipe($.size({title: 'build', gzip: true}));
110 | });
111 |
112 | gulp.task('wiredep', () => {
113 | gulp.src('app/*.html')
114 | .pipe(wiredep({
115 | ignorePath: /^(\.\.\/)*\.\./
116 | }))
117 | .pipe(gulp.dest('app'));
118 | });
119 |
120 | gulp.task('package', function () {
121 | var manifest = require('./dist/manifest.json');
122 | return gulp.src('dist/**')
123 | .pipe($.zip('chrome-multiwindow-positioner-' + manifest.version + '.zip'))
124 | .pipe(gulp.dest('package'));
125 | });
126 |
127 | gulp.task('build', (cb) => {
128 | runSequence(
129 | 'lint', 'babel', 'chromeManifest',
130 | ['html', 'images', 'extras'],
131 | 'size', cb);
132 | });
133 |
134 | gulp.task('default', ['clean'], cb => {
135 | runSequence('build', cb);
136 | });
137 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chrome-multiwindow-positioner",
3 | "private": true,
4 | "authors": [
5 | "Control Expert"
6 | ],
7 | "engines": {
8 | "node": ">=0.8.0"
9 | },
10 | "devDependencies": {
11 | "babel-core": "^6.26.3",
12 | "babel-preset-es2015": "^6.24.1",
13 | "brace-expansion": "1.1.7",
14 | "del": "^2.2.0",
15 | "gulp": "^3.9.1",
16 | "gulp-babel": "^6.1.2",
17 | "gulp-cache": "^0.4.3",
18 | "gulp-chrome-manifest": "0.0.13",
19 | "gulp-clean-css": "^2.0.3",
20 | "gulp-eslint": "^2.0.0",
21 | "gulp-htmlmin": "^1.3.0",
22 | "gulp-if": "^2.0.0",
23 | "gulp-imagemin": "^4.1.0",
24 | "gulp-livereload": "^3.8.1",
25 | "gulp-load-plugins": "^1.5.0",
26 | "gulp-size": "^2.1.0",
27 | "gulp-sourcemaps": "^1.6.0",
28 | "gulp-uglify": "^1.5.3",
29 | "gulp-useref": "^3.0.8",
30 | "gulp-zip": "^3.2.0",
31 | "main-bower-files": "^2.11.1",
32 | "run-sequence": "^1.1.5",
33 | "wiredep": "^4.0.0"
34 | },
35 | "eslintConfig": {
36 | "env": {
37 | "node": true,
38 | "browser": true
39 | },
40 | "globals": {
41 | "chrome": true
42 | },
43 | "rules": {
44 | "eol-last": 0,
45 | "quotes": [
46 | 2,
47 | "single"
48 | ]
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mocha Spec Runner
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/spec/test.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | describe('Give it some context', function () {
5 | describe('maybe a bit more context here', function () {
6 | it('should run here few assertions', function () {
7 |
8 | });
9 | });
10 | });
11 | })();
12 |
--------------------------------------------------------------------------------