├── .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 | ![alt text](https://github.com/w3yden/ultrascreen/blob/main/screenshots/Preview.png "") 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 |
35 |
STREAM
36 |
37 |
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 | 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 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 58 | 60 | 64 | 65 | 67 | 68 | X 80 | 81 | 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 | --------------------------------------------------------------------------------