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": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"parserOptions": {
"project": [
"./tsconfig.backend.json",
"./tsconfig.frontend.json"
]
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"no-trailing-spaces": "error",
"max-len": [
"error",
{
"code": 120,
"ignoreStrings": true,
"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
/!\ 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.
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
- 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
Pull requests are welcome and appreciated!
## License
[MIT - see LICENSE file](LICENSE)
[GPLv3 - see LICENSE file](LICENSE)

View File

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

View File

@ -3,6 +3,7 @@
"files": "ts/files.ts",
"layout": "sass/layout.scss",
"index": "sass/index.scss",
"error": "sass/error.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">
<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='">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<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="css/layout.css">
<link rel="stylesheet" href="css/index.css">
@ -50,7 +47,7 @@
<div id="services">
<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>
</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 {
display: flex;
flex-direction: row;
}
#navigation {
display: flex;
flex-direction: column;
height: 100%;
width: 48px;
}
#navigation > :not(#service-buttons) {
flex-shrink: 0;
display: flex;
flex-direction: row;
}
#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 {
display: block;
margin: 0;
padding: 0;
display: block;
margin: 0;
padding: 0;
list-style: none;
}
list-style: none;
#service-selector::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar {
width: 6px;
}
#service-selector [draggable] {
user-select: none;
background-color: rgb(43, 43, 43);
}
li {
position: relative;
#service-selector [draggable] img {
-webkit-user-drag: none;
user-drag: none;
button {
border-radius: 0;
}
&.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-last-drag-position.drag-target {
content: "";
height: 4px;
transform: translateY(-50%);
border: 0;
box-sizing: border-box;
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;
content: "";
height: 4px;
transform: translateY(-50%);
border: 0;
box-sizing: border-box;
background: #fff9;
}
#history {
display: flex;
flex-direction: column;
align-items: center;
padding: 2px;
display: flex;
flex-direction: column;
align-items: center;
padding: 2px;
}
#history button,
#history .status {
display: inline;
width: 24px;
height: 24px;
margin: 2px;
padding: initial;
font-size: 12px;
display: inline;
width: calc(var(--nav-width) / 2);
height: calc(var(--nav-width) / 2);
margin: 2px;
padding: initial;
font-size: calc(var(--nav-width) / 4);
background: #fff1;
background: #fff1;
color: #fff;
cursor: pointer;
color: #fff;
cursor: pointer;
border-radius: 3px;
border-radius: 3px;
}
#history button i {
font-size: inherit;
font-size: inherit;
}
#history button.disabled {
color: #888;
border: transparent;
background: transparent;
cursor: initial;
color: #888;
border: transparent;
background: transparent;
cursor: initial;
}
#history button:focus,
#history button:hover {
outline: none;
border-color: #fff9;
outline: none;
border-color: #fff9;
}
#history button:hover:not(.disabled) {
outline: none;
background-color: #fff2;
outline: none;
background-color: #fff2;
}
#history button:active:not(.disabled) {
background-color: #fff4;
background-color: #fff4;
}
#history #status {
background: transparent;
background: transparent;
}
#history #status .unknown,
#history #status .file {
color: #888;
color: #888;
}
#history #status .https {
color: #009800;
color: #009800;
}
#history #status .http {
color: #e20000;
color: #e20000;
}
#history #status > :not(.active) {
display: none;
display: none;
}
#history #status > * .tip {
position: absolute;
z-index: 1;
left: 100%;
top: 50%;
transform: translateY(-50%);
display: none;
margin-left: 8px;
position: absolute;
z-index: 30;
left: 100%;
top: 50%;
transform: translateY(-50%);
display: none;
margin-left: 8px;
background-color: #000;
padding: 8px;
border-radius: 4px;
white-space: nowrap;
color: #fff;
text-transform: none;
font-family: sans-serif;
font-size: 16px;
font-weight: 100;
background-color: #000;
padding: 8px;
border-radius: 4px;
white-space: nowrap;
color: #fff;
text-transform: none;
font-family: sans-serif;
font-size: 16px;
font-weight: 100;
}
#history #status > * .tip::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translate(-100%, -50%);
display: inline-block;
width: 0;
height: 0;
border: 0 solid transparent;
border-top-width: 4px;
border-bottom-width: 4px;
border-right: 7px solid black;
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translate(-100%, -50%);
display: inline-block;
width: 0;
height: 0;
border: 0 solid transparent;
border-top-width: 4px;
border-bottom-width: 4px;
border-right: 7px solid black;
}
#history #status:focus > * .tip {
display: block;
display: block;
}
#navigation #add-button:not(:hover) {
opacity: 0.75;
opacity: 0.75;
}
#services {
position: relative;
flex-grow: 1;
position: relative;
flex-grow: 1;
}
#services > *:not(.loader):not(#url-preview) {
height: 100%;
height: 100%;
}
#services > :not(.active):not(.loader):not(#url-preview) {
display: none;
#services > :not(.active):not(.loader):not(#url-preview):not(webview) {
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 {
width: 64px;
height: 64px;
width: 64px;
height: 64px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: -1;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: -1;
}
#url-preview {
position: absolute;
bottom: 0;
left: 0;
display: block;
padding: 2px 5px 1px 5px;
position: absolute;
z-index: 10000;
bottom: 0;
left: 0;
display: block;
padding: 2px 5px 1px 5px;
max-width: 48%;
overflow: hidden;
text-overflow: ellipsis;
max-width: 48%;
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
font-family: monospace;
border-width: 1px 1px 0 0;
border-color: #232323;
border-style: solid;
border-radius: 0 5px 0 0;
border-width: 1px 1px 0 0;
border-color: #232323;
border-style: solid;
border-radius: 0 5px 0 0;
background: #2b2b2b;
background: #2b2b2b;
opacity: 1;
transition: 100ms ease opacity;
transition-delay: 200ms;
opacity: 1;
transition: 100ms ease opacity;
transition-delay: 200ms;
}
#url-preview.right {
left: auto;
right: 0;
left: auto;
right: 0;
border-width: 1px 0 0 1px;
border-radius: 5px 0 0 0;
border-width: 1px 0 0 1px;
border-radius: 5px 0 0 0;
}
#url-preview.hidden {
opacity: 0;
#url-preview.invisible {
opacity: 0;
}
#services > #empty-message {
display: flex !important;
position: absolute;
z-index: -1;
bottom: 0;
width: 100%;
justify-content: center;
align-items: center;
display: flex !important;
position: absolute;
z-index: -1;
bottom: 0;
width: 100%;
justify-content: center;
align-items: center;
}
#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;
}

