Compare commits

...

147 Commits

Author SHA1 Message Date
8ea3f5b05a Version 1.3.1 2022-03-17 13:14:59 +01:00
dca81dd4f2 Upgrade to fontawesome 6 2022-03-17 13:06:17 +01:00
18c3219e9b Allow popups to be opened when their url starts with the referrer url 2022-03-17 12:56:40 +01:00
ab38ff51dc Upgrade to electron 17, upgrade dependencies 2022-03-17 12:36:40 +01:00
5f0f601a4c Version 1.3.0 2021-09-25 11:27:33 +02:00
94879e4258 Fix target="_blank" links not opening, use new api for new window handling 2021-09-25 11:22:14 +02:00
fd6fe7675a Version 1.2.14 2021-09-22 15:53:19 +02:00
42e636a3d1 Webpack: use new asset module instead of deprecated file-loader 2021-09-22 15:53:19 +02:00
7672a5fbe4 Rollback svgo to 9.0.0 while https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/237 is not fixed 2021-09-22 15:53:19 +02:00
de6d4b7e7f Fix svgo config deprecation 2021-09-22 15:53:19 +02:00
16ecfd112f Upgrade dependencies, bump electron to 15.0.0 2021-09-22 15:53:19 +02:00
969f5b65a9 Merge branch 'develop' 2021-07-11 12:08:00 +02:00
1c6291ef7c Update built electron version 2021-07-11 12:07:30 +02:00
1ce6a961fd Merge branch 'develop' 2021-07-11 12:06:00 +02:00
a6344769e5 Version 1.2.13 2021-07-11 12:05:45 +02:00
91af273af1 Fix icon path 2021-07-11 12:02:46 +02:00
35cdc94e1a Replace img-loader with image-minimizer-webpack-plugin 2021-07-11 12:02:34 +02:00
9b45602973 Upgrade dependencies 2021-07-11 11:47:48 +02:00
547cc26b5f Merge branch 'develop' 2021-05-25 12:16:38 +02:00
11d38a1cd9 Version 1.2.12 2021-05-25 12:16:19 +02:00
6ab51f7b38 Merge branch 'node-16' into develop 2021-05-25 12:15:41 +02:00
099f5bec64 Use local version of font-awesome instead of cdn 2021-05-25 12:14:52 +02:00
102aea4158 Upgrade dependencies, electron 13, electron-builder next 2021-05-25 11:22:46 +02:00
600ea07377 Upgrade dependencies, bump ts-loader 2021-05-11 10:30:36 +02:00
283080f00e Replace node-sass with sass 2021-05-11 10:10:47 +02:00
341b8a184a Merge branch 'develop' 2021-04-16 11:23:40 +02:00
8a463c682c Version 1.2.11 2021-04-16 11:22:59 +02:00
b761a644a4 Upgrade dependencies, electron 12.0.4 2021-04-16 11:22:33 +02:00
b85e562975 Merge branch 'develop' 2021-03-26 13:46:33 +01:00
ea8c45ef45 Version 1.2.10 2021-03-26 13:46:21 +01:00
9dff598011 Upgrade dependencies, electron 12.0.2 2021-03-26 13:45:17 +01:00
ec3c282a41 Merge branch 'develop' 2021-03-16 16:27:48 +01:00
7414e023e0 Version 1.2.9 2021-03-16 16:27:45 +01:00
e793d51039 Upgrade dependencies, electron 12.0.1 2021-03-16 16:26:57 +01:00
9d0bef5709 Merge branch 'develop' 2021-03-06 18:46:14 +01:00
9aef4047c9 Version 1.2.8 2021-03-06 18:45:37 +01:00
aeed83a9f7 Update electron updater url 2021-03-06 18:45:37 +01:00
b8861a7d45 Update default config service url 2021-03-06 18:45:37 +01:00
a6d7bd3604 Stop using remote in renderer process
Fix unused import
2021-03-06 18:45:37 +01:00
86e442f548 Disable contextIsolation in all UI windows 2021-03-06 18:45:37 +01:00
9fb8354878 Remove explicitly setting contextIsolation to true
contextIsolation is now true by default in electron 12.x
2021-03-06 18:45:37 +01:00
29f9b22d21 Upgrade dependencies i.e. electron 12.x
Remove explicit @types/node dependency to automatically align version with electron
Fix electron version
2021-03-06 18:45:37 +01:00
8d7f9a942e package.json: update homepage 2021-02-06 10:30:46 +01:00
17eafb33be Merge branch 'develop' 2021-02-06 10:20:39 +01:00
c2d221184d Version 1.2.7 2021-02-06 10:20:27 +01:00
23c11659e1 Upgrade dependencies i.e. electron 11.2.3 2021-02-06 10:19:13 +01:00
e851b10487 Merge branch 'develop' 2021-01-25 10:31:58 +01:00
bbaae1e287 Version 1.2.6 2021-01-25 10:31:53 +01:00
49991d11b1 Upgrade dependencies i.e. electron 11.2.1 2021-01-25 10:31:39 +01:00
5917d9bf1c Merge branch 'develop' 2021-01-20 12:50:48 +01:00
2e3586b28b Version 1.2.5 2021-01-20 12:46:33 +01:00
f3a5a5c0f8 Upgrade dependencies i.e. electron 11.2.0 2021-01-20 12:45:51 +01:00
61daa66396 Merge branch 'develop' 2020-12-21 15:41:14 +01:00
1d218f588d Version 1.2.4 2020-12-21 15:40:59 +01:00
e7bf51fcea Upgrade dependencies i.e. electron 11.1.0 2020-12-21 15:38:01 +01:00
d7c689db61 Merge branch 'develop' 2020-11-27 14:48:47 +01:00
68b545e342 Version 1.2.3 2020-11-27 14:48:28 +01:00
79b79e0aec Fix protocol status badge not updating for active service with id 0 2020-11-27 14:47:11 +01:00
ada191e971 Update new update download link 2020-11-27 14:31:42 +01:00
1d73545841 Update default config service url 2020-11-27 14:31:27 +01:00
79e8290971 Upgrade dependencies i.e. electron 11.0.3 2020-11-27 11:58:13 +01:00
64e1b59aeb Merge branch 'develop' 2020-11-17 12:14:09 +01:00
529a0abe13 Version 1.2.2 2020-11-17 12:13:54 +01:00
108d48b35d Add temporary fix for twitter (and others) not loading in webviews
Pending https://github.com/electron/electron/issues/25469
2020-11-17 12:11:41 +01:00
0e9ed7492c Upgrade dependencies i.e. electron v11.0.0 2020-11-17 11:52:48 +01:00
8e37cf3467 Merge branch 'develop' 2020-11-16 17:24:27 +01:00
3ee6c20842 Version 1.2.1 2020-11-16 17:23:47 +01:00
656d2c9589 Upgrade dependencies 2020-11-16 17:23:38 +01:00
effb625515 Update update/publish source 2020-11-16 16:15:58 +01:00
3ea3f47ff7 Merge branch 'develop' 2020-11-03 08:30:46 +01:00
d64a85c4c5 Version 1.2.0 2020-11-03 08:30:36 +01:00
3e4a1dbe81 Add zoom reset, zoom in and zoom out in service navigation context menu 2020-11-03 08:30:29 +01:00
cb20f656e9 Upgrade dependencies 2020-11-03 08:29:55 +01:00
ebf7126621 Merge branch 'develop' 2020-10-25 13:52:45 +01:00
cdbd0ff753 Version 1.1.15 2020-10-25 13:52:31 +01:00
7b07e32dc3 Upgrade dependencies i.e. electron 10.1.5 2020-10-25 13:52:12 +01:00
e1f15ba892 Fix all lint problems 2020-09-30 18:45:48 +02:00
b064d0ed87 Add eslint (with typescript plugin) 2020-09-29 20:43:34 +02:00
39f76c4a0e Upgrade dependencies i.e. electron 10.1.3 2020-09-29 20:35:54 +02:00
1a1c9c4af3 Merge branch 'develop' 2020-09-15 08:43:49 +02:00
ac94178bd8 Version 1.1.14 2020-09-15 08:43:37 +02:00
49e7b73714 Upgrade dependencies i.e. electron 10.1.2 2020-09-15 08:43:25 +02:00
0e39cd0efe Merge branch 'develop' 2020-09-03 12:50:36 +02:00
b5441d3014 Version 1.1.13 2020-09-03 12:50:28 +02:00
c201d4423e security: upgrade dependencies i.e. bl@1.2.3 2020-09-03 12:49:59 +02:00
8da3714a8e Merge branch 'develop' 2020-09-02 09:19:36 +02:00
f1ffa12375 Version 1.1.12 2020-09-02 09:18:36 +02:00
873a0c4d72 Upgrade dependencies i.e. electron 10.1.1 2020-09-02 09:18:36 +02:00
32109235de Merge branch 'develop' 2020-08-28 06:31:42 +02:00
1dd6b7fe50 Version 1.1.11 2020-08-28 06:31:35 +02:00
f95b8a66e8 Upgrade dependencies i.e. electron 10.1.0 2020-08-28 06:31:19 +02:00
c5997e9333 Merge branch 'develop' 2020-08-25 09:47:48 +02:00
c8e6e3d6eb Version 1.1.10 2020-08-25 09:47:42 +02:00
262da38624 Upgrade dependencies i.e. electron 10.0.0 2020-08-25 09:46:48 +02:00
38885490d4 Merge branch 'develop' 2020-08-22 09:34:34 +02:00
dd51acd136 Version 1.1.9 2020-08-22 09:34:24 +02:00
879d97da56 Upgrade dependencies i.e. electron 9.2.1, typescript 4.0.x 2020-08-22 09:29:53 +02:00
7f3c0a7e6a Merge branch 'develop' 2020-08-08 10:46:47 +02:00
5e4101d3f5 Version 1.1.8 2020-08-08 10:46:39 +02:00
b6c9e42a35 Upgrade dependencies i.e. electron 9.2.0 2020-08-08 10:46:26 +02:00
0c581f1cc2 Merge branch 'develop' 2020-07-29 10:17:42 +02:00
daac2f97f3 Version 1.1.7 2020-07-29 10:17:28 +02:00
d217af625b Upgrade dependencies i.e. electron 9.1.2 2020-07-29 10:17:16 +02:00
7ccaf96797 Merge branch 'develop' 2020-07-27 08:52:53 +02:00
f432d76f0c Version 1.1.6 2020-07-27 08:52:45 +02:00
c21505890d Upgrade dependencies i.e. electron 9.1.1 2020-07-27 08:52:30 +02:00
19e2ab8bc5 Merge branch 'develop' 2020-07-16 18:10:58 +02:00
aef592e48a Version 1.1.5 2020-07-16 18:10:51 +02:00
0c6d6a466b Add "file not found" error and do not catch user aborts as load errors 2020-07-16 18:10:33 +02:00
dea6c43573 Merge branch 'develop' 2020-07-14 11:11:10 +02:00
12f5d32322 Version 1.1.4 2020-07-14 11:10:45 +02:00
881e16895e Add default service icon pending favicon 2020-07-14 11:08:24 +02:00
92659c64f2 Add service loading navigation button animation 2020-07-14 11:08:09 +02:00
ae1c343560 Add error page on connection error 2020-07-14 10:31:09 +02:00
05acd44e98 Fix service navigation button sorted in wrong order when editing service 2020-07-14 10:01:34 +02:00
03f275e6e8 custom user-agent: rename "auto-fill" button to "Disguise as"
- Give Firefox and Chrome alternatives
- Update firefox user-agent to 78
2020-07-14 09:49:30 +02:00
a1e63c9e20 Merge branch 'develop' 2020-07-10 14:21:56 +02:00
e9b1b973c1 Version 1.1.3 2020-07-10 14:21:52 +02:00
87b233d14a Upgrade and update dependencies i.e. electron 9.1.0 2020-07-10 14:20:46 +02:00
9dbbed8da8 Fix url-preview visibility 2020-07-10 14:20:20 +02:00
521c8d2d48 Hide navigation bar when window is in fullscreen 2020-06-24 18:51:24 +02:00
e1b25ab62e Merge branch 'develop' 2020-06-23 15:11:05 +02:00
e9bca98de8 Version 1.1.2 2020-06-23 15:10:51 +02:00
bcf1673736 Upgrade dependencies i.e. electron 9.0.5 2020-06-23 15:08:55 +02:00
c327af9981 Remove human error 2020-06-15 16:34:20 +02:00
0e754c810b Merge branch 'develop' 2020-06-15 16:28:02 +02:00
a7424ca29e Version 1.1.1 2020-06-15 16:27:53 +02:00
3671a63442 Upgrade dependencies i.e. electron 9.0.4 2020-06-15 16:26:28 +02:00
1ce4cae217 Fix nav status tooltip z-index 2020-06-15 16:24:04 +02:00
49ba273927 Merge branch 'develop' 2020-06-15 16:00:01 +02:00
ecfaf30d1f Version 1.1.0 2020-06-15 15:59:49 +02:00
091750bcf2 Fix missing app icon 2020-06-15 15:58:04 +02:00
d90b8b0b05 Add config option to increase the navigation bar's size
Closes #4
2020-06-15 15:45:35 +02:00
2822a2e110 "Fake hide" service webviews to enable actual preloading
Closes #16
2020-06-15 15:25:58 +02:00
175b56d8f2 Add config option to start tabs minimized in system tray
Closes #14
2020-06-15 15:08:52 +02:00
a5394f3acd Do not verify code signature on windows and only check and prompt for updates on windows
Closes #15
2020-06-15 14:58:42 +02:00
d67c1e7573 Merge branch 'develop' 2020-06-08 20:10:51 +02:00
b8afb7a77d Version 1.0.3 2020-06-08 20:10:41 +02:00
e8e58184f7 Upgrade dependencies i.e. electron 9.0.3 2020-06-08 20:10:25 +02:00
30102605a2 package.json: fix repo url 2020-06-06 08:49:02 +02:00
c11e52d631 Merge branch 'develop' 2020-06-02 06:31:26 +02:00
2c74945dc0 Version 1.0.2 2020-06-02 06:27:33 +02:00
78865531b5 Upgrade dependencies i.e. electron 9.0.1 2020-06-02 06:27:18 +02:00
a5835ef85c Update README.md to reflect license change and 1.0 release 2020-05-26 11:05:52 +02:00
982c970a7a Merge branch 'develop' 2020-05-26 10:55:13 +02:00
abfadfb199 Version 1.0.1 2020-05-26 10:55:00 +02:00
9a71005014 Fix bad inline javascript usage 2020-05-26 10:54:47 +02:00
31 changed files with 4638 additions and 5830 deletions

View File

@ -1,34 +1,93 @@
{ {
"env": { "root": true,
"browser": true, "parser": "@typescript-eslint/parser",
"commonjs": true, "plugins": [
"es6": true, "@typescript-eslint"
"node": true ],
}, "parserOptions": {
"extends": "eslint:recommended", "project": [
"globals": { "./tsconfig.backend.json",
"Atomics": "readonly", "./tsconfig.frontend.json"
"SharedArrayBuffer": "readonly" ]
}, },
"parserOptions": { "extends": [
"ecmaVersion": 2018 "eslint:recommended",
}, "plugin:@typescript-eslint/recommended"
"rules": { ],
"indent": [ "rules": {
"error", "indent": [
"tab" "error",
], 4,
"linebreak-style": [ {
"error", "SwitchCase": 1
"unix" }
], ],
"quotes": [ "no-trailing-spaces": "error",
"error", "max-len": [
"single" "error",
], {
"semi": [ "code": 120,
"error", "ignoreStrings": true,
"always" "ignoreTemplateLiterals": true,
] "ignoreRegExpLiterals": true
} }
} ],
"semi": "off",
"@typescript-eslint/semi": [
"error"
],
"no-extra-semi": "error",
"eol-last": "error",
"comma-dangle": "off",
"@typescript-eslint/comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "always-multiline",
"enums": "always-multiline",
"generics": "always-multiline",
"tuples": "always-multiline"
}
],
"no-extra-parens": "off",
"@typescript-eslint/no-extra-parens": [
"error"
],
"no-nested-ternary": "error",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-non-null-assertion": "error",
"no-useless-return": "error",
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": [
"error"
],
"no-return-await": "off",
"@typescript-eslint/return-await": [
"error",
"always"
],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/no-floating-promises": "error"
},
"ignorePatterns": [
"build/**/*",
"resources/**/*",
"webpack.config.js"
]
}

