├── .editorconfig
├── .eslintrc.json
├── .github
├── dependabot.yml
├── stale.yml
└── workflows
│ └── deploy-to-gh-pages.yml
├── .gitignore
├── .node-version
├── .npmrc
├── .vscode
├── launch.json
└── tasks.json
├── LICENSE.md
├── README.md
├── _config.yml
├── angular.json
├── angular.webpack.js
├── app
├── main.ts
├── package-lock.json
└── package.json
├── e2e
├── main.spec.ts
├── playwright.config.ts
└── tsconfig.e2e.json
├── electron-builder.json
├── package-lock.json
├── package.json
├── screenshots
└── Preview.png
├── src
├── app
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── connected
│ │ ├── connected-routing.module.ts
│ │ ├── connected.component.html
│ │ ├── connected.component.scss
│ │ ├── connected.component.spec.ts
│ │ ├── connected.component.ts
│ │ └── connected.module.ts
│ ├── core
│ │ ├── core.module.ts
│ │ └── services
│ │ │ ├── electron
│ │ │ ├── electron.service.spec.ts
│ │ │ └── electron.service.ts
│ │ │ ├── index.ts
│ │ │ ├── peer-js.service.spec.ts
│ │ │ └── peer-js.service.ts
│ ├── peer-connection-dialog
│ │ ├── peer-connection-dialog.component.html
│ │ ├── peer-connection-dialog.component.scss
│ │ ├── peer-connection-dialog.component.spec.ts
│ │ └── peer-connection-dialog.component.ts
│ ├── pre-connect
│ │ ├── pre-connect-routing.module.ts
│ │ ├── pre-connect.component.html
│ │ ├── pre-connect.component.scss
│ │ ├── pre-connect.component.spec.ts
│ │ ├── pre-connect.component.ts
│ │ └── pre-connect.module.ts
│ ├── settings-dialog
│ │ ├── settings-dialog.component.html
│ │ ├── settings-dialog.component.scss
│ │ ├── settings-dialog.component.spec.ts
│ │ └── settings-dialog.component.ts
│ ├── shared
│ │ ├── components
│ │ │ ├── index.ts
│ │ │ ├── logo
│ │ │ │ ├── logo.component.html
│ │ │ │ ├── logo.component.scss
│ │ │ │ ├── logo.component.spec.ts
│ │ │ │ └── logo.component.ts
│ │ │ ├── page-not-found
│ │ │ │ ├── page-not-found.component.html
│ │ │ │ ├── page-not-found.component.scss
│ │ │ │ ├── page-not-found.component.spec.ts
│ │ │ │ └── page-not-found.component.ts
│ │ │ └── sidepanel
│ │ │ │ ├── sidepanel.component.html
│ │ │ │ ├── sidepanel.component.scss
│ │ │ │ ├── sidepanel.component.spec.ts
│ │ │ │ └── sidepanel.component.ts
│ │ ├── directives
│ │ │ ├── index.ts
│ │ │ └── webview
│ │ │ │ ├── webview.directive.spec.ts
│ │ │ │ └── webview.directive.ts
│ │ ├── peerjs
│ │ │ ├── PeerConfiguration.ts
│ │ │ ├── PeerConnection.ts
│ │ │ ├── PeerInstance.ts
│ │ │ ├── PeerMessageData.ts
│ │ │ └── PeerMessageDataType.ts
│ │ └── shared.module.ts
│ ├── stream-peer-block
│ │ ├── stream-peer-block.component.html
│ │ ├── stream-peer-block.component.scss
│ │ ├── stream-peer-block.component.spec.ts
│ │ ├── stream-peer-block.component.ts
│ │ └── stream-peer-block.module.ts
│ └── stream-screen-picker
│ │ ├── stream-screen-picker.component.html
│ │ ├── stream-screen-picker.component.scss
│ │ ├── stream-screen-picker.component.spec.ts
│ │ └── stream-screen-picker.component.ts
├── assets
│ ├── .gitkeep
│ ├── background.jpg
│ ├── i18n
│ │ └── en.json
│ └── icons
│ │ ├── clipboard.svg
│ │ ├── electron.bmp
│ │ ├── favicon.256x256.png
│ │ ├── favicon.512x512.png
│ │ ├── favicon.icns
│ │ ├── favicon.ico
│ │ ├── favicon.png
│ │ ├── screen.svg
│ │ └── screen_close.svg
├── environments
│ ├── environment.dev.ts
│ ├── environment.prod.ts
│ ├── environment.ts
│ ├── environment.web.prod.ts
│ └── environment.web.ts
├── favicon.ico
├── index.html
├── karma.conf.js
├── main.ts
├── polyfills-test.ts
├── polyfills.ts
├── styles.scss
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
├── tailwind.config.js
├── tsconfig.json
└── tsconfig.serve.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": [
4 | "app/**/*", // ignore nodeJs files
5 | "dist/**/*",
6 | "release/**/*"
7 | ],
8 | "overrides": [
9 | {
10 | "files": [
11 | "*.ts"
12 | ],
13 | "parserOptions": {
14 | "project": [
15 | "./tsconfig.serve.json",
16 | "./src/tsconfig.app.json",
17 | "./src/tsconfig.spec.json",
18 | "./e2e/tsconfig.e2e.json"
19 | ],
20 | "createDefaultProgram": true
21 | },
22 | "extends": [
23 | "plugin:@angular-eslint/ng-cli-compat",
24 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
25 | "plugin:@angular-eslint/template/process-inline-templates"
26 | ],
27 | "rules": {
28 | "prefer-arrow/prefer-arrow-functions": 0,
29 | "@angular-eslint/directive-selector": 0,
30 | "@angular-eslint/component-selector": [
31 | "error",
32 | {
33 | "type": "element",
34 | "prefix": "app",
35 | "style": "kebab-case"
36 | }
37 | ]
38 | }
39 | },
40 | {
41 | "files": [
42 | "*.html"
43 | ],
44 | "extends": [
45 | "plugin:@angular-eslint/template/recommended"
46 | ],
47 | "rules": {
48 | }
49 | }
50 | ]
51 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 15
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy
2 | on: [push]
3 | permissions:
4 | contents: write
5 | jobs:
6 | build-and-deploy:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout 🛎️
10 | uses: actions/checkout@v3
11 |
12 | - name: Install and Build 🔧
13 | run: |
14 | npm install
15 | npm run web:build
16 |
17 | - name: Deploy 🚀
18 | uses: JamesIves/github-pages-deploy-action@v4
19 | with:
20 | branch: gh-pages
21 | folder: dist
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /app-builds
8 | /release
9 | main.js
10 | src/**/*.js
11 | !src/karma.conf.js
12 | *.js.map
13 |
14 | # dependencies
15 | node_modules
16 |
17 | # IDEs and editors
18 | .idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | .vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 |
33 | # misc
34 | /.angular/cache
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | testem.log
41 | /typings
42 |
43 | # e2e
44 | /e2e/*.js
45 | !/e2e/protractor.conf.js
46 | /e2e/*.map
47 | /e2e/tracing
48 | /e2e/screenshots
49 |
50 | # System Files
51 | .DS_Store
52 | Thumbs.db
53 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 16
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save=true
2 | save-exact=true
3 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Renderer",
9 | "type": "chrome",
10 | "request": "attach",
11 | "port": 9876,
12 | "url": "http://localhost:4200",
13 | "sourceMaps": true,
14 | "timeout": 10000,
15 | "trace": "verbose",
16 | "sourceMapPathOverrides": {
17 | "webpack:///./*": "${workspaceFolder}/*"
18 | },
19 | "preLaunchTask": "Build.Renderer"
20 | },
21 | {
22 | "name": "Main",
23 | "type": "node",
24 | "request": "launch",
25 | "protocol": "inspector",
26 | "cwd": "${workspaceFolder}",
27 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
28 | "trace": "verbose",
29 | "runtimeArgs": [
30 | "--serve",
31 | ".",
32 | "--remote-debugging-port=9876"
33 | ],
34 | "windows": {
35 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
36 | },
37 | "preLaunchTask": "Build.Main"
38 | }
39 | ],
40 | "compounds": [
41 | {
42 | "name": "Application Debug",
43 | "configurations": [ "Renderer", "Main" ]
44 | }
45 | ]
46 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Build.Main",
6 | "type": "shell",
7 | "command": "npm run electron:serve-tsc",
8 | "isBackground": false,
9 | "group": {
10 | "kind": "build",
11 | "isDefault": true
12 | },
13 | "problemMatcher": {
14 | "owner": "typescript",
15 | "source": "ts",
16 | "applyTo": "closedDocuments",
17 | "fileLocation": ["relative", "${cwd}"],
18 | "pattern": "$tsc",
19 | "background": {
20 | "activeOnStart": true,
21 | "beginsPattern": "^.*",
22 | "endsPattern": "^.*Terminal will be reused by tasks, press any key to close it.*"
23 | }
24 | }
25 | },
26 | {
27 | "label": "Build.Renderer",
28 | "type": "shell",
29 | "command": "npm run ng:serve",
30 | "isBackground": true,
31 | "group": {
32 | "kind": "build",
33 | "isDefault": true
34 | },
35 | "problemMatcher": {
36 | "owner": "typescript",
37 | "source": "ts",
38 | "applyTo": "closedDocuments",
39 | "fileLocation": ["relative", "${cwd}"],
40 | "pattern": "$tsc",
41 | "background": {
42 | "activeOnStart": true,
43 | "beginsPattern": "^.*",
44 | "endsPattern": "^.*Compiled successfully.*"
45 | }
46 | }
47 | }
48 | ]
49 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2022 - Maxime GRIS, w3yden
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📺 UltraScreen
2 | UltraScreen is a screensharing web app.
3 |
4 | ## Introduction
5 | You can share your screen to another UltraScreen user. This project uses [PeerJS](https://peerjs.com/) to broker connections and abstract direct WebRTC interactions in code. As the [WebRTC standard](https://datatracker.ietf.org/doc/html/rfc8831) specifies, your stream is secured with [SRTP](https://datatracker.ietf.org/doc/html/rfc3711) and all stream data flows between you and your connected peers (No TURN server is specified in ultrascreen).
6 |
7 | For connection brokering, it uses the free PeerServer Cloud service provided by PeerJS, which you can support [here](https://opencollective.com/peer).
8 |
9 | This is a rewrite of the electron-vue version, which was a publish-and-forget project for me. I wanted to learn the Angular framework and decided to do this with this rewrite (and also address open issues). The rewrite is based upon the [angular-electron](https://github.com/maximegris/angular-electron) template, which currently uses Angular v14 and Electron v19.
10 |
11 | The default stream resolution is currently always 1280x720.
12 |
13 | ## Download UltraScreen
14 | You can download desktop versions of UltraScreen for Windows and Linux (AppImage) at the [Release Page](https://github.com/w3yden/ultrascreen/releases).
15 |
16 | Now you can also use it directly in your webbrowser at [w3yden.github.io/ultrascreen](https://w3yden.github.io/ultrascreen).
17 | ## Gallery
18 |
19 | 
20 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-architect
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "cli": {
4 | "defaultCollection": "@angular-eslint/schematics",
5 | "analytics": false
6 | },
7 | "version": 1,
8 | "newProjectRoot": "projects",
9 | "projects": {
10 | "ultrascreen": {
11 | "root": "",
12 | "sourceRoot": "src",
13 | "projectType": "application",
14 | "schematics": {
15 | "@schematics/angular:application": {
16 | "strict": true
17 | },
18 | "@schematics/angular:component": {
19 | "style": "scss"
20 | }
21 | },
22 | "prefix": "app",
23 | "architect": {
24 | "build": {
25 | "builder": "@angular-builders/custom-webpack:browser",
26 | "options": {
27 | "outputPath": "dist",
28 | "index": "src/index.html",
29 | "main": "src/main.ts",
30 | "tsConfig": "src/tsconfig.app.json",
31 | "polyfills": "src/polyfills.ts",
32 | "inlineStyleLanguage": "scss",
33 | "assets": [
34 | "src/favicon.ico",
35 | "src/assets"
36 | ],
37 | "styles": [
38 | "src/styles.scss",
39 | "node_modules/ngx-toastr/toastr.css",
40 | "node_modules/primeng/resources/themes/bootstrap4-light-blue/theme.css",
41 | "node_modules/primeng/resources/primeng.min.css",
42 | "node_modules/primeicons/primeicons.css"
43 | ],
44 | "scripts": [],
45 | "customWebpackConfig": {
46 | "path": "./angular.webpack.js",
47 | "replaceDuplicatePlugins": true
48 | }
49 | },
50 | "configurations": {
51 | "dev": {
52 | "optimization": false,
53 | "outputHashing": "none",
54 | "sourceMap": true,
55 | "namedChunks": false,
56 | "aot": false,
57 | "extractLicenses": true,
58 | "vendorChunk": false,
59 | "buildOptimizer": false,
60 | "fileReplacements": [
61 | {
62 | "replace": "src/environments/environment.ts",
63 | "with": "src/environments/environment.dev.ts"
64 | }
65 | ]
66 | },
67 | "production": {
68 | "optimization": true,
69 | "outputHashing": "all",
70 | "sourceMap": false,
71 | "namedChunks": false,
72 | "aot": true,
73 | "extractLicenses": true,
74 | "vendorChunk": false,
75 | "buildOptimizer": true,
76 | "fileReplacements": [
77 | {
78 | "replace": "src/environments/environment.ts",
79 | "with": "src/environments/environment.prod.ts"
80 | }
81 | ]
82 | },
83 | "web": {
84 | "optimization": false,
85 | "outputHashing": "none",
86 | "sourceMap": true,
87 | "namedChunks": false,
88 | "aot": false,
89 | "extractLicenses": true,
90 | "vendorChunk": false,
91 | "buildOptimizer": false,
92 | "fileReplacements": [
93 | {
94 | "replace": "src/environments/environment.ts",
95 | "with": "src/environments/environment.web.ts"
96 | }
97 | ]
98 | },
99 | "web-production": {
100 | "optimization": true,
101 | "outputHashing": "all",
102 | "sourceMap": false,
103 | "namedChunks": false,
104 | "aot": true,
105 | "extractLicenses": true,
106 | "vendorChunk": false,
107 | "buildOptimizer": true,
108 | "fileReplacements": [
109 | {
110 | "replace": "src/environments/environment.ts",
111 | "with": "src/environments/environment.web.prod.ts"
112 | }
113 | ]
114 | }
115 | }
116 | },
117 | "serve": {
118 | "builder": "@angular-builders/custom-webpack:dev-server",
119 | "options": {
120 | "browserTarget": "ultrascreen:build"
121 | },
122 | "configurations": {
123 | "dev": {
124 | "browserTarget": "ultrascreen:build:dev"
125 | },
126 | "production": {
127 | "browserTarget": "ultrascreen:build:production"
128 | },
129 | "web": {
130 | "browserTarget": "ultrascreen:build:web"
131 | },
132 | "web-production": {
133 | "browserTarget": "ultrascreen:build:web-production"
134 | }
135 | }
136 | },
137 | "extract-i18n": {
138 | "builder": "@angular-devkit/build-angular:extract-i18n",
139 | "options": {
140 | "browserTarget": "ultrascreen:build"
141 | }
142 | },
143 | "test": {
144 | "builder": "@angular-builders/custom-webpack:karma",
145 | "options": {
146 | "main": "src/test.ts",
147 | "polyfills": "src/polyfills-test.ts",
148 | "tsConfig": "src/tsconfig.spec.json",
149 | "karmaConfig": "src/karma.conf.js",
150 | "inlineStyleLanguage": "scss",
151 | "scripts": [],
152 | "styles": [
153 | "src/styles.scss"
154 | ],
155 | "assets": [
156 | "src/favicon.ico",
157 | "src/assets"
158 | ],
159 | "customWebpackConfig": {
160 | "path": "./angular.webpack.js",
161 | "replaceDuplicatePlugins": true
162 | }
163 | }
164 | },
165 | "lint": {
166 | "builder": "@angular-eslint/builder:lint",
167 | "options": {
168 | "lintFilePatterns": [
169 | "src/**/*.ts",
170 | "src/**/*.html"
171 | ]
172 | }
173 | }
174 | }
175 | },
176 | "ultrascreen-e2e": {
177 | "root": "e2e",
178 | "projectType": "application",
179 | "architect": {
180 | "lint": {
181 | "builder": "@angular-eslint/builder:lint",
182 | "options": {
183 | "lintFilePatterns": [
184 | "e2e/**/*.ts"
185 | ]
186 | }
187 | }
188 | }
189 | }
190 | },
191 | "defaultProject": "ultrascreen"
192 | }
193 |
--------------------------------------------------------------------------------
/angular.webpack.js:
--------------------------------------------------------------------------------
1 | //Polyfill Node.js core modules in Webpack. This module is only needed for webpack 5+.
2 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
3 |
4 | /**
5 | * Custom angular webpack configuration
6 | */
7 | module.exports = (config, options) => {
8 | config.target = 'electron-renderer';
9 |
10 | if (options.fileReplacements) {
11 | for(let fileReplacement of options.fileReplacements) {
12 | if (fileReplacement.replace !== 'src/environments/environment.ts') {
13 | continue;
14 | }
15 |
16 | let fileReplacementParts = fileReplacement['with'].split('.');
17 | if (fileReplacementParts.length > 1 && ['web'].indexOf(fileReplacementParts[1]) >= 0) {
18 | config.target = 'web';
19 | }
20 | break;
21 | }
22 | }
23 |
24 | config.plugins = [
25 | ...config.plugins,
26 | new NodePolyfillPlugin({
27 | excludeAliases: ["console"]
28 | })
29 | ];
30 |
31 | config.resolve = {
32 | ...config.resolve,
33 | alias: {
34 | ...config.resolve.alias,
35 | assets: require('path').resolve(__dirname, "src/assets")
36 | }
37 | };
38 |
39 | return config;
40 | }
41 |
--------------------------------------------------------------------------------
/app/main.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, screen, ipcMain, desktopCapturer } from 'electron';
2 | import * as path from 'path';
3 | import * as fs from 'fs';
4 | import * as url from 'url';
5 |
6 | let win: BrowserWindow = null;
7 | const args = process.argv.slice(1),
8 | serve = args.some(val => val === '--serve');
9 |
10 | function createWindow(): BrowserWindow {
11 |
12 | const electronScreen = screen;
13 | const size = electronScreen.getPrimaryDisplay().workAreaSize;
14 |
15 | // Create the browser window.
16 | win = new BrowserWindow({
17 | x: 0,
18 | y: 0,
19 | width: size.width,
20 | height: size.height,
21 | webPreferences: {
22 | nodeIntegration: true,
23 | allowRunningInsecureContent: (serve) ? true : false,
24 | contextIsolation: false, // false if you want to run e2e test with Spectron
25 | },
26 | });
27 |
28 | if (serve) {
29 | const debug = require('electron-debug');
30 | debug();
31 |
32 | require('electron-reloader')(module);
33 | win.loadURL('http://localhost:4200');
34 | } else {
35 | // Path when running electron executable
36 | let pathIndex = './index.html';
37 |
38 | if (fs.existsSync(path.join(__dirname, '../dist/index.html'))) {
39 | // Path when running electron in local folder
40 | pathIndex = '../dist/index.html';
41 | }
42 |
43 | win.loadURL(url.format({
44 | pathname: path.join(__dirname, pathIndex),
45 | protocol: 'file:',
46 | slashes: true
47 | }));
48 | }
49 |
50 | // Emitted when the window is closed.
51 | win.on('closed', () => {
52 | // Dereference the window object, usually you would store window
53 | // in an array if your app supports multi windows, this is the time
54 | // when you should delete the corresponding element.
55 | win = null;
56 | });
57 |
58 | ipcMain.handle('get-desktop-sources', async () => {
59 | return await desktopCapturer.getSources({ types: ['window', 'screen'] }).then((sources) => {return sources;});
60 | });
61 |
62 | return win;
63 | }
64 |
65 | try {
66 | // This method will be called when Electron has finished
67 | // initialization and is ready to create browser windows.
68 | // Some APIs can only be used after this event occurs.
69 | // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
70 | app.on('ready', () => setTimeout(createWindow, 400));
71 |
72 | // Quit when all windows are closed.
73 | app.on('window-all-closed', () => {
74 | // On OS X it is common for applications and their menu bar
75 | // to stay active until the user quits explicitly with Cmd + Q
76 | if (process.platform !== 'darwin') {
77 | app.quit();
78 | }
79 | });
80 |
81 | app.on('activate', () => {
82 | // On OS X it's common to re-create a window in the app when the
83 | // dock icon is clicked and there are no other windows open.
84 | if (win === null) {
85 | createWindow();
86 | }
87 | });
88 |
89 | } catch (e) {
90 | // Catch Error
91 | // throw e;
92 | }
93 |
--------------------------------------------------------------------------------
/app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-electron",
3 | "version": "11.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "angular-electron",
9 | "version": "11.0.0",
10 | "dependencies": {
11 | "@angular/animations": "^14.1.0"
12 | }
13 | },
14 | "node_modules/@angular/animations": {
15 | "version": "14.1.0",
16 | "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.1.0.tgz",
17 | "integrity": "sha512-OhEXi1u/M4QyltDCxSqo7YzF7ELgNDWNqbbM7vtWIcrc4c+Yiu1GXhW/GQRosF3WAuQVfdQzEI0VTeNoo98Kvw==",
18 | "dependencies": {
19 | "tslib": "^2.3.0"
20 | },
21 | "engines": {
22 | "node": "^14.15.0 || >=16.10.0"
23 | },
24 | "peerDependencies": {
25 | "@angular/core": "14.1.0"
26 | }
27 | },
28 | "node_modules/@angular/core": {
29 | "version": "14.1.0",
30 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.1.0.tgz",
31 | "integrity": "sha512-3quEsHmQifJOQ2oij5K+cjGjmhsKsyZI1+OTHWNZ6IXeuYviZv4U/Cui9fUJ1RN3CZxH3NzWB3gB/5qYFQfOgg==",
32 | "peer": true,
33 | "dependencies": {
34 | "tslib": "^2.3.0"
35 | },
36 | "engines": {
37 | "node": "^14.15.0 || >=16.10.0"
38 | },
39 | "peerDependencies": {
40 | "rxjs": "^6.5.3 || ^7.4.0",
41 | "zone.js": "~0.11.4"
42 | }
43 | },
44 | "node_modules/rxjs": {
45 | "version": "7.5.6",
46 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
47 | "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
48 | "peer": true,
49 | "dependencies": {
50 | "tslib": "^2.1.0"
51 | }
52 | },
53 | "node_modules/tslib": {
54 | "version": "2.4.0",
55 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
56 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
57 | },
58 | "node_modules/zone.js": {
59 | "version": "0.11.7",
60 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.7.tgz",
61 | "integrity": "sha512-e39K2EdK5JfA3FDuUTVRvPlYV4aBfnOOcGuILhQAT7nzeV12uSrLBzImUM9CDVoncDSX4brR/gwqu0heQ3BQ0g==",
62 | "peer": true,
63 | "dependencies": {
64 | "tslib": "^2.3.0"
65 | }
66 | }
67 | },
68 | "dependencies": {
69 | "@angular/animations": {
70 | "version": "14.1.0",
71 | "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.1.0.tgz",
72 | "integrity": "sha512-OhEXi1u/M4QyltDCxSqo7YzF7ELgNDWNqbbM7vtWIcrc4c+Yiu1GXhW/GQRosF3WAuQVfdQzEI0VTeNoo98Kvw==",
73 | "requires": {
74 | "tslib": "^2.3.0"
75 | }
76 | },
77 | "@angular/core": {
78 | "version": "14.1.0",
79 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.1.0.tgz",
80 | "integrity": "sha512-3quEsHmQifJOQ2oij5K+cjGjmhsKsyZI1+OTHWNZ6IXeuYviZv4U/Cui9fUJ1RN3CZxH3NzWB3gB/5qYFQfOgg==",
81 | "peer": true,
82 | "requires": {
83 | "tslib": "^2.3.0"
84 | }
85 | },
86 | "rxjs": {
87 | "version": "7.5.6",
88 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
89 | "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
90 | "peer": true,
91 | "requires": {
92 | "tslib": "^2.1.0"
93 | }
94 | },
95 | "tslib": {
96 | "version": "2.4.0",
97 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
98 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
99 | },
100 | "zone.js": {
101 | "version": "0.11.7",
102 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.7.tgz",
103 | "integrity": "sha512-e39K2EdK5JfA3FDuUTVRvPlYV4aBfnOOcGuILhQAT7nzeV12uSrLBzImUM9CDVoncDSX4brR/gwqu0heQ3BQ0g==",
104 | "peer": true,
105 | "requires": {
106 | "tslib": "^2.3.0"
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ultrascreen",
3 | "version": "1.0.0",
4 | "main": "main.js",
5 | "private": true,
6 | "dependencies": {
7 | "@angular/animations": "^14.1.0"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/e2e/main.spec.ts:
--------------------------------------------------------------------------------
1 | import { BrowserContext, ElectronApplication, Page, _electron as electron } from 'playwright';
2 | import { test, expect } from '@playwright/test';
3 | const PATH = require('path');
4 |
5 | test.describe('Check Home Page', async () => {
6 | let app: ElectronApplication;
7 | let firstWindow: Page;
8 | let context: BrowserContext;
9 |
10 | test.beforeAll( async () => {
11 | app = await electron.launch({ args: [PATH.join(__dirname, '../app/main.js'), PATH.join(__dirname, '../app/package.json')] });
12 | context = app.context();
13 | await context.tracing.start({ screenshots: true, snapshots: true });
14 | firstWindow = await app.firstWindow();
15 | await firstWindow.waitForLoadState('domcontentloaded');
16 | });
17 |
18 | test('Launch electron app', async () => {
19 |
20 | const windowState: { isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean } = await app.evaluate(async (process) => {
21 | const mainWindow = process.BrowserWindow.getAllWindows()[0];
22 |
23 | const getState = () => ({
24 | isVisible: mainWindow.isVisible(),
25 | isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
26 | isCrashed: mainWindow.webContents.isCrashed(),
27 | });
28 |
29 | return new Promise((resolve) => {
30 | if (mainWindow.isVisible()) {
31 | resolve(getState());
32 | } else {
33 | mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0));
34 | }
35 | });
36 | });
37 |
38 | expect(windowState.isVisible).toBeTruthy();
39 | expect(windowState.isDevToolsOpened).toBeFalsy();
40 | expect(windowState.isCrashed).toBeFalsy();
41 | });
42 |
43 | // test('Check Home Page design', async ({ browserName}) => {
44 | // // Uncomment if you change the design of Home Page in order to create a new screenshot
45 | // const screenshot = await firstWindow.screenshot({ path: '/tmp/home.png' });
46 | // expect(screenshot).toMatchSnapshot(`home-${browserName}.png`);
47 | // });
48 |
49 | test('Check title', async () => {
50 | const elem = await firstWindow.$('app-home h1');
51 | const text = await elem.innerText();
52 | expect(text).toBe('App works !');
53 | });
54 |
55 | test.afterAll( async () => {
56 | await context.tracing.stop({ path: 'e2e/tracing/trace.zip' });
57 | await app.close();
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/e2e/playwright.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('@playwright/test').PlaywrightTestConfig} */
2 | const config = {
3 | testDir: '.',
4 | timeout: 45000,
5 | outputDir: './screenshots',
6 | use: {
7 | headless: false,
8 | viewport: { width: 1280, height: 720 },
9 | launchOptions: {
10 | slowMo: 1000,
11 | },
12 | trace: 'on',
13 | },
14 | expect: {
15 | toMatchSnapshot: { threshold: 0.2 },
16 | },
17 | };
18 |
19 | module.exports = config;
20 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "types": [
7 | "node"
8 | ]
9 | },
10 | "include": [
11 | "**.spec.ts"
12 | ],
13 | }
14 |
--------------------------------------------------------------------------------
/electron-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "asar": false,
3 | "directories": {
4 | "output": "release/"
5 | },
6 | "files": [
7 | "**/*",
8 | "!**/*.ts",
9 | "!*.map",
10 | "!package.json",
11 | "!package-lock.json"
12 | ],
13 | "extraResources": [
14 | {
15 | "from": "dist",
16 | "to": "app",
17 | "filter": [
18 | "**/*"
19 | ]
20 | }
21 | ],
22 | "win": {
23 | "icon": "dist/assets/icons",
24 | "target": [
25 | "portable"
26 | ]
27 | },
28 | "portable": {
29 | "splashImage": "dist/assets/icons/electron.bmp"
30 | },
31 | "mac": {
32 | "icon": "dist/assets/icons",
33 | "target": [
34 | "dmg"
35 | ]
36 | },
37 | "linux": {
38 | "icon": "dist/assets/icons",
39 | "target": [
40 | "AppImage"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ultrascreen",
3 | "version": "1.0.0",
4 | "description": "Share your screen",
5 | "homepage": "https://github.com/w3yden/ultrascreen",
6 | "author": {
7 | "name": "W3yden",
8 | "email": "w3yden@protonmail.com"
9 | },
10 | "keywords": [
11 | "screenshare",
12 | "peerjs",
13 | "webrtc",
14 | "angular 14",
15 | "electron",
16 | "electron 19",
17 | "nodejs",
18 | "typescript",
19 | "eslint",
20 | "sass",
21 | "windows",
22 | "linux"
23 | ],
24 | "main": "app/main.js",
25 | "private": true,
26 | "scripts": {
27 | "postinstall": "electron-builder install-app-deps",
28 | "ng": "ng",
29 | "start": "npm-run-all -p electron:serve ng:serve",
30 | "ng:serve": "ng serve -c web -o",
31 | "build": "npm run electron:serve-tsc && ng build --base-href ./",
32 | "build:dev": "npm run build -- -c dev",
33 | "build:prod": "npm run build -- -c production",
34 | "web:build": "npm run build -- -c web-production",
35 | "electron": "electron",
36 | "electron:serve-tsc": "tsc -p tsconfig.serve.json",
37 | "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && electron . --serve",
38 | "electron:local": "npm run build:prod && electron .",
39 | "electron:build:win": "npm run build:prod && electron-builder build --publish=never --win",
40 | "electron:build:linux": "npm run build:prod && electron-builder build --publish=never",
41 | "test": "ng test --watch=false",
42 | "test:watch": "ng test",
43 | "e2e": "npm run build:prod && playwright test -c e2e/playwright.config.ts e2e/",
44 | "e2e:show-trace": "playwright show-trace e2e/tracing/trace.zip",
45 | "lint": "ng lint"
46 | },
47 | "dependencies": {
48 | "@angular/animations": "14.1.0",
49 | "@angular/common": "14.1.0",
50 | "@angular/compiler": "14.1.0",
51 | "@angular/core": "14.1.0",
52 | "@angular/forms": "14.1.0",
53 | "@angular/language-service": "14.1.0",
54 | "@angular/platform-browser": "14.1.0",
55 | "@angular/platform-browser-dynamic": "14.1.0",
56 | "@angular/router": "14.1.0",
57 | "@fortawesome/angular-fontawesome": "0.11.1",
58 | "@fortawesome/fontawesome-svg-core": "6.1.2",
59 | "@fortawesome/free-solid-svg-icons": "6.1.2",
60 | "ngx-toastr": "15.0.0",
61 | "peerjs": "1.4.6",
62 | "primeicons": "5.0.0",
63 | "primeng": "14.0.0",
64 | "rxjs": "7.5.6",
65 | "tslib": "^2.4.0",
66 | "zone.js": "~0.11.6"
67 | },
68 | "devDependencies": {
69 | "@angular-builders/custom-webpack": "14.0.0",
70 | "@angular-devkit/build-angular": "14.2.11",
71 | "@angular-eslint/builder": "14.0.2",
72 | "@angular-eslint/eslint-plugin": "14.0.2",
73 | "@angular-eslint/eslint-plugin-template": "14.0.2",
74 | "@angular-eslint/schematics": "14.0.2",
75 | "@angular-eslint/template-parser": "14.0.2",
76 | "@angular/cli": "14.0.6",
77 | "@angular/compiler-cli": "14.1.0",
78 | "@ngx-translate/core": "14.0.0",
79 | "@ngx-translate/http-loader": "7.0.0",
80 | "@playwright/test": "1.23.4",
81 | "@types/jasmine": "4.0.3",
82 | "@types/jasminewd2": "2.0.10",
83 | "@types/node": "18.0.6",
84 | "@typescript-eslint/eslint-plugin": "5.30.7",
85 | "@typescript-eslint/parser": "5.30.7",
86 | "autoprefixer": "10.4.8",
87 | "electron": "19.1.8",
88 | "electron-builder": "23.1.0",
89 | "electron-debug": "3.2.0",
90 | "electron-reloader": "1.2.3",
91 | "eslint": "8.20.0",
92 | "eslint-plugin-import": "2.26.0",
93 | "eslint-plugin-jsdoc": "39.3.3",
94 | "eslint-plugin-prefer-arrow": "1.2.3",
95 | "jasmine-core": "4.2.0",
96 | "jasmine-spec-reporter": "7.0.0",
97 | "karma": "6.4.0",
98 | "karma-coverage-istanbul-reporter": "3.0.3",
99 | "karma-electron": "7.2.0",
100 | "karma-jasmine": "5.1.0",
101 | "karma-jasmine-html-reporter": "2.0.0",
102 | "node-polyfill-webpack-plugin": "2.0.0",
103 | "npm-run-all": "4.1.5",
104 | "playwright": "1.23.4",
105 | "postcss": "8.4.14",
106 | "tailwindcss": "3.1.7",
107 | "ts-node": "10.9.1",
108 | "typescript": "~4.7.4",
109 | "wait-on": "6.0.1",
110 | "webdriver-manager": "12.1.8"
111 | },
112 | "engines": {
113 | "node": ">=14.0.0"
114 | },
115 | "browserslist": [
116 | "chrome 100"
117 | ]
118 | }
119 |
--------------------------------------------------------------------------------
/screenshots/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/screenshots/Preview.png
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import { PageNotFoundComponent } from './shared/components';
4 |
5 | import { ConnectedModule } from './connected/connected.module';
6 | import { PreConnectModule } from './pre-connect/pre-connect.module';
7 |
8 | const routes: Routes = [
9 | {
10 | path: '',
11 | redirectTo: 'home',
12 | pathMatch: 'full'
13 | },
14 | {
15 | path: '**',
16 | component: PageNotFoundComponent
17 | }
18 | ];
19 |
20 | @NgModule({
21 | imports: [
22 | RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' }),
23 | ConnectedModule,
24 | PreConnectModule
25 | ],
26 | exports: [RouterModule]
27 | })
28 | export class AppRoutingModule { }
29 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | width: 100%;
3 | height: 100%;
4 | display: block;
5 | }
6 |
7 | main {
8 | width: 100%;
9 | height: 100%;
10 | display: block;
11 | }
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, waitForAsync } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 | import { TranslateModule } from '@ngx-translate/core';
5 | import { ElectronService } from './core/services';
6 |
7 | describe('AppComponent', () => {
8 | beforeEach(waitForAsync(() => {
9 | TestBed.configureTestingModule({
10 | declarations: [AppComponent],
11 | providers: [ElectronService],
12 | imports: [RouterTestingModule, TranslateModule.forRoot()]
13 | }).compileComponents();
14 | }));
15 |
16 | it('should create the app', waitForAsync(() => {
17 | const fixture = TestBed.createComponent(AppComponent);
18 | const app = fixture.debugElement.componentInstance;
19 | expect(app).toBeTruthy();
20 | }));
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ElectronService } from './core/services';
3 | import { TranslateService } from '@ngx-translate/core';
4 | import { APP_CONFIG } from '../environments/environment';
5 | import { PrimeNGConfig } from 'primeng/api';
6 | import {
7 | trigger,
8 | animate,
9 | style,
10 | group,
11 | query as q,
12 | transition,
13 | } from '@angular/animations';
14 | import { RouterOutlet } from '@angular/router';
15 |
16 | const query = (pStyle, pAnimate, pOptional = { optional: true }) =>
17 | q(pStyle, pAnimate, pOptional);
18 |
19 | const fadeInFromDirection = direction => [
20 | query(':enter, :leave', style({ position: 'fixed', width: '100%' })),
21 | group([
22 | query(':leave', [
23 | animate('0.2s ease-out', style({ transform: 'translateX(100%)'})),
24 | ]),
25 | query(':enter', [
26 | style({transform: 'translateX(-90%)'}),
27 | animate('0.2s ease-out', style({ transform: 'translateX(0%)'})),
28 | ]),
29 | ]),
30 | ];
31 |
32 | const routerTransition = trigger('routerTransition', [
33 | transition('* => forward', fadeInFromDirection('forward')),
34 | transition('* => backward', fadeInFromDirection('backward')),
35 | ]);
36 |
37 | @Component({
38 | selector: 'app-root',
39 | templateUrl: './app.component.html',
40 | styleUrls: ['./app.component.scss'],
41 | animations: [routerTransition]
42 | })
43 | export class AppComponent implements OnInit {
44 | constructor(
45 | private electronService: ElectronService,
46 | private translate: TranslateService,
47 | private primengConfig: PrimeNGConfig,
48 | ) {
49 | this.translate.setDefaultLang('en');
50 | console.log('APP_CONFIG', APP_CONFIG);
51 |
52 | if (electronService.isElectron) {
53 | console.log(process.env);
54 | console.log('Run in electron');
55 | console.log('Electron ipcRenderer', this.electronService.ipcRenderer);
56 | console.log('NodeJS childProcess', this.electronService.childProcess);
57 | } else {
58 | console.log('Run in browser');
59 | }
60 | }
61 |
62 | ngOnInit() {
63 | this.primengConfig.ripple = true;
64 | }
65 |
66 | getPageTransition(routerOutlet: RouterOutlet) {
67 | if (routerOutlet.isActivated) {
68 | const { path } = routerOutlet.activatedRoute.routeConfig;
69 | if(path === 'connected') {
70 | return 'backward';
71 | }
72 | return 'forward';
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { HttpClientModule, HttpClient } from '@angular/common/http';
5 | import { CoreModule } from './core/core.module';
6 | import { SharedModule } from './shared/shared.module';
7 |
8 | import { AppRoutingModule } from './app-routing.module';
9 |
10 | // NG Translate
11 | import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
12 | import { TranslateHttpLoader } from '@ngx-translate/http-loader';
13 |
14 | import { ConnectedModule } from './connected/connected.module';
15 | import { PreConnectModule } from './pre-connect/pre-connect.module';
16 |
17 | import { AppComponent } from './app.component';
18 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
19 |
20 | import { ToastrModule } from 'ngx-toastr';
21 | import { PeerJsService } from './core/services/peer-js.service';
22 | import { ElectronService } from './core/services';
23 | // AoT requires an exported function for factories
24 | const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new TranslateHttpLoader(http, './assets/i18n/', '.json');
25 |
26 | @NgModule({
27 | declarations: [AppComponent],
28 | imports: [
29 | BrowserModule,
30 | FormsModule,
31 | HttpClientModule,
32 | CoreModule,
33 | SharedModule,
34 | ConnectedModule,
35 | PreConnectModule,
36 | AppRoutingModule,
37 | BrowserAnimationsModule,
38 | ToastrModule.forRoot(),
39 | TranslateModule.forRoot({
40 | loader: {
41 | provide: TranslateLoader,
42 | useFactory: httpLoaderFactory,
43 | deps: [HttpClient]
44 | }
45 | })
46 | ],
47 | providers: [PeerJsService, ElectronService],
48 | bootstrap: [AppComponent]
49 | })
50 | export class AppModule {}
51 |
--------------------------------------------------------------------------------
/src/app/connected/connected-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { Routes, RouterModule } from '@angular/router';
4 | import { ConnectedComponent } from './connected.component';
5 |
6 | const routes: Routes = [
7 | {
8 | path: 'connected',
9 | component: ConnectedComponent
10 | }
11 | ];
12 |
13 | @NgModule({
14 | declarations: [],
15 | imports: [CommonModule, RouterModule.forChild(routes)],
16 | exports: [RouterModule]
17 | })
18 | export class DetailRoutingModule {}
19 |
--------------------------------------------------------------------------------
/src/app/connected/connected.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
ID:
9 |
{{peerId}}
10 |
11 |
12 |
Name:
13 |
{{nickname}}
14 |
15 |
16 |
17 |
18 |
CONNECTIONS
19 |
20 |
21 |
22 |
23 |
24 | -
25 |
26 |
27 | {{connection.nickname}}
28 |
29 |
X
30 |
31 |
32 |
33 |
34 |
38 |
39 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
59 |
60 |
61 |
62 |
63 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/app/connected/connected.component.scss:
--------------------------------------------------------------------------------
1 | .connected-container {
2 | height: 100vh;
3 | width: 100vw;
4 | }
5 |
6 | .sidepanel-container {
7 | position: relative;
8 | display: flex;
9 | flex-direction: column;
10 | justify-items: center;
11 | align-items: center;
12 | padding-left: 25px;
13 | padding-top: 5px;
14 | height: 100%;
15 | }
16 |
17 | .peer-info {
18 | font-size: 1em;
19 | text-align: center;
20 | }
21 |
22 | .peer-status-line {
23 | margin-top: auto;
24 | margin-bottom: auto;
25 | }
26 |
27 | .pi {
28 | margin-left: 8px;
29 | margin-top: 2px;
30 | }
31 |
32 | .peer-stream-container {
33 | height: 100vh;
34 | background-image: linear-gradient(315deg, #bdd4e7 0%, #8693ab 74%);
35 | margin-left: 5px;
36 | background-color: #bdd4e7;
37 | display: flex;
38 | flex-direction: column;
39 | flex-wrap: wrap;
40 | justify-content: space-around;
41 | align-content: space-around;
42 | align-items: center;
43 | }
44 |
45 | .conn-stream-container {
46 | display: flex;
47 | justify-content: space-evenly;
48 | flex-direction: column;
49 | height: 100%;
50 | width: 250px;
51 | text-align: left;
52 | }
53 |
54 | .screen {
55 | width: 48px;
56 | height: 48px;
57 | background-image: url("~assets/icons/screen.svg");
58 | background-size: 48px;
59 | align-self: center;
60 | }
61 |
62 | .screen:hover,
63 | .clipboard:hover {
64 | filter: invert(52%) sepia(1%) saturate(0%) hue-rotate(225deg) brightness(96%)
65 | contrast(78%);
66 | }
67 |
68 | .screen-streaming {
69 | filter: invert(37%) sepia(70%) saturate(5777%) hue-rotate(6deg) brightness(107%) contrast(103%);
70 | }
71 |
72 | .id-line {
73 | display: flex;
74 | flex-direction: row;
75 | justify-content: space-between;
76 | }
77 |
78 | .stream-section {
79 | text-transform: uppercase;
80 | margin-top: 10px;
81 | text-align: center;
82 | width: 100%;
83 | display: flex;
84 | flex-direction: column;
85 | min-height: 64px;
86 | }
87 |
88 | .footer {
89 | width: 100%;
90 | position: absolute;
91 | padding: 0 10px 0 10px;
92 | bottom: 20px;
93 | display: flex;
94 | flex-direction: row;
95 | justify-content: space-between;
96 | }
97 |
98 | .add-peer {
99 | display: flex;
100 | flex-direction: row;
101 | justify-content: space-between;
102 | height: 30px;
103 | margin-bottom: 20px;
104 | }
105 |
106 | .add-peer-button {
107 | min-width: 25px;
108 | margin-left: 5px;
109 | border-radius: 4px;
110 | background: #007bff;
111 | border-color: #007bff;
112 | color: #ffffff;
113 | transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
114 | }
115 |
116 | .add-peer-button:hover {
117 | background: #0069d9;
118 | color: #ffffff;
119 | border-color: #0069d9;
120 | }
121 |
122 | .add-peer-textfield {
123 | width: 100%;
124 | }
125 |
126 | .disconnect-button {
127 | color: darkred;
128 | text-decoration: underline;
129 | float: left;
130 | }
131 |
132 | .disconnect-button:hover {
133 | color: red;
134 | text-decoration: underline;
135 | }
136 |
137 | .clipboard {
138 | display: inline-block;
139 | width: 16px;
140 | height: 16px;
141 | background-image: url("~assets/icons/clipboard.svg");
142 | background-size: 16px;
143 | }
144 |
145 | .peer-connection {
146 | padding-left: 10px;
147 | display: flex;
148 | justify-content: flex-start;
149 | flex-direction: row;
150 | margin-bottom: 3px;
151 | font-size: 0.9em;
152 | line-height: 36px;
153 |
154 | height: 36px;
155 | line-height: 20px;
156 | text-align: left;
157 |
158 | background-color: white;
159 | border: 2px solid lightgray;
160 | border-radius: 8px;
161 | }
162 |
163 | .peer-status-connected {
164 | height: 12px;
165 | width: 12px;
166 | border-radius: 50%;
167 | display: inline-block;
168 | align-self: center;
169 | margin-right: 5px;
170 | background-color: mediumseagreen;
171 | border: 1px solid green;
172 | }
173 |
174 | .peer-status-streaming {
175 | background-color: violet;
176 | border: 1px solid purple;
177 | }
178 |
179 | .peer-status-id {
180 | display: block;
181 | }
182 |
183 | .peer-remove {
184 | justify-self: flex-end;
185 | justify-content: flex-end;
186 | right: 15px;
187 | position: absolute;
188 | display: inline-block;
189 | color: red;
190 | width: 20px;
191 | height: 20px;
192 | text-align: center;
193 | user-select: none;
194 | }
195 |
196 | .peer-remove:hover {
197 | color: darkred;
198 | }
199 |
200 |
--------------------------------------------------------------------------------
/src/app/connected/connected.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { ConnectedComponent } from './connected.component';
4 | import { TranslateModule } from '@ngx-translate/core';
5 |
6 | import { RouterTestingModule } from '@angular/router/testing';
7 |
8 | describe('ConnectedComponent', () => {
9 | let component: ConnectedComponent;
10 | let fixture: ComponentFixture;
11 |
12 | beforeEach(waitForAsync(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [ConnectedComponent],
15 | imports: [TranslateModule.forRoot(), RouterTestingModule]
16 | }).compileComponents();
17 |
18 | fixture = TestBed.createComponent(ConnectedComponent);
19 | component = fixture.componentInstance;
20 | fixture.detectChanges();
21 | }));
22 |
23 | it('should create', () => {
24 | expect(component).toBeTruthy();
25 | });
26 |
27 | it('should render title in a h1 tag', waitForAsync(() => {
28 | const compiled = fixture.debugElement.nativeElement;
29 | expect(compiled.querySelector('h1').textContent).toContain(
30 | 'PAGES.DETAIL.TITLE'
31 | );
32 | }));
33 | });
34 |
--------------------------------------------------------------------------------
/src/app/connected/connected.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterContentInit,
3 | AfterViewInit,
4 | Component,
5 | ElementRef,
6 | OnDestroy,
7 | OnInit,
8 | ViewChild,
9 | } from '@angular/core';
10 | import { Router } from '@angular/router';
11 | import { PeerJsService } from '../core/services/peer-js.service';
12 | import { ToastrService } from 'ngx-toastr';
13 | import { PeerInstance } from '../shared/peerjs/PeerInstance';
14 | import { fromEvent, Subscription } from 'rxjs';
15 | import { StreamScreenPickerComponent } from '../stream-screen-picker/stream-screen-picker.component';
16 | import { PeerConnection } from '../shared/peerjs/PeerConnection';
17 | import Peer from 'peerjs';
18 | import { PeerConnectionDialogComponent } from '../peer-connection-dialog/peer-connection-dialog.component';
19 | import { SettingsDialogComponent } from '../settings-dialog/settings-dialog.component';
20 | import { PeerConfiguration } from '../shared/peerjs/PeerConfiguration';
21 |
22 | @Component({
23 | selector: 'app-connected',
24 | templateUrl: './connected.component.html',
25 | styleUrls: ['./connected.component.scss'],
26 | })
27 | export class ConnectedComponent implements OnInit, OnDestroy, AfterContentInit {
28 | @ViewChild('streamScreenPicker')
29 | streamScreenPicker: StreamScreenPickerComponent;
30 | @ViewChild('peerConnectionDialog')
31 | peerConnectionDialog: PeerConnectionDialogComponent;
32 | @ViewChild('settingsDialog')
33 | settingsDialog: SettingsDialogComponent;
34 |
35 | nickname: string;
36 | peerId: string;
37 | connectionList: Array;
38 | mediaStreams: Array;
39 | newPeerId = '';
40 | streaming = false;
41 | connection: PeerInstance = undefined;
42 | peerConnectionSubscription: Subscription;
43 | focusedConnection: PeerConnection = undefined;
44 | hasFocusedConnection = false;
45 | newConnection: PeerConnection = undefined;
46 | showConnectionDialog = false;
47 | configuration: PeerConfiguration;
48 |
49 | constructor(
50 | private router: Router,
51 | private peerJsService: PeerJsService,
52 | private toastrService: ToastrService
53 | ) {}
54 |
55 | ngAfterContentInit(): void {
56 | this.peerConnectionSubscription = this.peerJsService.connection.subscribe(
57 | (connection: PeerInstance | null) => {
58 | if (connection === null || connection === undefined) {
59 | console.log('No peer connection exists');
60 | this.connection = undefined;
61 | setTimeout(() => {
62 | this.router.navigate(['home']);
63 | }, 200);
64 | return;
65 | }
66 | if (this.connection !== undefined) {
67 | return;
68 | }
69 | console.log('PeerInstance: ', connection);
70 |
71 | this.connection = connection;
72 |
73 | connection?.onNewPeerConnectedEvent.subscribe(
74 | (newConnection: PeerConnection) => {
75 | this.connectionList.push(newConnection);
76 | // TODO: If another peer connects, the previous popup gets overwritten.
77 | if(newConnection.selfInitiated) {
78 | this.newConnection = newConnection;
79 | this.showConnectionDialog = true;
80 | }
81 | this.toastrService.success(newConnection.nickname, 'Peer connected');
82 | }
83 | );
84 | connection?.onPeerDisconnectedEvent.subscribe(
85 | (removedPeer: PeerConnection) => {
86 | const n = this.connectionList.findIndex(
87 | (value) => value.id === removedPeer.id
88 | );
89 | console.log(n);
90 | if (n !== -1) {
91 | this.toastrService.warning(removedPeer.nickname, 'Peer disconnected');
92 | this.connectionList.splice(n, 1);
93 | }
94 | }
95 | );
96 | connection?.onMediaStreamsChanged.subscribe(
97 | (mediaStreams: PeerConnection[]) => {
98 | console.log(mediaStreams);
99 | this.mediaStreams = mediaStreams;
100 | }
101 | );
102 | connection?.onStoppedStreaming.subscribe(() => {
103 | this.streaming = false;
104 | });
105 | this.nickname = connection?.nickname;
106 | this.peerId = connection?.id;
107 | }
108 | );
109 | this.configuration = this.peerJsService.peerConfiguration;
110 | }
111 |
112 | ngOnInit(): void {
113 | this.connectionList = new Array();
114 | this.hasFocusedConnection = false;
115 | this.focusedConnection = undefined;
116 |
117 | this.nickname = '';
118 | this.peerId = '';
119 | console.log('DetailComponent INIT');
120 | }
121 |
122 | ngOnDestroy(): void {
123 | console.log('DetailComponent DESTROY');
124 | this.peerConnectionSubscription.unsubscribe();
125 | }
126 |
127 | onStreamSelected(stream) {
128 | this.connection?.startStream(stream);
129 | this.streaming = true;
130 | }
131 |
132 | copyMyId() {
133 | navigator.clipboard.writeText(this.peerId);
134 | this.toastrService.info('Copied ID!', null, { timeOut: 1000 });
135 | }
136 |
137 | disconnect() {
138 | this.peerJsService.disconnect();
139 | }
140 |
141 | addPeer() {
142 | this.connection?.addPeer(this.newPeerId);
143 | this.newPeerId = '';
144 | }
145 |
146 | removePeer(peerId: string) {
147 | this.connection?.removePeer(peerId);
148 | }
149 |
150 | openStreamScreenPicker() {
151 | if (this.streaming) {
152 | this.connection?.stopStream();
153 | } else {
154 | this.streamScreenPicker.showPicker();
155 | }
156 | }
157 |
158 | toggleFocus(connection: PeerConnection) {
159 | console.log('focus');
160 | if (this.focusedConnection === undefined && connection !== undefined) {
161 | this.focusedConnection = connection;
162 | this.hasFocusedConnection = true;
163 | } else {
164 | this.hasFocusedConnection = false;
165 | this.focusedConnection = undefined;
166 | }
167 | }
168 |
169 | openCredits() {
170 | this.settingsDialog.showDialog();
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/app/connected/connected.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { DetailRoutingModule } from './connected-routing.module';
5 |
6 | import { ConnectedComponent } from './connected.component';
7 | import { SharedModule } from '../shared/shared.module';
8 |
9 | import { SidebarModule } from 'primeng/sidebar';
10 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
11 | import { StreamScreenPickerComponent } from '../stream-screen-picker/stream-screen-picker.component';
12 |
13 | @NgModule({
14 | declarations: [ConnectedComponent, StreamScreenPickerComponent],
15 | imports: [CommonModule, SharedModule, DetailRoutingModule, SidebarModule, BrowserAnimationsModule],
16 | })
17 | export class ConnectedModule {}
18 |
--------------------------------------------------------------------------------
/src/app/core/core.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | @NgModule({
5 | declarations: [],
6 | imports: [
7 | CommonModule
8 | ]
9 | })
10 | export class CoreModule { }
11 |
--------------------------------------------------------------------------------
/src/app/core/services/electron/electron.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { ElectronService } from './electron.service';
4 |
5 | describe('ElectronService', () => {
6 | beforeEach(() => TestBed.configureTestingModule({}));
7 |
8 | it('should be created', () => {
9 | const service: ElectronService = TestBed.get(ElectronService);
10 | expect(service).toBeTruthy();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/app/core/services/electron/electron.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | // If you import a module but never use any of the imported values other than as TypeScript types,
4 | // the resulting javascript file will look as if you never imported the module at all.
5 | import { ipcRenderer, webFrame, desktopCapturer } from 'electron';
6 | import * as childProcess from 'child_process';
7 | import * as fs from 'fs';
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class ElectronService {
13 | ipcRenderer: typeof ipcRenderer;
14 | webFrame: typeof webFrame;
15 | childProcess: typeof childProcess;
16 | fs: typeof fs;
17 | desktopCapturer: typeof desktopCapturer;
18 |
19 | constructor() {
20 | // Conditional imports
21 | if (this.isElectron) {
22 | this.ipcRenderer = window.require('electron').ipcRenderer;
23 | this.webFrame = window.require('electron').webFrame;
24 |
25 | this.childProcess = window.require('child_process');
26 | this.fs = window.require('fs');
27 |
28 | // Notes :
29 | // * A NodeJS's dependency imported with 'window.require' MUST BE present in `dependencies` of both `app/package.json`
30 | // and `package.json (root folder)` in order to make it work here in Electron's Renderer process (src folder)
31 | // because it will loaded at runtime by Electron.
32 | // * A NodeJS's dependency imported with TS module import (ex: import { Dropbox } from 'dropbox') CAN only be present
33 | // in `dependencies` of `package.json (root folder)` because it is loaded during build phase and does not need to be
34 | // in the final bundle. Reminder : only if not used in Electron's Main process (app folder)
35 |
36 | // If you want to use a NodeJS 3rd party deps in Renderer process,
37 | // ipcRenderer.invoke can serve many common use cases.
38 | // https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args
39 | }
40 | }
41 |
42 | get isElectron(): boolean {
43 | return !!(window && window.process && window.process.type);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/core/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './electron/electron.service';
2 |
--------------------------------------------------------------------------------
/src/app/core/services/peer-js.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { PeerJsService } from './peer-js.service';
4 |
5 | describe('PeerJsService', () => {
6 | let service: PeerJsService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(PeerJsService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/core/services/peer-js.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BehaviorSubject } from 'rxjs';
3 | import { PeerConfiguration } from '../../shared/peerjs/PeerConfiguration';
4 | import { PeerInstance } from '../../shared/peerjs/PeerInstance';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class PeerJsService {
10 | public connectionSubject = new BehaviorSubject(null);
11 | readonly connection = this.connectionSubject.asObservable();
12 | peerConnection: PeerInstance = undefined;
13 | peerConfiguration: PeerConfiguration = undefined;
14 |
15 | constructor() {
16 | this.peerConfiguration = new PeerConfiguration();
17 | this.peerConfiguration.host = '0.peerjs.com';
18 | this.peerConfiguration.port = 443;
19 | this.peerConfiguration.secure = true;
20 | this.peerConfiguration.pingInterval = 2000;
21 | this.peerConfiguration.iceServers = ['stun:stun.l.google.com:19302'];
22 | }
23 |
24 | connect(nickname: string, connectedCallback: () => void) {
25 | if(this.peerConnection !== undefined) {
26 | this.peerConnection.connected.unsubscribe();
27 | }
28 | console.log('Create PeerConnection Instance');
29 |
30 | this.peerConnection = new PeerInstance(nickname, this.peerConfiguration);
31 | this.peerConnection.connected.subscribe((isConnected) => {
32 | if(isConnected) {
33 | console.log('Notify new peerConnection instance');
34 | connectedCallback();
35 | this.connectionSubject.next(this.peerConnection);
36 | }
37 | });
38 | }
39 |
40 | disconnect() {
41 | if(this.connectionSubject.value !== null) {
42 | this.connectionSubject.getValue().destroy();
43 | this.connectionSubject.next(null);
44 | console.log('Disconnect.');
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/peer-connection-dialog/peer-connection-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{newConnection.id}}
5 |
6 |
7 |
{{newConnection.nickname}}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/peer-connection-dialog/peer-connection-dialog.component.scss:
--------------------------------------------------------------------------------
1 | button {
2 | margin-left: 16px;
3 | }
--------------------------------------------------------------------------------
/src/app/peer-connection-dialog/peer-connection-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { PeerConnectionDialogComponent } from './peer-connection-dialog.component';
4 |
5 | describe('PeerConnectionDialogComponent', () => {
6 | let component: PeerConnectionDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ PeerConnectionDialogComponent ]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(PeerConnectionDialogComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/peer-connection-dialog/peer-connection-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
2 | import { PeerConnection } from '../shared/peerjs/PeerConnection';
3 |
4 | @Component({
5 | selector: 'app-peer-connection-dialog',
6 | templateUrl: './peer-connection-dialog.component.html',
7 | styleUrls: ['./peer-connection-dialog.component.scss']
8 | })
9 | export class PeerConnectionDialogComponent implements OnInit {
10 | @Input() display: boolean;
11 | @Input() newConnection: PeerConnection;
12 | @Output() accepted: EventEmitter = new EventEmitter();
13 | @Output() rejected: EventEmitter = new EventEmitter();
14 |
15 | constructor() { }
16 |
17 | ngOnInit(): void {
18 | }
19 |
20 | public showDialog(peerConnection: PeerConnection) {
21 | this.display = true;
22 | this.newConnection = peerConnection;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/pre-connect/pre-connect-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { Routes, RouterModule } from '@angular/router';
4 | import { PreConnectComponent } from './pre-connect.component';
5 |
6 | const routes: Routes = [
7 | {
8 | path: 'home',
9 | component: PreConnectComponent
10 | }
11 | ];
12 |
13 | @NgModule({
14 | declarations: [],
15 | imports: [CommonModule, RouterModule.forChild(routes)],
16 | exports: [RouterModule]
17 | })
18 | export class HomeRoutingModule {}
19 |
--------------------------------------------------------------------------------
/src/app/pre-connect/pre-connect.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/app/pre-connect/pre-connect.component.scss:
--------------------------------------------------------------------------------
1 | .pre-connect-container {
2 | height: 100vh;
3 | }
--------------------------------------------------------------------------------
/src/app/pre-connect/pre-connect.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { PreConnectComponent } from './pre-connect.component';
4 | import { TranslateModule } from '@ngx-translate/core';
5 | import { RouterTestingModule } from '@angular/router/testing';
6 |
7 | describe('PreConnectComponent', () => {
8 | let component: PreConnectComponent;
9 | let fixture: ComponentFixture;
10 |
11 | beforeEach(waitForAsync(() => {
12 | TestBed.configureTestingModule({
13 | declarations: [PreConnectComponent],
14 | imports: [TranslateModule.forRoot(), RouterTestingModule]
15 | }).compileComponents();
16 |
17 | fixture = TestBed.createComponent(PreConnectComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | }));
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 |
26 | it('should render title in a h1 tag', waitForAsync(() => {
27 | const compiled = fixture.debugElement.nativeElement;
28 | expect(compiled.querySelector('h1').textContent).toContain(
29 | 'PAGES.HOME.TITLE'
30 | );
31 | }));
32 | });
33 |
--------------------------------------------------------------------------------
/src/app/pre-connect/pre-connect.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { PeerJsService } from '../core/services/peer-js.service';
4 | import { faSpinner } from '@fortawesome/free-solid-svg-icons';
5 | import { APP_CONFIG } from '../../environments/environment';
6 |
7 | @Component({
8 | selector: 'app-pre-connect',
9 | templateUrl: './pre-connect.component.html',
10 | styleUrls: ['./pre-connect.component.scss']
11 | })
12 | export class PreConnectComponent implements OnInit {
13 | nicknameField = '';
14 | missingNickname = false;
15 | faSpinner = faSpinner;
16 | loading = false;
17 |
18 | constructor(private router: Router, private peerJsService: PeerJsService) { }
19 |
20 | ngOnInit(): void {
21 | console.log('HomeComponent INIT');
22 | if(!APP_CONFIG.production) {
23 | this.peerJsService.connect('devlogin', () => {
24 | this.loading = false;
25 | this.router.navigate(['connected']);
26 | });
27 | }
28 |
29 | }
30 |
31 | tryConnect() {
32 | // Check nickname not empty
33 | if(this.nicknameField === '') {
34 | this.missingNickname = true;
35 | return;
36 | }
37 | this.missingNickname = false;
38 |
39 | this.loading = true;
40 | this.peerJsService.connect(this.nicknameField, () => {
41 | console.log('Connected?');
42 | this.loading = false;
43 | this.router.navigate(['connected']);
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/pre-connect/pre-connect.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { HomeRoutingModule } from './pre-connect-routing.module';
5 |
6 | import { PreConnectComponent } from './pre-connect.component';
7 | import { SharedModule } from '../shared/shared.module';
8 | import { InputTextModule } from 'primeng/inputtext';
9 | import { ButtonModule } from 'primeng/button';
10 |
11 | @NgModule({
12 | declarations: [PreConnectComponent],
13 | imports: [CommonModule, SharedModule, HomeRoutingModule, InputTextModule, ButtonModule]
14 | })
15 | export class PreConnectModule {}
16 |
--------------------------------------------------------------------------------
/src/app/settings-dialog/settings-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 | PeerJs Server: {{configuration.host}}:{{configuration.port}}
3 | IceServers:
4 |
5 | {{iceServer}}
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/settings-dialog/settings-dialog.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/app/settings-dialog/settings-dialog.component.scss
--------------------------------------------------------------------------------
/src/app/settings-dialog/settings-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SettingsDialogComponent } from './settings-dialog.component';
4 |
5 | describe('SettingsDialogComponent', () => {
6 | let component: SettingsDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ SettingsDialogComponent ]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(SettingsDialogComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/settings-dialog/settings-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { PeerConfiguration } from '../shared/peerjs/PeerConfiguration';
3 |
4 | @Component({
5 | selector: 'app-settings-dialog',
6 | templateUrl: './settings-dialog.component.html',
7 | styleUrls: ['./settings-dialog.component.scss']
8 | })
9 | export class SettingsDialogComponent implements OnInit {
10 | @Input() configuration: PeerConfiguration;
11 | display: boolean;
12 |
13 | constructor() { }
14 |
15 | ngOnInit(): void {
16 | }
17 |
18 | showDialog() {
19 | this.display = true;
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/shared/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './page-not-found/page-not-found.component';
2 | export * from './logo/logo.component'
--------------------------------------------------------------------------------
/src/app/shared/components/logo/logo.component.html:
--------------------------------------------------------------------------------
1 |
2 | UltraScreen
3 |
--------------------------------------------------------------------------------
/src/app/shared/components/logo/logo.component.scss:
--------------------------------------------------------------------------------
1 | .title {
2 | color: black;
3 | font-size: 2.5em;
4 | user-select: none;
5 | -webkit-user-select: none;
6 | }
7 |
8 | .title .blue {
9 | color: #03A9F4;
10 | letter-spacing: 5px;
11 | font-size: 1.25em;
12 | }
--------------------------------------------------------------------------------
/src/app/shared/components/logo/logo.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { LogoComponent } from './logo.component';
4 |
5 | describe('LogoComponent', () => {
6 | let component: LogoComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ LogoComponent ]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(LogoComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/shared/components/logo/logo.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-logo',
5 | templateUrl: './logo.component.html',
6 | styleUrls: ['./logo.component.scss']
7 | })
8 | export class LogoComponent implements OnInit {
9 |
10 | constructor() { }
11 |
12 | ngOnInit(): void {
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.html:
--------------------------------------------------------------------------------
1 |
2 | Page not found.
3 |
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/app/shared/components/page-not-found/page-not-found.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { PageNotFoundComponent } from './page-not-found.component';
4 |
5 | describe('PageNotFoundComponent', () => {
6 | let component: PageNotFoundComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(waitForAsync(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [PageNotFoundComponent]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(PageNotFoundComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/shared/components/page-not-found/page-not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-page-not-found',
5 | templateUrl: './page-not-found.component.html',
6 | styleUrls: ['./page-not-found.component.scss']
7 | })
8 | export class PageNotFoundComponent implements OnInit {
9 | constructor() {}
10 |
11 | ngOnInit(): void {
12 | console.log('PageNotFoundComponent INIT');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/shared/components/sidepanel/sidepanel.component.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 | <
14 | >
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
30 |
--------------------------------------------------------------------------------
/src/app/shared/components/sidepanel/sidepanel.component.scss:
--------------------------------------------------------------------------------
1 | .left-panel {
2 | height: 100vh;
3 | width: 300px;
4 | background-color: rgba(245, 245, 245, 1);
5 | position: relative;
6 | float: left;
7 | border-right: 2px solid gray;
8 | }
9 | .panel {
10 | position: fixed;
11 | z-index: 9999;
12 | }
13 |
14 | .expand-button {
15 | color: black;
16 | background-color: rgba(245, 245, 245, 1);
17 | border-radius: 50%;
18 | display: inline;
19 | width: 25px;
20 | height: 25px;
21 | text-align: center;
22 | user-select: none;
23 | font-weight: bolder;
24 | font-size: 1.2em;
25 | line-height: 23px;
26 | top: calc( 50% - 25px) ;
27 | float: right;
28 | position: relative;
29 | left: 10px;
30 | padding-left: 2px;
31 | z-index: 9999;
32 | border-right: 2px solid gray;
33 | }
34 |
35 | .expand-button:hover {
36 | color: gray;
37 | }
38 |
39 | .expand-enter-active {
40 | animation: expand .5s;
41 | }
42 |
43 | .expand-leave-active {
44 | animation: expand .5s reverse;
45 | }
46 |
47 | .collapsed {
48 | height: 100vh;
49 | width: 5px;
50 | background-color: rgba(245, 245, 245, 1);
51 | margin: 0;
52 | }
53 |
54 | @keyframes expand {
55 | 0% {
56 | width: 5px;
57 | }
58 | 100% {
59 | width: 300px;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/app/shared/components/sidepanel/sidepanel.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SidepanelComponent } from './sidepanel.component';
4 |
5 | describe('SidepanelComponent', () => {
6 | let component: SidepanelComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ SidepanelComponent ]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(SidepanelComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/shared/components/sidepanel/sidepanel.component.ts:
--------------------------------------------------------------------------------
1 | import { animate, state, style, transition, trigger } from '@angular/animations';
2 | import { Component, Input, OnInit, TemplateRef } from '@angular/core';
3 |
4 | @Component({
5 | selector: 'app-sidepanel',
6 | templateUrl: './sidepanel.component.html',
7 | styleUrls: ['./sidepanel.component.scss'],
8 | animations: [
9 | trigger('expandedCollapsed', [
10 | state('expanded', style({
11 | transform: 'translateX(0%)'
12 | })),
13 | state('collapsed', style({
14 | transform: 'translateX(-95%)'
15 | })),
16 | transition('expanded => collapsed', [
17 | animate('0.25s ease-out')
18 | ]),
19 | transition('collapsed => expanded', [
20 | animate('0.25s ease-in')
21 | ])
22 | ])
23 | ]
24 | })
25 | export class SidepanelComponent implements OnInit {
26 |
27 | constructor() { }
28 |
29 | @Input()
30 | public panelTemplate: TemplateRef;
31 |
32 | ngOnInit(): void {
33 | }
34 |
35 | public toggleExpanded() {
36 | this.expanded = !this.expanded;
37 | }
38 |
39 |
40 |
41 | expanded = true;
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/shared/directives/index.ts:
--------------------------------------------------------------------------------
1 | export * from './webview/webview.directive';
2 |
--------------------------------------------------------------------------------
/src/app/shared/directives/webview/webview.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { WebviewDirective } from './webview.directive';
2 |
3 | describe('WebviewDirective', () => {
4 | it('should create an instance', () => {
5 | const directive = new WebviewDirective();
6 | expect(directive).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/directives/webview/webview.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from '@angular/core';
2 |
3 | @Directive({
4 | selector: 'webview'
5 | })
6 | export class WebviewDirective {
7 | constructor() { }
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/shared/peerjs/PeerConfiguration.ts:
--------------------------------------------------------------------------------
1 | export class PeerConfiguration {
2 | host: string;
3 | port: number;
4 | pingInterval: number;
5 | secure: boolean;
6 | iceServers: string[];
7 | };
8 |
--------------------------------------------------------------------------------
/src/app/shared/peerjs/PeerConnection.ts:
--------------------------------------------------------------------------------
1 | import { DataConnection, MediaConnection } from 'peerjs';
2 | import { PeerInstance } from './PeerInstance';
3 | import { PeerMessageData } from './PeerMessageData';
4 | import { PeerMessageDataType } from './PeerMessageDataType';
5 |
6 | export class PeerConnection {
7 | id: string;
8 | nickname: string;
9 | dataConnection: DataConnection;
10 | call: MediaConnection = undefined;
11 | stream: MediaStream = undefined;
12 | selfInitiated = false; // This connection was incoming
13 |
14 | constructor(pDataConnection: DataConnection) {
15 | if(pDataConnection) {
16 | this.dataConnection = pDataConnection;
17 | this.id = this.dataConnection.peer;
18 | this.nickname = '?';
19 | }
20 | }
21 |
22 | close() {
23 | this.closeCall();
24 | this.dataConnection.close();
25 | }
26 |
27 | closeCall() {
28 | if(this.call !== undefined && this.call.open) {
29 | console.log('Closed call to: ' + this.call.peer);
30 | this.call.close();
31 | }
32 | this.call = undefined;
33 | this.stream = undefined;
34 | }
35 |
36 | sendStopMyStream() {
37 | this.dataConnection.send(new PeerMessageData(PeerMessageDataType.stopMyStream));
38 | }
39 |
40 | sendConnectionClose() {
41 | this.dataConnection.send(new PeerMessageData(PeerMessageDataType.close));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/shared/peerjs/PeerInstance.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from '@angular/core';
2 | import { Console } from 'console';
3 | import Peer, { DataConnection, MediaConnection } from 'peerjs';
4 | import { BehaviorSubject } from 'rxjs';
5 | import { PeerConfiguration } from './PeerConfiguration';
6 | import { PeerConnection } from './PeerConnection';
7 | import { PeerMessageData } from './PeerMessageData';
8 | import { PeerMessageDataType } from './PeerMessageDataType';
9 |
10 | export class PeerInstance {
11 | peer: Peer;
12 | id: string;
13 | nickname: string;
14 | connected = new BehaviorSubject(false);
15 | peerConnections: Map = new Map();
16 | isStreaming: boolean;
17 | myStream: MediaStream = undefined;
18 | peerNicknameDictionary: {[key: string]: string};
19 | onNewPeerConnectedEvent: EventEmitter;
20 | onPeerDisconnectedEvent: EventEmitter;
21 | onMediaStreamsChanged: EventEmitter>;
22 | onStoppedStreaming: EventEmitter;
23 | selfConnection: PeerConnection;
24 |
25 | constructor(nickname: string, configuration: PeerConfiguration) {
26 | this.onNewPeerConnectedEvent = new EventEmitter();
27 | this.onPeerDisconnectedEvent = new EventEmitter();
28 | this.onMediaStreamsChanged = new EventEmitter();
29 | this.onStoppedStreaming = new EventEmitter();
30 | this.nickname = nickname;
31 | this.isStreaming = false;
32 | const iceConfig = [];
33 | for(const ice of configuration.iceServers) {
34 | iceConfig.push({url: ice});
35 | }
36 | this.peer = new Peer(undefined,
37 | {
38 | host: configuration.host,
39 | port: configuration.port,
40 | pingInterval: configuration.pingInterval,
41 | secure: configuration.secure,
42 | config: {iceServers: iceConfig}});
43 | this.peer.on('open', this.onPeerServerConnected.bind(this));
44 | this.peer.on('call', this.onPeerRemoteCallAttempt.bind(this));
45 | this.peer.on('error', this.onPeerError.bind(this));
46 | this.peer.on('connection', this.onPeerRemoteConnectionEstablished.bind(this));
47 |
48 | this.selfConnection = new PeerConnection(null);
49 | this.selfConnection.id = this.id;
50 | this.selfConnection.nickname = this.nickname;
51 | }
52 |
53 | destroy() {
54 | // TODO: Disconnect all connections
55 | this.peer.disconnect();
56 | }
57 |
58 | addPeer(remoteId: string) {
59 | if (this.isPeerIdInvalid(remoteId))
60 | {
61 | return;
62 | }
63 |
64 | const conn = this.peer.connect(remoteId, {
65 | metadata: {
66 | nickname: this.nickname
67 | }
68 | });
69 |
70 |
71 | this.registerPeerConnectionCallbacks(conn);
72 |
73 | console.log('Connecting...');
74 | }
75 |
76 | removePeer(remoteId: string) {
77 | const remoteConnection = this.peerConnections.get(remoteId);
78 | if(remoteConnection !== undefined) {
79 | remoteConnection.sendConnectionClose();
80 | this.handleCallClose(remoteConnection.call);
81 | remoteConnection.close();
82 | }
83 | }
84 |
85 | startStream(stream) {
86 | this.myStream = stream;
87 | this.isStreaming = true;
88 |
89 | for (const remoteId of this.peerConnections.keys()) {
90 | this.initateStreamCall(remoteId);
91 | }
92 | this.emitOnMediaStreamsChanged();
93 | }
94 |
95 | stopStream() {
96 | this.myStream.getTracks().forEach(function(track) {
97 | track.stop();
98 | });
99 |
100 | for (const connection of this.peerConnections.values()) {
101 | connection.sendStopMyStream();
102 | if(connection.call?.localStream) {
103 | this.handleCallClose(connection.call);
104 | }
105 | }
106 |
107 | this.myStream = undefined;
108 | this.isStreaming = false;
109 | this.onStoppedStreaming.emit();
110 | this.emitOnMediaStreamsChanged();
111 | }
112 |
113 | acceptConnection(peerConnection: PeerConnection) {
114 | if (this.isStreaming) {
115 | this.initateStreamCall(peerConnection.id);
116 | }
117 | }
118 |
119 | denyConnection(peerConnection: PeerConnection) {
120 | peerConnection.close();
121 | }
122 |
123 | // Helper functions
124 |
125 | private isPeerIdInvalid(peerId: string) {
126 | return peerId === '' || // Empty
127 | peerId === this.id || // Self
128 | this.peerConnections.hasOwnProperty(peerId); // Already connected
129 | }
130 |
131 | private initateStreamCall(remoteId: string) {
132 | const stream = this.myStream;
133 | const call = this.peer.call(remoteId, stream);
134 | call.on('error', this.onPeerError.bind(this));
135 | this.peerConnections.get(remoteId).call = call;
136 | console.log('Calling remote ' + remoteId);
137 | }
138 |
139 | private registerPeerConnectionCallbacks(remoteConnection: DataConnection, incoming = false) {
140 | remoteConnection.on('open', () => {
141 | console.log(remoteConnection.peer + ' connected!');
142 | const peerConnection = new PeerConnection(remoteConnection);
143 | peerConnection.nickname = remoteConnection.metadata.nickname;
144 | peerConnection.selfInitiated = incoming;
145 | this.peerConnections.set(remoteConnection.peer, peerConnection);
146 | this.onNewPeerConnectedEvent.emit(peerConnection);
147 | });
148 | remoteConnection.on('error', this.onPeerConnectionError.bind(this));
149 | remoteConnection.on('data', (data: PeerMessageData) => {
150 | this.onPeerConnectionData(data, remoteConnection);
151 | });
152 | remoteConnection.on('close', () => {
153 | const otherPeer = this.peerConnections.get(remoteConnection.peer);
154 | otherPeer.close();
155 | this.handleCallClose(otherPeer.call);
156 | this.peerConnections.delete(remoteConnection.peer);
157 | this.onPeerDisconnectedEvent.emit(otherPeer);
158 | console.log(this.peerConnections);
159 | console.log('Disconnected');
160 | });
161 | }
162 |
163 | private handleCallClose(call: MediaConnection) {
164 | if(call) {
165 | console.log(call);
166 | this.peerConnections.get(call.peer).closeCall();
167 | this.emitOnMediaStreamsChanged();
168 | }
169 | }
170 |
171 | private emitOnMediaStreamsChanged() {
172 | const streams: PeerConnection[] = [];
173 | if(this.myStream) {
174 | this.selfConnection.stream = this.myStream;
175 | streams.push(this.selfConnection);
176 | }
177 |
178 | for(const peerConnection of this.peerConnections.values()) {
179 | if(peerConnection.stream) {
180 | streams.push(peerConnection);
181 | }
182 | }
183 | console.log(streams);
184 |
185 | this.onMediaStreamsChanged.emit(streams);
186 | }
187 |
188 | // Peer Callbacks
189 |
190 | private onPeerServerConnected(assignedId: string) {
191 | console.log(this);
192 | this.connected.next(true);
193 | this.id = assignedId;
194 | console.log('My ID: %s', this.id);
195 | }
196 |
197 | private onPeerError(error: Error) {
198 | console.error(error);
199 | }
200 |
201 | private onPeerRemoteConnectionEstablished(remoteConnection: DataConnection) {
202 | console.log('Got remote connection: ' + remoteConnection.peer);
203 | this.registerPeerConnectionCallbacks(remoteConnection, true);
204 | }
205 |
206 | private onPeerRemoteCallStream(stream: MediaStream, call: MediaConnection) {
207 | this.peerConnections.get(call.peer).stream = stream;
208 | this.emitOnMediaStreamsChanged();
209 | }
210 |
211 | private onPeerRemoteCallAttempt(call: MediaConnection) {
212 | console.log('Receiving a call from: ' + call.peer);
213 | call.answer();
214 | this.peerConnections.get(call.peer).call = call;
215 | call.on('error', this.onPeerError.bind(this));
216 | call.on('stream', (stream: MediaStream) => {
217 | this.onPeerRemoteCallStream(stream, call);
218 | });
219 | call.on('close', () => {
220 | this.handleCallClose(call);
221 | });
222 | }
223 |
224 | // Peer Connection Callbacks
225 |
226 | private onPeerConnectionError(error: Error) {
227 | console.error(error);
228 | }
229 |
230 | private onPeerConnectionData(data: PeerMessageData, conn: DataConnection) {
231 | console.log('Got data: ');
232 | console.log(data);
233 | const peerConnection = this.peerConnections.get(conn.peer);
234 | switch(data.type) {
235 | case PeerMessageDataType.close:
236 | console.log('Close his call');
237 | this.handleCallClose(peerConnection.call);
238 | peerConnection.close();
239 | break;
240 | case PeerMessageDataType.stopMyStream:
241 | this.handleCallClose(this.peerConnections.get(conn.peer).call);
242 | break;
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/src/app/shared/peerjs/PeerMessageData.ts:
--------------------------------------------------------------------------------
1 | import { PeerMessageDataType } from './PeerMessageDataType';
2 |
3 | export class PeerMessageData {
4 | type: PeerMessageDataType;
5 | content: string;
6 |
7 | constructor(type: PeerMessageDataType, content: string = '') {
8 | this.type = type;
9 | this.content = content;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/shared/peerjs/PeerMessageDataType.ts:
--------------------------------------------------------------------------------
1 | export enum PeerMessageDataType {
2 | close,
3 | stopMyStream,
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { TranslateModule } from '@ngx-translate/core';
5 |
6 | import { PageNotFoundComponent, LogoComponent } from './components/';
7 | import { WebviewDirective } from './directives/';
8 | import { FormsModule } from '@angular/forms';
9 | import { SidepanelComponent } from './components/sidepanel/sidepanel.component';
10 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
11 | import { InputTextModule } from 'primeng/inputtext';
12 | import { ButtonModule } from 'primeng/button';
13 | import { DialogModule } from 'primeng/dialog';
14 | import { StreamPeerBlockModule } from '../stream-peer-block/stream-peer-block.module';
15 | import { TooltipModule } from 'primeng/tooltip';
16 | import { PeerConnectionDialogComponent } from '../peer-connection-dialog/peer-connection-dialog.component';
17 | import { SettingsDialogComponent } from '../settings-dialog/settings-dialog.component';
18 |
19 | @NgModule({
20 | declarations: [
21 | PageNotFoundComponent,
22 | LogoComponent,
23 | WebviewDirective,
24 | SidepanelComponent,
25 | PeerConnectionDialogComponent,
26 | SettingsDialogComponent
27 | ],
28 | imports: [CommonModule, DialogModule, TranslateModule, FontAwesomeModule, FormsModule],
29 | exports: [
30 | TooltipModule,
31 | StreamPeerBlockModule,
32 | DialogModule,
33 | TranslateModule,
34 | WebviewDirective,
35 | FormsModule,
36 | LogoComponent,
37 | SidepanelComponent,
38 | FontAwesomeModule,
39 | InputTextModule,
40 | ButtonModule,
41 | PeerConnectionDialogComponent,
42 | SettingsDialogComponent
43 | ],
44 | })
45 | export class SharedModule {}
46 |
--------------------------------------------------------------------------------
/src/app/stream-peer-block/stream-peer-block.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}}
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/stream-peer-block/stream-peer-block.component.scss:
--------------------------------------------------------------------------------
1 | .block {
2 | min-width: 25%;
3 | color: white;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | align-content: center;
8 | flex: 1 1;
9 | margin: 5px;
10 | max-width: 90%;
11 | height: 100%;
12 | }
13 |
14 | .video-container {
15 | height: 600px;
16 | display: flex;
17 | align-items: center;
18 | position: relative;
19 | }
20 |
21 | .focused-video-container {
22 | height: 95%;
23 | border: 1px dashed blue;
24 | }
25 |
26 | .wrap-video {
27 | height: 100%;
28 | display: block;
29 | position: relative;
30 | }
31 |
32 | .focused-wrap-video {
33 | width: 100%;
34 | height: 100%;
35 | display: block;
36 | position: relative;
37 | }
38 |
39 | .focused-block {
40 | margin-top: auto;
41 | margin-bottom: auto;
42 | max-width: 100%;
43 | width:100vw;
44 | height: 100vh;
45 | color: white;
46 | display: flex;
47 | flex-direction: column;
48 | align-items: center;
49 | }
50 |
51 | .media {
52 | width: 100%;
53 | height: 100%;
54 | }
55 |
56 | .title {
57 | color: #2c3e50;
58 | font-size: 20px;
59 | font-weight: bold;
60 | margin-bottom: 6px;
61 | }
62 |
63 | .title.alt {
64 | font-size: 18px;
65 | margin-bottom: 10px;
66 | }
67 |
68 | .video-overlay {
69 | position: absolute;
70 | top: 0;
71 | left: 0;
72 | background-image: -webkit-linear-gradient(bottom, rgba(0,0,0,0.75) 0%,rgba(0,0,0,0.04) 34%,rgba(0,0,0,0) 100%);
73 | z-index:9990;
74 | width: 100%;
75 | height: 100%;
76 | pointer-events: none;
77 | align-items: flex-end;
78 | justify-content: center;
79 | display: none;
80 | opacity: 1;
81 | -webkit-animation: fadein 0.25s;
82 | }
83 |
84 | @keyframes fadein {
85 | from { opacity: 0; }
86 | to { opacity: 1; }
87 | }
88 |
89 | video:hover + .video-overlay{
90 | display: flex;
91 | }
92 |
93 | .subtitle {
94 | user-select: none;
95 | }
96 |
97 | .fullscreen {
98 | width: 80%;
99 | height: 80%;
100 | border: blue 2px solid;
101 | }
--------------------------------------------------------------------------------
/src/app/stream-peer-block/stream-peer-block.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { StreamPeerBlockComponent } from './stream-peer-block.component';
4 |
5 | describe('StreamPeerBlockComponent', () => {
6 | let component: StreamPeerBlockComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ StreamPeerBlockComponent ]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(StreamPeerBlockComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/stream-peer-block/stream-peer-block.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-stream-peer-block',
5 | templateUrl: './stream-peer-block.component.html',
6 | styleUrls: ['./stream-peer-block.component.scss']
7 | })
8 | export class StreamPeerBlockComponent implements OnInit, AfterViewInit {
9 | @Input() media: MediaStream;
10 | @Input() title: string;
11 | @Input() focused: boolean;
12 | @ViewChild('streamVideo') videoElement: ElementRef;
13 |
14 | constructor() { }
15 |
16 | ngOnInit(): void {
17 |
18 | }
19 |
20 | ngAfterViewInit() {
21 | this.videoElement.nativeElement.onloadedmetadata = (e) => this.videoElement.nativeElement.play();
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/stream-peer-block/stream-peer-block.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { StreamPeerBlockComponent } from './stream-peer-block.component';
5 |
6 | @NgModule({
7 | declarations: [StreamPeerBlockComponent],
8 | imports: [CommonModule],
9 | exports: [StreamPeerBlockComponent],
10 | })
11 | export class StreamPeerBlockModule {}
12 |
--------------------------------------------------------------------------------
/src/app/stream-screen-picker/stream-screen-picker.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 | {{ source.name }}
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/stream-screen-picker/stream-screen-picker.component.scss:
--------------------------------------------------------------------------------
1 | .sources {
2 | display: block;
3 | position: relative;
4 | width: 200px;
5 | height: 100px;
6 | text-overflow: ellipsis;
7 | text-align: center;
8 | font-size: 0.7em;
9 | margin: 10px 10px 70px;
10 | padding: 10px;
11 | }
12 |
13 | .p-dialog, .modal-body, .p-element, p-dialog{
14 | width: 50% !important;
15 | max-width: 50%;
16 | }
17 |
18 | .sourceWrap {
19 | z-index: 999;
20 | width: 200px;
21 | height: 100px;
22 | }
23 |
24 | .sourceWrap:hover {
25 | background-color: #039BE5;
26 | }
27 |
28 | img:hover {
29 | background-color: #039BE5;
30 | }
31 |
32 | img {
33 | height: 100%;
34 | }
35 |
36 | .sourcesContainer {
37 | display: flex;
38 | justify-content: flex-start;
39 | flex-wrap: wrap;
40 | width: 100%;
41 | }
42 |
43 | .title {
44 | margin-bottom: 50px;
45 | }
--------------------------------------------------------------------------------
/src/app/stream-screen-picker/stream-screen-picker.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { StreamScreenPickerComponent } from './stream-screen-picker.component';
4 |
5 | describe('StreamScreenPickerComponent', () => {
6 | let component: StreamScreenPickerComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ StreamScreenPickerComponent ]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(StreamScreenPickerComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/stream-screen-picker/stream-screen-picker.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core';
2 | import { ElectronService } from '../core/services';
3 |
4 |
5 | @Component({
6 | selector: 'app-stream-screen-picker',
7 | templateUrl: './stream-screen-picker.component.html',
8 | styleUrls: ['./stream-screen-picker.component.scss']
9 | })
10 | export class StreamScreenPickerComponent implements OnInit {
11 | @Output() mediaStreamSelected: EventEmitter = new EventEmitter();
12 | display = false;
13 | sources: any;
14 |
15 | constructor(private electronService: ElectronService) {
16 | }
17 |
18 | ngOnInit(): void {
19 | }
20 |
21 | showPicker() {
22 | if (this.electronService.isElectron) {
23 | console.log(this.electronService);
24 | this.electronService.ipcRenderer.invoke('get-desktop-sources').then((srcs) => {
25 | console.log(srcs);
26 | this.sources = srcs;
27 | this.display = true;
28 | });
29 | } else {
30 | navigator.mediaDevices.getDisplayMedia().then((stream) => {
31 | this.mediaStreamSelected.emit(stream);
32 | });
33 | }
34 |
35 | }
36 |
37 | selectWindow(source) {
38 | this.display = false;
39 | (navigator.mediaDevices as any).getUserMedia({
40 | audio: false,
41 | video: {
42 | mandatory: {
43 | chromeMediaSource: 'desktop',
44 | chromeMediaSourceId: source.id,
45 | minWidth: 1280,
46 | maxWidth: 1280,
47 | minHeight: 720,
48 | maxHeight: 720
49 | }
50 | }
51 | }).then((stream) => {
52 | this.mediaStreamSelected.emit(stream);
53 | });
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/background.jpg
--------------------------------------------------------------------------------
/src/assets/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "PAGES": {
3 | "HOME": {
4 | "CONNECT": "Connect"
5 | },
6 | "CONNECTED": {
7 | "DISCONNECT": "Disconnect",
8 | "CREDITS": "Credits"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/assets/icons/clipboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/electron.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/icons/electron.bmp
--------------------------------------------------------------------------------
/src/assets/icons/favicon.256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/icons/favicon.256x256.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/icons/favicon.512x512.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/icons/favicon.icns
--------------------------------------------------------------------------------
/src/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/assets/icons/favicon.png
--------------------------------------------------------------------------------
/src/assets/icons/screen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/screen_close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
82 |
--------------------------------------------------------------------------------
/src/environments/environment.dev.ts:
--------------------------------------------------------------------------------
1 | export const APP_CONFIG = {
2 | production: false,
3 | environment: 'DEV'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const APP_CONFIG = {
2 | production: true,
3 | environment: 'PROD'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const APP_CONFIG = {
2 | production: false,
3 | environment: 'LOCAL'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.web.prod.ts:
--------------------------------------------------------------------------------
1 | export const APP_CONFIG = {
2 | production: true,
3 | environment: 'WEB-PROD'
4 | };
5 |
--------------------------------------------------------------------------------
/src/environments/environment.web.ts:
--------------------------------------------------------------------------------
1 | export const APP_CONFIG = {
2 | production: false,
3 | environment: 'WEB'
4 | };
5 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w3yden/ultrascreen/f5e492a0b8f235279fd6e52d2e3fb6ec200f2ef2/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Angular Electron
6 |
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-electron'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../coverage'),
20 | reports: [ 'html', 'lcovonly' ],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | browsers: ['AngularElectron'],
28 | customLaunchers: {
29 | AngularElectron: {
30 | base: 'Electron',
31 | flags: [
32 | '--remote-debugging-port=9222'
33 | ],
34 | browserWindowOptions: {
35 | webPreferences: {
36 | nodeIntegration: true,
37 | nodeIntegrationInSubFrames: true,
38 | allowRunningInsecureContent: true,
39 | contextIsolation: false
40 | }
41 | }
42 | }
43 | }
44 | });
45 | };
46 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { APP_CONFIG } from './environments/environment';
6 |
7 | if (APP_CONFIG.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule, {
13 | preserveWhitespaces: false
14 | })
15 | .catch(err => console.error(err));
16 |
--------------------------------------------------------------------------------
/src/polyfills-test.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags.ts';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* Tailwind CSS directives */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | /* You can add global styles to this file, and also import other style files */
7 | html, body {
8 | margin: 0;
9 | padding: 0;
10 |
11 | height: 100%;
12 | font-family: Arial, Helvetica, sans-serif;
13 | }
14 |
15 | .app-root {
16 | width: 100%;
17 | }
18 |
19 | /* Sample Global style */
20 | .container {
21 | width: 100%;
22 | height: 100%;
23 | display: flex;
24 | flex-direction: column;
25 | align-items: center;
26 | justify-content: center;
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting(), {
16 | teardown: { destroyAfterEach: false }
17 | }
18 | );
19 | // Then we find all the tests.
20 | const context = require.context('./', true, /\.spec\.ts$/);
21 | // And load the modules.
22 | context.keys().map(context);
23 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "",
6 | "types": [
7 | "node"
8 | ],
9 | "preserveSymlinks": true,
10 | "paths": { "@angular/*": [ "../node_modules/@angular/*" ] }
11 | },
12 | "files": [
13 | "main.ts",
14 | "polyfills.ts"
15 | ],
16 | "include": [
17 | "**/*.d.ts"
18 | ],
19 | "exclude": [
20 | "**/*.spec.ts"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "test.ts",
12 | "polyfills-test.ts"
13 | ],
14 | "include": [
15 | "**/*.spec.ts",
16 | "**/*.d.ts"
17 | ],
18 | "exclude": [
19 | "dist",
20 | "release",
21 | "node_modules"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare const nodeModule: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 | interface Window {
7 | process: any;
8 | require: any;
9 | }
10 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/app/**/*.{html,ts}"
5 | ],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "module": "es2020",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "preserveSymlinks": true,
12 | "allowJs": true,
13 | "target": "es2015",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2017",
19 | "es2016",
20 | "es2015",
21 | "dom"
22 | ]
23 | },
24 | "exclude": [
25 | "node_modules"
26 | ],
27 | "angularCompilerOptions": {
28 | "strictTemplates": true,
29 | "fullTemplateTypeCheck": true,
30 | "annotateForClosureCompiler": true,
31 | "strictInjectionParameters": true,
32 | "skipTemplateCodegen": false,
33 | "preserveWhitespaces": true,
34 | "skipMetadataEmit": false,
35 | "disableTypeScriptVersionCheck": true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "module": "commonjs",
9 | "target": "es2015",
10 | "types": [
11 | "node"
12 | ],
13 | "lib": [
14 | "es2017",
15 | "es2016",
16 | "es2015",
17 | "dom"
18 | ]
19 | },
20 | "files": [
21 | "app/main.ts"
22 | ],
23 | "exclude": [
24 | "node_modules",
25 | "**/*.spec.ts"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------