View File

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

View File

@ -4,10 +4,7 @@
<meta charset="UTF-8">
<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='">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<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="css/layout.css">
<link rel="stylesheet" href="css/service-settings.css">
@ -16,7 +13,7 @@
</head>
<body>
<form action="javascript: save();">
<form>
<div class="form-header">
<h1>Loading...</h1>
</div>
@ -42,11 +39,18 @@
<textarea name="customCSS" id="custom-css" rows="3"></textarea>
</div>
<div class="form-group">
<label for="custom-user-agent">Custom UserAgent (i.e. google services)</label>
<input type="text" name="customUserAgent" id="custom-user-agent">
<button type="button" id="userAgentAutoFill">Auto-fill</button>
</div>
<fieldset>
<legend>Disguise</legend>
<div class="form-group">
<label for="custom-user-agent">Custom UserAgent (i.e. google services)</label>
<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 class="form-group-header">
@ -78,7 +82,7 @@
<div class="form-footer">
<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>
</div>
</div>

View File

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

View File

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

View File

@ -1,143 +1,50 @@
import {
clipboard,
ipcRenderer,
PageFaviconUpdatedEvent,
remote,
shell,
UpdateTargetUrlEvent,
WebContents
} from "electron";
import {DidFailLoadEvent, ipcRenderer, PageFaviconUpdatedEvent, UpdateTargetUrlEvent} from "electron";
import Service from "../../src/Service";
import {IconProperties, IconSet, SpecialPages} from "../../src/Meta";
import Config from "../../src/Config";
const {
Menu,
MenuItem,
dialog,
session,
} = remote;
const appInfo: {
title?: string;
} = {};
let icons: IconProperties[] = [];
const appInfo: any = {};
let icons: any[] = [];
let services: any[] = [];
let selectedService: any = null;
let securityButton: HTMLElement | null,
homeButton: HTMLElement | null,
forwardButton: HTMLElement | null,
backButton: HTMLElement | null,
refreshButton: HTMLElement | null;
let services: (FrontService | undefined)[] = [];
let selectedServiceId: number | null = null;
let securityButton: HTMLElement | null = null,
homeButton: HTMLElement | null = null,
forwardButton: HTMLElement | null = null,
backButton: HTMLElement | null = null,
refreshButton: HTMLElement | null = null;
let addButton, settingsButton;
let emptyPage: string;
let urlPreview: HTMLElement | null;
let serviceSelector: HTMLElement | null;
let specialPages: SpecialPages | null = null;
let urlPreview: HTMLElement | null = null;
let serviceSelector: HTMLElement | null = null;
// Service reordering
let lastDragPosition: HTMLElement | null, oldActiveService: number;
// 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()});
}
let lastDragPosition: HTMLElement | null = null;
let oldActiveService: number | null = null;
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
appInfo.title = appData.title;
appInfo.title = appTitle;
// Icons
icons = [];
for (const set of iconSets) {
icons = icons.concat(set);
icons.push(...set);
}
// Special pages
specialPages = _specialPages;
console.log('Updating services ...');
services = config.services;
@ -154,7 +61,7 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
}
for (let i = 0; i < services.length; i++) {
createService(i);
createServiceNavigationElement(i);
}
// Init drag last position
@ -164,7 +71,7 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
const index = services.length;
if (draggedId !== index && draggedId !== index - 1) {
resetDrag();
lastDragTarget = dragTargetId = index;
lastDragTarget = index;
lastDragPosition?.classList.remove('hidden');
lastDragPosition?.classList.add('drag-target');
}
@ -172,22 +79,20 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
}
// Set active service
if (actualSelectedService < 0 || actualSelectedService >= services.length) {
actualSelectedService = 0;
if (activeServiceId < 0 || activeServiceId >= services.length) {
activeServiceId = 0;
}
setActiveService(actualSelectedService);
// Empty
emptyPage = emptyUrl;
setActiveService(activeServiceId);
// Url preview element
urlPreview = document.getElementById("url-preview");
if (urlPreview) {
const _urlPreview = urlPreview;
urlPreview.addEventListener('mouseover', () => {
if (urlPreview!.classList.contains('right')) {
urlPreview!.classList.remove('right');
if (_urlPreview.classList.contains('right')) {
_urlPreview.classList.remove('right');
} else {
urlPreview!.classList.add('right');
_urlPreview.classList.add('right');
}
});
}
@ -207,44 +112,91 @@ ipcRenderer.on('data', (event, appData, iconSets, actualSelectedService, emptyUr
// Other elements
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
const nav = document.querySelector('#service-selector');
let oldNavButton: HTMLElement | null = null;
let nextSibling: Element | null = null;
if (nav) {
oldNavButton = nav.querySelector('li:nth-of-type(' + (id + 1) + ')');
if (oldNavButton) {
nextSibling = oldNavButton.nextElementSibling;
nav.removeChild(oldNavButton);
}
}
// Remove webview
if (services[id] && services[id].view) {
document.querySelector('#services')?.removeChild(services[id].view);
const service = services[id];
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) {
console.log('Adding new service');
services.push(data);
createService(services.length - 1);
createServiceNavigationElement(services.length - 1);
} else {
const oldNavButton = removeServiceFeatures(id);
console.log('Updating existing service', id);
const nextSibling = removeServiceFeatures(id);
// Create new service
services[id] = data;
createService(id, oldNavButton ? oldNavButton.nextElementSibling : null);
if (parseInt(selectedService) === id) {
createServiceNavigationElement(id, nextSibling);
if (selectedServiceId === id) {
setActiveService(id);
}
}
});
ipcRenderer.on('reorderService', (e, serviceId, targetId) => {
ipcRenderer.on('reorderService', (e, serviceId: number, targetId: number) => {
const oldServices = services;
services = [];
@ -268,48 +220,69 @@ ipcRenderer.on('reorderService', (e, serviceId, targetId) => {
serviceSelector.innerHTML = '';
}
for (let i = 0; i < services.length; i++) {
services[i].li = undefined;
createService(i);
const service = services[i];
if (service) service.li = undefined;
createServiceNavigationElement(i);
}
setActiveService(newId);
});
ipcRenderer.on('deleteService', (e, id) => {
ipcRenderer.on('deleteService', (e, id: number) => {
removeServiceFeatures(id);
if (parseInt(selectedService) === id) {
if (selectedServiceId === id) {
setActiveService(0);
}
delete services[id];
services = services.filter(s => s !== null);
services.splice(id, 1);
});
function createService(index: number, nextNavButton?: Element | null) {
let service = services[index];
let li = <any>document.createElement('li');
function createServiceNavigationElement(index: number, nextNavButton?: Element | null) {
const service = services[index];
if (!service) throw new Error('Service doesn\'t exist.');
const li = document.createElement('li') as NavigationElement;
service.li = li;
let button = document.createElement('button');
const button = document.createElement('button');
button.dataset.serviceId = '' + index;
button.dataset.tooltip = service.name;
button.addEventListener('click', () => {
if (button.dataset.serviceId) {
setActiveService(parseInt(button.dataset.serviceId));
const rawId = 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) {
icon = document.createElement('img');
icon.src = service.favicon;
icon.alt = service.name;
} else if (service.isImage) {
if (icon instanceof HTMLImageElement) {
icon.src = service.favicon;
icon.alt = service.name;
}
} else if (service.isImage && service.icon) {
icon = document.createElement('img');
icon.src = service.icon;
icon.alt = service.name;
if (icon instanceof HTMLImageElement) {
icon.src = service.icon;
icon.alt = service.name;
}
} else {
icon = document.createElement('i');
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) => {
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 lastDragTarget = -1;
let dragTargetId = -1;
let dragTargetCount = 0;
function initDrag(index: number, li: any) {
function initDrag(index: number, li: NavigationElement) {
li.serviceId = index;
li.draggable = true;
li.addEventListener('dragstart', (event: DragEvent) => {
@ -361,7 +334,7 @@ function initDrag(index: number, li: any) {
});
li.addEventListener('dragover', (e: DragEvent) => {
let realIndex = index;
let rect = li.getBoundingClientRect();
const rect = li.getBoundingClientRect();
if ((e.clientY - rect.y) / rect.height >= 0.5) {
realIndex++;
@ -372,12 +345,18 @@ function initDrag(index: number, li: any) {
}
resetDrag();
let el = realIndex === services.length ? lastDragPosition : services[realIndex].li;
lastDragTarget = dragTargetId = realIndex;
const el = realIndex === services.length ?
lastDragPosition :
services[realIndex]?.li;
lastDragTarget = realIndex;
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', () => {
reorderService(draggedId, lastDragTarget);
@ -387,8 +366,6 @@ function initDrag(index: number, li: any) {
function resetDrag() {
lastDragTarget = -1;
dragTargetId = -1;
dragTargetCount = 0;
serviceSelector?.querySelectorAll('li').forEach(li => {
li.classList.remove('drag-target');
li.classList.remove('drag-target-self');
@ -401,7 +378,7 @@ function resetDrag() {
function reorderService(serviceId: number, targetId: number) {
console.log('Reordering service', serviceId, targetId);
if (targetId >= 0) {
oldActiveService = selectedService;
oldActiveService = selectedServiceId;
setActiveService(-1);
ipcRenderer.send('reorderService', serviceId, targetId);
}
@ -423,7 +400,7 @@ document.addEventListener('DOMContentLoaded', () => {
refreshButton?.addEventListener('click', () => reload());
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?.addEventListener('click', () => ipcRenderer.send('openSettings', null));
@ -437,135 +414,91 @@ function setActiveService(serviceId: number) {
}
// Hide previous service
if (services[selectedService] && services[selectedService].view) {
services[selectedService].view.classList.remove('active');
if (typeof selectedServiceId === 'number') {
const selectedService = services[selectedServiceId];
if (selectedService && selectedService.view) {
selectedService.view.classList.remove('active');
}
}
// Show service
if (currentService) {
currentService.view.classList.add('active');
}
currentService?.view?.classList.add('active');
// Save active service ID
selectedService = serviceId;
selectedServiceId = serviceId;
// Refresh navigation
updateNavigation();
});
}
function loadService(serviceId: number, service: any) {
function loadService(serviceId: number, service: FrontService) {
// Load service if not loaded yet
if (!service.view && !service.viewReady) {
console.log('Loading service', serviceId);
document.querySelector('#services > .loader')?.classList.remove('hidden');
service.view = document.createElement('webview');
service.view.setAttribute('enableRemoteModule', 'false');
service.view.setAttribute('partition', 'persist:service_' + service.partition);
service.view.setAttribute('autosize', 'true');
service.view.setAttribute('src', emptyPage);
const view = service.view = document.createElement('webview');
updateNavigation(); // Start loading animation
view.setAttribute('enableRemoteModule', 'false');
view.setAttribute('partition', 'persist:service_' + service.partition);
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
// eventual future human mistakes.
service.view.setAttribute('webpreferences', 'contextIsolation=yes');
// Error handling
view.addEventListener('did-fail-load', (e: DidFailLoadEvent) => {
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
document.querySelector('#services')?.appendChild(service.view);
document.querySelector('#services')?.appendChild(view);
// Load chain
let listener: Function;
service.view.addEventListener('dom-ready', listener = () => {
service.view.removeEventListener('dom-ready', listener);
let listener: () => void;
view.addEventListener('dom-ready', listener = () => {
view.removeEventListener('dom-ready', listener);
service.view.addEventListener('dom-ready', listener = () => {
view.addEventListener('dom-ready', listener = () => {
if (service.customCSS) {
service.view.insertCSS(service.customCSS);
view.insertCSS(service.customCSS)
.catch(console.error);
}
document.querySelector('#services > .loader')?.classList.add('hidden');
service.li.classList.add('loaded');
service.li?.classList.add('loaded');
service.viewReady = true;
updateNavigation();
if (selectedService === null) {
if (selectedServiceId === null) {
setActiveService(serviceId);
}
});
const webContents = remote.webContents.fromId(service.view.getWebContentsId());
// Set custom user agent
if (typeof service.customUserAgent === 'string') {
webContents.setUserAgent(service.customUserAgent);
ipcRenderer.send('set-web-contents-user-agent', view.getWebContentsId(), service.customUserAgent);
}
// Set context menu
setContextMenu(webContents);
ipcRenderer.send('open-service-content-context-menu', view.getWebContentsId());
// Set permission request handler
function getUrlDomain(url: string) {
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;
}
ipcRenderer.send('set-partition-permissions', serviceId, view.partition);
return '';
}
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);
view.setAttribute('src', service.url);
});
// 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);
if (event.favicons.length > 0 && service.favicon !== 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.alt = service.name;
img.onload = () => {
service.li.button.innerHTML = '';
service.li.button.appendChild(img);
if (service.li) {
service.li.button.innerHTML = '';
service.li.button.appendChild(img);
}
};
}
}
});
// Display target urls
service.view.addEventListener('update-target-url', (event: UpdateTargetUrlEvent) => {
view.addEventListener('update-target-url', (event: UpdateTargetUrlEvent) => {
if (event.url.length === 0) {
urlPreview?.classList.add('hidden');
urlPreview?.classList.add('invisible');
} else {
urlPreview?.classList.remove('hidden');
urlPreview?.classList.remove('invisible');
if (urlPreview) {
urlPreview.innerHTML = event.url;
}
@ -597,21 +532,28 @@ function loadService(serviceId: number, service: any) {
function unloadService(serviceId: number) {
const service = services[serviceId];
if (!service) throw new Error('Service doesn\'t exist.');
if (service.view && service.viewReady) {
service.view.remove();
service.view = null;
service.li.classList.remove('loaded');
service.view = undefined;
service.li?.classList.remove('loaded');
service.viewReady = false;
if (parseInt(selectedService) === serviceId) {
selectedService = null;
if (selectedServiceId === serviceId) {
selectedServiceId = null;
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);
break;
}
}
if (selectedService === null) {
// false positive:
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (selectedServiceId === null) {
updateNavigation();
}
}
@ -620,6 +562,8 @@ function unloadService(serviceId: number) {
function reloadService(serviceId: number) {
const service = services[serviceId];
if (!service) throw new Error('Service doesn\'t exist.');
if (service.view && service.viewReady) {
console.log('Reloading service', serviceId);
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() {
console.debug('Updating navigation');
// Update active list element
for (let i = 0; i < services.length; i++) {
const service = services[i];
if (!service) continue;
if (!service.li) continue;
// Active?
if (parseInt(selectedService) === i) service.li.classList.add('active');
if (selectedServiceId === i) service.li.classList.add('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?
if (service.viewReady) service.li.classList.add('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');
// Update history navigation
let view = services[selectedService].view;
const view = services[selectedServiceId]?.view;
homeButton?.classList.remove('disabled');
@ -671,164 +617,70 @@ function updateNavigation() {
}
function updateStatusButton() {
let protocol = services[selectedService].view.getURL().split('://')[0];
if (!protocol) protocol = 'unknown';
for (const c of <any>securityButton?.children) {
if (c.classList.contains(protocol)) c.classList.add('active');
else c.classList.remove('active');
}
if (typeof selectedServiceId !== 'number') return;
const protocol = services[selectedServiceId]?.view?.getURL().split('://')[0] || 'unknown';
securityButton?.childNodes.forEach(el => {
if (el instanceof HTMLElement) {
if (el.classList.contains(protocol)) el.classList.add('active');
else el.classList.remove('active');
}
});
}
function updateWindowTitle() {
if (selectedService === null) {
ipcRenderer.send('updateWindowTitle', null);
} else if (services[selectedService].viewReady) {
ipcRenderer.send('updateWindowTitle', selectedService, remote.webContents.fromId(services[selectedService].view.getWebContentsId()).getTitle());
if (selectedServiceId === null) {
ipcRenderer.send('update-window-title', null);
} else {
const service = services[selectedServiceId];
if (service?.viewReady && service.view) {
ipcRenderer.send('update-window-title', selectedServiceId, service.view.getWebContentsId());
}
}
}
function goHome() {
let service = services[selectedService];
service.view.loadURL(service.url)
if (selectedServiceId === null) return;
const service = services[selectedServiceId];
if (!service) throw new Error('Service doesn\'t exist.');
service.view?.loadURL(service.url)
.catch(console.error);
}
function goForward() {
let view = services[selectedService].view;
if (view) remote.webContents.fromId(view.getWebContentsId()).goForward();
if (selectedServiceId === null) return;
const view = services[selectedServiceId]?.view;
if (view) ipcRenderer.send('go-forward', view.getWebContentsId());
}
function goBack() {
let view = services[selectedService].view;
if (view) remote.webContents.fromId(view.getWebContentsId()).goBack();
if (selectedServiceId === null) return;
const view = services[selectedServiceId]?.view;
if (view) ipcRenderer.send('go-back', view.getWebContentsId());
}
function reload() {
reloadService(selectedService);
if (selectedServiceId === null) return;
reloadService(selectedServiceId);
}
function setContextMenu(webContents: WebContents) {
webContents.on('context-menu', (event, props) => {
const menu = new Menu();
const {editFlags} = props;
ipcRenderer.on('fullscreenchange', (e, fullscreen: boolean) => {
if (fullscreen) document.body.classList.add('fullscreen');
else document.body.classList.remove('fullscreen');
});
// linkURL
if (props.linkURL.length > 0) {
if (menu.items.length > 0) {
menu.append(new MenuItem({type: 'separator'}));
}
type FrontService = Service & {
view?: Electron.WebviewTag;
viewReady?: boolean;
li?: NavigationElement;
};
menu.append(new MenuItem({
label: 'Copy link URL',
click: () => {
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(),
});
});
}
type NavigationElement = HTMLLIElement & {
serviceId: number;
button: HTMLButtonElement;
};

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 builtInIconSearchField: HTMLInputElement | null;
@ -12,22 +14,22 @@ let autoLoadInput: HTMLInputElement | null;
let customCssInput: HTMLInputElement | null;
let customUserAgentInput: HTMLInputElement | null;
let serviceId: number;
let service: any;
let serviceId: number | null = null;
let service: Service | null = null;
ipcRenderer.on('syncIcons', (event, iconSets) => {
let icons: any[] = [];
ipcRenderer.on('syncIcons', (event, iconSets: IconSet[]) => {
let icons: IconProperties[] = [];
for (const set of iconSets) {
icons = icons.concat(set);
}
loadIcons(icons);
});
ipcRenderer.on('loadService', (e, id, data) => {
ipcRenderer.on('loadService', (e, id: number | null, data?: Service) => {
console.log('Load service', id);
if (id === null) {
if (id === null || !data) {
document.title = 'Add a new service';
service = {};
service = null;
if (h1) h1.innerText = 'Add a new service';
} else {
@ -54,41 +56,56 @@ document.addEventListener('DOMContentLoaded', () => {
isImageCheckbox?.addEventListener('click', () => {
updateIconChoiceForm(isImageCheckbox?.checked);
updateIconChoiceForm(!!isImageCheckbox?.checked);
});
updateIconChoiceForm(isImageCheckbox?.checked);
updateIconChoiceForm(!!isImageCheckbox?.checked);
builtInIconSearchField?.addEventListener('input', updateIconSearchResults);
document.getElementById('cancel-button')?.addEventListener('click', e => {
e.preventDefault();
remote.getCurrentWindow().close();
ipcRenderer.send('close-window', 'ServiceSettingsWindow');
});
document.querySelector('form')?.addEventListener('submit', e => {
e.preventDefault();
save();
});
ipcRenderer.send('sync-settings');
document.getElementById('userAgentAutoFill')?.addEventListener('click', () => {
let customUserAgent = document.getElementById('custom-user-agent');
document.getElementById('userAgentAutoFillFirefox')?.addEventListener('click', () => {
const customUserAgent = document.querySelector<HTMLInputElement>('#custom-user-agent');
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() {
const searchStr: string = builtInIconSearchField!.value;
(<any>iconSelect?.childNodes).forEach((c: HTMLElement) => {
let parts = c.dataset.icon?.split('/') || '';
let iconName = parts[1] || parts[0];
if (iconName.match(searchStr) || searchStr.match(iconName)) {
c.classList.remove('hidden');
} else {
c.classList.add('hidden');
if (!builtInIconSearchField) throw new Error('builtInIconSearchField no initialized.');
const searchStr: string = builtInIconSearchField.value;
iconSelect?.childNodes.forEach((el) => {
if (el instanceof HTMLElement) {
const parts = el.dataset.icon?.split('/') || '';
const iconName = parts[1] || parts[0];
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);
for (const icon of icons) {
if (icon.name.length === 0) continue;
@ -121,16 +138,16 @@ function loadIcons(icons: any[]) {
function selectIcon(choice: HTMLElement) {
if (builtInIconSearchField) builtInIconSearchField.value = choice.dataset.icon || '';
if (iconSelect) {
for (const otherChoice of <any>iconSelect.children) {
otherChoice.classList.remove('selected');
}
iconSelect.childNodes.forEach(el => {
if (el instanceof Element) el.classList.remove('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;
}
function updateIconChoiceForm(isUrl: any) {
function updateIconChoiceForm(isUrl: boolean) {
if (isUrl) {
iconSelect?.classList.add('hidden');
builtInIconSearchField?.parentElement?.classList.add('hidden');
@ -148,21 +165,22 @@ function loadServiceValues() {
}
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 (autoLoadInput) autoLoadInput.checked = service.autoLoad;
if (customCssInput) customCssInput.value = service.customCSS;
if (customUserAgentInput) customUserAgentInput.value = service.customUserAgent;
if (customCssInput) customCssInput.value = service.customCSS || '';
if (customUserAgentInput) customUserAgentInput.value = service.customUserAgent || '';
isImageCheckbox.checked = service.isImage;
if (service.isImage) {
if (service.isImage && service.icon) {
if (iconUrlField) iconUrlField.value = service.icon;
} else {
if (builtInIconSearchField) builtInIconSearchField.value = service.icon;
if (builtInIconSearchField && service.icon) builtInIconSearchField.value = service.icon;
updateIconSearchResults();
let labels = iconSelect?.querySelectorAll('label');
const labels = iconSelect?.querySelectorAll('label');
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) {
selectIcon(icon);
}
@ -170,43 +188,59 @@ function loadServiceValues() {
}
}
(window as any).save = () => {
let form = document.querySelector('form');
function save() {
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;
const formData = new FormData(form);
service.name = formData.get('name');
if (typeof service.partition !== 'string' || service.partition.length === 0) {
service.name = String(formData.get('name'));
if (service.partition.length === 0) {
service.partition = service.name.replace(/ /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.icon = formData.get('icon');
service.icon = String(formData.get('icon'));
service.useFavicon = formData.get('useFavicon') === '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();
service.customUserAgent = customUserAgent.length === 0 ? null : customUserAgent;
const customUserAgent = (<string>formData.get('customUserAgent')).trim();
service.customUserAgent = customUserAgent.length === 0 ? undefined : customUserAgent;
if (!isValid()) {
return;
}
ipcRenderer.send('saveService', serviceId, service);
remote.getCurrentWindow().close();
};
ipcRenderer.send('close-window', 'ServiceSettingsWindow');
}
function isValid() {
if (typeof service.name !== 'string' || service.name.length === 0) {
if (!service) return false;
if (service.name.length === 0) {
console.log('Invalid name');
return false;
}
if (typeof service.partition !== 'string' || service.partition.length === 0) {
if (service.partition.length === 0) {
console.log('Invalid partition');
return false;
}
if (typeof service.url !== 'string' || service.url.length === 0) {
if (service.url.length === 0) {
console.log('Invalid url');
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 updateStatus: HTMLElement | null;
let updateInfo: any;
let updateInfo: UpdateInfo;
let updateButton: HTMLElement | null;
let config: any;
let config: Config;
let startMinimizedField: HTMLInputElement | null;
let bigNavBarField: HTMLInputElement | null;
let securityButtonField: HTMLInputElement | null,
homeButtonField: HTMLInputElement | null,
@ -12,12 +19,16 @@ let securityButtonField: HTMLInputElement | null,
forwardButtonField: 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}`;
});
ipcRenderer.on('config', (e, c) => {
ipcRenderer.on('config', (e, c: Config) => {
config = c;
if (startMinimizedField) startMinimizedField.checked = config.startMinimized;
if (bigNavBarField) bigNavBarField.checked = config.bigNavBar;
if (securityButtonField) securityButtonField.checked = config.securityButton;
if (homeButtonField) homeButtonField.checked = config.homeButton;
if (backButtonField) backButtonField.checked = config.backButton;
@ -25,7 +36,7 @@ ipcRenderer.on('config', (e, c) => {
if (refreshButtonField) refreshButtonField.checked = config.refreshButton;
});
ipcRenderer.on('updateStatus', (e, available, version) => {
ipcRenderer.on('updateStatus', (e, available: boolean, version: UpdateInfo) => {
console.log(available, version);
updateInfo = version;
if (available) {
@ -36,11 +47,15 @@ ipcRenderer.on('updateStatus', (e, available, version) => {
}
});
(window as any).save = () => {
let form = document.querySelector('form');
function save() {
const form = document.querySelector('form');
if (!form) return;
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.homeButton = formData.get('home-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';
ipcRenderer.send('save-config', config);
remote.getCurrentWindow().close();
};
ipcRenderer.send('close-window', 'SettingsWindow');
}
document.addEventListener('DOMContentLoaded', () => {
currentVersion = document.getElementById('current-version');
updateStatus = document.getElementById('update-status');
updateButton = document.getElementById('download-button');
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);
});
startMinimizedField = <HTMLInputElement>document.getElementById('start-minimized');
bigNavBarField = <HTMLInputElement>document.getElementById('big-nav-bar');
securityButtonField = <HTMLInputElement>document.getElementById('security-button');
homeButtonField = <HTMLInputElement>document.getElementById('home-button');
backButtonField = <HTMLInputElement>document.getElementById('back-button');
@ -68,7 +87,12 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('cancel-button')?.addEventListener('click', e => {
e.preventDefault();
remote.getCurrentWindow().close();
ipcRenderer.send('close-window', 'SettingsWindow');
});
document.querySelector('form')?.addEventListener('submit', e => {
e.preventDefault();
save();
});
ipcRenderer.send('syncSettings');

View File

@ -1,22 +1,24 @@
{
"name": "tabs",
"version": "1.0.0",
"version": "1.3.1",
"description": "Persistent and separate browser tabs in one window.",
"author": {
"name": "Alice Gaudon",
"email": "alice@gaudon.pro"
},
"homepage": "https://gitlab.com/ArisuOngaku/tabs",
"homepage": "https://eternae.ink/ashpie/tabs",
"license": "GPL-3.0-only",
"main": "build/main.js",
"scripts": {
"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",
"compile-dev": "yarn clean && tsc -p tsconfig.backend.json && tsc -p tsconfig.frontend.json && webpack --mode development",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"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 .",
"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",
"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": {
"appdata-path": "^1.0.0",
@ -28,28 +30,31 @@
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@fortawesome/fontawesome-free": "^5.13.0",
"@types/node": "^12.12.41",
"@fortawesome/fontawesome-free": "^6.1.0",
"@types/node": "^14.6.2",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"babel-loader": "^8.1.0",
"concurrently": "^5.2.0",
"copy-webpack-plugin": "^6.0.1",
"css-loader": "^3.5.3",
"electron": "^9.0.0",
"electron-builder": "^22.4.0",
"file-loader": "^6.0.0",
"imagemin": "^7.0.1",
"concurrently": "^7.0.0",
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.3.0",
"electron": "^17.1.2",
"electron-builder": "^22.11.5",
"eslint": "^8.11.0",
"image-minimizer-webpack-plugin": "^3.2.3",
"imagemin": "^8.0.1",
"imagemin-gifsicle": "^7.0.0",
"imagemin-mozjpeg": "^8.0.0",
"imagemin-pngquant": "^8.0.0",
"imagemin-svgo": "^8.0.0",
"img-loader": "^3.0.1",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.14.1",
"sass-loader": "^8.0.2",
"ts-loader": "^7.0.4",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
"imagemin-mozjpeg": "^10.0.0",
"imagemin-pngquant": "^9.0.2",
"imagemin-svgo": "^10.0.1",
"mini-css-extract-plugin": "^2.1.0",
"sass": "^1.32.12",
"sass-loader": "^12.1.0",
"svgo": "^2.3.1",
"ts-loader": "^9.1.2",
"typescript": "^4.0.2",
"webpack": "^5.2.0",
"webpack-cli": "^4.1.0"
},
"build": {
"appId": "tabs-app",
@ -57,48 +62,33 @@
"resources/**/*",
"build/**/*"
],
"publish": [
{
"provider": "generic",
"url": "https://update.eternae.ink/ashpie/tabs"
}
],
"linux": {
"target": "AppImage",
"icon": "resources/logo.png",
"icon": "frontend/images/logo.png",
"category": "Utility",
"executableName": "tabs",
"desktop": {
"StartupWMClass": "Tabs",
"MimeType": "x-scheme-handler/tabs"
},
"publish": [
{
"provider": "github",
"owner": "ArisuOngaku",
"repo": "tabs"
}
]
}
},
"win": {
"target": "nsis",
"icon": "resources/logo.png",
"icon": "frontend/images/logo.png",
"publisherName": "Alice Gaudon",
"verifyUpdateCodeSignature": "true",
"publish": [
{
"provider": "github",
"owner": "ArisuOngaku",
"repo": "tabs"
}
]
"verifyUpdateCodeSignature": "false"
},
"mac": {
"target": "default",
"icon": "resources/logo.png",
"category": "public.app-category.utilities",
"publish": [
{
"provider": "github",
"owner": "ArisuOngaku",
"repo": "tabs"
}
]
"icon": "frontend/images/logo.png",
"category": "public.app-category.utilities"
},
"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 Config from "./Config";
import Updater from "./Updater";
import MainWindow from "./windows/MainWindow";
import * as os from "os";
export default class Application {
private readonly devMode: boolean;
@ -11,10 +12,10 @@ export default class Application {
private readonly mainWindow: MainWindow;
private tray?: Tray;
constructor(devMode: boolean) {
public constructor(devMode: boolean) {
this.devMode = devMode;
this.config = new Config();
this.updater = new Updater(this.config);
this.updater = new Updater(this.config, this);
this.mainWindow = new MainWindow(this);
}
@ -26,9 +27,11 @@ export default class Application {
this.setupElectronTweaks();
// Check for updates
this.updater.checkAndPromptForUpdates(this.mainWindow.getWindow()).then(() => {
console.log('Update check successful.');
}).catch(console.error);
if (os.platform() === 'win32') {
this.updater.checkAndPromptForUpdates(this.mainWindow.getWindow()).then(() => {
console.log('Update check successful.');
}).catch(console.error);
}
console.log('App started');
}
@ -49,27 +52,48 @@ export default class Application {
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() {
// Open external links in default OS browser
app.on('web-contents-created', (e, contents) => {
if (contents.getType() === 'webview') {
console.log('Setting external links to open in default OS browser');
contents.on('new-window', (e, url) => {
e.preventDefault();
if (url.startsWith('https://')) {
shell.openExternal(url).catch(console.error);
}
contents.setWindowOpenHandler(details => {
if (details.url.startsWith(details.referrer.url)) return {action: 'allow'};
const url = details.url;
this.openExternalLink(url)
.catch(console.error);
return {action: 'deny'};
});
}
});
// Disable unused features
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;
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,7 +106,7 @@ export default class Application {
{label: 'Tabs', enabled: false},
{label: 'Open Tabs', click: () => this.mainWindow.getWindow().show()},
{type: 'separator'},
{label: 'Quit', role: 'quit'}
{label: 'Quit', role: 'quit'},
]));
this.tray.on('click', () => this.mainWindow.toggle());
}

View File

@ -10,7 +10,13 @@ const configFile = path.resolve(configDir, 'config.json');
export default class Config {
public services: Service[] = [];
public updateCheckSkip?: string;
public startMinimized: boolean = false;
public bigNavBar: boolean = false;
public securityButton: boolean = true;
public homeButton: boolean = false;
public backButton: boolean = true;
@ -19,9 +25,11 @@ export default class Config {
private properties: string[] = [];
constructor() {
[p: string]: unknown;
public constructor() {
// Load data from config file
let data: any = {};
let data: Record<string, unknown> = {};
if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
if (fs.existsSync(configFile) && fs.statSync(configFile).isFile())
data = JSON.parse(fs.readFileSync(configFile, 'utf8'));
@ -30,7 +38,7 @@ export default class Config {
}
// Parse services
if (typeof data.services === 'object') {
if (typeof data.services === 'object' && Array.isArray(data.services)) {
let i = 0;
for (const service of data.services) {
this.services[i] = new Service(service);
@ -39,11 +47,22 @@ export default class Config {
}
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('startMinimized', data);
this.defineProperty('bigNavBar', data);
this.defineProperty('securityButton', data);
this.defineProperty('homeButton', data);
this.defineProperty('backButton', data);
@ -53,25 +72,24 @@ export default class Config {
this.save();
}
save() {
public save(): void {
console.log('Saving config');
this.services = this.services.filter(s => s !== null);
fs.writeFileSync(configFile, JSON.stringify(this, null, 4));
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) {
(<any>this)[name] = data[name];
this[name] = data[name];
}
this.properties.push(name);
}
update(data: any) {
public update(data: Record<string, unknown>): void {
for (const prop of this.properties) {
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 SOLID_ICONS = Meta.listIcons('solid');
public static readonly REGULAR_ICONS = Meta.listIcons('regular');
public static readonly ICON_SETS = [
public static readonly ICON_SETS: IconSet[] = [
Meta.BRAND_ICONS,
Meta.SOLID_ICONS,
Meta.REGULAR_ICONS,
@ -21,7 +21,7 @@ export default class Meta {
private static devMode?: boolean;
public static isDevMode() {
public static isDevMode(): boolean {
if (this.devMode === undefined) {
this.devMode = process.argv.length > 2 && process.argv[2] === '--dev';
console.debug('Dev mode:', this.devMode);
@ -30,7 +30,7 @@ export default class Meta {
return this.devMode;
}
public static getTitleForService(service: Service, viewTitle: string) {
public static getTitleForService(service: Service, viewTitle: string): string {
let suffix = '';
if (viewTitle.length > 0) {
suffix = ' - ' + viewTitle;
@ -38,7 +38,7 @@ export default class Meta {
return this.title + ' - ' + service.name + suffix;
}
private static listIcons(set: string) {
private static listIcons(set: string): IconSet {
console.log('Loading icon set', set);
const directory = path.resolve(Meta.RESOURCES_PATH, `images/icons/${set}`);
const icons: { name: string; faIcon: string; set: string; }[] = [];
@ -51,3 +51,17 @@ export default class Meta {
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 {
public partition?: string;
public name?: string;
public name: string;
public partition: string;
public url: string;
public icon?: string;
public isImage?: boolean = false;
public url?: string;
public useFavicon?: boolean = true;
public isImage: boolean = false;
public useFavicon: boolean = true;
public favicon?: string;
public autoLoad?: boolean = false;
public autoLoad: boolean = false;
public customCSS?: string;
public customUserAgent?: string;
public permissions?: {} = {};
public permissions: ServicePermissions = {};
constructor(partition: string, name?: string, icon?: string, isImage?: boolean, url?: string, useFavicon?: boolean) {
if (arguments.length === 1) {
const data = arguments[0];
for (const k in data) {
if (data.hasOwnProperty(k)) {
(<any>this)[k] = data[k];
[p: string]: unknown;
public constructor(
partition: string | Pick<Service, keyof Service>,
name?: string,
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 {dialog, shell} from "electron";
import {dialog} from "electron";
import Config from "./Config";
import Application from "./Application";
import {SemVer} from "semver";
import BrowserWindow = Electron.BrowserWindow;
export default class Updater {
private readonly config: Config;
private readonly application: Application;
private updateInfo?: UpdateInfo;
public constructor(config: Config) {
public constructor(config: Config, application: Application) {
this.config = config;
this.application = application;
// Configure auto updater
autoUpdater.autoDownload = false;
@ -33,7 +37,7 @@ export default class Updater {
}
}
public getCurrentVersion() {
public getCurrentVersion(): SemVer {
return autoUpdater.currentVersion;
}
@ -42,16 +46,16 @@ export default class Updater {
if (updateInfo && updateInfo.version !== this.config.updateCheckSkip) {
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: [
'Cancel',
'Download',
'Install',
],
checkboxChecked: false,
checkboxLabel: `Don't remind me for this version`,
cancelId: 0,
defaultId: 1,
type: 'question'
type: 'question',
});
if (input.checkboxChecked) {
@ -61,7 +65,9 @@ export default class Updater {
}
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";
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)[] = [];
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);
if (this.parent) {
@ -30,9 +32,22 @@ export default abstract class Window {
this.teardown();
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);
for (const listener of this.onCloseListeners) {
@ -40,7 +55,10 @@ export default abstract class Window {
}
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);
}
}
@ -48,18 +66,20 @@ export default abstract class Window {
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 {
ipcMain.on(channel, listener);
if (!this.listeners[channel]) this.listeners[channel] = [];
this.listeners[channel].push(listener);
this.listeners[channel]?.push(listener);
return this;
}
public onClose(listener: () => void) {
public onClose(listener: () => void): void {
this.onCloseListeners.push(listener);
}
public toggle() {
public toggle(): void {
if (this.window) {
if (!this.window.isFocused()) {
console.log('Showing window', this.constructor.name);
@ -73,6 +93,7 @@ export default abstract class Window {
public getWindow(): BrowserWindow {
if (!this.window) throw Error('Window not initialized.');
else if (this.window.isDestroyed()) throw Error('Window destroyed.');
return this.window;
}
}

View File

@ -4,6 +4,9 @@ import {app} from "electron";
import Meta from "./Meta";
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());
// Check if application is already running

View File

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

View File

@ -1,39 +1,42 @@
import path from "path";
import {ipcMain} from "electron";
import {clipboard, ContextMenuParams, dialog, ipcMain, Menu, MenuItem, session, webContents} from "electron";
import ServiceSettingsWindow from "./ServiceSettingsWindow";
import SettingsWindow from "./SettingsWindow";
import Application from "../Application";
import Meta from "../Meta";
import Meta, {SpecialPages} from "../Meta";
import Window from "../Window";
export default class MainWindow extends Window {
private activeService: number = 0;
private activeServiceId: number = 0;
private serviceSettingsWindow?: ServiceSettingsWindow;
private settingsWindow?: SettingsWindow;
constructor(application: Application) {
public constructor(application: Application) {
super(application);
}
public setup() {
public setup(): void {
super.setup({
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
webviewTag: true,
contextIsolation: false,
},
autoHideMenuBar: true,
icon: Meta.ICON_PATH,
title: Meta.title,
show: !this.application.getConfig().startMinimized,
});
const window = this.getWindow();
window.maximize();
if (!this.application.getConfig().startMinimized) {
window.maximize();
}
if (this.application.isDevMode()) {
window.webContents.openDevTools({
mode: 'right'
mode: 'right',
});
}
@ -43,19 +46,19 @@ export default class MainWindow extends Window {
});
// Load active service
this.onIpc('setActiveService', (event, index) => {
this.onIpc('setActiveService', (event, index: number) => {
this.setActiveService(index);
});
// 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);
this.config.services[index].favicon = favicon;
this.config.save();
});
// Reorder services
this.onIpc('reorderService', (event, serviceId, targetId) => {
this.onIpc('reorderService', (event, serviceId: number, targetId: number) => {
console.log('Reordering services', serviceId, targetId);
const oldServices = this.config.services;
@ -77,40 +80,14 @@ export default class MainWindow extends Window {
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
ipcMain.on('updateWindowTitle', (event, serviceId, viewTitle) => {
ipcMain.on('update-window-title', (event, serviceId: number | null, webContentsId?: number) => {
if (serviceId === null) {
window.setTitle(Meta.title);
} else {
} else if (webContentsId) {
const service = this.config.services[serviceId];
window.setTitle(Meta.getTitleForService(service, viewTitle));
}
});
// 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;
});
const serviceWebContents = webContents.fromId(webContentsId);
window.setTitle(Meta.getTitleForService(service, serviceWebContents.getTitle()));
}
});
@ -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
window.loadFile(path.resolve(Meta.RESOURCES_PATH, 'index.html'))
.catch(console.error);
}
public syncData() {
public syncData(): void {
this.getWindow().webContents.send('data',
Meta.title,
Meta.ICON_SETS,
this.activeService,
path.resolve(Meta.RESOURCES_PATH, 'empty.html'),
this.config
this.activeServiceId,
<SpecialPages>{
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) {
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";
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);
this.serviceId = serviceId;
}
public setup() {
public setup(): void {
super.setup({
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
webviewTag: true,
contextIsolation: false,
},
modal: true,
autoHideMenuBar: true,
@ -28,16 +28,20 @@ export default class ServiceSettingsWindow extends Window {
if (this.application.isDevMode()) {
window.webContents.openDevTools({
mode: 'right'
mode: 'right',
});
}
this.onIpc('sync-settings', () => {
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);
const newService = new Service(data);
if (typeof id === 'number') {
@ -45,6 +49,8 @@ export default class ServiceSettingsWindow extends Window {
} else {
this.config.services.push(newService);
id = this.config.services.indexOf(newService);
if (id < 0) id = null;
}
this.config.save();

View File

@ -3,14 +3,15 @@ import path from "path";
import Window from "../Window";
import MainWindow from "./MainWindow";
import Meta from "../Meta";
import Config from "../Config";
export default class SettingsWindow extends Window {
public setup() {
public setup(): void {
super.setup({
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
webviewTag: true,
contextIsolation: false,
},
modal: true,
autoHideMenuBar: true,
@ -21,7 +22,7 @@ export default class SettingsWindow extends Window {
if (this.application.isDevMode()) {
window.webContents.openDevTools({
mode: 'right'
mode: 'right',
});
}
@ -36,7 +37,7 @@ export default class SettingsWindow extends Window {
}).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.save();
if (this.parent instanceof MainWindow) {

View File

@ -1,5 +1,7 @@
const path = require('path');
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 dev = process.env.NODE_ENV === 'development';
@ -37,9 +39,6 @@ const config = {
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/',
}
},
'css-loader',
'sass-loader',
@ -47,25 +46,17 @@ const config = {
},
{
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,
use: [
'file-loader?name=../images/[name].[ext]',
{
loader: 'img-loader',
options: {
enabled: !dev,
plugins: [
require('imagemin-gifsicle')({}),
require('imagemin-mozjpeg')({}),
require('imagemin-pngquant')({}),
require('imagemin-svgo')({}),
]
}
}
]
type: 'asset/resource',
generator: {
filename: '../images/[name][ext]',
},
},
{
test: /\.ts$/i,
@ -75,13 +66,14 @@ const config = {
configFile: 'tsconfig.frontend.json',
}
},
exclude: '/node_modules/'
exclude: '/node_modules/',
},
{
test: /\.html$/i,
use: [
'file-loader?name=../[name].[ext]',
]
type: 'asset/resource',
generator: {
filename: '../[name][ext]',
},
}
],
},
@ -94,6 +86,56 @@ const config = {
{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"}],
// },
// },
// },
// },
// },
// },
],
],
},
},
}),
]
};

7875
yarn.lock

File diff suppressed because it is too large Load Diff