View File

@ -1,7 +1,5 @@
# Tabs # Tabs
/!\ Still in development! I aim to improve security, usability and add more features. Wait for the 1.0 release to use tabs for critical jobs. /!\
Tabs is an electron app than allows you to make **persistent** and **isolated** sessions on any website. Tabs is an electron app than allows you to make **persistent** and **isolated** sessions on any website.
You could create `Services` for Facebook Messenger, GMail, or your personnal nextcloud instance. There is not limit to what home URL you can set. You could create `Services` for Facebook Messenger, GMail, or your personnal nextcloud instance. There is not limit to what home URL you can set.
@ -10,15 +8,15 @@ You could create `Services` for Facebook Messenger, GMail, or your personnal nex
- A service can be either automatically loaded or not - A service can be either automatically loaded or not
- You can customize a service css (i.e. remove unwanted elements like menus or nav bars) - You can customize a service css (i.e. remove unwanted elements like menus or nav bars)
You can navigate inside the focused service using the navigation buttons (previous page, next page). Tabs aims to provide both complete and simple customization.
## When's 1.0 being released? ## When's next major version being released?
You can track the development progress [here](https://github.com/ArisuOngaku/tabs/projects/3). You can track the development progress [here](https://github.com/ArisuOngaku/tabs/projects).
## Contributing ## Contributing
Pull requests are welcome and appreciated! Pull requests are welcome and appreciated!
## License ## License
[MIT - see LICENSE file](LICENSE) [GPLv3 - see LICENSE file](LICENSE)

View File

@ -1,6 +1,5 @@
owner: ArisuOngaku provider: generic
repo: tabs url: "https://update.eternae.ink/ashpie/tabs"
provider: github
updaterCacheDirName: tabs-updater updaterCacheDirName: tabs-updater
publisherName: publisherName:
- Alice Gaudon - Alice Gaudon

View File

@ -3,6 +3,7 @@
"files": "ts/files.ts", "files": "ts/files.ts",
"layout": "sass/layout.scss", "layout": "sass/layout.scss",
"index": "sass/index.scss", "index": "sass/index.scss",
"error": "sass/error.scss",
"service-settings": "sass/service-settings.scss" "service-settings": "sass/service-settings.scss"
} }
} }

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>An error occured</title>
<meta http-equiv="Content-Security-Policy"
content="style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/error.css">
</head>
<body>
<h1>Oops</h1>
<p>A connection error has occurred.</p>
<p>Please check your internet connection and this service's URL.</p>
<p>If you can load the actual URL in your favorite web browser, please file us a bug report.</p>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>An error occured</title>
<meta http-equiv="Content-Security-Policy"
content="style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/error.css">
</head>
<body>
<h1>Oops</h1>
<p>File not found.</p>
</body>
</html>

View File

@ -4,10 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Tabs</title> <title>Tabs</title>
<meta http-equiv="Content-Security-Policy" content="style-src 'self' 'unsafe-inline' https://use.fontawesome.com; font-src 'self' https://use.fontawesome.com; script-src 'self' 'sha256-oPC0l5nxLnJ2LX6qU9Laxa4/cjhuHDRIqdUsBDWYqnw='"> <meta http-equiv="Content-Security-Policy" content="style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'sha256-oPC0l5nxLnJ2LX6qU9Laxa4/cjhuHDRIqdUsBDWYqnw='">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<link rel="stylesheet" href="css/layout.css"> <link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/index.css"> <link rel="stylesheet" href="css/index.css">
@ -50,7 +47,7 @@
<div id="services"> <div id="services">
<div class="loader"></div> <div class="loader"></div>
<div id="url-preview" class="hidden"></div> <div id="url-preview" class="invisible"></div>
<div id="empty-message">Load a service using the menu on the left.</div> <div id="empty-message">Load a service using the menu on the left.</div>
</div> </div>
</body> </body>

9
frontend/sass/error.scss Normal file
View File

@ -0,0 +1,9 @@
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
align-self: center;
}

View File

@ -1,332 +1,356 @@
:root {
--nav-width: 48px;
}
body { body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
}
#navigation {
display: flex;
flex-direction: column;
height: 100%;
width: 48px;
}
#navigation > :not(#service-buttons) {
flex-shrink: 0;
} }
#service-buttons { #service-buttons {
flex-grow: 1; flex-grow: 1;
overflow: hidden auto; overflow: hidden auto;
}
*:focus {
outline-color: rgb(118, 93, 176);
}
#navigation {
display: flex;
flex-direction: column;
height: 100%;
width: var(--nav-width);
body.fullscreen & {
display: none;
}
> :not(#service-buttons) {
flex-shrink: 0;
}
button {
position: relative;
display: block;
width: var(--nav-width);
height: var(--nav-width);
margin: 0;
padding: 0;
color: #fff;
border: 0;
background: transparent;
cursor: pointer;
border-radius: 0;
&:focus {
outline: none;
background: #fff3 !important;
}
&:hover {
background-color: #fff3;
}
i {
font-size: calc(var(--nav-width) / 2);
}
img {
width: calc(var(--nav-width) / 2);
}
}
} }
#service-selector { #service-selector {
display: block; display: block;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
}
#service-selector::-webkit-scrollbar { &::-webkit-scrollbar {
width: 6px; width: 6px;
} }
#service-selector [draggable] { li {
user-select: none; position: relative;
background-color: rgb(43, 43, 43);
}
#service-selector [draggable] img { button {
-webkit-user-drag: none; border-radius: 0;
user-drag: none; }
&.active button {
background-color: #fff2;
}
&.loading, &.loaded {
button::before {
content: "";
display: block;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 75%;
border-right: 4px solid #ffffff3e;
}
}
&.loading button::before {
animation: loading-button-after linear 500ms infinite alternate;
height: 45%;
@keyframes loading-button-after {
from {
opacity: 0.1;
}
to {
opacity: 0.5;
}
}
}
}
[draggable] {
user-select: none;
background-color: rgb(43, 43, 43);
img {
-webkit-user-drag: none;
user-drag: none;
}
}
.drag-target-self button::after {
height: var(--nav-width);
border: 1px dashed #fff;
transform: none;
}
.drag-target button::after {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.drag-target::after {
top: 75% !important;
}
} }
#service-selector .drag-target button::after, #service-selector .drag-target button::after,
#service-last-drag-position.drag-target { #service-last-drag-position.drag-target {
content: ""; content: "";
height: 4px; height: 4px;
transform: translateY(-50%); transform: translateY(-50%);
border: 0; border: 0;
box-sizing: border-box; box-sizing: border-box;
background: #fff9; background: #fff9;
}
#service-selector .drag-target-self button::after {
height: 48px;
border: 1px dashed #fff;
transform: none;
}
#service-selector .drag-target button::after {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
#service-selector .drag-target::after {
top: 75% !important;
}
*:focus {
outline-color: rgb(118, 93, 176);
}
#navigation button {
position: relative;
display: block;
width: 48px;
height: 48px;
margin: 0;
padding: 0;
color: #fff;
border: 0;
background: transparent;
cursor: pointer;
}
#navigation button:focus {
outline: none;
background: #fff3 !important;
}
#navigation button img {
width: 24px;
}
#navigation button i {
font-size: 24px;
}
#service-selector li {
position: relative;
}
#service-selector li button,
#navigation > button {
border-radius: 0;
}
#service-selector li.active button {
background-color: #fff2;
}
#service-selector li.loaded button::before {
content: "";
display: block;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 75%;
border-right: 4px solid #ffffff3e;
}
/*#service-selector li.loaded::after {*/
/* content: "";*/
/* position: absolute;*/
/* top: 50%;*/
/* right: 2px;*/
/* transform: translateY(-50%);*/
/* width: 4px;*/
/* height: 4px;*/
/* background-color: #fff;*/
/* border-radius: 100%;*/
/*}*/
#navigation button:hover {
background-color: #fff3;
} }
#history { #history {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 2px; padding: 2px;
} }
#history button, #history button,
#history .status { #history .status {
display: inline; display: inline;
width: 24px; width: calc(var(--nav-width) / 2);
height: 24px; height: calc(var(--nav-width) / 2);
margin: 2px; margin: 2px;
padding: initial; padding: initial;
font-size: 12px; font-size: calc(var(--nav-width) / 4);
background: #fff1; background: #fff1;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
border-radius: 3px; border-radius: 3px;
} }
#history button i { #history button i {
font-size: inherit; font-size: inherit;
} }
#history button.disabled { #history button.disabled {
color: #888; color: #888;
border: transparent; border: transparent;
background: transparent; background: transparent;
cursor: initial; cursor: initial;
} }
#history button:focus, #history button:focus,
#history button:hover { #history button:hover {
outline: none; outline: none;
border-color: #fff9; border-color: #fff9;
} }
#history button:hover:not(.disabled) { #history button:hover:not(.disabled) {
outline: none; outline: none;
background-color: #fff2; background-color: #fff2;
} }
#history button:active:not(.disabled) { #history button:active:not(.disabled) {
background-color: #fff4; background-color: #fff4;
} }
#history #status { #history #status {
background: transparent; background: transparent;
} }
#history #status .unknown, #history #status .unknown,
#history #status .file { #history #status .file {
color: #888; color: #888;
} }
#history #status .https { #history #status .https {
color: #009800; color: #009800;
} }
#history #status .http { #history #status .http {
color: #e20000; color: #e20000;
} }
#history #status > :not(.active) { #history #status > :not(.active) {
display: none; display: none;
} }
#history #status > * .tip { #history #status > * .tip {
position: absolute; position: absolute;
z-index: 1; z-index: 30;
left: 100%; left: 100%;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
display: none; display: none;
margin-left: 8px; margin-left: 8px;
background-color: #000; background-color: #000;
padding: 8px; padding: 8px;
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
color: #fff; color: #fff;
text-transform: none; text-transform: none;
font-family: sans-serif; font-family: sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 100; font-weight: 100;
} }
#history #status > * .tip::before { #history #status > * .tip::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;
transform: translate(-100%, -50%); transform: translate(-100%, -50%);
display: inline-block; display: inline-block;
width: 0; width: 0;
height: 0; height: 0;
border: 0 solid transparent; border: 0 solid transparent;
border-top-width: 4px; border-top-width: 4px;
border-bottom-width: 4px; border-bottom-width: 4px;
border-right: 7px solid black; border-right: 7px solid black;
} }
#history #status:focus > * .tip { #history #status:focus > * .tip {
display: block; display: block;
} }
#navigation #add-button:not(:hover) { #navigation #add-button:not(:hover) {
opacity: 0.75; opacity: 0.75;
} }
#services { #services {
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
} }
#services > *:not(.loader):not(#url-preview) { #services > *:not(.loader):not(#url-preview) {
height: 100%; height: 100%;
} }
#services > :not(.active):not(.loader):not(#url-preview) { #services > :not(.active):not(.loader):not(#url-preview):not(webview) {
display: none; display: none;
}
#services > webview {
position: absolute;
width: 100%;
height: 100%;
z-index: 10;
background-color: rgb(43, 43, 43);
}
#services > webview.active {
z-index: 20;
} }
#services > .loader { #services > .loader {
width: 64px; width: 64px;
height: 64px; height: 64px;
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: -1; z-index: -1;
} }
#url-preview { #url-preview {
position: absolute; position: absolute;
bottom: 0; z-index: 10000;
left: 0; bottom: 0;
display: block; left: 0;
padding: 2px 5px 1px 5px; display: block;
padding: 2px 5px 1px 5px;
max-width: 48%; max-width: 48%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: monospace; font-family: monospace;
border-width: 1px 1px 0 0; border-width: 1px 1px 0 0;
border-color: #232323; border-color: #232323;
border-style: solid; border-style: solid;
border-radius: 0 5px 0 0; border-radius: 0 5px 0 0;
background: #2b2b2b; background: #2b2b2b;
opacity: 1; opacity: 1;
transition: 100ms ease opacity; transition: 100ms ease opacity;
transition-delay: 200ms; transition-delay: 200ms;
} }
#url-preview.right { #url-preview.right {
left: auto; left: auto;
right: 0; right: 0;
border-width: 1px 0 0 1px; border-width: 1px 0 0 1px;
border-radius: 5px 0 0 0; border-radius: 5px 0 0 0;
} }
#url-preview.hidden { #url-preview.invisible {
opacity: 0; opacity: 0;
} }
#services > #empty-message { #services > #empty-message {
display: flex !important; display: flex !important;
position: absolute; position: absolute;
z-index: -1; z-index: -1;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
#services > .loader:not(.hidden) ~ #empty-message { #services > .loader:not(.hidden) ~ #empty-message {
display: none !important; display: none !important;
} }

View File

