",
29 | "contextMenus",
30 | "tabs",
31 | "storage"
32 | ],
33 | "options_ui": {
34 | "page": "options/options.html"
35 | },
36 | "action": {
37 | "default_icon": "icon.png",
38 | "theme_icons": [{
39 | "light": "icon_white.png",
40 | "dark": "icon.png",
41 | "size": 32
42 | }],
43 | "default_title": "Open in Popup Window",
44 | "default_popup": "options/options.html"
45 | },
46 | "browser_specific_settings": {
47 | "gecko": {
48 | "id": "open_in_popup_window@emvaized.dev"
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/options/icons/donate.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/options/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/options/icons/review.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/options/options.css:
--------------------------------------------------------------------------------
1 | body {
2 | min-width: 381px;
3 | padding: 4px;
4 | }
5 | #settingsTitle{
6 | padding: 0 6px;
7 | }
8 | .option {
9 | padding: 5px 6px;
10 | }
11 | .option, .option label {
12 | cursor: pointer;
13 | }
14 |
15 | .option:hover {
16 | background-color: lightgrey;
17 | transition: background-color 50ms ease-in-out;
18 | border-radius: 4px;
19 | }
20 |
21 | input[type="text"]{
22 | min-width: 40%;
23 | margin-right: 4px;
24 | }
25 |
26 | hr { width: 100%; opacity: 0.2; }
27 |
28 | .disabled-option {
29 | opacity: 0.5;
30 | transition: opacity 150ms ease-in-out;
31 | pointer-events: none;
32 | }
33 |
34 | #footer-buttons{
35 | padding: 6px;
36 | margin-top: 8px;
37 | }
38 | #footer-buttons img {
39 | vertical-align: bottom;
40 | }
41 | #footer-buttons button{
42 | cursor: pointer;
43 | }
44 |
45 | #versionLabel {
46 | color: gray;
47 | }
48 |
49 |
50 | h2{
51 | margin: 6px 0;
52 | }
53 |
54 | h5{
55 | color: gray;
56 | margin: 6px;
57 | min-width: fit-content !important;
58 | margin-right: 4px;
59 | font-weight: normal;
60 | }
61 |
62 | div:has(h5){
63 | display: flex; align-items: center; margin-top: 2px; margin-bottom: 2px;
64 | }
65 |
66 | .hint {
67 | border-radius: 50%;
68 | line-height: 1;
69 | vertical-align: middle;
70 | height: 12px;
71 | width: 12px;
72 | min-width: 12px;
73 | font-size: 13px;
74 | display: inline-block;
75 | text-align: center;
76 | border: 1px solid gray;
77 | color: gray;
78 | }
79 | .hint:hover{
80 | background: rgba(0,0,0,0.4);
81 | color:white;
82 | }
83 |
84 | .subgroup{
85 | padding-left: 4px;
86 | margin-left: 8px;
87 | border-left: 1px solid gray;
88 | }
89 |
90 | @media (prefers-color-scheme: dark) {
91 | body {
92 | background: rgb(40,41,44);
93 | color: white;
94 | }
95 |
96 | .option:hover {
97 | background-color: rgb(256, 256, 256, 0.1);
98 | }
99 | }
100 |
101 | @-moz-document url-prefix() {
102 | body {
103 | font-family: sans-serif !important;
104 | /* line-height: 1.0 !important; */
105 | }
106 | }
--------------------------------------------------------------------------------
/src/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | General settings
13 |
14 |
15 |
25 |
26 |
27 |
28 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | ?
53 |
54 |
55 |
56 |
57 |
58 |
59 |
72 |
73 |
74 |
75 | ?
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
94 | Image viewer
95 |
96 |
97 |
98 |
99 |
100 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
133 |
134 |
135 |
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/src/options/options.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", init);
2 |
3 | function init(){
4 | loadUserConfigs(function(userConfigs){
5 | const keys = Object.keys(configs);
6 |
7 | for (let i = 0, l = keys.length; i < l; i++) {
8 | const key = keys[i];
9 |
10 | /// set corresponing input value
11 | let input = document.getElementById(key.toString());
12 |
13 | /// Set input value
14 | if (input !== null && input !== undefined) {
15 | if (input.type == 'checkbox') {
16 | if ((userConfigs[key] !== null && userConfigs[key] == true) || (userConfigs[key] == null && configs[key] == true))
17 | input.setAttribute('checked', 0);
18 | else input.removeAttribute('checked', 0);
19 | } else if (input.tagName == 'SELECT') {
20 | let options = input.querySelectorAll('option');
21 | if (options)
22 | options.forEach(function (option) {
23 | let selectedValue = userConfigs[key] ?? configs[key];
24 | if (option.value == selectedValue) option.setAttribute('selected', true);
25 |
26 | try {
27 | if (chrome.i18n.getMessage(option.innerHTML) != '')
28 | option.innerHTML = chrome.i18n.getMessage(option.innerHTML);
29 | else if (chrome.i18n.getMessage(option['value']) != '')
30 | option.innerHTML = chrome.i18n.getMessage(option['value']);
31 | } catch (e) { }
32 |
33 | });
34 | } else {
35 | input.setAttribute('value', userConfigs[key] ?? configs[key]);
36 | }
37 |
38 | /// Set translated label for input
39 | let translatedLabel = chrome.i18n.getMessage(key);
40 | translatedLabel = translatedLabel
41 | .replace('Shift','Shift')
42 | .replace('Escape','Escape');
43 | if (!input.parentNode.innerHTML.includes(translatedLabel)) {
44 | if (input.type == 'checkbox'){
45 | input.parentNode.innerHTML += ' ' + translatedLabel;
46 | } else {
47 | input.parentNode.innerHTML = translatedLabel + ' ' + input.parentNode.innerHTML;
48 | }
49 | }
50 |
51 | /// Check if needs hint tooltip
52 | const hintMark = document.querySelector(`.option:has(#${key}) .hint`);
53 | if (hintMark) {
54 | const hintText = chrome.i18n.getMessage(key + 'Hint');
55 | if (hintText) hintMark.title = hintText;
56 | }
57 |
58 | input = document.querySelector('#' + key.toString());
59 |
60 | /// Set event listener
61 | input.addEventListener("input", function (e) {
62 | let id = input.getAttribute('id');
63 | let inputValue = input.getAttribute('type') == 'checkbox' ? input.checked : input.value;
64 | configs[id] = inputValue;
65 |
66 | saveAllSettings();
67 | updateDisabledOptions();
68 | });
69 | }
70 | }
71 | updateDisabledOptions();
72 | setFooterButtons();
73 | setVersionLabel();
74 | });
75 |
76 | setTranslatedLabels();
77 | }
78 |
79 | function setTranslatedLabels(){
80 | /// Set translations
81 | // document.getElementById('settingsTitle').innerText = chrome.i18n.getMessage('settingsTitle');
82 | document.getElementById('donateButton').innerHTML += chrome.i18n.getMessage('donateButton');
83 | document.getElementById('githubButton').innerHTML += chrome.i18n.getMessage('githubButton');
84 | document.getElementById('writeAReviewButton').innerHTML += chrome.i18n.getMessage('writeAReviewButton');
85 | document.getElementById('textSelectionHeader').innerText = chrome.i18n.getMessage('textSelectionHeader');
86 | document.getElementById('imageViewer').innerText = chrome.i18n.getMessage('imageViewer');
87 | document.getElementById('popupWindowSize').innerText = chrome.i18n.getMessage('popupWindowSize');
88 | document.getElementById('generalSettings').innerText = chrome.i18n.getMessage('generalSettings');
89 | document.getElementById('openPageInPopupWindowHeader').innerText = chrome.i18n.getMessage('openPageInPopupWindow');
90 | }
91 |
92 | function updateDisabledOptions() {
93 | /// Grey out unavailable optoins
94 | document.getElementById("popupSearchUrl").parentNode.className = document.getElementById("searchInPopupEnabled").checked ? 'enabled-option' : 'disabled-option';
95 | document.getElementById("tryFitWindowSizeToImage").parentNode.className = document.getElementById("useBuiltInImageViewer").checked ? 'enabled-option' : 'disabled-option';
96 | document.getElementById("tryFitWindowSizeToImage").parentNode.className = document.getElementById("viewInPopupEnabled").checked ? 'enabled-option' : 'disabled-option';
97 | document.getElementById("useBuiltInImageViewer").parentNode.className = document.getElementById("viewInPopupEnabled").checked ? 'enabled-option' : 'disabled-option';
98 | document.getElementById("minimalDragDistance").parentNode.className = document.getElementById("openByDragAndDrop").checked ? 'enabled-option' : 'disabled-option';
99 | document.getElementById("openDragAndDropUnderMouse").parentNode.className = document.getElementById("openByDragAndDrop").checked ? 'enabled-option' : 'disabled-option';
100 | document.getElementById("keepOpenPageInPopupWindowOpen").parentNode.className = document.getElementById("addOptionOpenPageInPopupWindow").checked ? 'enabled-option' : 'disabled-option';
101 | document.getElementById("fallbackPopupWindowLocation").parentNode.className =
102 | document.getElementById("popupWindowLocation").value == "mousePosition" ||
103 | document.getElementById("popupWindowLocation").value == "nearMousePosition"
104 | ? 'enabled-option' : 'disabled-option';
105 | }
106 |
107 | function setFooterButtons(){
108 | document.querySelector("#donateButton").addEventListener("click", function (val) {
109 | window.open('https://github.com/emvaized/open-in-popup-window-extension?tab=readme-ov-file#support-project-%EF%B8%8F', '_blank');
110 | });
111 |
112 | document.querySelector("#githubButton").addEventListener("click", function (val) {
113 | window.open('https://github.com/emvaized/open-in-popup-window-extension', '_blank');
114 | });
115 | document.querySelector("#writeAReviewButton").addEventListener("click", function (val) {
116 |
117 | const isFirefox = navigator.userAgent.indexOf("Firefox") > -1;
118 | window.open(isFirefox ? 'https://addons.mozilla.org/firefox/addon/open-in-popup-window/' : 'https://chrome.google.com/webstore/detail/open-in-popup-window/gmnkpkmmkhbgnljljcchnakehlkihhie/reviews', '_blank');
119 | });
120 | }
121 |
122 | function setVersionLabel() {
123 | const label = document.getElementById('versionLabel');
124 | const manifestData = chrome.runtime.getManifest();
125 | label.innerHTML = 'v' + manifestData.version;
126 | label.title = 'Release notes';
127 | label.onclick = function () {
128 | window.open('https://github.com/emvaized/open-in-popup-window-extension/blob/main/CHANGELOG.md')
129 | }
130 | }
131 |
132 | function saveAllSettings(){
133 | chrome.storage.sync.set(configs)
134 | }
--------------------------------------------------------------------------------
/src/viewer/viewer.css:
--------------------------------------------------------------------------------
1 | body {
2 | max-height: 100%; overflow: hidden;
3 | }
4 |
5 | .backgroundPattern {
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | height: 100vh;
11 | z-index: -1;
12 | opacity: 0.1;
13 | }
14 |
15 | .backgroundPattern::before {
16 | content: "";
17 | position: absolute;
18 | width: 100%;
19 | height: 100%;
20 | background: repeating-linear-gradient(
21 | 0deg, #000 0, #000 25px,
22 | #fff 25px, #fff 50px);
23 | }
24 |
25 | .backgroundPattern::after {
26 | content: "";
27 | position: absolute;
28 | width: 100%;
29 | height: 100%;
30 | background: repeating-linear-gradient(
31 | 90deg, #000 0, #000 25px,
32 | #fff 25px, #fff 50px);
33 | mix-blend-mode: difference;
34 | }
35 |
36 | #mouseListener {
37 | height: 100%; width: 100%; position: fixed; top: 0; bottom: 0; left: 0; right: 0;
38 | }
39 |
40 | #wrapper:not(.noTransition) {
41 | transition: transform 300ms ease;
42 | }
43 |
44 | #image {
45 | width: 100%; height: 100%;
46 | transform-origin: 0% 0%;
47 | }
48 |
49 | @media (prefers-color-scheme: dark) {
50 | body {
51 | background: black;
52 | }
53 | }
--------------------------------------------------------------------------------
/src/viewer/viewer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Image Viewer
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/viewer/viewer.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", init);
2 |
3 | let dxToShow = 0, dyToShow = 0, scale = 1.0, image, stepsCounter = 0, mouseListener;
4 | const minScale = 0.1, maxScale = 50.0, initialScale = 1.0, initialDx = 0, initialDy = 0, transitionDuration = 300;
5 | let rotationWrapper, scaleSlider, rotationStepsCounter = 0;
6 | const rotationSteps = [0, 90, 180, 270, 360], scaleSteps = [1.0, 2.0, 4.0];
7 | let mirroredX = false, mirroredY = false;
8 |
9 | function init(){
10 | const imageUrl = window.location.href.split('?src=')[1];
11 | if (!imageUrl) return;
12 |
13 | image = document.getElementById('image');
14 | image.src = imageUrl;
15 | document.title = imageUrl;
16 | mouseListener = document.getElementById('mouseListener');
17 | rotationWrapper = document.getElementById('wrapper');
18 |
19 | /// add mouse listeners
20 | setImageListeners();
21 |
22 | /// update window size on image load (in case we got it wrong)
23 | image.onload = function(){
24 | const aspectRatio = (image.naturalWidth ?? image.clientWidth) / (image.naturalHeight ?? image.clientHeight),
25 | toolbarHeight = window.outerHeight - window.innerHeight,
26 | toolbarWidth = window.outerWidth - window.innerWidth,
27 | availHeight = window.screen.availHeight, availWidth = window.screen.availWidth;
28 |
29 | chrome.runtime.sendMessage({
30 | action: 'updateAspectRatio', aspectRatio: aspectRatio,
31 | toolbarHeight: toolbarHeight, toolbarWidth: toolbarWidth,
32 | availHeight: availHeight, availWidth: availWidth
33 | });
34 | }
35 | }
36 |
37 | function setImageListeners(){
38 | /// scale on wheel
39 | mouseListener.addEventListener('wheel', imageWheelListener, { passive: false });
40 | /// move on pad down
41 | mouseListener.addEventListener('mousedown', function (e) {
42 | e.preventDefault();
43 | evt = e || window.event;
44 | if ("buttons" in evt) {
45 | if (evt.button == 1) {
46 | /// Middle click to close view
47 | closeView();
48 | } else if (evt.button == 0) {
49 | /// Left button
50 | panMouseDownListener(e)
51 | }
52 | // else if (evt.button == 2) {
53 | // /// Right button
54 | // rotateMouseDownListener(e)
55 | // }
56 | }
57 | });
58 | /// Double click to scale up listener
59 | mouseListener.addEventListener('dblclick', function (e) {
60 | evt = e || window.event;
61 | if ("buttons" in evt) {
62 | if (evt.button == 0) {
63 |
64 | /// take the scale into account with the offset
65 | let xs = (e.clientX - dxToShow) / scale,
66 | ys = (e.clientY - dyToShow) / scale;
67 |
68 | let scaleValueWithinSteps = false;
69 | scaleSteps.forEach(function (step) {
70 | if (scale == initialScale * step) scaleValueWithinSteps = true;
71 | })
72 |
73 | if (scaleValueWithinSteps) {
74 | if (stepsCounter == scaleSteps.length - 1) {
75 | stepsCounter = 0;
76 | scale = initialScale;
77 | dxToShow = initialDx;
78 | dyToShow = initialDy;
79 | }
80 | else {
81 | stepsCounter += 1;
82 | scale = initialScale * scaleSteps[stepsCounter];
83 | /// reverse the offset amount with the new scale
84 | dxToShow = e.clientX - xs * scale;
85 | dyToShow = e.clientY - ys * scale;
86 | }
87 | image.style.transform = `translate(${dxToShow}px,${dyToShow}px) scale(${scale})`;
88 | } else {
89 | /// Return image to initial scale
90 | scale = initialScale;
91 | stepsCounter = 0;
92 | rotationStepsCounter = 0;
93 | rotationWrapper.style.transform = 'rotate(0deg)';
94 |
95 | dxToShow = 0; dyToShow = 0;
96 | image.style.transform = 'translate(0,0)';
97 | }
98 |
99 | if (image.style.transition == '')
100 | image.style.transition = `transform ${transitionDuration}ms ease-in-out, scale ${transitionDuration}ms ease-in-out`;
101 | // image.style.scale = scale;
102 |
103 | setTimeout(function(){
104 | image.style.transition = '';
105 | }, transitionDuration)
106 | }
107 | }
108 | });
109 | }
110 |
111 | function panMouseDownListener(e) {
112 | e.preventDefault();
113 |
114 | image.style.cursor = 'grabbing';
115 | document.body.style.cursor = 'move';
116 | image.style.transition = '';
117 |
118 | function mouseMoveListener(e) {
119 | dxToShow = dxToShow + e.movementX;
120 | dyToShow = dyToShow + e.movementY;
121 |
122 | image.style.transform = `translate(${dxToShow}px, ${dyToShow}px) scale(${scale})`;
123 | }
124 |
125 | document.addEventListener('mousemove', mouseMoveListener);
126 | document.addEventListener('mouseup', function () {
127 | document.body.style.cursor = 'unset';
128 | image.style.cursor = 'grab';
129 | document.removeEventListener('mousemove', mouseMoveListener);
130 | });
131 | }
132 |
133 | function imageWheelListener(e) {
134 | e.preventDefault();
135 |
136 | /// take the scale into account with the offset
137 | const xs = (e.clientX - dxToShow) / scale,
138 | ys = (e.clientY - dyToShow) / scale;
139 |
140 | const wheelDelta = e.wheelDeltaY ?? -e.deltaY;
141 | scale += wheelDelta / 300;
142 |
143 | if (scale < minScale) scale = minScale;
144 | if (scale > maxScale) scale = maxScale;
145 |
146 | scale = parseFloat(scale);
147 |
148 | /// reverse the offset amount with the new scale
149 | dxToShow = e.clientX - xs * scale;
150 | dyToShow = e.clientY - ys * scale;
151 |
152 | // image.style.transition = '';
153 | image.style.transform = `translate(${dxToShow}px, ${dyToShow}px) scale(${scale})`;
154 | }
155 |
156 | function closeView() {
157 | window.close()
158 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require("terser-webpack-plugin");
3 | const CopyPlugin = require("copy-webpack-plugin");
4 | const ConcatPlugin = require('@mcler/webpack-concat-plugin');
5 | const JsonMinimizerPlugin = require("json-minimizer-webpack-plugin");
6 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
7 |
8 | module.exports = {
9 | /// background script
10 | entry: {
11 | index: "./src/content.js"
12 | },
13 | output: {
14 | path: path.resolve(__dirname, 'dist'),
15 | filename: "[name].js"
16 | },
17 | plugins: [
18 | /// content scripts
19 | new ConcatPlugin({
20 | name: 'content',
21 | outputPath: './',
22 | fileName: '[name].js',
23 | filesToConcat: [
24 | "./src/configs.js",
25 | "./src/content.js",
26 | ]
27 | }),
28 | new ConcatPlugin({
29 | name: 'background',
30 | outputPath: './',
31 | fileName: '[name].js',
32 | filesToConcat: [
33 | "./src/configs.js",
34 | "./src/background.js",
35 | ]
36 | }),
37 | /// static files
38 | new CopyPlugin({
39 | patterns: [
40 | { from: "src/manifest.json", to: "manifest.json" },
41 | { from: "src/assets/_locales", to: "_locales" },
42 | { from: "src/assets/icon.png", to: "icon.png" },
43 | { from: "src/assets/icon_white.png", to: "icon_white.png" },
44 | { from: "src/options", to: "options" },
45 | { from: "src/viewer", to: "viewer" },
46 | /// additional dependencies for the options page
47 | { from: "src/configs.js", to: "configs.js" },
48 |
49 | ],
50 | }),
51 | ],
52 | mode: 'production',
53 | optimization: {
54 | minimize: true,
55 | minimizer: [
56 | new TerserPlugin(),
57 | new CssMinimizerPlugin(),
58 | new JsonMinimizerPlugin(),
59 | ],
60 | },
61 | };
--------------------------------------------------------------------------------