@ -1,3 +1,5 @@
@import url('../../node_modules/@fortawesome/fontawesome-free/css/all.css');
* { * {
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -71,21 +71,27 @@ form {
margin: 8px; margin: 8px;
grid-template-columns: 0fr auto 0fr; grid-template-columns: 0fr auto 0fr;
}
.form-group > * { &.no-expand {
margin: 8px; display: flex;
padding: 8px; flex-direction: row;
white-space: nowrap; justify-content: center;
align-self: center; }
}
.form-group > :first-child { > * {
justify-self: end; margin: 8px;
} padding: 8px;
white-space: nowrap;
align-self: center;
}
.form-group > :not(:first-child) { > :first-child {
margin-left: 8px; justify-self: end;
}
> :not(:first-child) {
margin-left: 8px;
}
} }
label.form-group { label.form-group {

View File

@ -4,10 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Service settings</title> <title>Service settings</title>
<meta http-equiv="Content-Security-Policy" content="style-src 'self' 'unsafe-inline' https://use.fontawesome.com; font-src 'self' https://use.fontawesome.com; script-src 'self' 'sha256-5gY/z34s5Mtc3YL8GkwZQhzk9LymQIuFUQRVvs7Gh0o='"> <meta http-equiv="Content-Security-Policy" content="style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'sha256-5gY/z34s5Mtc3YL8GkwZQhzk9LymQIuFUQRVvs7Gh0o='">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<link rel="stylesheet" href="css/layout.css"> <link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/service-settings.css"> <link rel="stylesheet" href="css/service-settings.css">
@ -16,7 +13,7 @@
</head> </head>
<body> <body>
<form action="javascript: save();"> <form>
<div class="form-header"> <div class="form-header">
<h1>Loading...</h1> <h1>Loading...</h1>
</div> </div>
@ -42,11 +39,18 @@
<textarea name="customCSS" id="custom-css" rows="3"></textarea> <textarea name="customCSS" id="custom-css" rows="3"></textarea>
</div> </div>
<div class="form-group"> <fieldset>
<label for="custom-user-agent">Custom UserAgent (i.e. google services)</label> <legend>Disguise</legend>
<input type="text" name="customUserAgent" id="custom-user-agent"> <div class="form-group">
<button type="button" id="userAgentAutoFill">Auto-fill</button> <label for="custom-user-agent">Custom UserAgent (i.e. google services)</label>
</div> <input type="text" name="customUserAgent" id="custom-user-agent">
</div>
<div class="form-group no-expand">
<button type="button" id="userAgentAutoFillFirefox">Disguise as firefox</button>
<button type="button" id="userAgentAutoFillChrome">Disguise as chrome</button>
</div>
</fieldset>
<div id="icon-choice"> <div id="icon-choice">
<div class="form-group-header"> <div class="form-group-header">
@ -78,7 +82,7 @@
<div class="form-footer"> <div class="form-footer">
<div class="form-group" id="buttons"> <div class="form-group" id="buttons">
<button id="cancel-button">Cancel</button> <button id="cancel-button" type="button">Cancel</button>
<button type="submit">Save</button> <button type="submit">Save</button>
</div> </div>
</div> </div>

View File

@ -5,10 +5,7 @@
<title>Service settings</title> <title>Service settings</title>
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="style-src 'self' 'unsafe-inline' https://use.fontawesome.com; font-src 'self' https://use.fontawesome.com; script-src 'self' 'sha256-UoPUIMX0PZl7cy3YoegZ0EDleSaHxTURPMyK09xsa0E='"> content="style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'sha256-UoPUIMX0PZl7cy3YoegZ0EDleSaHxTURPMyK09xsa0E='">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<link rel="stylesheet" href="css/layout.css"> <link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/service-settings.css"> <link rel="stylesheet" href="css/service-settings.css">
@ -17,7 +14,7 @@
</head> </head>
<body> <body>
<form action="javascript: save();"> <form>
<div class="form-header"> <div class="form-header">
<h1>Settings</h1> <h1>Settings</h1>
</div> </div>
@ -29,18 +26,32 @@
<button id="download-button" type="button" class="hidden">Download</button> <button id="download-button" type="button" class="hidden">Download</button>
</div> </div>
<h2 class="form-header">History navigation buttons</h2> <section>
<h2 class="form-header">Startup</h2>
<label class="form-group"> <label class="form-group"><input type="checkbox" name="start-minimized" id="start-minimized"> Start minimized in system tray</label>
<input type="checkbox" name="security-button" id="security-button"> Security info (recommended)</label> </section>
<label class="form-group">
<input type="checkbox" name="home-button" id="home-button"> Home button</label> <section>
<label class="form-group"> <h2 class="form-header">Appearance</h2>
<input type="checkbox" name="back-button" id="back-button"> Go back button</label>
<label class="form-group"> <label class="form-group"><input type="checkbox" name="big-nav-bar" id="big-nav-bar"> Increase the size of the navigation bar</label>
<input type="checkbox" name="forward-button" id="forward-button"> Go forward button</label> </section>
<label class="form-group">
<input type="checkbox" name="refresh-button" id="refresh-button"> Refresh page button</label> <section>
<h2 class="form-header">History navigation buttons</h2>
<label class="form-group">
<input type="checkbox" name="security-button" id="security-button"> Security info (recommended)</label>
<label class="form-group">
<input type="checkbox" name="home-button" id="home-button"> Home button</label>
<label class="form-group">
<input type="checkbox" name="back-button" id="back-button"> Go back button</label>
<label class="form-group">
<input type="checkbox" name="forward-button" id="forward-button"> Go forward button</label>
<label class="form-group">
<input type="checkbox" name="refresh-button" id="refresh-button"> Refresh page button</label>
</section>
</div> </div>
<div class="form-footer"> <div class="form-footer">

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
function requireAll(r: any) { function requireAll(r: any) {
r.keys().forEach(r); r.keys().forEach(r);
} }

View File

@ -1,143 +1,50 @@
import { import {DidFailLoadEvent, ipcRenderer, PageFaviconUpdatedEvent, UpdateTargetUrlEvent} from "electron";
clipboard, import Service from "../../src/Service";
ipcRenderer, import {IconProperties, IconSet, SpecialPages} from "../../src/Meta";
PageFaviconUpdatedEvent, import Config from "../../src/Config";
remote,
shell,
UpdateTargetUrlEvent,
WebContents
} from "electron";
const { const appInfo: {
Menu, title?: string;
MenuItem, } = {};
dialog, let icons: IconProperties[] = [];
session,
} = remote;
const appInfo: any = {}; let services: (FrontService | undefined)[] = [];
let icons: any[] = []; let selectedServiceId: number | null = null;
let securityButton: HTMLElement | null = null,
let services: any[] = []; homeButton: HTMLElement | null = null,
let selectedService: any = null; forwardButton: HTMLElement | null = null,
let securityButton: HTMLElement | null, backButton: HTMLElement | null = null,
homeButton: HTMLElement | null, refreshButton: HTMLElement | null = null;
forwardButton: HTMLElement | null,
backButton: HTMLElement | null,
refreshButton: HTMLElement | null;
let addButton, settingsButton; let addButton, settingsButton;
let emptyPage: string; let specialPages: SpecialPages | null = null;
let urlPreview: HTMLElement | null; let urlPreview: HTMLElement | null = null;
let serviceSelector: HTMLElement | null; let serviceSelector: HTMLElement | null = null;
// Service reordering // Service reordering
let lastDragPosition: HTMLElement | null, oldActiveService: number; let lastDragPosition: HTMLElement | null = null;
let oldActiveService: number | null = null;
// Service context menu
function openServiceContextMenu(event: Event, serviceId: number) {
event.preventDefault();
const service = services[serviceId];
const menu = new Menu();
const ready = service.view && service.viewReady, notReady = !service.view && !service.viewReady;
menu.append(new MenuItem({
label: 'Home', click: () => {
service.view.loadURL(service.url)
.catch(console.error);
},
enabled: ready,
}));
menu.append(new MenuItem({
label: ready ? 'Reload' : 'Load', click: () => {
reloadService(serviceId);
},
enabled: ready || notReady,
}));
menu.append(new MenuItem({
label: 'Close', click: () => {
unloadService(serviceId);
},
enabled: ready,
}));
menu.append(new MenuItem({type: "separator"}));
let permissionsMenu = [];
if (ready) {
for (const domain in service.permissions) {
if (service.permissions.hasOwnProperty(domain)) {
const domainPermissionsMenu = [];
const domainPermissions = service.permissions[domain];
for (const permission of domainPermissions) {
domainPermissionsMenu.push({
label: (permission.authorized ? '✓' : '❌') + ' ' + permission.name,
submenu: [{
label: 'Toggle',
click: () => {
permission.authorized = !permission.authorized;
updateServicePermissions(serviceId);
},
}, {
label: 'Forget',
click: () => {
service.permissions[domain] = domainPermissions.filter((p: any) => p !== permission);
},
}],
});
}
if (domainPermissionsMenu.length > 0) {
permissionsMenu.push({
label: domain,
submenu: domainPermissionsMenu,
});
}
}
}
}
menu.append(new MenuItem({
label: 'Permissions',
enabled: ready,
submenu: permissionsMenu,
}));
menu.append(new MenuItem({type: "separator"}));
menu.append(new MenuItem({
label: 'Edit', click: () => {
ipcRenderer.send('openServiceSettings', serviceId);
}
}));
menu.append(new MenuItem({
label: 'Delete', click: () => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'question',
title: 'Confirm',
message: 'Are you sure you want to delete this service?',
buttons: ['Cancel', 'Confirm'],
cancelId: 0,
}).then(result => {
if (result.response === 1) {
ipcRenderer.send('deleteService', serviceId);
}
}).catch(console.error);
}
}));
menu.popup({window: remote.getCurrentWindow()});
}
ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUrl, config) => { ipcRenderer.on('data', (
event,
appTitle: string,
iconSets: IconSet[],
activeServiceId: number,
_specialPages: SpecialPages,
config: Config,
) => {
// App info // App info
appInfo.title = appData.title; appInfo.title = appTitle;
// Icons // Icons
icons = []; icons = [];
for (const set of iconSets) { for (const set of iconSets) {
icons = icons.concat(set); icons.push(...set);
} }
// Special pages
specialPages = _specialPages;
console.log('Updating services ...'); console.log('Updating services ...');
services = config.services; services = config.services;
@ -154,7 +61,7 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
} }
for (let i = 0; i < services.length; i++) { for (let i = 0; i < services.length; i++) {
createService(i); createServiceNavigationElement(i);
} }
// Init drag last position // Init drag last position
@ -164,7 +71,7 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
const index = services.length; const index = services.length;
if (draggedId !== index && draggedId !== index - 1) { if (draggedId !== index && draggedId !== index - 1) {
resetDrag(); resetDrag();
lastDragTarget = dragTargetId = index; lastDragTarget = index;
lastDragPosition?.classList.remove('hidden'); lastDragPosition?.classList.remove('hidden');
lastDragPosition?.classList.add('drag-target'); lastDragPosition?.classList.add('drag-target');
} }
@ -172,22 +79,20 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
} }
// Set active service // Set active service
if (actualSelectedService < 0 || actualSelectedService >= services.length) { if (activeServiceId < 0 || activeServiceId >= services.length) {
actualSelectedService = 0; activeServiceId = 0;
} }
setActiveService(actualSelectedService); setActiveService(activeServiceId);
// Empty
emptyPage = emptyUrl;
// Url preview element // Url preview element
urlPreview = document.getElementById("url-preview"); urlPreview = document.getElementById("url-preview");
if (urlPreview) { if (urlPreview) {
const _urlPreview = urlPreview;
urlPreview.addEventListener('mouseover', () => { urlPreview.addEventListener('mouseover', () => {
if (urlPreview!.classList.contains('right')) { if (_urlPreview.classList.contains('right')) {
urlPreview!.classList.remove('right'); _urlPreview.classList.remove('right');
} else { } else {
urlPreview!.classList.add('right'); _urlPreview.classList.add('right');
} }
}); });
} }
@ -207,44 +112,91 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
// Other elements // Other elements
serviceSelector = document.getElementById('service-selector'); serviceSelector = document.getElementById('service-selector');
// Navbar size
document.documentElement.style.setProperty('--nav-width', config.bigNavBar ? '64px' : '48px');
}); });
function removeServiceFeatures(id: number): HTMLElement | null { ipcRenderer.on('load-service-home', (event, serviceId: number) => {
const service = services[serviceId];
if (!service) throw new Error('Service doesn\'t exist.');
service.view?.loadURL(service.url)
.catch(console.error);
});
ipcRenderer.on('reload-service', (event, serviceId: number) => {
reloadService(serviceId);
});
ipcRenderer.on('unload-service', (event, serviceId: number) => {
unloadService(serviceId);
});
ipcRenderer.on('reset-service-zoom-level', (event, serviceId: number) => {
const service = services[serviceId];
if (service?.view) {
service.view.setZoomFactor(1);
service.view.setZoomLevel(0);
}
});
ipcRenderer.on('zoom-in-service', (event, serviceId: number) => {
const service = services[serviceId];
if (service?.view) {
service.view.setZoomLevel(service.view.getZoomLevel() + 1);
}
});
ipcRenderer.on('zoom-out-service', (event, serviceId: number) => {
const service = services[serviceId];
if (service?.view) {
service.view.setZoomLevel(service.view.getZoomLevel() - 1);
}
});
function removeServiceFeatures(id: number): Element | null {
// Remove nav // Remove nav
const nav = document.querySelector('#service-selector'); const nav = document.querySelector('#service-selector');
let oldNavButton: HTMLElement | null = null; let oldNavButton: HTMLElement | null = null;
let nextSibling: Element | null = null;
if (nav) { if (nav) {
oldNavButton = nav.querySelector('li:nth-of-type(' + (id + 1) + ')'); oldNavButton = nav.querySelector('li:nth-of-type(' + (id + 1) + ')');
if (oldNavButton) { if (oldNavButton) {
nextSibling = oldNavButton.nextElementSibling;
nav.removeChild(oldNavButton); nav.removeChild(oldNavButton);
} }
} }
// Remove webview // Remove webview
if (services[id] && services[id].view) { const service = services[id];
document.querySelector('#services')?.removeChild(services[id].view); if (service) {
const view = service.view;
if (view) document.querySelector('#services')?.removeChild(view);
} }
return oldNavButton; return nextSibling;
} }
ipcRenderer.on('updateService', (e, id, data) => { ipcRenderer.on('updateService', (e, id: number | null, data: Service) => {
if (id === null) { if (id === null) {
console.log('Adding new service');
services.push(data); services.push(data);
createService(services.length - 1); createServiceNavigationElement(services.length - 1);
} else { } else {
const oldNavButton = removeServiceFeatures(id); console.log('Updating existing service', id);
const nextSibling = removeServiceFeatures(id);
// Create new service // Create new service
services[id] = data; services[id] = data;
createService(id, oldNavButton ? oldNavButton.nextElementSibling : null); createServiceNavigationElement(id, nextSibling);
if (parseInt(selectedService) === id) { if (selectedServiceId === id) {
setActiveService(id); setActiveService(id);
} }
} }
}); });
ipcRenderer.on('reorderService', (e, serviceId, targetId) => { ipcRenderer.on('reorderService', (e, serviceId: number, targetId: number) => {
const oldServices = services; const oldServices = services;
services = []; services = [];
@ -268,48 +220,69 @@ ipcRenderer.on('reorderService', (e, serviceId, targetId) => {
serviceSelector.innerHTML = ''; serviceSelector.innerHTML = '';
} }
for (let i = 0; i < services.length; i++) { for (let i = 0; i < services.length; i++) {
services[i].li = undefined; const service = services[i];
createService(i); if (service) service.li = undefined;
createServiceNavigationElement(i);
} }
setActiveService(newId); setActiveService(newId);
}); });
ipcRenderer.on('deleteService', (e, id) => { ipcRenderer.on('deleteService', (e, id: number) => {
removeServiceFeatures(id); removeServiceFeatures(id);
if (parseInt(selectedService) === id) { if (selectedServiceId === id) {
setActiveService(0); setActiveService(0);
} }
delete services[id]; services.splice(id, 1);
services = services.filter(s => s !== null);
}); });
function createService(index: number, nextNavButton?: Element | null) { function createServiceNavigationElement(index: number, nextNavButton?: Element | null) {
let service = services[index]; const service = services[index];
let li = <any>document.createElement('li'); if (!service) throw new Error('Service doesn\'t exist.');
const li = document.createElement('li') as NavigationElement;
service.li = li; service.li = li;
let button = document.createElement('button'); const button = document.createElement('button');
button.dataset.serviceId = '' + index; button.dataset.serviceId = '' + index;
button.dataset.tooltip = service.name; button.dataset.tooltip = service.name;
button.addEventListener('click', () => { button.addEventListener('click', () => {
if (button.dataset.serviceId) { const rawId = button.dataset.serviceId;
setActiveService(parseInt(button.dataset.serviceId)); if (rawId) {
const id = parseInt(rawId);
setActiveService(id);
ipcRenderer.send('setActiveService', id);
} }
ipcRenderer.send('setActiveService', button.dataset.serviceId);
}); });
button.addEventListener('contextmenu', e => openServiceContextMenu(e, index)); button.addEventListener('contextmenu', e => {
e.preventDefault();
let icon: any; const service = services[index];
if (!service) throw new Error('Service doesn\'t exist.');
ipcRenderer.send(
'open-service-navigation-context-menu',
index,
!!service.view && !!service.viewReady,
!service.view && !service.viewReady,
service.view?.getZoomFactor() !== 1 && service.view?.getZoomLevel() !== 0,
);
});
let icon: HTMLImageElement | HTMLElement;
if (service.useFavicon && service.favicon != null) { if (service.useFavicon && service.favicon != null) {
icon = document.createElement('img'); icon = document.createElement('img');
icon.src = service.favicon; if (icon instanceof HTMLImageElement) {
icon.alt = service.name; icon.src = service.favicon;
} else if (service.isImage) { icon.alt = service.name;
}
} else if (service.isImage && service.icon) {
icon = document.createElement('img'); icon = document.createElement('img');
icon.src = service.icon; if (icon instanceof HTMLImageElement) {
icon.alt = service.name; icon.src = service.icon;
icon.alt = service.name;
}
} else { } else {
icon = document.createElement('i'); icon = document.createElement('i');
let iconProperties = icons.find(i => `${i.set}/${i.name}` === service.icon); let iconProperties = icons.find(i => `${i.set}/${i.name}` === service.icon);
@ -321,6 +294,8 @@ function createService(index: number, nextNavButton?: Element | null) {
iconProperties.faIcon.split(' ').forEach((cl: string) => { iconProperties.faIcon.split(' ').forEach((cl: string) => {
icon.classList.add(cl); icon.classList.add(cl);
}); });
} else {
icon.classList.add('fas', 'fa-circle');
} }
} }
@ -346,10 +321,8 @@ function createService(index: number, nextNavButton?: Element | null) {
let draggedId: number; let draggedId: number;
let lastDragTarget = -1; let lastDragTarget = -1;
let dragTargetId = -1;
let dragTargetCount = 0;
function initDrag(index: number, li: any) { function initDrag(index: number, li: NavigationElement) {
li.serviceId = index; li.serviceId = index;
li.draggable = true; li.draggable = true;
li.addEventListener('dragstart', (event: DragEvent) => { li.addEventListener('dragstart', (event: DragEvent) => {
@ -361,7 +334,7 @@ function initDrag(index: number, li: any) {
}); });
li.addEventListener('dragover', (e: DragEvent) => { li.addEventListener('dragover', (e: DragEvent) => {
let realIndex = index; let realIndex = index;
let rect = li.getBoundingClientRect(); const rect = li.getBoundingClientRect();
if ((e.clientY - rect.y) / rect.height >= 0.5) { if ((e.clientY - rect.y) / rect.height >= 0.5) {
realIndex++; realIndex++;
@ -372,12 +345,18 @@ function initDrag(index: number, li: any) {
} }
resetDrag(); resetDrag();
let el = realIndex === services.length ? lastDragPosition : services[realIndex].li; const el = realIndex === services.length ?
lastDragTarget = dragTargetId = realIndex; lastDragPosition :
services[realIndex]?.li;
lastDragTarget = realIndex;
lastDragPosition?.classList.remove('hidden'); lastDragPosition?.classList.remove('hidden');
el.classList.add('drag-target');
if (draggedId === realIndex || draggedId === realIndex - 1) el.classList.add('drag-target-self'); if (el) {
el.classList.add('drag-target');
if (draggedId === realIndex || draggedId === realIndex - 1)
el.classList.add('drag-target-self');
}
}); });
li.addEventListener('dragend', () => { li.addEventListener('dragend', () => {
reorderService(draggedId, lastDragTarget); reorderService(draggedId, lastDragTarget);
@ -387,8 +366,6 @@ function initDrag(index: number, li: any) {
function resetDrag() { function resetDrag() {
lastDragTarget = -1; lastDragTarget = -1;
dragTargetId = -1;
dragTargetCount = 0;
serviceSelector?.querySelectorAll('li').forEach(li => { serviceSelector?.querySelectorAll('li').forEach(li => {
li.classList.remove('drag-target'); li.classList.remove('drag-target');
li.classList.remove('drag-target-self'); li.classList.remove('drag-target-self');
@ -401,7 +378,7 @@ function resetDrag() {
function reorderService(serviceId: number, targetId: number) { function reorderService(serviceId: number, targetId: number) {
console.log('Reordering service', serviceId, targetId); console.log('Reordering service', serviceId, targetId);
if (targetId >= 0) { if (targetId >= 0) {
oldActiveService = selectedService; oldActiveService = selectedServiceId;
setActiveService(-1); setActiveService(-1);
ipcRenderer.send('reorderService', serviceId, targetId); ipcRenderer.send('reorderService', serviceId, targetId);
} }
@ -423,7 +400,7 @@ document.addEventListener('DOMContentLoaded', () => {
refreshButton?.addEventListener('click', () => reload()); refreshButton?.addEventListener('click', () => reload());
addButton = document.getElementById('add-button'); addButton = document.getElementById('add-button');
addButton?.addEventListener('click', () => ipcRenderer.send('openServiceSettings', null)); addButton?.addEventListener('click', () => ipcRenderer.send('create-new-service', null));
settingsButton = document.getElementById('settings-button'); settingsButton = document.getElementById('settings-button');
settingsButton?.addEventListener('click', () => ipcRenderer.send('openSettings', null)); settingsButton?.addEventListener('click', () => ipcRenderer.send('openSettings', null));
@ -437,135 +414,91 @@ function setActiveService(serviceId: number) {
} }
// Hide previous service // Hide previous service
if (services[selectedService] && services[selectedService].view) { if (typeof selectedServiceId === 'number') {
services[selectedService].view.classList.remove('active'); const selectedService = services[selectedServiceId];
if (selectedService && selectedService.view) {
selectedService.view.classList.remove('active');
}
} }
// Show service // Show service
if (currentService) { currentService?.view?.classList.add('active');
currentService.view.classList.add('active');
}
// Save active service ID // Save active service ID
selectedService = serviceId; selectedServiceId = serviceId;
// Refresh navigation // Refresh navigation
updateNavigation(); updateNavigation();
}); });
} }
function loadService(serviceId: number, service: any) { function loadService(serviceId: number, service: FrontService) {
// Load service if not loaded yet // Load service if not loaded yet
if (!service.view && !service.viewReady) { if (!service.view && !service.viewReady) {
console.log('Loading service', serviceId); console.log('Loading service', serviceId);
document.querySelector('#services > .loader')?.classList.remove('hidden'); document.querySelector('#services > .loader')?.classList.remove('hidden');
service.view = document.createElement('webview'); const view = service.view = document.createElement('webview');
service.view.setAttribute('enableRemoteModule', 'false'); updateNavigation(); // Start loading animation
service.view.setAttribute('partition', 'persist:service_' + service.partition); view.setAttribute('enableRemoteModule', 'false');
service.view.setAttribute('autosize', 'true'); view.setAttribute('partition', 'persist:service_' + service.partition);
service.view.setAttribute('src', emptyPage); view.setAttribute('autosize', 'true');
view.setAttribute('allowpopups', 'true');
if (specialPages) view.setAttribute('src', specialPages.empty);
// Enable context isolation. This is currently not used as there is no preload script; however it could prevent // Error handling
// eventual future human mistakes. view.addEventListener('did-fail-load', (e: DidFailLoadEvent) => {
service.view.setAttribute('webpreferences', 'contextIsolation=yes'); if (e.errorCode <= -100 && e.errorCode > -200) {
if (specialPages) view.setAttribute('src', specialPages.connectionError);
} else if (e.errorCode === -6) {
if (specialPages) view.setAttribute('src', specialPages.fileNotFound);
} else if (e.errorCode !== -3) {
console.error('Unhandled error:', e);
}
});
// Append element to DOM // Append element to DOM
document.querySelector('#services')?.appendChild(service.view); document.querySelector('#services')?.appendChild(view);
// Load chain // Load chain
let listener: Function; let listener: () => void;
service.view.addEventListener('dom-ready', listener = () => { view.addEventListener('dom-ready', listener = () => {
service.view.removeEventListener('dom-ready', listener); view.removeEventListener('dom-ready', listener);
service.view.addEventListener('dom-ready', listener = () => { view.addEventListener('dom-ready', listener = () => {
if (service.customCSS) { if (service.customCSS) {
service.view.insertCSS(service.customCSS); view.insertCSS(service.customCSS)
.catch(console.error);
} }
document.querySelector('#services > .loader')?.classList.add('hidden'); document.querySelector('#services > .loader')?.classList.add('hidden');
service.li.classList.add('loaded'); service.li?.classList.add('loaded');
service.viewReady = true; service.viewReady = true;
updateNavigation(); updateNavigation();
if (selectedService === null) { if (selectedServiceId === null) {
setActiveService(serviceId); setActiveService(serviceId);
} }
}); });
const webContents = remote.webContents.fromId(service.view.getWebContentsId());
// Set custom user agent // Set custom user agent
if (typeof service.customUserAgent === 'string') { if (typeof service.customUserAgent === 'string') {
webContents.setUserAgent(service.customUserAgent); ipcRenderer.send('set-web-contents-user-agent', view.getWebContentsId(), service.customUserAgent);
} }
// Set context menu // Set context menu
setContextMenu(webContents); ipcRenderer.send('open-service-content-context-menu', view.getWebContentsId());
// Set permission request handler // Set permission request handler
function getUrlDomain(url: string) { ipcRenderer.send('set-partition-permissions', serviceId, view.partition);
let matches = url.match(/^https?:\/\/((.+?)\/|(.+))/i);
if (matches !== null) {
let domain = matches[1];
if (domain.endsWith('/')) domain = domain.substr(0, domain.length - 1);
return domain;
}
return ''; view.setAttribute('src', service.url);
}
function getDomainPermissions(domain: string) {
let domainPermissions = service.permissions[domain];
if (!domainPermissions) domainPermissions = service.permissions[domain] = [];
return domainPermissions;
}
let serviceSession = session.fromPartition(service.view.partition);
serviceSession.setPermissionRequestHandler(((webContents, permissionName, callback, details) => {
let domain = getUrlDomain(details.requestingUrl);
let domainPermissions = getDomainPermissions(domain);
let existingPermissions = domainPermissions.filter((p: any) => p.name === permissionName);
if (existingPermissions.length > 0) {
callback(existingPermissions[0].authorized);
return;
}
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'question',
title: 'Grant ' + permissionName + ' permission',
message: 'Do you wish to grant the ' + permissionName + ' permission to ' + domain + '?',
buttons: ['Deny', 'Authorize'],
cancelId: 0,
}).then(result => {
const authorized = result.response === 1;
domainPermissions.push({
name: permissionName,
authorized: authorized,
});
updateServicePermissions(serviceId);
console.log(authorized ? 'Granted' : 'Denied', permissionName, 'for domain', domain);
callback(authorized);
}).catch(console.error);
}));
serviceSession.setPermissionCheckHandler((webContents1, permissionName, requestingOrigin, details) => {
console.log('Permission check', permissionName, requestingOrigin, details);
let domain = getUrlDomain(details.requestingUrl);
let domainPermissions = getDomainPermissions(domain);
let existingPermissions = domainPermissions.filter((p: any) => p.name === permissionName);
return existingPermissions.length > 0 && existingPermissions[0].authorized;
});
service.view.setAttribute('src', service.url);
}); });
// Load favicon // Load favicon
service.view.addEventListener('page-favicon-updated', (event: PageFaviconUpdatedEvent) => { view.addEventListener('page-favicon-updated', (event: PageFaviconUpdatedEvent) => {
console.debug('Loaded favicons for', service.name, event.favicons); console.debug('Loaded favicons for', service.name, event.favicons);
if (event.favicons.length > 0 && service.favicon !== event.favicons[0]) { if (event.favicons.length > 0 && service.favicon !== event.favicons[0]) {
ipcRenderer.send('setServiceFavicon', serviceId, event.favicons[0]); ipcRenderer.send('setServiceFavicon', serviceId, event.favicons[0]);
@ -574,19 +507,21 @@ function loadService(serviceId: number, service: any) {
img.src = event.favicons[0]; img.src = event.favicons[0];
img.alt = service.name; img.alt = service.name;
img.onload = () => { img.onload = () => {
service.li.button.innerHTML = ''; if (service.li) {
service.li.button.appendChild(img); service.li.button.innerHTML = '';
service.li.button.appendChild(img);
}
}; };
} }
} }
}); });
// Display target urls // Display target urls
service.view.addEventListener('update-target-url', (event: UpdateTargetUrlEvent) => { view.addEventListener('update-target-url', (event: UpdateTargetUrlEvent) => {
if (event.url.length === 0) { if (event.url.length === 0) {
urlPreview?.classList.add('hidden'); urlPreview?.classList.add('invisible');
} else { } else {
urlPreview?.classList.remove('hidden'); urlPreview?.classList.remove('invisible');
if (urlPreview) { if (urlPreview) {
urlPreview.innerHTML = event.url; urlPreview.innerHTML = event.url;
} }
@ -597,21 +532,28 @@ function loadService(serviceId: number, service: any) {
function unloadService(serviceId: number) { function unloadService(serviceId: number) {
const service = services[serviceId]; const service = services[serviceId];
if (!service) throw new Error('Service doesn\'t exist.');
if (service.view && service.viewReady) { if (service.view && service.viewReady) {
service.view.remove(); service.view.remove();
service.view = null; service.view = undefined;
service.li.classList.remove('loaded'); service.li?.classList.remove('loaded');
service.viewReady = false; service.viewReady = false;
if (parseInt(selectedService) === serviceId) { if (selectedServiceId === serviceId) {
selectedService = null; selectedServiceId = null;
for (let i = 0; i < services.length; i++) { for (let i = 0; i < services.length; i++) {
if (services[i].view && services[i].viewReady) { const otherService = services[i];
if (otherService && otherService.view && otherService.viewReady) {
setActiveService(i); setActiveService(i);
break; break;
} }
} }
if (selectedService === null) {
// false positive:
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (selectedServiceId === null) {
updateNavigation(); updateNavigation();
} }
} }
@ -620,6 +562,8 @@ function unloadService(serviceId: number) {
function reloadService(serviceId: number) { function reloadService(serviceId: number) {
const service = services[serviceId]; const service = services[serviceId];
if (!service) throw new Error('Service doesn\'t exist.');
if (service.view && service.viewReady) { if (service.view && service.viewReady) {
console.log('Reloading service', serviceId); console.log('Reloading service', serviceId);
document.querySelector('#services > .loader')?.classList.remove('hidden'); document.querySelector('#services > .loader')?.classList.remove('hidden');
@ -629,30 +573,32 @@ function reloadService(serviceId: number) {
} }
} }
function updateServicePermissions(serviceId: number) {
const service = services[serviceId];
ipcRenderer.send('updateServicePermissions', serviceId, service.permissions);
}
function updateNavigation() { function updateNavigation() {
console.debug('Updating navigation'); console.debug('Updating navigation');
// Update active list element // Update active list element
for (let i = 0; i < services.length; i++) { for (let i = 0; i < services.length; i++) {
const service = services[i]; const service = services[i];
if (!service) continue;
if (!service.li) continue;
// Active? // Active?
if (parseInt(selectedService) === i) service.li.classList.add('active'); if (selectedServiceId === i) service.li.classList.add('active');
else service.li.classList.remove('active'); else service.li.classList.remove('active');
// Loading?
if (service.view && !service.viewReady) service.li.classList.add('loading');
else service.li.classList.remove('loading');
// Loaded? // Loaded?
if (service.viewReady) service.li.classList.add('loaded'); if (service.viewReady) service.li.classList.add('loaded');
else service.li.classList.remove('loaded'); else service.li.classList.remove('loaded');
} }
if (selectedService !== null && services[selectedService].viewReady) { if (selectedServiceId !== null && services[selectedServiceId]?.viewReady) {
console.debug('Updating navigation buttons because view is ready'); console.debug('Updating navigation buttons because view is ready');
// Update history navigation // Update history navigation
let view = services[selectedService].view; const view = services[selectedServiceId]?.view;
homeButton?.classList.remove('disabled'); homeButton?.classList.remove('disabled');
@ -671,164 +617,70 @@ function updateNavigation() {
} }
function updateStatusButton() { function updateStatusButton() {
let protocol = services[selectedService].view.getURL().split('://')[0]; if (typeof selectedServiceId !== 'number') return;
if (!protocol) protocol = 'unknown';
for (const c of <any>securityButton?.children) { const protocol = services[selectedServiceId]?.view?.getURL().split('://')[0] || 'unknown';
if (c.classList.contains(protocol)) c.classList.add('active'); securityButton?.childNodes.forEach(el => {
else c.classList.remove('active'); if (el instanceof HTMLElement) {
} if (el.classList.contains(protocol)) el.classList.add('active');
else el.classList.remove('active');
}
});
} }
function updateWindowTitle() { function updateWindowTitle() {
if (selectedService === null) { if (selectedServiceId === null) {
ipcRenderer.send('updateWindowTitle', null); ipcRenderer.send('update-window-title', null);
} else if (services[selectedService].viewReady) { } else {
ipcRenderer.send('updateWindowTitle', selectedService, remote.webContents.fromId(services[selectedService].view.getWebContentsId()).getTitle()); const service = services[selectedServiceId];
if (service?.viewReady && service.view) {
ipcRenderer.send('update-window-title', selectedServiceId, service.view.getWebContentsId());
}
} }
} }
function goHome() { function goHome() {
let service = services[selectedService]; if (selectedServiceId === null) return;
service.view.loadURL(service.url)
const service = services[selectedServiceId];
if (!service) throw new Error('Service doesn\'t exist.');
service.view?.loadURL(service.url)
.catch(console.error); .catch(console.error);
} }
function goForward() { function goForward() {
let view = services[selectedService].view; if (selectedServiceId === null) return;
if (view) remote.webContents.fromId(view.getWebContentsId()).goForward();
const view = services[selectedServiceId]?.view;
if (view) ipcRenderer.send('go-forward', view.getWebContentsId());
} }
function goBack() { function goBack() {
let view = services[selectedService].view; if (selectedServiceId === null) return;
if (view) remote.webContents.fromId(view.getWebContentsId()).goBack();
const view = services[selectedServiceId]?.view;
if (view) ipcRenderer.send('go-back', view.getWebContentsId());
} }
function reload() { function reload() {
reloadService(selectedService); if (selectedServiceId === null) return;
reloadService(selectedServiceId);
} }
function setContextMenu(webContents: WebContents) { ipcRenderer.on('fullscreenchange', (e, fullscreen: boolean) => {
webContents.on('context-menu', (event, props) => { if (fullscreen) document.body.classList.add('fullscreen');
const menu = new Menu(); else document.body.classList.remove('fullscreen');
const {editFlags} = props; });
// linkURL type FrontService = Service & {
if (props.linkURL.length > 0) { view?: Electron.WebviewTag;
if (menu.items.length > 0) { viewReady?: boolean;
menu.append(new MenuItem({type: 'separator'})); li?: NavigationElement;
} };
menu.append(new MenuItem({ type NavigationElement = HTMLLIElement & {
label: 'Copy link URL', serviceId: number;
click: () => { button: HTMLButtonElement;
clipboard.writeText(props.linkURL); };
},
}));
menu.append(new MenuItem({
label: 'Open URL in default browser',
click: () => {
if (props.linkURL.startsWith('https://')) {
shell.openExternal(props.linkURL)
.catch(console.error);
}
},
}));
}
// Image
if (props.hasImageContents) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Copy image',
click: () => {
webContents.copyImageAt(props.x, props.y);
},
}));
menu.append(new MenuItem({
label: 'Save image as',
click: () => {
webContents.downloadURL(props.srcURL);
},
}));
}
// Text clipboard
if (editFlags.canUndo || editFlags.canRedo || editFlags.canCut || editFlags.canCopy || editFlags.canPaste || editFlags.canDelete) {
if (editFlags.canUndo || editFlags.canRedo) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
if (editFlags.canUndo) {
menu.append(new MenuItem({
label: 'Undo',
role: 'undo',
}));
}
if (editFlags.canRedo) {
menu.append(new MenuItem({
label: 'Redo',
role: 'redo',
}));
}
}
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Cut',
role: 'cut',
enabled: editFlags.canCut,
}));
menu.append(new MenuItem({
label: 'Copy',
role: 'copy',
enabled: editFlags.canCopy,
}));
menu.append(new MenuItem({
label: 'Paste',
role: 'paste',
enabled: editFlags.canPaste,
}));
menu.append(new MenuItem({
label: 'Delete',
role: 'delete',
enabled: editFlags.canDelete,
}));
}
if (editFlags.canSelectAll) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Select all',
role: 'selectAll',
}));
}
// Inspect element
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Inspect element',
click: () => {
webContents.inspectElement(props.x, props.y);
},
}));
menu.popup({
window: remote.getCurrentWindow(),
});
});
}

View File

@ -1,4 +1,6 @@
import {ipcRenderer, remote} from "electron"; import {ipcRenderer} from "electron";
import Service from "../../src/Service";
import {IconProperties, IconSet} from "../../src/Meta";
let isImageCheckbox: HTMLInputElement | null; let isImageCheckbox: HTMLInputElement | null;
let builtInIconSearchField: HTMLInputElement | null; let builtInIconSearchField: HTMLInputElement | null;
@ -12,22 +14,22 @@ let autoLoadInput: HTMLInputElement | null;
let customCssInput: HTMLInputElement | null; let customCssInput: HTMLInputElement | null;
let customUserAgentInput: HTMLInputElement | null; let customUserAgentInput: HTMLInputElement | null;
let serviceId: number; let serviceId: number | null = null;
let service: any; let service: Service | null = null;
ipcRenderer.on('syncIcons', (event, iconSets) => { ipcRenderer.on('syncIcons', (event, iconSets: IconSet[]) => {
let icons: any[] = []; let icons: IconProperties[] = [];
for (const set of iconSets) { for (const set of iconSets) {
icons = icons.concat(set); icons = icons.concat(set);
} }
loadIcons(icons); loadIcons(icons);
}); });
ipcRenderer.on('loadService', (e, id, data) => { ipcRenderer.on('loadService', (e, id: number | null, data?: Service) => {
console.log('Load service', id); console.log('Load service', id);
if (id === null) { if (id === null || !data) {
document.title = 'Add a new service'; document.title = 'Add a new service';
service = {}; service = null;
if (h1) h1.innerText = 'Add a new service'; if (h1) h1.innerText = 'Add a new service';
} else { } else {
@ -54,41 +56,56 @@ document.addEventListener('DOMContentLoaded', () => {
isImageCheckbox?.addEventListener('click', () => { isImageCheckbox?.addEventListener('click', () => {
updateIconChoiceForm(isImageCheckbox?.checked); updateIconChoiceForm(!!isImageCheckbox?.checked);
}); });
updateIconChoiceForm(isImageCheckbox?.checked); updateIconChoiceForm(!!isImageCheckbox?.checked);
builtInIconSearchField?.addEventListener('input', updateIconSearchResults); builtInIconSearchField?.addEventListener('input', updateIconSearchResults);
document.getElementById('cancel-button')?.addEventListener('click', e => { document.getElementById('cancel-button')?.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
remote.getCurrentWindow().close(); ipcRenderer.send('close-window', 'ServiceSettingsWindow');
});
document.querySelector('form')?.addEventListener('submit', e => {
e.preventDefault();
save();
}); });
ipcRenderer.send('sync-settings'); ipcRenderer.send('sync-settings');
document.getElementById('userAgentAutoFill')?.addEventListener('click', () => { document.getElementById('userAgentAutoFillFirefox')?.addEventListener('click', () => {
let customUserAgent = document.getElementById('custom-user-agent'); const customUserAgent = document.querySelector<HTMLInputElement>('#custom-user-agent');
if (customUserAgent) { if (customUserAgent) {
(<HTMLInputElement>customUserAgent).value = 'Mozilla/5.0 (X11; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0'; customUserAgent.value = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0';
}
});
document.getElementById('userAgentAutoFillChrome')?.addEventListener('click', () => {
const customUserAgent = document.querySelector<HTMLInputElement>('#custom-user-agent');
if (customUserAgent) {
customUserAgent.value = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36';
} }
}); });
}); });
function updateIconSearchResults() { function updateIconSearchResults() {
const searchStr: string = builtInIconSearchField!.value; if (!builtInIconSearchField) throw new Error('builtInIconSearchField no initialized.');
(<any>iconSelect?.childNodes).forEach((c: HTMLElement) => {
let parts = c.dataset.icon?.split('/') || ''; const searchStr: string = builtInIconSearchField.value;
let iconName = parts[1] || parts[0]; iconSelect?.childNodes.forEach((el) => {
if (iconName.match(searchStr) || searchStr.match(iconName)) { if (el instanceof HTMLElement) {
c.classList.remove('hidden'); const parts = el.dataset.icon?.split('/') || '';
} else { const iconName = parts[1] || parts[0];
c.classList.add('hidden'); if (iconName.match(searchStr) || searchStr.match(iconName)) {
el.classList.remove('hidden');
} else {
el.classList.add('hidden');
}
} }
}); });
} }
function loadIcons(icons: any[]) { function loadIcons(icons: IconProperties[]) {
icons.sort((a, b) => a.name > b.name ? 1 : -1); icons.sort((a, b) => a.name > b.name ? 1 : -1);
for (const icon of icons) { for (const icon of icons) {
if (icon.name.length === 0) continue; if (icon.name.length === 0) continue;
@ -121,16 +138,16 @@ function loadIcons(icons: any[]) {
function selectIcon(choice: HTMLElement) { function selectIcon(choice: HTMLElement) {
if (builtInIconSearchField) builtInIconSearchField.value = choice.dataset.icon || ''; if (builtInIconSearchField) builtInIconSearchField.value = choice.dataset.icon || '';
if (iconSelect) { if (iconSelect) {
for (const otherChoice of <any>iconSelect.children) { iconSelect.childNodes.forEach(el => {
otherChoice.classList.remove('selected'); if (el instanceof Element) el.classList.remove('selected');
} });
} }
choice.classList.add('selected'); choice.classList.add('selected');
let radio: HTMLInputElement | null = choice.querySelector('input[type=radio]'); const radio: HTMLInputElement | null = choice.querySelector('input[type=radio]');
if (radio) radio.checked = true; if (radio) radio.checked = true;
} }
function updateIconChoiceForm(isUrl: any) { function updateIconChoiceForm(isUrl: boolean) {
if (isUrl) { if (isUrl) {
iconSelect?.classList.add('hidden'); iconSelect?.classList.add('hidden');
builtInIconSearchField?.parentElement?.classList.add('hidden'); builtInIconSearchField?.parentElement?.classList.add('hidden');
@ -148,21 +165,22 @@ function loadServiceValues() {
} }
if (nameInput) nameInput.value = service.name; if (nameInput) nameInput.value = service.name;
if (urlInput) urlInput.value = service.url; if (urlInput) urlInput.value = service.url || '';
if (useFaviconInput) useFaviconInput.checked = service.useFavicon; if (useFaviconInput) useFaviconInput.checked = service.useFavicon;
if (autoLoadInput) autoLoadInput.checked = service.autoLoad; if (autoLoadInput) autoLoadInput.checked = service.autoLoad;
if (customCssInput) customCssInput.value = service.customCSS; if (customCssInput) customCssInput.value = service.customCSS || '';
if (customUserAgentInput) customUserAgentInput.value = service.customUserAgent; if (customUserAgentInput) customUserAgentInput.value = service.customUserAgent || '';
isImageCheckbox.checked = service.isImage; isImageCheckbox.checked = service.isImage;
if (service.isImage) { if (service.isImage && service.icon) {
if (iconUrlField) iconUrlField.value = service.icon; if (iconUrlField) iconUrlField.value = service.icon;
} else { } else {
if (builtInIconSearchField) builtInIconSearchField.value = service.icon; if (builtInIconSearchField && service.icon) builtInIconSearchField.value = service.icon;
updateIconSearchResults(); updateIconSearchResults();
let labels = iconSelect?.querySelectorAll('label'); const labels = iconSelect?.querySelectorAll('label');
if (labels) { if (labels) {
const icon = Array.from(labels).find(i => i.dataset.icon === service.icon); const _service = service;
const icon = Array.from(labels).find(i => i.dataset.icon === _service.icon);
if (icon) { if (icon) {
selectIcon(icon); selectIcon(icon);
} }
@ -170,43 +188,59 @@ function loadServiceValues() {
} }
} }
(window as any).save = () => { function save() {
let form = document.querySelector('form'); if (!service) {
// Don't use new Service() to avoid depending on src/ file.
service = {
name: '',
partition: '',
url: '',
isImage: false,
useFavicon: false,
autoLoad: false,
permissions: {},
};
}
const form = document.querySelector('form');
if (!form) return; if (!form) return;
const formData = new FormData(form); const formData = new FormData(form);
service.name = formData.get('name'); service.name = String(formData.get('name'));
if (typeof service.partition !== 'string' || service.partition.length === 0) { if (service.partition.length === 0) {
service.partition = service.name.replace(/ /g, '-'); service.partition = service.name.replace(/ /g, '-');
service.partition = service.partition.replace(/[^a-zA-Z-_]/g, ''); service.partition = service.partition.replace(/[^a-zA-Z-_]/g, '');
} }
service.url = formData.get('url');
service.url = String(formData.get('url'));
service.isImage = formData.get('isImage') === 'on'; service.isImage = formData.get('isImage') === 'on';
service.icon = formData.get('icon'); service.icon = String(formData.get('icon'));
service.useFavicon = formData.get('useFavicon') === 'on'; service.useFavicon = formData.get('useFavicon') === 'on';
service.autoLoad = formData.get('autoLoad') === 'on'; service.autoLoad = formData.get('autoLoad') === 'on';
service.customCSS = formData.get('customCSS'); service.customCSS = String(formData.get('customCSS'));
let customUserAgent = (<string>formData.get('customUserAgent')).trim(); const customUserAgent = (<string>formData.get('customUserAgent')).trim();
service.customUserAgent = customUserAgent.length === 0 ? null : customUserAgent; service.customUserAgent = customUserAgent.length === 0 ? undefined : customUserAgent;
if (!isValid()) { if (!isValid()) {
return; return;
} }
ipcRenderer.send('saveService', serviceId, service); ipcRenderer.send('saveService', serviceId, service);
remote.getCurrentWindow().close(); ipcRenderer.send('close-window', 'ServiceSettingsWindow');
}; }
function isValid() { function isValid() {
if (typeof service.name !== 'string' || service.name.length === 0) { if (!service) return false;
if (service.name.length === 0) {
console.log('Invalid name'); console.log('Invalid name');
return false; return false;
} }
if (typeof service.partition !== 'string' || service.partition.length === 0) { if (service.partition.length === 0) {
console.log('Invalid partition'); console.log('Invalid partition');
return false; return false;
} }
if (typeof service.url !== 'string' || service.url.length === 0) { if (service.url.length === 0) {
console.log('Invalid url'); console.log('Invalid url');
return false; return false;
} }

View File

@ -1,10 +1,17 @@
import {ipcRenderer, remote, shell} from "electron"; import {ipcRenderer, shell} from "electron";
import Config from "../../src/Config";
import {SemVer} from "semver";
import {UpdateInfo} from "electron-updater";
let currentVersion: HTMLElement | null; let currentVersion: HTMLElement | null;
let updateStatus: HTMLElement | null; let updateStatus: HTMLElement | null;
let updateInfo: any; let updateInfo: UpdateInfo;
let updateButton: HTMLElement | null; let updateButton: HTMLElement | null;
let config: any; let config: Config;
let startMinimizedField: HTMLInputElement | null;
let bigNavBarField: HTMLInputElement | null;
let securityButtonField: HTMLInputElement | null, let securityButtonField: HTMLInputElement | null,
homeButtonField: HTMLInputElement | null, homeButtonField: HTMLInputElement | null,
@ -12,12 +19,16 @@ let securityButtonField: HTMLInputElement | null,
forwardButtonField: HTMLInputElement | null, forwardButtonField: HTMLInputElement | null,
refreshButtonField: HTMLInputElement | null; refreshButtonField: HTMLInputElement | null;
ipcRenderer.on('current-version', (e, version) => { ipcRenderer.on('current-version', (e, version: SemVer) => {
if (currentVersion) currentVersion.innerText = `Version: ${version.version}`; if (currentVersion) currentVersion.innerText = `Version: ${version.version}`;
}); });
ipcRenderer.on('config', (e, c) => { ipcRenderer.on('config', (e, c: Config) => {
config = c; config = c;
if (startMinimizedField) startMinimizedField.checked = config.startMinimized;
if (bigNavBarField) bigNavBarField.checked = config.bigNavBar;
if (securityButtonField) securityButtonField.checked = config.securityButton; if (securityButtonField) securityButtonField.checked = config.securityButton;
if (homeButtonField) homeButtonField.checked = config.homeButton; if (homeButtonField) homeButtonField.checked = config.homeButton;
if (backButtonField) backButtonField.checked = config.backButton; if (backButtonField) backButtonField.checked = config.backButton;
@ -25,7 +36,7 @@ ipcRenderer.on('config', (e, c) => {
if (refreshButtonField) refreshButtonField.checked = config.refreshButton; if (refreshButtonField) refreshButtonField.checked = config.refreshButton;
}); });
ipcRenderer.on('updateStatus', (e, available, version) => { ipcRenderer.on('updateStatus', (e, available: boolean, version: UpdateInfo) => {
console.log(available, version); console.log(available, version);
updateInfo = version; updateInfo = version;
if (available) { if (available) {
@ -36,11 +47,15 @@ ipcRenderer.on('updateStatus', (e, available, version) => {
} }
}); });
(window as any).save = () => { function save() {
let form = document.querySelector('form'); const form = document.querySelector('form');
if (!form) return; if (!form) return;
const formData = new FormData(form); const formData = new FormData(form);
config.startMinimized = formData.get('start-minimized') === 'on';
config.bigNavBar = formData.get('big-nav-bar') === 'on';
config.securityButton = formData.get('security-button') === 'on'; config.securityButton = formData.get('security-button') === 'on';
config.homeButton = formData.get('home-button') === 'on'; config.homeButton = formData.get('home-button') === 'on';
config.backButton = formData.get('back-button') === 'on'; config.backButton = formData.get('back-button') === 'on';
@ -48,18 +63,22 @@ ipcRenderer.on('updateStatus', (e, available, version) => {
config.refreshButton = formData.get('refresh-button') === 'on'; config.refreshButton = formData.get('refresh-button') === 'on';
ipcRenderer.send('save-config', config); ipcRenderer.send('save-config', config);
remote.getCurrentWindow().close(); ipcRenderer.send('close-window', 'SettingsWindow');
}; }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
currentVersion = document.getElementById('current-version'); currentVersion = document.getElementById('current-version');
updateStatus = document.getElementById('update-status'); updateStatus = document.getElementById('update-status');
updateButton = document.getElementById('download-button'); updateButton = document.getElementById('download-button');
updateButton?.addEventListener('click', () => { updateButton?.addEventListener('click', () => {
shell.openExternal(`https://github.com/ArisuOngaku/tabs/releases/download/v${updateInfo.version}/${updateInfo.path}`) shell.openExternal(`https://update.eternae.ink/ashpie/tabs/${updateInfo.path}`)
.catch(console.error); .catch(console.error);
}); });
startMinimizedField = <HTMLInputElement>document.getElementById('start-minimized');
bigNavBarField = <HTMLInputElement>document.getElementById('big-nav-bar');
securityButtonField = <HTMLInputElement>document.getElementById('security-button'); securityButtonField = <HTMLInputElement>document.getElementById('security-button');
homeButtonField = <HTMLInputElement>document.getElementById('home-button'); homeButtonField = <HTMLInputElement>document.getElementById('home-button');
backButtonField = <HTMLInputElement>document.getElementById('back-button'); backButtonField = <HTMLInputElement>document.getElementById('back-button');
@ -68,9 +87,14 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('cancel-button')?.addEventListener('click', e => { document.getElementById('cancel-button')?.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
remote.getCurrentWindow().close(); ipcRenderer.send('close-window', 'SettingsWindow');
});
document.querySelector('form')?.addEventListener('submit', e => {
e.preventDefault();
save();
}); });
ipcRenderer.send('syncSettings'); ipcRenderer.send('syncSettings');
ipcRenderer.send('checkForUpdates'); ipcRenderer.send('checkForUpdates');
}); });

View File

@ -1,22 +1,24 @@
{ {
"name": "tabs", "name": "tabs",
"version": "1.0.0", "version": "1.3.1",
"description": "Persistent and separate browser tabs in one window.", "description": "Persistent and separate browser tabs in one window.",
"author": { "author": {
"name": "Alice Gaudon", "name": "Alice Gaudon",
"email": "alice@gaudon.pro" "email": "alice@gaudon.pro"
}, },
"homepage": "https://gitlab.com/ArisuOngaku/tabs", "homepage": "https://eternae.ink/ashpie/tabs",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"main": "build/main.js", "main": "build/main.js",
"scripts": { "scripts": {
"clean": "(! test -d build || rm -r build) && (! test -d resources || rm -r resources)", "clean": "(! test -d build || rm -r build) && (! test -d resources || rm -r resources)",
"compile": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && webpack --mode production", "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"compile-dev": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && webpack --mode development", "compile": "yarn compile-common && webpack --mode production",
"compile-dev": "yarn compile-common && webpack --mode development",
"compile-common": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && rm -r resources/js/src && mv resources/js/frontend/ts/* resources/js/ && rm -r resources/js/frontend",
"start": "yarn compile && electron .", "start": "yarn compile && electron .",
"dev": "yarn compile-dev && concurrently -k -n \"Electron,Webpack,TSC-Frontend\" -p \"[{name}]\" -c \"green,yellow\" \"electron . --dev\" \"webpack --mode development --watch\" \"tsc -p tsconfig.frontend.json --watch\"", "dev": "yarn compile-dev && concurrently -k -n \"Electron,Webpack,TSC-Frontend\" -p \"[{name}]\" -c \"green,yellow\" \"electron . --dev\" \"webpack --mode development --watch\" \"tsc -p tsconfig.frontend.json --watch\"",
"build": "yarn compile && electron-builder -wl", "build": "yarn compile && electron-builder -wl",
"release": "yarn compile && GH_TOKEN=$(cat GH_TOKEN) electron-builder -wlp always" "release": "yarn lint && yarn compile && GH_TOKEN=$(cat GH_TOKEN) electron-builder -wlp always"
}, },
"dependencies": { "dependencies": {
"appdata-path": "^1.0.0", "appdata-path": "^1.0.0",
@ -28,28 +30,31 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.6", "@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6", "@babel/preset-env": "^7.9.6",
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^6.1.0",
"@types/node": "^12.12.41", "@types/node": "^14.6.2",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"concurrently": "^5.2.0", "concurrently": "^7.0.0",
"copy-webpack-plugin": "^6.0.1", "copy-webpack-plugin": "^10.2.4",
"css-loader": "^3.5.3", "css-loader": "^6.3.0",
"electron": "^9.0.0", "electron": "^17.1.2",
"electron-builder": "^22.4.0", "electron-builder": "^22.11.5",
"file-loader": "^6.0.0", "eslint": "^8.11.0",
"imagemin": "^7.0.1", "image-minimizer-webpack-plugin": "^3.2.3",
"imagemin": "^8.0.1",
"imagemin-gifsicle": "^7.0.0", "imagemin-gifsicle": "^7.0.0",
"imagemin-mozjpeg": "^8.0.0", "imagemin-mozjpeg": "^10.0.0",
"imagemin-pngquant": "^8.0.0", "imagemin-pngquant": "^9.0.2",
"imagemin-svgo": "^8.0.0", "imagemin-svgo": "^10.0.1",
"img-loader": "^3.0.1", "mini-css-extract-plugin": "^2.1.0",
"mini-css-extract-plugin": "^0.9.0", "sass": "^1.32.12",
"node-sass": "^4.14.1", "sass-loader": "^12.1.0",
"sass-loader": "^8.0.2", "svgo": "^2.3.1",
"ts-loader": "^7.0.4", "ts-loader": "^9.1.2",
"typescript": "^3.9.3", "typescript": "^4.0.2",
"webpack": "^4.43.0", "webpack": "^5.2.0",
"webpack-cli": "^3.3.11" "webpack-cli": "^4.1.0"
}, },
"build": { "build": {
"appId": "tabs-app", "appId": "tabs-app",
@ -57,48 +62,33 @@
"resources/**/*", "resources/**/*",
"build/**/*" "build/**/*"
], ],
"publish": [
{
"provider": "generic",
"url": "https://update.eternae.ink/ashpie/tabs"
}
],
"linux": { "linux": {
"target": "AppImage", "target": "AppImage",
"icon": "resources/logo.png", "icon": "frontend/images/logo.png",
"category": "Utility", "category": "Utility",
"executableName": "tabs", "executableName": "tabs",
"desktop": { "desktop": {
"StartupWMClass": "Tabs", "StartupWMClass": "Tabs",
"MimeType": "x-scheme-handler/tabs" "MimeType": "x-scheme-handler/tabs"
}, }
"publish": [
{
"provider": "github",
"owner": "ArisuOngaku",
"repo": "tabs"
}
]
}, },
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "resources/logo.png", "icon": "frontend/images/logo.png",
"publisherName": "Alice Gaudon", "publisherName": "Alice Gaudon",
"verifyUpdateCodeSignature": "true", "verifyUpdateCodeSignature": "false"
"publish": [
{
"provider": "github",
"owner": "ArisuOngaku",
"repo": "tabs"
}
]
}, },
"mac": { "mac": {
"target": "default", "target": "default",
"icon": "resources/logo.png", "icon": "frontend/images/logo.png",
"category": "public.app-category.utilities", "category": "public.app-category.utilities"
"publish": [
{
"provider": "github",
"owner": "ArisuOngaku",
"repo": "tabs"
}
]
}, },
"electronVersion": "9.0.0" "electronVersion": "15.0.0"
} }
} }

View File

@ -1,8 +1,9 @@
import {app, Menu, shell, Tray} from "electron"; import {app, dialog, Menu, shell, Tray} from "electron";
import Meta from "./Meta"; import Meta from "./Meta";
import Config from "./Config"; import Config from "./Config";
import Updater from "./Updater"; import Updater from "./Updater";
import MainWindow from "./windows/MainWindow"; import MainWindow from "./windows/MainWindow";
import * as os from "os";
export default class Application { export default class Application {
private readonly devMode: boolean; private readonly devMode: boolean;
@ -11,10 +12,10 @@ export default class Application {
private readonly mainWindow: MainWindow; private readonly mainWindow: MainWindow;
private tray?: Tray; private tray?: Tray;
constructor(devMode: boolean) { public constructor(devMode: boolean) {
this.devMode = devMode; this.devMode = devMode;
this.config = new Config(); this.config = new Config();
this.updater = new Updater(this.config); this.updater = new Updater(this.config, this);
this.mainWindow = new MainWindow(this); this.mainWindow = new MainWindow(this);
} }
@ -26,9 +27,11 @@ export default class Application {
this.setupElectronTweaks(); this.setupElectronTweaks();
// Check for updates // Check for updates
this.updater.checkAndPromptForUpdates(this.mainWindow.getWindow()).then(() => { if (os.platform() === 'win32') {
console.log('Update check successful.'); this.updater.checkAndPromptForUpdates(this.mainWindow.getWindow()).then(() => {
}).catch(console.error); console.log('Update check successful.');
}).catch(console.error);
}
console.log('App started'); console.log('App started');
} }
@ -49,27 +52,48 @@ export default class Application {
return this.devMode; return this.devMode;
} }
public async openExternalLink(url: string): Promise<void> {
if (url.startsWith('https://')) {
console.log('Opening link', url);
await shell.openExternal(url);
} else {
const {response} = await dialog.showMessageBox({
message: 'Are you sure you want to open this link?\n' + url,
type: 'question',
buttons: ['Cancel', 'Open link'],
});
if (response === 1) {
console.log('Opening link', url);
await shell.openExternal(url);
}
}
}
private setupElectronTweaks() { private setupElectronTweaks() {
// Open external links in default OS browser // Open external links in default OS browser
app.on('web-contents-created', (e, contents) => { app.on('web-contents-created', (e, contents) => {
if (contents.getType() === 'webview') { if (contents.getType() === 'webview') {
console.log('Setting external links to open in default OS browser'); console.log('Setting external links to open in default OS browser');
contents.on('new-window', (e, url) => { contents.setWindowOpenHandler(details => {
e.preventDefault(); if (details.url.startsWith(details.referrer.url)) return {action: 'allow'};
if (url.startsWith('https://')) {
shell.openExternal(url).catch(console.error); const url = details.url;
} this.openExternalLink(url)
.catch(console.error);
return {action: 'deny'};
}); });
} }
}); });
// Disable unused features // Disable unused features
app.on('web-contents-created', (e, contents) => { app.on('web-contents-created', (e, contents) => {
contents.on('will-attach-webview', (e, webPreferences, params) => { contents.on('will-attach-webview', (e, webPreferences) => {
delete webPreferences.preload; delete webPreferences.preload;
webPreferences.nodeIntegration = false; webPreferences.nodeIntegration = false;
// TODO: Here would be a good place to filter accessed urls (params.src). Also consider 'will-navigate' event on contents. // TODO: Here would be a good place to filter accessed urls (params.src).
// Also consider 'will-navigate' event on contents.
}); });
}); });
} }
@ -82,8 +106,8 @@ export default class Application {
{label: 'Tabs', enabled: false}, {label: 'Tabs', enabled: false},
{label: 'Open Tabs', click: () => this.mainWindow.getWindow().show()}, {label: 'Open Tabs', click: () => this.mainWindow.getWindow().show()},
{type: 'separator'}, {type: 'separator'},
{label: 'Quit', role: 'quit'} {label: 'Quit', role: 'quit'},
])); ]));
this.tray.on('click', () => this.mainWindow.toggle()); this.tray.on('click', () => this.mainWindow.toggle());
} }
} }

View File

@ -10,7 +10,13 @@ const configFile = path.resolve(configDir, 'config.json');
export default class Config { export default class Config {
public services: Service[] = []; public services: Service[] = [];
public updateCheckSkip?: string; public updateCheckSkip?: string;
public startMinimized: boolean = false;
public bigNavBar: boolean = false;
public securityButton: boolean = true; public securityButton: boolean = true;
public homeButton: boolean = false; public homeButton: boolean = false;
public backButton: boolean = true; public backButton: boolean = true;
@ -19,9 +25,11 @@ export default class Config {
private properties: string[] = []; private properties: string[] = [];
constructor() { [p: string]: unknown;
public constructor() {
// Load data from config file // Load data from config file
let data: any = {}; let data: Record<string, unknown> = {};
if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) { if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
if (fs.existsSync(configFile) && fs.statSync(configFile).isFile()) if (fs.existsSync(configFile) && fs.statSync(configFile).isFile())
data = JSON.parse(fs.readFileSync(configFile, 'utf8')); data = JSON.parse(fs.readFileSync(configFile, 'utf8'));
@ -30,7 +38,7 @@ export default class Config {
} }
// Parse services // Parse services
if (typeof data.services === 'object') { if (typeof data.services === 'object' && Array.isArray(data.services)) {
let i = 0; let i = 0;
for (const service of data.services) { for (const service of data.services) {
this.services[i] = new Service(service); this.services[i] = new Service(service);
@ -39,11 +47,22 @@ export default class Config {
} }
if (this.services.length === 0) { if (this.services.length === 0) {
this.services.push(new Service('welcome', 'Welcome', 'rocket', false, 'https://github.com/ArisuOngaku/tabs', false)); this.services.push(new Service(
'welcome',
'Welcome',
'rocket',
false,
'https://eternae.ink/ashpie/tabs',
false,
));
} }
this.defineProperty('updateCheckSkip', data); this.defineProperty('updateCheckSkip', data);
this.defineProperty('startMinimized', data);
this.defineProperty('bigNavBar', data);
this.defineProperty('securityButton', data); this.defineProperty('securityButton', data);
this.defineProperty('homeButton', data); this.defineProperty('homeButton', data);
this.defineProperty('backButton', data); this.defineProperty('backButton', data);
@ -53,26 +72,25 @@ export default class Config {
this.save(); this.save();
} }
save() { public save(): void {
console.log('Saving config'); console.log('Saving config');
this.services = this.services.filter(s => s !== null);
fs.writeFileSync(configFile, JSON.stringify(this, null, 4)); fs.writeFileSync(configFile, JSON.stringify(this, null, 4));
console.log('> Config saved to', configFile.toString()); console.log('> Config saved to', configFile.toString());
} }
defineProperty(name: string, data: any) { public defineProperty(name: string, data: Record<string, unknown>): void {
if (data[name] !== undefined) { if (data[name] !== undefined) {
(<any>this)[name] = data[name]; this[name] = data[name];
} }
this.properties.push(name); this.properties.push(name);
} }
update(data: any) { public update(data: Record<string, unknown>): void {
for (const prop of this.properties) { for (const prop of this.properties) {
if (data[prop] !== undefined) { if (data[prop] !== undefined) {
(<any>this)[prop] = data[prop]; this[prop] = data[prop];
} }
} }
} }
} }

View File

@ -13,7 +13,7 @@ export default class Meta {
public static readonly BRAND_ICONS = Meta.listIcons('brands'); public static readonly BRAND_ICONS = Meta.listIcons('brands');
public static readonly SOLID_ICONS = Meta.listIcons('solid'); public static readonly SOLID_ICONS = Meta.listIcons('solid');
public static readonly REGULAR_ICONS = Meta.listIcons('regular'); public static readonly REGULAR_ICONS = Meta.listIcons('regular');
public static readonly ICON_SETS = [ public static readonly ICON_SETS: IconSet[] = [
Meta.BRAND_ICONS, Meta.BRAND_ICONS,
Meta.SOLID_ICONS, Meta.SOLID_ICONS,
Meta.REGULAR_ICONS, Meta.REGULAR_ICONS,
@ -21,7 +21,7 @@ export default class Meta {
private static devMode?: boolean; private static devMode?: boolean;
public static isDevMode() { public static isDevMode(): boolean {
if (this.devMode === undefined) { if (this.devMode === undefined) {
this.devMode = process.argv.length > 2 && process.argv[2] === '--dev'; this.devMode = process.argv.length > 2 && process.argv[2] === '--dev';
console.debug('Dev mode:', this.devMode); console.debug('Dev mode:', this.devMode);
@ -30,7 +30,7 @@ export default class Meta {
return this.devMode; return this.devMode;
} }
public static getTitleForService(service: Service, viewTitle: string) { public static getTitleForService(service: Service, viewTitle: string): string {
let suffix = ''; let suffix = '';
if (viewTitle.length > 0) { if (viewTitle.length > 0) {
suffix = ' - ' + viewTitle; suffix = ' - ' + viewTitle;
@ -38,7 +38,7 @@ export default class Meta {
return this.title + ' - ' + service.name + suffix; return this.title + ' - ' + service.name + suffix;
} }
private static listIcons(set: string) { private static listIcons(set: string): IconSet {
console.log('Loading icon set', set); console.log('Loading icon set', set);
const directory = path.resolve(Meta.RESOURCES_PATH, `images/icons/${set}`); const directory = path.resolve(Meta.RESOURCES_PATH, `images/icons/${set}`);
const icons: { name: string; faIcon: string; set: string; }[] = []; const icons: { name: string; faIcon: string; set: string; }[] = [];
@ -51,3 +51,17 @@ export default class Meta {
return icons; return icons;
} }
} }
export type SpecialPages = {
empty: string;
connectionError: string;
fileNotFound: string;
};
export type IconProperties = {
name: string;
faIcon: string;
set: string;
};
export type IconSet = IconProperties[];

View File

@ -1,31 +1,77 @@
export default class Service { export default class Service {
public partition?: string; public name: string;
public name?: string; public partition: string;
public url: string;
public icon?: string; public icon?: string;
public isImage?: boolean = false; public isImage: boolean = false;
public url?: string;
public useFavicon?: boolean = true; public useFavicon: boolean = true;
public favicon?: string; public favicon?: string;
public autoLoad?: boolean = false;
public autoLoad: boolean = false;
public customCSS?: string; public customCSS?: string;
public customUserAgent?: string; public customUserAgent?: string;
public permissions?: {} = {}; public permissions: ServicePermissions = {};
constructor(partition: string, name?: string, icon?: string, isImage?: boolean, url?: string, useFavicon?: boolean) { [p: string]: unknown;
if (arguments.length === 1) {
const data = arguments[0]; public constructor(
for (const k in data) { partition: string | Pick<Service, keyof Service>,
if (data.hasOwnProperty(k)) { name?: string,
(<any>this)[k] = data[k]; icon?: string,
isImage?: boolean,
url?: string,
useFavicon?: boolean,
) {
const data = arguments.length === 1 ? partition as Record<string, unknown> : {
name,
icon,
isImage,
url,
useFavicon,
};
if (typeof data.name === 'string') this.name = data.name;
else throw new Error('A service must have a name');
if (typeof data.partition === 'string') this.partition = data.partition;
else this.partition = this.name;
if (typeof data.url === 'string') this.url = data.url;
else throw new Error('A service must have a url.');
if (typeof data.icon === 'string') this.icon = data.icon;
if (typeof data.isImage === 'boolean') this.isImage = data.isImage;
if (typeof data.useFavicon === 'boolean') this.useFavicon = data.useFavicon;
if (typeof data.favicon === 'string') this.favicon = data.favicon;
if (typeof data.autoLoad === 'boolean') this.autoLoad = data.autoLoad;
if (typeof data.customCSS === 'string') this.customCSS = data.customCSS;
if (typeof data.customUserAgent === 'string') this.customUserAgent = data.customUserAgent;
if (typeof data.permissions === 'object' && data.permissions !== null) {
for (const domain of Object.keys(data.permissions)) {
this.permissions[domain] = [];
const permissions = (data.permissions as Record<string, unknown>)[domain];
if (Array.isArray(permissions)) {
for (const permission of permissions) {
if (typeof permission.name === 'string' &&
typeof permission.authorized === 'boolean') {
this.permissions[domain]?.push(permission);
}
}
} }
} }
} else {
this.partition = partition;
this.name = name;
this.icon = icon;
this.isImage = isImage;
this.url = url;
this.useFavicon = useFavicon;
} }
} }
} }
export type ServicePermission = {
name: string;
authorized: boolean;
};
export type ServicePermissions = Record<string, ServicePermission[] | undefined>;

View File

@ -1,14 +1,18 @@
import {autoUpdater, UpdateInfo} from "electron-updater"; import {autoUpdater, UpdateInfo} from "electron-updater";
import {dialog, shell} from "electron"; import {dialog} from "electron";
import Config from "./Config"; import Config from "./Config";
import Application from "./Application";
import {SemVer} from "semver";
import BrowserWindow = Electron.BrowserWindow; import BrowserWindow = Electron.BrowserWindow;
export default class Updater { export default class Updater {
private readonly config: Config; private readonly config: Config;
private readonly application: Application;
private updateInfo?: UpdateInfo; private updateInfo?: UpdateInfo;
public constructor(config: Config) { public constructor(config: Config, application: Application) {
this.config = config; this.config = config;
this.application = application;
// Configure auto updater // Configure auto updater
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
@ -33,7 +37,7 @@ export default class Updater {
} }
} }
public getCurrentVersion() { public getCurrentVersion(): SemVer {
return autoUpdater.currentVersion; return autoUpdater.currentVersion;
} }
@ -42,16 +46,16 @@ export default class Updater {
if (updateInfo && updateInfo.version !== this.config.updateCheckSkip) { if (updateInfo && updateInfo.version !== this.config.updateCheckSkip) {
const input = await dialog.showMessageBox(mainWindow, { const input = await dialog.showMessageBox(mainWindow, {
message: `Version ${updateInfo.version} of tabs is available. Do you wish to download this update?`, message: `Version ${updateInfo.version} of tabs is available. Do you wish to install this update?`,
buttons: [ buttons: [
'Cancel', 'Cancel',
'Download', 'Install',
], ],
checkboxChecked: false, checkboxChecked: false,
checkboxLabel: `Don't remind me for this version`, checkboxLabel: `Don't remind me for this version`,
cancelId: 0, cancelId: 0,
defaultId: 1, defaultId: 1,
type: 'question' type: 'question',
}); });
if (input.checkboxChecked) { if (input.checkboxChecked) {
@ -61,7 +65,9 @@ export default class Updater {
} }
if (input.response === 1) { if (input.response === 1) {
await shell.openExternal(`https://github.com/ArisuOngaku/tabs/releases/download/v${updateInfo.version}/${updateInfo.path}`); await this.application.stop();
await autoUpdater.downloadUpdate();
autoUpdater.quitAndInstall();
} }
} }
} }

View File

@ -3,7 +3,9 @@ import Application from "./Application";
import Config from "./Config"; import Config from "./Config";
export default abstract class Window { export default abstract class Window {
private readonly listeners: { [channel: string]: ((event: IpcMainEvent, ...args: any[]) => void)[] } = {}; private readonly listeners: {
[channel: string]: ((event: IpcMainEvent, ...args: unknown[]) => void)[] | undefined
} = {};
private readonly onCloseListeners: (() => void)[] = []; private readonly onCloseListeners: (() => void)[] = [];
protected readonly application: Application; protected readonly application: Application;
@ -18,7 +20,7 @@ export default abstract class Window {
} }
public setup(options: BrowserWindowConstructorOptions) { public setup(options: BrowserWindowConstructorOptions): void {
console.log('Creating window', this.constructor.name); console.log('Creating window', this.constructor.name);
if (this.parent) { if (this.parent) {
@ -30,9 +32,22 @@ export default abstract class Window {
this.teardown(); this.teardown();
this.window = undefined; this.window = undefined;
}); });
this.onIpc('close-window', (
event,
constructorName: string,
) => {
if (constructorName === this.constructor.name) {
console.log('Closing', this.constructor.name);
const window = this.getWindow();
if (window.closable) {
window.close();
}
}
});
} }
public teardown() { public teardown(): void {
console.log('Tearing down window', this.constructor.name); console.log('Tearing down window', this.constructor.name);
for (const listener of this.onCloseListeners) { for (const listener of this.onCloseListeners) {
@ -40,7 +55,10 @@ export default abstract class Window {
} }
for (const channel in this.listeners) { for (const channel in this.listeners) {
for (const listener of this.listeners[channel]) { const listeners = this.listeners[channel];
if (!listeners) continue;
for (const listener of listeners) {
ipcMain.removeListener(channel, listener); ipcMain.removeListener(channel, listener);
} }
} }
@ -48,18 +66,20 @@ export default abstract class Window {
this.window = undefined; this.window = undefined;
} }
// This is the spec of ipcMain.on()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected onIpc(channel: string, listener: (event: IpcMainEvent, ...args: any[]) => void): this { protected onIpc(channel: string, listener: (event: IpcMainEvent, ...args: any[]) => void): this {
ipcMain.on(channel, listener); ipcMain.on(channel, listener);
if (!this.listeners[channel]) this.listeners[channel] = []; if (!this.listeners[channel]) this.listeners[channel] = [];
this.listeners[channel].push(listener); this.listeners[channel]?.push(listener);
return this; return this;
} }
public onClose(listener: () => void) { public onClose(listener: () => void): void {
this.onCloseListeners.push(listener); this.onCloseListeners.push(listener);
} }
public toggle() { public toggle(): void {
if (this.window) { if (this.window) {
if (!this.window.isFocused()) { if (!this.window.isFocused()) {
console.log('Showing window', this.constructor.name); console.log('Showing window', this.constructor.name);
@ -73,6 +93,7 @@ export default abstract class Window {
public getWindow(): BrowserWindow { public getWindow(): BrowserWindow {
if (!this.window) throw Error('Window not initialized.'); if (!this.window) throw Error('Window not initialized.');
else if (this.window.isDestroyed()) throw Error('Window destroyed.');
return this.window; return this.window;
} }
} }

View File

@ -4,6 +4,9 @@ import {app} from "electron";
import Meta from "./Meta"; import Meta from "./Meta";
import Application from "./Application"; import Application from "./Application";
// Fix for twitter (and others) in webviews - pending https://github.com/electron/electron/issues/25469
app.commandLine.appendSwitch('disable-features', 'CrossOriginOpenerPolicy');
const application = new Application(Meta.isDevMode()); const application = new Application(Meta.isDevMode());
// Check if application is already running // Check if application is already running

View File

@ -1,7 +1,7 @@
declare module "single-instance" { declare module "single-instance" {
export default class SingleInstance { export default class SingleInstance {
constructor(lockName: string); public constructor(lockName: string);
public lock(): Promise<void>; public lock(): Promise<void>;
} }
} }

View File

@ -1,39 +1,42 @@
import path from "path"; import path from "path";
import {ipcMain} from "electron"; import {clipboard, ContextMenuParams, dialog, ipcMain, Menu, MenuItem, session, webContents} from "electron";
import ServiceSettingsWindow from "./ServiceSettingsWindow"; import ServiceSettingsWindow from "./ServiceSettingsWindow";
import SettingsWindow from "./SettingsWindow"; import SettingsWindow from "./SettingsWindow";
import Application from "../Application"; import Application from "../Application";
import Meta from "../Meta"; import Meta, {SpecialPages} from "../Meta";
import Window from "../Window"; import Window from "../Window";
export default class MainWindow extends Window { export default class MainWindow extends Window {
private activeService: number = 0; private activeServiceId: number = 0;
private serviceSettingsWindow?: ServiceSettingsWindow; private serviceSettingsWindow?: ServiceSettingsWindow;
private settingsWindow?: SettingsWindow; private settingsWindow?: SettingsWindow;
constructor(application: Application) { public constructor(application: Application) {
super(application); super(application);
} }
public setup() { public setup(): void {
super.setup({ super.setup({
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
enableRemoteModule: true,
webviewTag: true, webviewTag: true,
contextIsolation: false,
}, },
autoHideMenuBar: true, autoHideMenuBar: true,
icon: Meta.ICON_PATH, icon: Meta.ICON_PATH,
title: Meta.title, title: Meta.title,
show: !this.application.getConfig().startMinimized,
}); });
const window = this.getWindow(); const window = this.getWindow();
window.maximize(); if (!this.application.getConfig().startMinimized) {
window.maximize();
}
if (this.application.isDevMode()) { if (this.application.isDevMode()) {
window.webContents.openDevTools({ window.webContents.openDevTools({
mode: 'right' mode: 'right',
}); });
} }
@ -43,19 +46,19 @@ export default class MainWindow extends Window {
}); });
// Load active service // Load active service
this.onIpc('setActiveService', (event, index) => { this.onIpc('setActiveService', (event, index: number) => {
this.setActiveService(index); this.setActiveService(index);
}); });
// Set a service's favicon // Set a service's favicon
this.onIpc('setServiceFavicon', (event, index, favicon) => { this.onIpc('setServiceFavicon', (event, index: number, favicon?: string) => {
console.log('Setting service', index, 'favicon', favicon); console.log('Setting service', index, 'favicon', favicon);
this.config.services[index].favicon = favicon; this.config.services[index].favicon = favicon;
this.config.save(); this.config.save();
}); });
// Reorder services // Reorder services
this.onIpc('reorderService', (event, serviceId, targetId) => { this.onIpc('reorderService', (event, serviceId: number, targetId: number) => {
console.log('Reordering services', serviceId, targetId); console.log('Reordering services', serviceId, targetId);
const oldServices = this.config.services; const oldServices = this.config.services;
@ -77,40 +80,14 @@ export default class MainWindow extends Window {
this.config.save(); this.config.save();
}); });
// Delete service
this.onIpc('deleteService', (e, id) => {
console.log('Deleting service', id);
delete this.config.services[id];
this.config.save();
window.webContents.send('deleteService', id);
});
// Update service permissions
ipcMain.on('updateServicePermissions', (e, serviceId, permissions) => {
this.config.services[serviceId].permissions = permissions;
this.config.save();
});
// Update window title // Update window title
ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => { ipcMain.on('update-window-title', (event, serviceId: number | null, webContentsId?: number) => {
if (serviceId === null) { if (serviceId === null) {
window.setTitle(Meta.title); window.setTitle(Meta.title);
} else { } else if (webContentsId) {
const service = this.config.services[serviceId]; const service = this.config.services[serviceId];
window.setTitle(Meta.getTitleForService(service, viewTitle)); const serviceWebContents = webContents.fromId(webContentsId);
} window.setTitle(Meta.getTitleForService(service, serviceWebContents.getTitle()));
});
// Open service settings window
ipcMain.on('openServiceSettings', (e, serviceId) => {
if (!this.serviceSettingsWindow) {
console.log('Opening service settings', serviceId);
this.serviceSettingsWindow = new ServiceSettingsWindow(this.application, this, serviceId);
this.serviceSettingsWindow.setup();
this.serviceSettingsWindow.onClose(() => {
this.serviceSettingsWindow = undefined;
});
} }
}); });
@ -126,23 +103,409 @@ export default class MainWindow extends Window {
} }
}); });
// Context menus
ipcMain.on('open-service-navigation-context-menu', (
event,
serviceId: number,
ready: boolean,
notReady: boolean,
canResetZoom: boolean,
) => {
this.openServiceNavigationContextMenu(serviceId, ready, notReady, canResetZoom);
});
ipcMain.on('open-service-content-context-menu', (
event,
webContentsId: number,
) => {
this.openServiceContentContextMenu(webContentsId);
});
// User agent
ipcMain.on('set-web-contents-user-agent', (event, webContentsId: number, userAgent: string) => {
webContents.fromId(webContentsId).setUserAgent(userAgent);
});
// Permission management
ipcMain.on('set-partition-permissions', (
event,
serviceId: number,
partition: string,
) => {
this.setPartitionPermissions(serviceId, partition);
});
// Navigation
ipcMain.on('go-forward', (event, webContentsId: number) => {
webContents.fromId(webContentsId).goForward();
});
ipcMain.on('go-back', (event, webContentsId: number) => {
webContents.fromId(webContentsId).goBack();
});
// Create new service
ipcMain.on('create-new-service', () => {
this.openServiceSettings(null);
});
window.on('enter-full-screen', () => {
window.webContents.send('fullscreenchange', true);
});
window.on('leave-full-screen', () => {
window.webContents.send('fullscreenchange', false);
});
// Load navigation view // Load navigation view
window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'index.html')) window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'index.html'))
.catch(console.error); .catch(console.error);
} }
public syncData() { public syncData(): void {
this.getWindow().webContents.send('data', this.getWindow().webContents.send('data',
Meta.title, Meta.title,
Meta.ICON_SETS, Meta.ICON_SETS,
this.activeService, this.activeServiceId,
path.resolve(Meta.RESOURCES_PATH, 'empty.html'), <SpecialPages>{
this.config empty: path.resolve(Meta.RESOURCES_PATH, 'empty.html'),
connectionError: path.resolve(Meta.RESOURCES_PATH, 'connection_error.html'),
fileNotFound: path.resolve(Meta.RESOURCES_PATH, 'file_not_found_error.html'),
},
this.config,
); );
} }
private setActiveService(index: number) { private setActiveService(index: number) {
console.log('Set active service', index); console.log('Set active service', index);
this.activeService = index; this.activeServiceId = index;
} }
}
private openServiceSettings(serviceId: number | null): void {
console.log('o', serviceId, !!this.serviceSettingsWindow);
if (!this.serviceSettingsWindow) {
console.log('Opening service settings', serviceId);
this.serviceSettingsWindow = new ServiceSettingsWindow(this.application, this, serviceId);
this.serviceSettingsWindow.setup();
this.serviceSettingsWindow.onClose(() => {
this.serviceSettingsWindow = undefined;
});
}
}
private deleteService(serviceId: number): void {
console.log('Deleting service', serviceId);
this.config.services.splice(serviceId, 1);
this.config.save();
this.getWindow().webContents.send('deleteService', serviceId);
}
private openServiceNavigationContextMenu(
serviceId: number,
ready: boolean,
notReady: boolean,
canResetZoom: boolean,
): void {
const ipc = this.getWindow().webContents;
const permissions = this.config.services[serviceId].permissions;
const menu = new Menu();
menu.append(new MenuItem({
label: 'Home', click: () => {
ipc.send('load-service-home', serviceId);
},
enabled: ready,
}));
menu.append(new MenuItem({
label: ready ? 'Reload' : 'Load', click: () => {
ipc.send('reload-service', serviceId);
},
enabled: ready || notReady,
}));
menu.append(new MenuItem({
label: 'Close', click: () => {
ipc.send('unload-service', serviceId);
},
enabled: ready,
}));
menu.append(new MenuItem({type: "separator"}));
menu.append(new MenuItem({
label: 'Reset zoom level', click: () => {
ipc.send('reset-service-zoom-level', serviceId);
},
enabled: ready && canResetZoom,
}));
menu.append(new MenuItem({
label: 'Zoom in', click: () => {
ipc.send('zoom-in-service', serviceId);
},
enabled: ready,
}));
menu.append(new MenuItem({
label: 'Zoom out', click: () => {
ipc.send('zoom-out-service', serviceId);
},
enabled: ready,
}));
menu.append(new MenuItem({type: "separator"}));
const permissionsMenu = [];
if (ready) {
for (const domain of Object.keys(permissions)) {
const domainPermissionsMenu = [];
const domainPermissions = permissions[domain];
if (domainPermissions) {
for (const permission of domainPermissions) {
domainPermissionsMenu.push({
label: (permission.authorized ? '✓' : '❌') + ' ' + permission.name,
submenu: [{
label: 'Toggle',
click: () => {
permission.authorized = !permission.authorized;
this.config.save();
},
}, {
label: 'Forget',
click: () => {
permissions[domain] = domainPermissions.filter(p => p !== permission);
},
}],
});
}
}
if (domainPermissionsMenu.length > 0) {
permissionsMenu.push({
label: domain,
submenu: domainPermissionsMenu,
});
}
}
}
menu.append(new MenuItem({
label: 'Permissions',
enabled: ready,
submenu: permissionsMenu,
}));
menu.append(new MenuItem({type: "separator"}));
menu.append(new MenuItem({
label: 'Edit', click: () => {
this.openServiceSettings(serviceId);
},
}));
menu.append(new MenuItem({
label: 'Delete', click: () => {
dialog.showMessageBox(this.getWindow(), {
type: 'question',
title: 'Confirm',
message: 'Are you sure you want to delete this service?',
buttons: ['Cancel', 'Confirm'],
cancelId: 0,
}).then(result => {
if (result.response === 1) {
this.deleteService(serviceId);
}
}).catch(console.error);
},
}));
menu.popup({window: this.getWindow()});
}
private openServiceContentContextMenu(
webContentsId: number,
): void {
const serviceWebContents = webContents.fromId(webContentsId);
serviceWebContents.on('context-menu', (event, props: ContextMenuParams) => {
const menu = new Menu();
const {editFlags} = props;
// linkURL
if (props.linkURL.length > 0) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Copy link URL',
click: () => {
clipboard.writeText(props.linkURL);
},
}));
menu.append(new MenuItem({
label: 'Open URL in default browser',
click: () => {
this.application.openExternalLink(props.linkURL)
.catch(console.error);
},
}));
}
// Image
if (props.hasImageContents) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Copy image',
click: () => {
serviceWebContents.copyImageAt(props.x, props.y);
},
}));
menu.append(new MenuItem({
label: 'Save image as',
click: () => {
serviceWebContents.downloadURL(props.srcURL);
},
}));
}
// Text clipboard
if (editFlags.canUndo || editFlags.canRedo || editFlags.canCut || editFlags.canCopy || editFlags.canPaste ||
editFlags.canDelete) {
if (editFlags.canUndo || editFlags.canRedo) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
if (editFlags.canUndo) {
menu.append(new MenuItem({
label: 'Undo',
role: 'undo',
}));
}
if (editFlags.canRedo) {
menu.append(new MenuItem({
label: 'Redo',
role: 'redo',
}));
}
}
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Cut',
role: 'cut',
enabled: editFlags.canCut,
}));
menu.append(new MenuItem({
label: 'Copy',
role: 'copy',
enabled: editFlags.canCopy,
}));
menu.append(new MenuItem({
label: 'Paste',
role: 'paste',
enabled: editFlags.canPaste,
}));
menu.append(new MenuItem({
label: 'Delete',
role: 'delete',
enabled: editFlags.canDelete,
}));
}
if (editFlags.canSelectAll) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Select all',
role: 'selectAll',
}));
}
// Inspect element
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
menu.append(new MenuItem({
label: 'Inspect element',
click: () => {
serviceWebContents.inspectElement(props.x, props.y);
},
}));
menu.popup({
window: this.getWindow(),
});
});
}
private setPartitionPermissions(
serviceId: number,
partition: string,
): void {
const service = this.config.services[serviceId];
function getUrlDomain(url: string | undefined) {
if (!url) return '';
const matches = url.match(/^https?:\/\/((.+?)\/|(.+))/i);
if (matches !== null) {
let domain = matches[1];
if (domain.endsWith('/')) domain = domain.substr(0, domain.length - 1);
return domain;
}
return '';
}
function getDomainPermissions(domain: string) {
let domainPermissions = service.permissions[domain];
if (!domainPermissions) domainPermissions = service.permissions[domain] = [];
return domainPermissions;
}
const serviceSession = session.fromPartition(partition);
serviceSession.setPermissionRequestHandler((webContents, permissionName, callback, details) => {
const domain = getUrlDomain(details.requestingUrl);
const domainPermissions = getDomainPermissions(domain);
const existingPermissions = domainPermissions.filter(p => p.name === permissionName);
if (existingPermissions.length > 0) {
callback(existingPermissions[0].authorized);
return;
}
dialog.showMessageBox(this.getWindow(), {
type: 'question',
title: 'Grant ' + permissionName + ' permission',
message: 'Do you wish to grant the ' + permissionName + ' permission to ' + domain + '?',
buttons: ['Deny', 'Authorize'],
cancelId: 0,
}).then(result => {
const authorized = result.response === 1;
domainPermissions.push({
name: permissionName,
authorized: authorized,
});
this.config.save();
console.log(authorized ? 'Granted' : 'Denied', permissionName, 'for domain', domain);
callback(authorized);
}).catch(console.error);
});
serviceSession.setPermissionCheckHandler((webContents1, permissionName, requestingOrigin, details) => {
console.log('Permission check', permissionName, requestingOrigin, details);
const domain = getUrlDomain(details.requestingUrl);
const domainPermissions = getDomainPermissions(domain);
const existingPermissions = domainPermissions.filter(p => p.name === permissionName);
return existingPermissions.length > 0 && existingPermissions[0].authorized;
});
}
}

View File

@ -5,19 +5,19 @@ import Meta from "../Meta";
import Service from "../Service"; import Service from "../Service";
export default class ServiceSettingsWindow extends Window { export default class ServiceSettingsWindow extends Window {
private readonly serviceId: number; private readonly serviceId: number | null;
constructor(application: Application, parent: Window, serviceId: number) { public constructor(application: Application, parent: Window, serviceId: number | null) {
super(application, parent); super(application, parent);
this.serviceId = serviceId; this.serviceId = serviceId;
} }
public setup() { public setup(): void {
super.setup({ super.setup({
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
enableRemoteModule: true,
webviewTag: true, webviewTag: true,
contextIsolation: false,
}, },
modal: true, modal: true,
autoHideMenuBar: true, autoHideMenuBar: true,
@ -28,16 +28,20 @@ export default class ServiceSettingsWindow extends Window {
if (this.application.isDevMode()) { if (this.application.isDevMode()) {
window.webContents.openDevTools({ window.webContents.openDevTools({
mode: 'right' mode: 'right',
}); });
} }
this.onIpc('sync-settings', () => { this.onIpc('sync-settings', () => {
window.webContents.send('syncIcons', Meta.ICON_SETS); window.webContents.send('syncIcons', Meta.ICON_SETS);
window.webContents.send('loadService', this.serviceId, this.config.services[this.serviceId]); window.webContents.send('loadService',
this.serviceId, typeof this.serviceId === 'number' ?
this.config.services[this.serviceId] :
undefined,
);
}); });
this.onIpc('saveService', (e, id, data) => { this.onIpc('saveService', (e, id: number | null, data: Service) => {
console.log('Saving service', id, data); console.log('Saving service', id, data);
const newService = new Service(data); const newService = new Service(data);
if (typeof id === 'number') { if (typeof id === 'number') {
@ -45,6 +49,8 @@ export default class ServiceSettingsWindow extends Window {
} else { } else {
this.config.services.push(newService); this.config.services.push(newService);
id = this.config.services.indexOf(newService); id = this.config.services.indexOf(newService);
if (id < 0) id = null;
} }
this.config.save(); this.config.save();
@ -55,4 +61,4 @@ export default class ServiceSettingsWindow extends Window {
.catch(console.error); .catch(console.error);
} }
} }

View File

@ -3,14 +3,15 @@ import path from "path";
import Window from "../Window"; import Window from "../Window";
import MainWindow from "./MainWindow"; import MainWindow from "./MainWindow";
import Meta from "../Meta"; import Meta from "../Meta";
import Config from "../Config";
export default class SettingsWindow extends Window { export default class SettingsWindow extends Window {
public setup() { public setup(): void {
super.setup({ super.setup({
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
enableRemoteModule: true,
webviewTag: true, webviewTag: true,
contextIsolation: false,
}, },
modal: true, modal: true,
autoHideMenuBar: true, autoHideMenuBar: true,
@ -21,7 +22,7 @@ export default class SettingsWindow extends Window {
if (this.application.isDevMode()) { if (this.application.isDevMode()) {
window.webContents.openDevTools({ window.webContents.openDevTools({
mode: 'right' mode: 'right',
}); });
} }
@ -36,7 +37,7 @@ export default class SettingsWindow extends Window {
}).catch(console.error); }).catch(console.error);
}); });
this.onIpc('save-config', (e: Event, data: any) => { this.onIpc('save-config', (e: Event, data: Config) => {
this.config.update(data); this.config.update(data);
this.config.save(); this.config.save();
if (this.parent instanceof MainWindow) { if (this.parent instanceof MainWindow) {
@ -47,4 +48,4 @@ export default class SettingsWindow extends Window {
window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'settings.html')) window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'settings.html'))
.catch(console.error); .catch(console.error);
} }
} }

View File

@ -1,5 +1,7 @@
const path = require('path'); const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const {extendDefaultPlugins} = require("svgo");
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const dev = process.env.NODE_ENV === 'development'; const dev = process.env.NODE_ENV === 'development';
@ -37,9 +39,6 @@ const config = {
use: [ use: [
{ {
loader: MiniCssExtractPlugin.loader, loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/',
}
}, },
'css-loader', 'css-loader',
'sass-loader', 'sass-loader',
@ -47,25 +46,17 @@ const config = {
}, },
{ {
test: /\.(woff2?|eot|ttf|otf)$/i, test: /\.(woff2?|eot|ttf|otf)$/i,
use: 'file-loader?name=../fonts/[name].[ext]', type: 'asset/resource',
generator: {
filename: '../fonts/[name][ext]',
},
}, },
{ {
test: /\.(png|jpe?g|gif|svg)$/i, test: /\.(png|jpe?g|gif|svg)$/i,
use: [ type: 'asset/resource',
'file-loader?name=../images/[name].[ext]', generator: {
{ filename: '../images/[name][ext]',
loader: 'img-loader', },
options: {
enabled: !dev,
plugins: [
require('imagemin-gifsicle')({}),
require('imagemin-mozjpeg')({}),
require('imagemin-pngquant')({}),
require('imagemin-svgo')({}),
]
}
}
]
}, },
{ {
test: /\.ts$/i, test: /\.ts$/i,
@ -75,13 +66,14 @@ const config = {
configFile: 'tsconfig.frontend.json', configFile: 'tsconfig.frontend.json',
} }
}, },
exclude: '/node_modules/' exclude: '/node_modules/',
}, },
{ {
test: /\.html$/i, test: /\.html$/i,
use: [ type: 'asset/resource',
'file-loader?name=../[name].[ext]', generator: {
] filename: '../[name][ext]',
},
} }
], ],
}, },
@ -94,7 +86,57 @@ const config = {
{from: 'node_modules/@fortawesome/fontawesome-free/svgs', to: '../images/icons'} {from: 'node_modules/@fortawesome/fontawesome-free/svgs', to: '../images/icons'}
] ]
}), }),
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminMinify,
options: {
// Lossless optimization with custom option
// Feel free to experiment with options for better result for you
plugins: [
["gifsicle", {}],
["mozjpeg", {}],
["pngquant", {}],
// Svgo configuration here https://github.com/svg/svgo#configuration
[
"svgo",
{
plugins: extendDefaultPlugins([
{
name: "removeViewBox",
active: false,
},
{
name: "addAttributesToSVGElement",
params: {
attributes: [{ xmlns: "http://www.w3.org/2000/svg" }],
},
},
]),
},
// todo: still not fixed
// {
// plugins: {
// name: 'preset-default',
// params: {
// overrides: {
// removeViewBox: {
// active: false,
// },
// addAttributesToSVGElement: {
// params: {
// attributes: [{xmlns: "http://www.w3.org/2000/svg"}],
// },
// },
// },
// },
// },
// },
],
],
},
},
}),
] ]
}; };
module.exports = config; module.exports = config;

7875
yarn.lock

File diff suppressed because it is too large Load Diff