├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc.json ├── CHANGELOG.md ├── README.md ├── angular.json ├── installer └── install.nsi ├── karma.conf.js ├── package-lock.json ├── package.json ├── scripts ├── better-ui.ts └── tsconfig.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── better-ui │ │ │ ├── better-ui.component.scss │ │ │ ├── better-ui.component.spec.ts │ │ │ └── better-ui.component.ts │ │ ├── chat │ │ │ ├── chat.component.html │ │ │ ├── chat.component.scss │ │ │ ├── chat.component.spec.ts │ │ │ └── chat.component.ts │ │ ├── event-handler │ │ │ ├── event-handler.component.scss │ │ │ ├── event-handler.component.spec.ts │ │ │ └── event-handler.component.ts │ │ ├── exit-dialog │ │ │ ├── exit-dialog.component.html │ │ │ ├── exit-dialog.component.scss │ │ │ ├── exit-dialog.component.spec.ts │ │ │ └── exit-dialog.component.ts │ │ ├── garage-handler │ │ │ ├── garage-handler.component.scss │ │ │ ├── garage-handler.component.spec.ts │ │ │ └── garage-handler.component.ts │ │ ├── multiplayer-handler │ │ │ ├── multiplayer-handler.component.scss │ │ │ ├── multiplayer-handler.component.spec.ts │ │ │ └── multiplayer-handler.component.ts │ │ ├── race-countdown-timer │ │ │ ├── race-countdown-timer.component.html │ │ │ ├── race-countdown-timer.component.scss │ │ │ ├── race-countdown-timer.component.spec.ts │ │ │ └── race-countdown-timer.component.ts │ │ ├── race-handler │ │ │ ├── race-handler.component.scss │ │ │ ├── race-handler.component.spec.ts │ │ │ └── race-handler.component.ts │ │ ├── sessions-handler │ │ │ ├── sessions-handler.component.scss │ │ │ └── sessions-handler.component.ts │ │ └── start-handler │ │ │ ├── start-handler.component.scss │ │ │ ├── start-handler.component.spec.ts │ │ │ └── start-handler.component.ts │ ├── interfaces │ │ ├── game-phase.ts │ │ └── session-info.ts │ ├── modules │ │ ├── garage │ │ │ ├── components │ │ │ │ ├── copy-setup-popup │ │ │ │ │ ├── copy-setup-popup.component.html │ │ │ │ │ └── copy-setup-popup.component.ts │ │ │ │ ├── delete-confirm-popup │ │ │ │ │ ├── delete-confirm-popup.component.html │ │ │ │ │ └── delete-confirm-popup.component.ts │ │ │ │ ├── setup-tree │ │ │ │ │ ├── setup-tree.component.html │ │ │ │ │ └── setup-tree.component.ts │ │ │ │ └── setups │ │ │ │ │ ├── setups.component.html │ │ │ │ │ └── setups.component.ts │ │ │ ├── garage.module.ts │ │ │ ├── interfaces │ │ │ │ └── setup.ts │ │ │ ├── services │ │ │ │ └── garage.service.ts │ │ │ └── state │ │ │ │ ├── garage.actions.ts │ │ │ │ ├── garage.effects.ts │ │ │ │ ├── garage.reducer.ts │ │ │ │ ├── garage.selectors.ts │ │ │ │ └── index.ts │ │ ├── settings │ │ │ ├── components │ │ │ │ ├── settings-dialog │ │ │ │ │ ├── settings-dialog.component.html │ │ │ │ │ └── settings-dialog.component.ts │ │ │ │ └── settings-handler │ │ │ │ │ ├── settings-handler.component.html │ │ │ │ │ └── settings-handler.component.ts │ │ │ ├── index.ts │ │ │ └── settings.module.ts │ │ └── ui-components │ │ │ ├── components │ │ │ ├── popup-header │ │ │ │ ├── popup-header.component.html │ │ │ │ └── popup-header.component.ts │ │ │ ├── rf-button │ │ │ │ ├── rf-button.component.html │ │ │ │ ├── rf-button.component.scss │ │ │ │ ├── rf-button.component.spec.ts │ │ │ │ └── rf-button.component.ts │ │ │ └── spin-box │ │ │ │ ├── spin-box.component.html │ │ │ │ └── spin-box.component.ts │ │ │ ├── index.ts │ │ │ ├── model │ │ │ └── spin-box-item.ts │ │ │ └── ui-components.module.ts │ ├── services │ │ ├── race-button.service.spec.ts │ │ └── race-button.service.ts │ └── utils │ │ └── utils.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── proxy.conf.json ├── scss │ ├── _mixins.scss │ ├── _vars.scss │ ├── common-styles.scss │ └── rf-pages │ │ └── event.scss ├── styles.scss ├── test.ts └── typings.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── utils ├── builder.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | end_of_line = lf 11 | max_line_length = 120 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/recommended", 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@angular-eslint/template/process-inline-templates", 16 | "plugin:prettier/recommended" 17 | ], 18 | "rules": { 19 | "@angular-eslint/directive-selector": [ 20 | "error", 21 | { 22 | "type": "attribute", 23 | "prefix": "rf", 24 | "style": "camelCase" 25 | } 26 | ], 27 | "@angular-eslint/component-selector": [ 28 | "error", 29 | { 30 | "type": "element", 31 | "prefix": "rf", 32 | "style": "kebab-case" 33 | } 34 | ], 35 | "@typescript-eslint/no-inferrable-types": "off" 36 | } 37 | }, 38 | { 39 | "files": ["*.html"], 40 | "extends": ["plugin:@angular-eslint/template/recommended"], 41 | "rules": {} 42 | }, 43 | { 44 | "files": ["*.html"], 45 | "excludedFiles": ["*inline-template-*.component.html"], 46 | "extends": ["plugin:prettier/recommended"], 47 | "rules": { 48 | "prettier/prettier": ["error", { "parser": "angular" }] 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: 'Build Better-UI' 12 | runs-on: windows-latest 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | 20 | - name: Cache node modules 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | 28 | - name: Node ${{ matrix.node-version }} 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | - name: npm install and npm run build 34 | run: | 35 | npm ci 36 | npm run build.all 37 | 38 | - uses: suisei-cn/actions-download-file@v1 39 | id: zip 40 | name: Download 7-zip 41 | with: 42 | url: 'https://www.7-zip.org/a/7za920.zip' 43 | target: dist/7-zip 44 | 45 | - name: Decompress 7-zip 46 | run: 7z x dist\7-zip\7za920.zip -odist\7-zip\ 47 | 48 | - uses: suisei-cn/actions-download-file@v1 49 | id: locate-plugin 50 | name: Download locate plguin 51 | with: 52 | url: 'https://nsis.sourceforge.io/mediawiki/images/a/af/Locate.zip' 53 | target: dist/plugins 54 | 55 | - name: Decompress Locate plugin 56 | run: 7z x dist\plugins\Locate.zip -odist\plugins\ 57 | 58 | - name: Install NSIS 59 | run: | 60 | Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') 61 | scoop bucket add extras 62 | scoop install nsis 63 | 64 | - name: Print NSIS version 65 | run: makensis -VERSION 66 | 67 | - name: Print NSIS compile flags 68 | run: makensis -HDRINFO 69 | 70 | - name: Install Nsis plguins 71 | run: | 72 | copy dist\plugins\Plugin\locate.dll "C:\Program Files (x86)\NSIS\Plugins\x86-ansi\" 73 | copy dist\plugins\Include\Locate.nsh "c:\Program Files (x86)\NSIS\Include\" 74 | 75 | - name: Create installer directory 76 | run: mkdir dist\installer 77 | 78 | - name: Read package.json 79 | uses: tyankatsu0105/read-package-version-actions@v1 80 | id: package-version 81 | 82 | - name: Update installer version number to ${{ steps.package-version.outputs.version }} 83 | run: (Get-Content install.nsi) -replace 'PRODUCT_VERSION "0.0.0"', 'PRODUCT_VERSION "${{ steps.package-version.outputs.version }}"' | Out-File -encoding ASCII install.nsi 84 | shell: powershell 85 | working-directory: installer 86 | 87 | - name: Create installer 88 | uses: joncloud/makensis-action@v3.4 89 | with: 90 | script-file: 'installer/install.nsi' 91 | 92 | - name: Push Build to Releases 93 | uses: ncipollo/release-action@v1 94 | with: 95 | artifacts: 'dist/installer/*' 96 | token: ${{ secrets.TOKEN }} 97 | prerelease: true 98 | -------------------------------------------------------------------------------- /.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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 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 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [4.0.0] 6 | 7 | ## Added 8 | 9 | - Reimplemented setup popup with search functionality 10 | 11 | ## Fixed 12 | 13 | - Exit dialog popup styling 14 | 15 | ## [3.3.0] 16 | 17 | ## Fixed 18 | 19 | - Support latest rf2 build: remove remember password and remove favorite servers popup patch 20 | 21 | ## [3.2.0] 22 | 23 | ## Added 24 | 25 | - Car select 26 | 27 | ## Fixed 28 | 29 | - Opponent selector freezes the game 30 | 31 | ## [3.1.1] 32 | 33 | ## Fixed 34 | 35 | - Multiple quit button at the navbar 36 | 37 | ## [3.1.0] 38 | 39 | ## Added 40 | 41 | - Exit button to installing content dialog 42 | - Race button to event view 43 | 44 | ## Fixed 45 | 46 | - Add quit button on hash location change 47 | - Change button order in quit dialog 48 | 49 | ## [3.0.0] 50 | 51 | ## Added 52 | 53 | - Uninstaller (comes with SteamCmd) 54 | - Display unlimited number of favorite servers on race screen 55 | - Do not display popup while loading favorite servers on race screen 56 | - Increase visible content size on garage view 57 | 58 | ### Fixed 59 | 60 | - Remember server filters in multiplayer view 61 | - Load Better-UI code more reliably 62 | - Apply patches again when hash location changes on 63 | 64 | ## [2.4.1] 65 | 66 | ### Fixed 67 | 68 | - use FontAwesome coming with rF 69 | - do not ask user for path if location was not found in registry 70 | 71 | ## [2.4.0] 72 | 73 | ### Added 74 | 75 | - Exit popup to navbar 76 | - Let the user select rFactor 2 location in the installer 77 | 78 | ### Fixed 79 | 80 | - update password event if it was wrong 81 | 82 | ## [2.3.2] - 2021-01-10 83 | 84 | ### Added 85 | 86 | - Installer generated via Github Actions 87 | 88 | ### Fixed 89 | 90 | - Ask user to enter rFactor 2 location if it's not found in registry 91 | 92 | ## [2.3.1] - 2021-01-20 93 | 94 | ### Changed 95 | 96 | - Modified installer to pick official jar file and patch with Better-UI 97 | 98 | ## [2.3.0] - 2021-01-19 99 | 100 | ### Added 101 | 102 | - Installer 103 | - Support latest rFactor2 release 104 | 105 | ## [2.2.0] - 2021-01-19 106 | 107 | ### Added 108 | 109 | - Add countdown timer to lap limited multiplayer race events 110 | - Support latest rFactor2 release 111 | 112 | ## [2.1.0] - 2021-01-10 113 | 114 | ### Added 115 | 116 | - Remember selected server filters 117 | 118 | ### Fixed 119 | 120 | - Display rf-chat on tab change 121 | - Fix server admin commands in rf-chat 122 | 123 | ## [2.0.1] - 2021-01-09 124 | 125 | ### Fixed 126 | 127 | - Update saved password on input value change 128 | 129 | ## [2.0.0] - 2021-01-08 130 | 131 | ### Added 132 | 133 | - Highlight current track in setup popup 134 | - Disable opacity change animation 135 | 136 | ### Changed 137 | 138 | - Removed jQuery scripts and started to use Angular to manipulate with DOM 139 | 140 | ### Fixed 141 | 142 | - Chat window will scroll down if you are scrolled to the bottom 143 | - Opponent selector highlights current selection after navigated to other tab and back 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rf2-better-ui 2 | 3 | In this repository you can find small modifications for the new rFactor 2 UI 4 | 5 | # How to install 6 | 7 | 1. Go to latest relaese page and download installer: [link](https://github.com/Cselt/rf2-better-ui/releases/latest) 8 | 2. Run the installer 9 | 3. Start rFactor 2 & enjoy 10 | 11 | # How to uninstall 12 | 13 | Run `Uninstall-Better-UI.exe` located at your rFactor 2 directory 14 | 15 | Or 16 | 17 | Let Steam re-download modified files: [link](https://support.steampowered.com/kb_article.php?ref=2037-QEUH-3335) 18 | 19 | # Features 20 | 21 | - add left and right arrow navigation to the main screen 22 | - fix z-index issue with "exit game" button 23 | - add remember password feature to favorite multiplayer servers 24 | - add left and right arrow navigation to the race screen 25 | - add left and right arrow navigation to the garage screen 26 | - fix too big tiles on garage screen 27 | - add permanent chat window below camera view 28 | - highlight current track in setup popup 29 | - disable opacity change animation 30 | - remember selected multiplayer server filters 31 | - add countdown timer to lap limited multiplayer race events 32 | 33 | # How to contribute 34 | 35 | ## Easy way 36 | 37 | - Download and install Better-UI version 4+ 38 | - Install dependencies: `npm install` 39 | - Start Better-UI: `npm run start` 40 | - Start rFactor 2 41 | - Check WebUI port in your `player.JSON` (rFactor2\UserData\player\player.JSON) 42 | Find the following: 43 | 44 | ```json 45 | "WebUI port": 5396, 46 | "WebUI port#": "Port for the WebUI", 47 | ``` 48 | 49 | - Open your browser and navigate to http://localhost:/start/index.html e.g.: http://localhost:5396/start/index.html 50 | - Double-click on version number in the bottom right corner 51 | - Click OK to switch to dev mode 52 | - Now you have a working environment. Feel free to make changes in the code. Then you need to manually refresh the page to see the change 53 | 54 | ## How to manually patch UI files 55 | 56 | - Compile Better ui: `npm run build.all` 57 | - Go to `rFactor 2\Bin\Bundles` 58 | - Open zip archive `net.rfactor2.ui.framework.jar` 59 | - Go to`static\framework` 60 | - Copy here `dist/scripts/better-ui.js` 61 | - Copy `dist/rf2-better-ui` to `rf2-better-ui` 62 | - Edit `main-xxxx.js` file 63 | - Append code at the end:
64 | ``` 65 | require(["./framework/better-ui"]); 66 | ``` 67 | 68 | ## How to create an installer 69 | 70 | - Install Locate NSIS plugin 71 | - Compile Better ui: `npm run build.all` 72 | - Create directory `dist\installer` 73 | - Use the NSIS compiler (MakeNSISW) to compile `installer/install.nsi` 74 | - The installer will be located at `dist/installer` 75 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "rf2-better-ui": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "rf", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/rf2-better-ui", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": ["src/favicon.ico", "src/assets"], 29 | "styles": ["node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss"], 30 | "scripts": [], 31 | "vendorChunk": true, 32 | "extractLicenses": false, 33 | "buildOptimizer": false, 34 | "sourceMap": true, 35 | "optimization": false, 36 | "namedChunks": true 37 | }, 38 | "configurations": { 39 | "production": { 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "6kb", 62 | "maximumError": "10kb" 63 | } 64 | ] 65 | } 66 | }, 67 | "defaultConfiguration": "" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "options": { 72 | "browserTarget": "rf2-better-ui:build", 73 | "proxyConfig": "src/proxy.conf.json" 74 | }, 75 | "configurations": { 76 | "production": { 77 | "browserTarget": "rf2-better-ui:build:production" 78 | } 79 | } 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "rf2-better-ui:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "main": "src/test.ts", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "tsconfig.spec.json", 93 | "karmaConfig": "karma.conf.js", 94 | "assets": ["src/favicon.ico", "src/assets"], 95 | "styles": ["node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss"], 96 | "scripts": [] 97 | } 98 | }, 99 | "lint": { 100 | "builder": "@angular-eslint/builder:lint", 101 | "options": { 102 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "defaultProject": "rf2-better-ui", 109 | "schematics": { 110 | "@schematics/angular:component": { 111 | "changeDetection": "OnPush", 112 | "inlineStyle": true, 113 | "skipTests": true 114 | } 115 | }, 116 | "cli": { 117 | "defaultCollection": "@angular-eslint/schematics" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /installer/install.nsi: -------------------------------------------------------------------------------- 1 | # For this script to use you need to install Locate plugin 2 | # And you need to put 7za.exe to dist\7-zip\7za.exe 3 | ;-------------------------------- 4 | ;Include Modern UI 5 | 6 | !include "MUI2.nsh" 7 | 8 | !include LogicLib.nsh 9 | !include "Locate.nsh" 10 | !include nsDialogs.nsh 11 | 12 | !define PRODUCT_VERSION "0.0.0" 13 | 14 | Name "BetterUI" 15 | ShowInstDetails show 16 | 17 | # the file to write 18 | OutFile "..\\dist\\installer\\Better-UI-${PRODUCT_VERSION}.exe" 19 | 20 | Var rFLocation 21 | Var steamCmd 22 | Var completedText 23 | Var mainFile 24 | Var unpackedDir 25 | var /global SOURCE 26 | var /global SOURCETEXT 27 | 28 | CompletedText $completedText 29 | ; -------------------- 30 | ;Version Information - 31 | ; -------------------- 32 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\English.nlf" 33 | VIProductVersion "${PRODUCT_VERSION}.0" 34 | VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductName" "Better-UI" 35 | VIAddVersionKey "OriginalFilename" "Better-UI.exe" 36 | VIAddVersionKey /LANG=${LANG_ENGLISH} "CompanyName" "Cselt" 37 | VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "Copyright Cselt" 38 | VIAddVersionKey /LANG=${LANG_ENGLISH} "FileDescription" "rFactor 2 Better-UI" 39 | VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "${PRODUCT_VERSION}" 40 | VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "${PRODUCT_VERSION}" 41 | VIAddVersionKey /LANG=${LANG_ENGLISH} "InternalName" "Better-UI" 42 | ;-------------------------------- 43 | 44 | ;-------------------------------- 45 | ;Interface Settings 46 | 47 | !define MUI_ABORTWARNING 48 | 49 | ;-------------------------------- 50 | ;Pages 51 | 52 | !define MUI_DIRECTORYPAGE_VARIABLE $rFLocation 53 | !define MUI_PAGE_HEADER_TEXT "APP Installation folder location." 54 | !define MUI_DIRECTORYPAGE_TEXT_TOP "Please select the folder where rFactor 2 has been installed. If you are unsure where rFactor 2 has been installed, please keep the default value." 55 | !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "rFactor 2 location" 56 | !insertmacro MUI_PAGE_DIRECTORY 57 | !insertmacro MUI_PAGE_INSTFILES 58 | 59 | Function .onInit 60 | InitPluginsDir 61 | 62 | StrCpy $completedText "Better-UI successfully installed!" 63 | # read the value from the registry into the $0 register 64 | SetRegView 64 65 | ReadRegStr $0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 365960" 'InstallLocation' 66 | StrCpy $rfLocation $0 67 | 68 | # remove tailing "\" OUTDIR is a special parameter, will remove extra slashes 69 | StrCpy $OUTDIR $rfLocation 70 | StrCpy $rfLocation $OUTDIR 71 | FunctionEnd 72 | 73 | Function FindMainJs 74 | ${locate::Open} "$unpackedDir\static\framework" "/F=1 /D=0 /M=main*.js /B=1" $0 75 | StrCmp $0 0 0 found 76 | MessageBox MB_OK "Did not found" IDOK close 77 | 78 | found: 79 | ${locate::Find} $0 $1 $2 $3 $4 $5 $6 80 | DetailPrint "Found file $3" 81 | StrCpy $mainFile $3 82 | 83 | close: 84 | ${locate::Close} $0 85 | ${locate::Unload} 86 | FunctionEnd 87 | 88 | Function UnPackJar 89 | CreateDirectory $rfLocation\Bin\Cache 90 | SetOutPath $rfLocation\Bin\Cache 91 | File ..\dist\7-zip\7za.exe 92 | CopyFiles $rfLocation\Bin\Bundles\net.rfactor2.ui.framework.jar $rfLocation\Bin\Cache 93 | 94 | nsExec::Exec '7za.exe x net.rfactor2.ui.framework.jar -o"$unpackedDir"' 95 | FunctionEnd 96 | 97 | Function PatchFiles 98 | Call FindMainJs 99 | 100 | ExecWait 'find /C "./framework/better-ui" "$unpackedDir\static\framework\$mainFile"' $0 101 | 102 | ${If} $0 > 0 103 | DetailPrint "Patching main file $unpackedDir\static\framework\$mainFile" 104 | FileOpen $4 "$unpackedDir\static\framework\$mainFile" a 105 | FileSeek $4 0 END 106 | FileWrite $4 'require(["./framework/better-ui"]);' 107 | FileClose $4 108 | ${Else} 109 | DetailPrint "Main file $mainFile already patched" 110 | ${EndIf} 111 | FunctionEnd 112 | 113 | Function AddBetterUI 114 | DetailPrint "Add Better UI" 115 | RMDir /r $unpackedDir\static\framework\rf2-better-ui 116 | CreateDirectory $unpackedDir\static\framework\rf2-better-ui 117 | SetOutPath $unpackedDir\static\framework\rf2-better-ui 118 | File /r ..\dist\rf2-better-ui\* 119 | 120 | SetOutPath $unpackedDir\static\framework 121 | File ..\dist\scripts\better-ui.js 122 | FunctionEnd 123 | 124 | Function CreateJarFile 125 | DetailPrint "Create Jar file" 126 | SetOutPath $unpackedDir 127 | nsExec::Exec '..\7za.exe a -r net.rfactor2.ui.framework.jar *.*' 128 | FunctionEnd 129 | 130 | Function downloadSteamCmd 131 | IfFileExists "$steamCmd" done 132 | 133 | DetailPrint "Download SteamCmd" 134 | CreateDirectory $rfLocation\SteamCmd 135 | NSISdl::download https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip $rfLocation\SteamCmd\steamcmd.zip 136 | SetOutPath $rfLocation\SteamCmd 137 | File ..\dist\7-zip\7za.exe 138 | nsExec::Exec '7za.exe x steamcmd.zip -o"$rfLocation\SteamCmd"' 139 | Delete $rfLocation\SteamCmd\steamcmd.zip 140 | Delete $rfLocation\SteamCmd\7za.exe 141 | 142 | # initialize steam cmd 143 | DetailPrint "Initialize SteamCmd" 144 | nsExec::Exec 'steamcmd.exe +quit' 145 | 146 | done: 147 | FunctionEnd 148 | 149 | # default section start; every NSIS script has at least one section. 150 | Section 151 | DetailPrint "rFactor 2 location: $rfLocation" 152 | StrCpy $unpackedDir $rfLocation\Bin\Cache\out 153 | StrCpy $steamCmd "$rfLocation\SteamCmd\steamcmd.exe" 154 | DetailPrint "SteamCmd: $steamCmd" 155 | DetailPrint "Unpacked dir: $unpackedDir" 156 | 157 | # Delete Cache folder 158 | RMDir /r $rfLocation\Bin\Cache 159 | 160 | # define the output path for this file 161 | SetOutPath $rfLocation\Bin\Bundles 162 | 163 | Call downloadSteamCmd 164 | Call UnpackJar 165 | Call PatchFiles 166 | Call AddBetterUI 167 | Call CreateJarFile 168 | 169 | CopyFiles $unpackedDir\net.rfactor2.ui.framework.jar $rfLocation\Bin\Bundles\ 170 | 171 | # Delete Cache folder 172 | DetailPrint "Cleanup Cache folder" 173 | RMDir /r $rfLocation\Bin\Cache 174 | 175 | WriteUninstaller "$rfLocation\Uninstall-Better-UI.exe" 176 | 177 | # default section end 178 | SectionEnd 179 | 180 | # Uninstaller 181 | Section "Uninstall" 182 | StrCpy $completedText "Better-UI successfully uninstalled!" 183 | StrCpy $steamCmd "$INSTDIR\SteamCmd\steamcmd.exe" 184 | DetailPrint "rfLocation: $INSTDIR" 185 | DetailPrint "SteamCmd: $steamCmd" 186 | 187 | Delete "$INSTDIR\Uninstall-Better-UI.exe" 188 | 189 | DetailPrint "Restoring changes" 190 | nsExec::Exec '"$steamCmd" +login anonymous +force_install_dir "$INSTDIR" +app_update 400300 validate +quit' 191 | 192 | RMDir /r "$INSTDIR\SteamCmd" 193 | SectionEnd 194 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: 'projects/rf2-better-ui', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/rf2-better-ui'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rf2-better-ui", 3 | "version": "4.0.1", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "cross-env NG_PERSISTENT_BUILD_CACHE=1 ng serve --live-reload=false", 7 | "build": "cross-env NODE_ENV=production ng build --configuration production", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "prettier": "prettier --write \"./src/**/*.{ts,js,html,scss}\"", 11 | "prettier:check": "prettier --list-different \"./src/**/*.{ts,js,html,scss}\"", 12 | "postinstall": "npm run prettier -s && husky install", 13 | "build.scripts": "tsc --project scripts/tsconfig.json", 14 | "build.all": "ts-node --project utils/tsconfig.json utils/builder.ts" 15 | }, 16 | "private": true, 17 | "dependencies": { 18 | "@angular/animations": "~12.2.12", 19 | "@angular/cdk": "^12.2.12", 20 | "@angular/common": "~12.2.12", 21 | "@angular/compiler": "~12.2.12", 22 | "@angular/core": "~12.2.12", 23 | "@angular/elements": "^12.2.12", 24 | "@angular/forms": "~12.2.12", 25 | "@angular/material": "^12.2.12", 26 | "@angular/platform-browser": "~12.2.12", 27 | "@angular/platform-browser-dynamic": "~12.2.12", 28 | "@angular/router": "~12.2.12", 29 | "@ngrx/effects": "^12.5.1", 30 | "@ngrx/store": "^12.5.1", 31 | "@ngrx/store-devtools": "^12.5.1", 32 | "autoprefixer": "^10.2.6", 33 | "document-register-element": "^1.7.2", 34 | "postcss": "^8.3.5", 35 | "rxjs": "~6.6.0", 36 | "tslib": "^2.0.0", 37 | "zone.js": "~0.11.4" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/build-angular": "~12.2.12", 41 | "@angular-eslint/builder": "12.6.1", 42 | "@angular-eslint/eslint-plugin": "12.6.1", 43 | "@angular-eslint/eslint-plugin-template": "12.6.1", 44 | "@angular-eslint/schematics": "12.6.1", 45 | "@angular-eslint/template-parser": "12.6.1", 46 | "@angular/cli": "~12.2.12", 47 | "@angular/compiler-cli": "~12.2.12", 48 | "@ngrx/schematics": "^12.5.1", 49 | "@types/jasmine": "~3.6.0", 50 | "@types/node": "^12.11.1", 51 | "@typescript-eslint/eslint-plugin": "4.28.2", 52 | "@typescript-eslint/parser": "4.28.2", 53 | "cross-env": "^7.0.3", 54 | "eslint": "^7.26.0", 55 | "eslint-config-prettier": "^8.3.0", 56 | "eslint-plugin-prettier": "^4.0.0", 57 | "husky": "^7.0.4", 58 | "jasmine-core": "~3.6.0", 59 | "jasmine-spec-reporter": "~5.0.0", 60 | "karma": "~6.3.4", 61 | "karma-chrome-launcher": "~3.1.0", 62 | "karma-coverage": "~2.0.3", 63 | "karma-jasmine": "~4.0.0", 64 | "karma-jasmine-html-reporter": "^1.5.0", 65 | "prettier": "^2.4.1", 66 | "pretty-quick": "^3.1.1", 67 | "protractor": "~7.0.0", 68 | "tailwindcss": "^2.2.4", 69 | "ts-node": "~8.3.0", 70 | "typescript": "~4.3.5" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/better-ui.ts: -------------------------------------------------------------------------------- 1 | function main(): void { 2 | function addScriptTag(src: string): Promise { 3 | return new Promise((resolve: () => void, reject: (err: any) => void) => { 4 | const script: HTMLScriptElement = document.createElement('script'); 5 | script.onload = () => resolve(); 6 | script.onerror = (err: any) => reject(err); 7 | script.src = src; 8 | script.defer = true; 9 | 10 | document.getElementsByTagName('body')[0].appendChild(script); 11 | }); 12 | } 13 | 14 | function addLinkTag(href: string): Promise { 15 | const link: HTMLLinkElement = document.createElement('link'); 16 | link.rel = 'stylesheet'; 17 | link.href = href; 18 | document.getElementsByTagName('head')[0].appendChild(link); 19 | return Promise.resolve(); 20 | } 21 | 22 | function waitForElem(selector): Promise { 23 | return new Promise((resolve: any) => { 24 | if (document.querySelector(selector)) { 25 | return resolve(document.querySelector(selector)); 26 | } 27 | 28 | const observer = new MutationObserver(mutations => { 29 | if (document.querySelector(selector)) { 30 | resolve(document.querySelector(selector)); 31 | observer.disconnect(); 32 | } 33 | }); 34 | 35 | observer.observe(document.body, { 36 | childList: true, 37 | subtree: true 38 | }); 39 | }); 40 | } 41 | 42 | const div: HTMLDivElement = document.createElement('div'); 43 | div.innerHTML = 44 | `Better-UI `; 45 | 46 | const devMode: boolean = localStorage.getItem('betterUi.devMode') === 'true'; 47 | div.ondblclick = () => { 48 | if (devMode) { 49 | if (confirm('Are you sure you want to switch to Prod mode?')) { 50 | localStorage.removeItem('betterUi.devMode'); 51 | window.location.reload(); 52 | } 53 | } else { 54 | if (confirm('Are you sure you want to switch to Dev mode?')) { 55 | localStorage.setItem('betterUi.devMode', 'true'); 56 | window.location.reload(); 57 | } 58 | } 59 | }; 60 | 61 | document.getElementsByTagName('body')[0].prepend(div); 62 | 63 | let allPromise: Promise; 64 | if (devMode) { 65 | allPromise = Promise.all([ 66 | addScriptTag('http://localhost:4200/runtime.js'), 67 | addScriptTag('http://localhost:4200/polyfills.js'), 68 | addScriptTag('http://localhost:4200/vendor.js'), 69 | addScriptTag('http://localhost:4200/main.js'), 70 | addLinkTag('http://localhost:4200/styles.css') 71 | ]); 72 | } else { 73 | allPromise = Promise.all([ 74 | addScriptTag('../framework/rf2-better-ui/runtime..js'), 75 | addScriptTag('../framework/rf2-better-ui/polyfills..js'), 76 | addScriptTag('../framework/rf2-better-ui/main.
.js'), 77 | addLinkTag('../framework/rf2-better-ui/styles..css') 78 | ]); 79 | } 80 | 81 | allPromise.then(() => { 82 | waitForElem('ui-view div').then(() => { 83 | const rfBetterUi: HTMLElement = document.createElement('rf-better-ui'); 84 | document.getElementsByTagName('body')[0].prepend(rfBetterUi); 85 | }); 86 | }); 87 | 88 | } 89 | 90 | main(); 91 | 92 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/scripts", 5 | "types": [], 6 | "sourceMap": false 7 | }, 8 | "files": [ 9 | "better-ui.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | declarations: [AppComponent] 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'rf2-better-ui'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('rf2-better-ui'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement; 27 | expect(compiled.querySelector('.content span').textContent).toContain('rf2-better-ui app is running!'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { SetupsComponent } from './modules/garage/components/setups/setups.component'; 4 | 5 | @Component({ 6 | selector: 'rf-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent implements OnInit { 11 | title = 'rf2-better-ui'; 12 | 13 | constructor(private dialog: MatDialog) {} 14 | 15 | ngOnInit(): void { 16 | this.openDialog(); 17 | } 18 | 19 | openDialog(): void { 20 | this.dialog.open(SetupsComponent, { 21 | height: '50vh', 22 | maxHeight: '90vh', 23 | width: '70vw' 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { DoBootstrap, Injector, NgModule } from '@angular/core'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog'; 6 | import { EffectsModule } from '@ngrx/effects'; 7 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 8 | import { StoreModule } from '@ngrx/store'; 9 | 10 | import { createCustomElement } from '@angular/elements'; 11 | import { ChatComponent } from './components/chat/chat.component'; 12 | import { BetterUiComponent } from './components/better-ui/better-ui.component'; 13 | import { StartHandlerComponent } from './components/start-handler/start-handler.component'; 14 | import { RaceHandlerComponent } from './components/race-handler/race-handler.component'; 15 | import { GarageHandlerComponent } from './components/garage-handler/garage-handler.component'; 16 | import { EventHandlerComponent } from './components/event-handler/event-handler.component'; 17 | import { SessionsHandlerComponent } from './components/sessions-handler/sessions-handler.component'; 18 | import { MultiplayerHandlerComponent } from './components/multiplayer-handler/multiplayer-handler.component'; 19 | import { RaceCountdownTimerComponent } from './components/race-countdown-timer/race-countdown-timer.component'; 20 | import { ExitDialogComponent } from './components/exit-dialog/exit-dialog.component'; 21 | import { GarageModule } from './modules/garage/garage.module'; 22 | import { environment } from '../environments/environment'; 23 | import { UiComponentsModule } from './modules/ui-components/ui-components.module'; 24 | import { AppComponent } from './app.component'; 25 | import { SettingsModule } from './modules/settings/settings.module'; 26 | 27 | @NgModule({ 28 | imports: [ 29 | BrowserModule, 30 | HttpClientModule, 31 | BrowserAnimationsModule, 32 | MatDialogModule, 33 | StoreModule.forRoot({}, {}), 34 | StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }), 35 | EffectsModule.forRoot([]), 36 | UiComponentsModule, 37 | GarageModule, 38 | SettingsModule 39 | ], 40 | declarations: [ 41 | AppComponent, 42 | ChatComponent, 43 | BetterUiComponent, 44 | StartHandlerComponent, 45 | RaceHandlerComponent, 46 | GarageHandlerComponent, 47 | EventHandlerComponent, 48 | SessionsHandlerComponent, 49 | MultiplayerHandlerComponent, 50 | RaceCountdownTimerComponent, 51 | ExitDialogComponent 52 | ], 53 | providers: [ 54 | { 55 | provide: MAT_DIALOG_DEFAULT_OPTIONS, 56 | useValue: { 57 | ...new MatDialogConfig(), 58 | panelClass: 'rfPanel' 59 | } as MatDialogConfig 60 | } 61 | ] 62 | // bootstrap: [AppComponent] 63 | }) 64 | export class AppModule implements DoBootstrap { 65 | constructor(private injector: Injector) { 66 | const chatWebComponent = createCustomElement(ChatComponent, { injector }); 67 | customElements.define('rf-chat', chatWebComponent); 68 | 69 | const betterUiWebComponent = createCustomElement(BetterUiComponent, { injector }); 70 | customElements.define('rf-better-ui', betterUiWebComponent); 71 | 72 | const raceCountDownTimerWebComponent = createCustomElement(RaceCountdownTimerComponent, { injector }); 73 | customElements.define('rf-race-countdown-timer', raceCountDownTimerWebComponent); 74 | } 75 | 76 | // eslint-disable-next-line 77 | ngDoBootstrap(): void { 78 | // do nothing 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/components/better-ui/better-ui.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/app/components/better-ui/better-ui.component.scss -------------------------------------------------------------------------------- /src/app/components/better-ui/better-ui.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BetterUiComponent } from './better-ui.component'; 4 | 5 | describe('BetterUiComponent', () => { 6 | let component: BetterUiComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [BetterUiComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(BetterUiComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/better-ui/better-ui.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ComponentFactory, 5 | ComponentFactoryResolver, 6 | OnInit, 7 | ViewChild, 8 | ViewContainerRef 9 | } from '@angular/core'; 10 | import packageInfo from '../../../../package.json'; 11 | import { ExitDialogComponent } from '../exit-dialog/exit-dialog.component'; 12 | import { MatDialog } from '@angular/material/dialog'; 13 | import { waitForElement } from '../../utils/utils'; 14 | import { StartHandlerComponent } from '../start-handler/start-handler.component'; 15 | import { RaceHandlerComponent } from '../race-handler/race-handler.component'; 16 | import { GarageHandlerComponent } from '../garage-handler/garage-handler.component'; 17 | import { EventHandlerComponent } from '../event-handler/event-handler.component'; 18 | import { SessionsHandlerComponent } from '../sessions-handler/sessions-handler.component'; 19 | import { MultiplayerHandlerComponent } from '../multiplayer-handler/multiplayer-handler.component'; 20 | import { SettingsHandlerComponent } from '../../modules/settings'; 21 | import { environment } from '../../../environments/environment'; 22 | 23 | @Component({ 24 | selector: 'rf-better-ui', 25 | template: ``, 26 | styleUrls: ['./better-ui.component.scss'] 27 | }) 28 | export class BetterUiComponent implements OnInit, AfterViewInit { 29 | @ViewChild('container', { read: ViewContainerRef }) 30 | container: ViewContainerRef; 31 | 32 | constructor(private dialog: MatDialog, private resolver: ComponentFactoryResolver) { 33 | console.log(`Better UI v${packageInfo.version} loaded`); 34 | 35 | const spanElement: HTMLSpanElement = document.querySelector('#betterUIVersion'); 36 | spanElement.innerHTML += packageInfo.version + (environment.production ? '' : '-DEV'); 37 | } 38 | 39 | ngOnInit(): void { 40 | this.addQuitButton(); 41 | window.addEventListener('hashchange', () => this.addQuitButton()); 42 | } 43 | 44 | ngAfterViewInit(): void { 45 | this.applyHandlers(); 46 | } 47 | 48 | private async addQuitButton(): Promise { 49 | try { 50 | const ol: HTMLOListElement = await waitForElement('nav ol.right', 1000); 51 | 52 | if (ol) { 53 | if (ol.querySelector('li.fa-power-off')) { 54 | // Already added 55 | return; 56 | } 57 | 58 | const quitLi: HTMLLIElement = document.createElement('li'); 59 | quitLi.classList.add('fa', 'fa-power-off'); 60 | quitLi.addEventListener('click', () => 61 | this.dialog.open(ExitDialogComponent, { 62 | panelClass: ['noDialogPadding', 'rfPanel'], 63 | width: '25vw' 64 | }) 65 | ); 66 | ol.appendChild(quitLi); 67 | } 68 | } catch (e) { 69 | console.warn("Can't find nav element to add quit button"); 70 | } 71 | } 72 | 73 | private applyHandlers(): void { 74 | type HandlerTypes = 75 | | StartHandlerComponent 76 | | RaceHandlerComponent 77 | | GarageHandlerComponent 78 | | EventHandlerComponent 79 | | SessionsHandlerComponent 80 | | MultiplayerHandlerComponent 81 | | SettingsHandlerComponent; 82 | 83 | let factory: ComponentFactory; 84 | switch (location.pathname) { 85 | case '/start/index.html': 86 | factory = this.resolver.resolveComponentFactory(StartHandlerComponent); 87 | break; 88 | 89 | case '/race/index.html': 90 | factory = this.resolver.resolveComponentFactory(RaceHandlerComponent); 91 | break; 92 | 93 | case '/garage/index.html': 94 | factory = this.resolver.resolveComponentFactory(GarageHandlerComponent); 95 | break; 96 | 97 | case '/event/index.html': 98 | factory = this.resolver.resolveComponentFactory(EventHandlerComponent); 99 | break; 100 | 101 | case '/sessions/index.html': 102 | factory = this.resolver.resolveComponentFactory(SessionsHandlerComponent); 103 | break; 104 | 105 | case '/multiplayer/index.html': 106 | factory = this.resolver.resolveComponentFactory(MultiplayerHandlerComponent); 107 | break; 108 | 109 | case '/options/index.html': 110 | factory = this.resolver.resolveComponentFactory(SettingsHandlerComponent); 111 | break; 112 | } 113 | 114 | this.container.clear(); 115 | this.container.createComponent(factory); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
  1. 3 |
    4 | 7 |

    {{ msg.message }}

    8 |
    9 |
  2. 10 |
11 | 12 |
13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | height: auto; 5 | min-height: 0; 6 | } 7 | 8 | .chat { 9 | display: flex; 10 | flex-direction: column; 11 | flex: 1 1 100%; 12 | min-height: 0; 13 | margin: 0; 14 | width: 100%; 15 | 16 | margin-top: 1rem; 17 | padding: 0 0.5rem 0; 18 | overflow: auto; 19 | list-style-type: none; 20 | 21 | li { 22 | flex: 0 0 auto; 23 | margin-bottom: 5px; 24 | display: flex; 25 | 26 | .body { 27 | overflow: hidden; 28 | overflow-wrap: break-word; 29 | flex: 8; 30 | 31 | .metadata { 32 | justify-content: space-between; 33 | font-size: 0.75rem; 34 | opacity: 0.8; 35 | display: flex; 36 | color: #b5b8ba; 37 | } 38 | 39 | p { 40 | white-space: pre-wrap; 41 | margin-top: 0; 42 | margin-block-end: 0; 43 | font-size: 0.9rem; 44 | font-weight: 300; 45 | line-height: 1.5; 46 | } 47 | } 48 | } 49 | } 50 | 51 | .bottom { 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | width: 100%; 56 | padding: 0.5rem; 57 | text-align: center; 58 | opacity: 0.8; 59 | 60 | input { 61 | flex: 1 1 auto; 62 | margin-right: 10px; 63 | font-size: 0.9rem; 64 | font-weight: 300; 65 | line-height: 1; 66 | background: #555; 67 | border: none; 68 | resize: none; 69 | padding-left: 0.3rem; 70 | overflow: auto; 71 | color: inherit; 72 | } 73 | 74 | .fi:before { 75 | color: #ffffff; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatComponent } from './chat.component'; 4 | 5 | describe('ChatComponent', () => { 6 | let component: ChatComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ChatComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ChatComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, HostBinding, OnDestroy, OnInit, ViewChild } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, Subject, timer } from 'rxjs'; 4 | import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; 5 | 6 | interface ChatMessage { 7 | message: string; 8 | timestamp: number; 9 | } 10 | 11 | @Component({ 12 | selector: 'rf-chat', 13 | templateUrl: './chat.component.html', 14 | styleUrls: ['./chat.component.scss'] 15 | }) 16 | export class ChatComponent implements OnInit, OnDestroy { 17 | @HostBinding('style.height.px') 18 | height: number = undefined; 19 | 20 | @HostBinding('style.opacity') 21 | opacity: number = 0; 22 | 23 | @ViewChild('chatList') 24 | private chatListElement: ElementRef; 25 | 26 | messages$: Observable; 27 | 28 | private destroy$: Subject = new Subject(); 29 | 30 | constructor(private http: HttpClient) {} 31 | 32 | ngOnInit(): void { 33 | setTimeout(() => { 34 | this.calculateHeight(); 35 | 36 | this.messages$ = timer(1, 1000).pipe( 37 | switchMap(() => this.http.get('/rest/chat')), 38 | distinctUntilChanged((prev: ChatMessage[], curr: ChatMessage[]) => prev.length === curr.length), 39 | tap(() => { 40 | const top: number = 41 | this.chatListElement.nativeElement.scrollTop + this.chatListElement.nativeElement.offsetHeight; 42 | const scrollHeight: number = this.chatListElement.nativeElement.scrollHeight; 43 | 44 | if (top === scrollHeight) { 45 | // user is at the bottom 46 | this.scrollToBottom(); 47 | } 48 | }) 49 | ); 50 | 51 | this.opacity = 1; 52 | }, 1000); 53 | } 54 | 55 | ngOnDestroy(): void { 56 | this.destroy$.next(true); 57 | this.destroy$.complete(); 58 | } 59 | 60 | sendMessage(msg: string): void { 61 | this.http.post('/rest/chat', msg).subscribe(); 62 | } 63 | 64 | private scrollToBottom(): void { 65 | setTimeout(() => { 66 | try { 67 | this.chatListElement.nativeElement.scrollTop = this.chatListElement.nativeElement.scrollHeight; 68 | } catch (err) { 69 | // it's ok 70 | } 71 | }, 0); 72 | } 73 | 74 | public calculateHeight(): void { 75 | const cameraControls: Element = document.getElementsByClassName('camera-controls')[0]; 76 | const cameraBottom: number = cameraControls?.getBoundingClientRect().bottom; 77 | 78 | const buttonNav: Element = document.getElementsByClassName('buttonnavigation')[0]; 79 | const buttonNavTop: number = buttonNav?.getBoundingClientRect().top; 80 | 81 | this.height = buttonNavTop - cameraBottom; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/components/event-handler/event-handler.component.scss: -------------------------------------------------------------------------------- 1 | .cameraHolder { 2 | display: flex; 3 | height: 100%; 4 | flex-direction: column; 5 | min-height: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/event-handler/event-handler.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EventHandlerComponent } from './event-handler.component'; 4 | 5 | describe('EventHandlerComponent', () => { 6 | let component: EventHandlerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [EventHandlerComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(EventHandlerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/event-handler/event-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { waitForElement } from '../../utils/utils'; 3 | import { RaceButtonService } from '../../services/race-button.service'; 4 | 5 | interface AppSwitchService { 6 | openAppWithTab: (a: string, b: string) => void; 7 | } 8 | 9 | @Component({ 10 | selector: 'rf-event-handler', 11 | template: '', 12 | styleUrls: ['./event-handler.component.scss'], 13 | encapsulation: ViewEncapsulation.None 14 | }) 15 | export class EventHandlerComponent implements OnInit, OnDestroy { 16 | private tabHolder: HTMLDivElement; 17 | private observer: MutationObserver; 18 | private injector: { get: (value: string) => T }; 19 | 20 | constructor(private raceButtonService: RaceButtonService) {} 21 | 22 | ngOnInit(): void { 23 | this.injector = angular.element('ui-view').injector(); 24 | this.raceButtonService.addRaceButton(); 25 | waitForElement('nav em').then((e: Element) => { 26 | const countDown: HTMLElement = document.createElement('rf-race-countdown-timer'); 27 | e.parentElement.appendChild(countDown); 28 | }); 29 | 30 | // add rf-chat component to the DOM 31 | waitForElement('.camera-wrapper').then(() => { 32 | this.tabHolder = document.querySelector("div[ng-if='eventCtrl.gameState.navigationState']"); 33 | this.injectChat(); 34 | this.observeTabChange(); 35 | }); 36 | 37 | waitForElement('svg.track-map', 2000).then(() => { 38 | this.tabHolder = document.querySelector("div[ng-if='eventCtrl.gameState.navigationState']"); 39 | this.addCarSelect(); 40 | this.observeTabChange(); 41 | }); 42 | } 43 | 44 | private observeTabChange(): void { 45 | this.observer = new MutationObserver((mutations: MutationRecord[]) => { 46 | for (const m of mutations) { 47 | // if class changed and the selected tab is camera 48 | if (m.attributeName === 'class' && this.tabHolder.classList.contains('tab-camera')) { 49 | this.injectChat(); 50 | break; 51 | } 52 | 53 | if (m.attributeName === 'class' && this.tabHolder.classList.contains('tab-eventinfo')) { 54 | this.addCarSelect(); 55 | break; 56 | } 57 | } 58 | }); 59 | 60 | this.observer.observe(this.tabHolder, { attributes: true }); 61 | } 62 | 63 | private async addCarSelect(): Promise { 64 | const holderDiv: HTMLDivElement = document.querySelector('.left-content-bottom div'); 65 | 66 | if (holderDiv.querySelector('button.carSelect')) { 67 | // already added 68 | return; 69 | } 70 | 71 | const carSelect: HTMLButtonElement = document.createElement('button'); 72 | carSelect.classList.add('secondary', 'carSelect'); 73 | carSelect.innerHTML = `Car Select`; 74 | 75 | const appSwitchService: AppSwitchService = this.injector.get('appSwitchService'); 76 | 77 | carSelect.onclick = () => { 78 | sessionStorage.setItem('betterUI.carSelect', 'true'); 79 | appSwitchService.openAppWithTab('race', 'car'); 80 | }; 81 | 82 | holderDiv.appendChild(carSelect); 83 | } 84 | 85 | private injectChat(): void { 86 | const ngIncludeElement: HTMLElement = document.querySelector('.camera-wrapper').parentElement; 87 | 88 | if (ngIncludeElement.querySelector('rf-chat')) { 89 | // already injected 90 | return; 91 | } 92 | 93 | ngIncludeElement.classList.add('cameraHolder'); 94 | 95 | const rfChat: HTMLElement = document.createElement('rf-chat'); 96 | ngIncludeElement.appendChild(rfChat); 97 | } 98 | 99 | ngOnDestroy(): void { 100 | this.observer.disconnect(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app/components/exit-dialog/exit-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Confirm exit

4 |
Are you sure you want to quit?
5 |
6 | 7 | 11 |
12 | -------------------------------------------------------------------------------- /src/app/components/exit-dialog/exit-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/app/components/exit-dialog/exit-dialog.component.scss -------------------------------------------------------------------------------- /src/app/components/exit-dialog/exit-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExitDialogComponent } from './exit-dialog.component'; 4 | 5 | describe('ExitDialogComponent', () => { 6 | let component: ExitDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ExitDialogComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ExitDialogComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/exit-dialog/exit-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | @Component({ 6 | selector: 'rf-exit-dialog', 7 | templateUrl: './exit-dialog.component.html', 8 | styleUrls: ['./exit-dialog.component.scss'] 9 | }) 10 | export class ExitDialogComponent { 11 | constructor(private dialogRef: MatDialogRef, private http: HttpClient) {} 12 | 13 | close(): void { 14 | this.dialogRef.close(); 15 | } 16 | 17 | exit(): void { 18 | this.http.post('/rest/start/quitGame', undefined).subscribe(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/garage-handler/garage-handler.component.scss: -------------------------------------------------------------------------------- 1 | @use '../../../scss/common-styles' as common; 2 | 3 | .modal-dialog .setup-content .setup-tree-wrapper { 4 | font-weight: normal; 5 | 6 | .track-selected { 7 | font-style: italic; 8 | font-weight: bold; 9 | } 10 | } 11 | 12 | .templ-setting-view .setting-panel { 13 | h1 { 14 | margin-bottom: 0; 15 | } 16 | } 17 | 18 | ol.buttonnavigation { 19 | display: none; 20 | } 21 | 22 | main.garage { 23 | ui-view { 24 | margin: 1rem 0 0; 25 | } 26 | 27 | .left-section { 28 | padding-bottom: 0; 29 | } 30 | 31 | h1 { 32 | margin-bottom: 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/garage-handler/garage-handler.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GarageHandlerComponent } from './garage-handler.component'; 4 | 5 | describe('GarageHandlerComponent', () => { 6 | let component: GarageHandlerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [GarageHandlerComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(GarageHandlerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/garage-handler/garage-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { arrowNavigation, waitForElement } from '../../utils/utils'; 4 | import { RaceButtonService } from '../../services/race-button.service'; 5 | import { MatDialog } from '@angular/material/dialog'; 6 | import { SetupsComponent } from '../../modules/garage/components/setups/setups.component'; 7 | 8 | @Component({ 9 | selector: 'rf-garage-handler', 10 | template: '', 11 | styleUrls: ['./garage-handler.component.scss'], 12 | encapsulation: ViewEncapsulation.None 13 | }) 14 | export class GarageHandlerComponent implements OnInit { 15 | private listItems: NodeListOf; 16 | 17 | @HostListener('window:keyup', ['$event']) 18 | handleKeyboardEvent(event: KeyboardEvent): void { 19 | return arrowNavigation(event, this.listItems, 'main section div.selected'); 20 | } 21 | 22 | constructor(private raceButtonService: RaceButtonService, private http: HttpClient, private dialog: MatDialog) {} 23 | 24 | ngOnInit(): void { 25 | waitForElement('nav em').then((e: Element) => { 26 | const countDown: HTMLElement = document.createElement('rf-race-countdown-timer'); 27 | e.parentElement.appendChild(countDown); 28 | }); 29 | 30 | this.raceButtonService.addRaceButton(); 31 | 32 | this.listItems = document.querySelectorAll('main section div.thumbnail'); 33 | 34 | this.findSetupButtons(); 35 | 36 | window.addEventListener('hashchange', () => { 37 | if (location.hash.startsWith('#/summary')) { 38 | this.findSetupButtons(); 39 | } 40 | }); 41 | } 42 | 43 | private findSetupButtons(): void { 44 | if (localStorage.getItem('betterUi.disableSetup') === 'true') { 45 | return; 46 | } 47 | const setups: HTMLButtonElement = document.querySelectorAll('left-section button')[0] as HTMLButtonElement; 48 | 49 | setups.onclick = () => { 50 | this.dialog.open(SetupsComponent, { 51 | height: '80vh', 52 | maxHeight: '90vh', 53 | width: '80vw' 54 | }); 55 | }; 56 | angular.element('left-section button:first').unbind('click'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/components/multiplayer-handler/multiplayer-handler.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/app/components/multiplayer-handler/multiplayer-handler.component.scss -------------------------------------------------------------------------------- /src/app/components/multiplayer-handler/multiplayer-handler.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MultiplayerHandlerComponent } from './multiplayer-handler.component'; 4 | 5 | describe('MultiplayerHandlerComponent', () => { 6 | let component: MultiplayerHandlerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [MultiplayerHandlerComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(MultiplayerHandlerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/multiplayer-handler/multiplayer-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { waitForElement } from '../../utils/utils'; 3 | 4 | @Component({ 5 | selector: 'rf-multiplayer-handler', 6 | template: '', 7 | styleUrls: ['./multiplayer-handler.component.scss'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class MultiplayerHandlerComponent implements OnInit { 11 | private savedFilters: number[] = []; 12 | 13 | constructor() { 14 | console.log('Multiplayer handler works'); 15 | } 16 | 17 | ngOnInit(): void { 18 | this.savedFilters = JSON.parse(localStorage.getItem('savedFilters')) || []; 19 | 20 | // wait for servers to be loaded 21 | waitForElement('section.content table') 22 | .then(() => { 23 | const listItems: NodeListOf = document.querySelectorAll( 24 | 'section.content section.card section.filter-options li' 25 | ); 26 | listItems.forEach((li: HTMLLIElement, index: number) => { 27 | // if the filter was saved, then select it 28 | if (this.savedFilters.includes(index)) { 29 | li.querySelector('span').click(); 30 | } 31 | 32 | li.addEventListener('click', () => { 33 | if (li.querySelector('input').checked) { 34 | // filter selected 35 | if (!this.savedFilters.includes(index)) { 36 | this.savedFilters.push(index); 37 | } 38 | } else { 39 | // filter deselected 40 | if (this.savedFilters.includes(index)) { 41 | this.savedFilters.splice(this.savedFilters.indexOf(index), 1); 42 | } 43 | } 44 | this.savedFilters.sort(); 45 | 46 | localStorage.setItem('savedFilters', JSON.stringify(this.savedFilters)); 47 | }); 48 | }); 49 | }) 50 | .catch(() => console.warn("Can't find multiplayer table")); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/components/race-countdown-timer/race-countdown-timer.component.html: -------------------------------------------------------------------------------- 1 | {{ time }}s 2 | -------------------------------------------------------------------------------- /src/app/components/race-countdown-timer/race-countdown-timer.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/app/components/race-countdown-timer/race-countdown-timer.component.scss -------------------------------------------------------------------------------- /src/app/components/race-countdown-timer/race-countdown-timer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RaceCountdownTimerComponent } from './race-countdown-timer.component'; 4 | 5 | describe('RaceCountdownTimerComponent', () => { 6 | let component: RaceCountdownTimerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RaceCountdownTimerComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RaceCountdownTimerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/race-countdown-timer/race-countdown-timer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, timer } from 'rxjs'; 4 | import { filter, map, switchMap } from 'rxjs/operators'; 5 | import { SessionInfo } from '../../interfaces/session-info'; 6 | import { GamePhase } from '../../interfaces/game-phase'; 7 | 8 | @Component({ 9 | selector: 'rf-race-countdown-timer', 10 | templateUrl: './race-countdown-timer.component.html', 11 | styleUrls: ['./race-countdown-timer.component.scss'] 12 | }) 13 | export class RaceCountdownTimerComponent implements OnInit { 14 | public timeLeft$: Observable; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | ngOnInit(): void { 19 | this.timeLeft$ = timer(1, 1000).pipe( 20 | switchMap(() => this.http.get('/rest/watch/sessionInfo')), 21 | filter( 22 | (info: SessionInfo) => 23 | info.session.startsWith('RACE') && // race session 24 | info.gamePhase === GamePhase.BEFORE && // before formation lap start 25 | info.maximumLaps < 1500 26 | ), // lap limited race 27 | map((info: SessionInfo) => Math.round(info.endEventTime - info.currentEventTime)) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/race-handler/race-handler.component.scss: -------------------------------------------------------------------------------- 1 | .cdk-overlay-container { 2 | z-index: 9999 !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/race-handler/race-handler.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RaceHandlerComponent } from './race-handler.component'; 4 | 5 | describe('RaceHandlerComponent', () => { 6 | let component: RaceHandlerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RaceHandlerComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RaceHandlerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/race-handler/race-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { arrowNavigation, timeout, waitForElement } from '../../utils/utils'; 3 | import { MatDialog } from '@angular/material/dialog'; 4 | import { ExitDialogComponent } from '../exit-dialog/exit-dialog.component'; 5 | 6 | @Component({ 7 | selector: 'rf-race-handler', 8 | template: '', 9 | styleUrls: ['./race-handler.component.scss'], 10 | encapsulation: ViewEncapsulation.None 11 | }) 12 | export class RaceHandlerComponent implements OnInit { 13 | private listItems: NodeListOf; 14 | 15 | @HostListener('window:keyup', ['$event']) 16 | handleKeyboardEvent(event: KeyboardEvent): void { 17 | return arrowNavigation(event, this.listItems); 18 | } 19 | 20 | constructor(private dialog: MatDialog) { 21 | console.log('RACE handler activated'); 22 | } 23 | 24 | ngOnInit(): void { 25 | this.carSelect(); 26 | 27 | window.addEventListener('hashchange', () => { 28 | this.listItems = document.querySelectorAll('ol.tabnavigation:not(.bottom) li'); 29 | this.rememberPassword(); 30 | }); 31 | 32 | this.listItems = document.querySelectorAll('ol.tabnavigation:not(.bottom) li'); 33 | this.rememberPassword(); 34 | } 35 | 36 | private rememberPassword(): void { 37 | waitForElement('section#multiplayer ul li') 38 | .then(() => { 39 | document.querySelectorAll('section#multiplayer ul li').forEach((node: HTMLLIElement) => { 40 | node.addEventListener('click', () => { 41 | const selected: string = node.querySelector('span').textContent; 42 | this.handlePasswordPopup(selected); 43 | }); 44 | }); 45 | }) 46 | .catch(() => console.warn("Can't find multiplayer list")); 47 | } 48 | 49 | private handlePasswordPopup(serverName: string): void { 50 | const input: HTMLInputElement = document.querySelector('#server-password'); 51 | const submitButton: HTMLButtonElement = document.querySelector('.modal-form button.primary'); 52 | 53 | if (!input) { 54 | return; 55 | } 56 | 57 | submitButton.addEventListener('click', async () => { 58 | // if the password is wrong then start again 59 | await timeout(500); 60 | this.checkForDownloadsPopup(); 61 | try { 62 | await waitForElement('.modal-dialog p.validation-message', 2000); 63 | this.handlePasswordPopup(serverName); 64 | } catch (e) { 65 | // password was ok 66 | } 67 | }); 68 | } 69 | 70 | private async checkForDownloadsPopup(): Promise { 71 | try { 72 | const modalForm: HTMLElement = ( 73 | (await waitForElement( 74 | '.modal-dialog div[modal-title="\'Installing content\' | translate"', 75 | 2000 76 | )) as HTMLDivElement 77 | ).parentElement; 78 | 79 | if (modalForm.querySelector('div.modal-footer')) { 80 | // already added 81 | return; 82 | } 83 | 84 | const footer: HTMLDivElement = document.createElement('div'); 85 | footer.classList.add('modal-footer'); 86 | 87 | const exitButton: HTMLButtonElement = document.createElement('button'); 88 | exitButton.classList.add('btn', 'secondary'); 89 | exitButton.innerHTML = `Exit game`; 90 | exitButton.addEventListener('click', () => 91 | this.dialog.open(ExitDialogComponent, { 92 | panelClass: ['noDialogPadding', 'rfPanel'], 93 | width: '25vw' 94 | }) 95 | ); 96 | 97 | footer.appendChild(exitButton); 98 | modalForm.appendChild(footer); 99 | } catch (e) { 100 | // There is no download 101 | } 102 | } 103 | 104 | private carSelect(): void { 105 | if (sessionStorage.getItem('betterUI.carSelect') === 'true') { 106 | const carSelectCtrl = angular.element('ui-view').controller(); 107 | const origFun: () => void = carSelectCtrl.joinServer; 108 | 109 | carSelectCtrl.joinServer = () => { 110 | origFun(); 111 | carSelectCtrl.appSwitchService.openAppWithTab('event'); 112 | }; 113 | 114 | angular.element('nav.jumpmenu').scope().config.back = () => { 115 | carSelectCtrl.appSwitchService.openAppWithTab('event'); 116 | }; 117 | 118 | sessionStorage.removeItem('betterUI.carSelect'); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/app/components/sessions-handler/sessions-handler.component.scss: -------------------------------------------------------------------------------- 1 | section { 2 | right-section { 3 | div.opponent-filter { 4 | ul { 5 | li { 6 | &.selected { 7 | font-style: italic; 8 | font-weight: bold; 9 | } 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/sessions-handler/sessions-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | interface OpponentFilter { 6 | state: 'OP_NOTHING' | 'OP_OR'; 7 | stringValue: string; 8 | gVehFilterIndex: number; 9 | gSelectedListIndex: number; 10 | } 11 | 12 | @Component({ 13 | selector: 'rf-sessions-handler', 14 | template: '', 15 | styleUrls: ['./sessions-handler.component.scss'], 16 | encapsulation: ViewEncapsulation.None 17 | }) 18 | export class SessionsHandlerComponent implements OnInit { 19 | private rightSection: HTMLElement; 20 | 21 | constructor(private http: HttpClient) {} 22 | 23 | ngOnInit(): void { 24 | // if the selected thumbnail is the opponents already 25 | if (location.hash === '#/opponents') { 26 | this.findSelectButtons(); 27 | this.loadCurrentFilter(); 28 | } 29 | 30 | // find opponents thumbnail 31 | (document.querySelectorAll('main section div.thumbnail').item(1) as HTMLDivElement).addEventListener( 32 | 'click', 33 | () => { 34 | this.findSelectButtons(); 35 | this.loadCurrentFilter(); 36 | } 37 | ); 38 | } 39 | 40 | private findSelectButtons(): void { 41 | this.rightSection = document.querySelector('section right-section'); 42 | this.rightSection 43 | .querySelectorAll('button') 44 | .forEach((node: HTMLButtonElement) => node.addEventListener('click', () => this.loadCurrentFilter())); 45 | } 46 | 47 | private loadCurrentFilter(): void { 48 | this.http 49 | .get('/rest/sessions/opponents/filter') 50 | .pipe(map((filters: OpponentFilter[]) => filters.filter((f: OpponentFilter) => !!f))) 51 | .subscribe((filters: OpponentFilter[]) => { 52 | filters.forEach((filter: OpponentFilter) => { 53 | setTimeout(() => { 54 | this.rightSection.querySelectorAll(`div.opponent-filter ul li`).forEach((li: HTMLLIElement) => { 55 | if (filter.state === 'OP_OR' && li.innerText === filter.stringValue) { 56 | li.classList.add('selected'); 57 | } 58 | 59 | li.onclick = () => { 60 | setTimeout(() => { 61 | this.loadCurrentFilter(); 62 | }, 200); 63 | }; 64 | }); 65 | }, 0); 66 | }); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/components/start-handler/start-handler.component.scss: -------------------------------------------------------------------------------- 1 | ol.tabnavigation.bottom { 2 | z-index: 0; 3 | } 4 | 5 | main.largepanels { 6 | z-index: 1; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/components/start-handler/start-handler.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { StartHandlerComponent } from './start-handler.component'; 4 | 5 | describe('StartHandlerComponent', () => { 6 | let component: StartHandlerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [StartHandlerComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(StartHandlerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/components/start-handler/start-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { arrowNavigation } from '../../utils/utils'; 3 | 4 | @Component({ 5 | selector: 'rf-start-handler', 6 | template: '', 7 | styleUrls: ['./start-handler.component.scss'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class StartHandlerComponent implements OnInit { 11 | private listItems: NodeListOf; 12 | 13 | @HostListener('window:keyup', ['$event']) 14 | handleKeyboardEvent(event: KeyboardEvent): void { 15 | return arrowNavigation(event, this.listItems); 16 | } 17 | 18 | constructor() { 19 | console.log('Start handler activated'); 20 | } 21 | 22 | ngOnInit(): void { 23 | this.listItems = document.querySelectorAll('ol.tabnavigation:not(.bottom) li'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/interfaces/game-phase.ts: -------------------------------------------------------------------------------- 1 | export enum GamePhase { 2 | BEFORE, 3 | RECONNAISSANCE, 4 | WALKTHROUGH, 5 | FORMATION, 6 | COUNTDOWN, 7 | GREEN, 8 | SAFETY_CAR, 9 | RED, 10 | CHECKERED, 11 | INVALID 12 | } 13 | -------------------------------------------------------------------------------- /src/app/interfaces/session-info.ts: -------------------------------------------------------------------------------- 1 | import { GamePhase } from './game-phase'; 2 | 3 | export interface SessionInfo { 4 | trackName: string; 5 | session: string; 6 | currentEventTime: number; 7 | endEventTime: number; 8 | maximumLaps: number; 9 | lapDistance: number; 10 | numberOfVehicles: number; 11 | gamePhase: GamePhase; 12 | yellowFlagState: string; 13 | sectorFlag: string[]; 14 | startLightFrame: number; 15 | numRedLights: number; 16 | inRealtime: boolean; 17 | playerName: string; 18 | playerFileName: string; 19 | darkCloud: number; 20 | raining: number; 21 | ambientTemp: number; 22 | trackTemp: number; 23 | windSpeed: { 24 | x: number; 25 | y: number; 26 | z: number; 27 | velocity: number; 28 | }; 29 | 30 | minPathWetness: number; 31 | averagePathWetness: number; 32 | maxPathWetness: number; 33 | gameMode: string; 34 | passwordProtected: boolean; 35 | serverPort: number; 36 | maxPlayers: number; 37 | serverName: string; 38 | startEventTime: number; 39 | raceCompletion: unknown; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/copy-setup-popup/copy-setup-popup.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Copy setup to folder 4 |
5 | 6 | 7 |
8 |
15 | {{ folder.name }} 16 |
17 |
18 | 19 | 20 |
21 |
22 | {{ selectedFolder?.name }} 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/copy-setup-popup/copy-setup-popup.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Inject, ViewChild } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { Setup } from '../../interfaces/setup'; 4 | 5 | @Component({ 6 | selector: 'rf-copy-setup-popup', 7 | templateUrl: './copy-setup-popup.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class CopySetupPopupComponent implements AfterViewInit { 11 | @ViewChild('setupList') 12 | private setupList: ElementRef; 13 | 14 | public selectedFolder: Setup; 15 | public setupName: string; 16 | 17 | constructor( 18 | @Inject(MAT_DIALOG_DATA) public data: { setupFolders: Setup[]; setupName: string }, 19 | private dialog: MatDialogRef 20 | ) { 21 | const copySetupFolder: string = this.data.setupName.split('\\')[0]; 22 | this.setupName = this.data.setupName.split('\\')[1]; 23 | this.selectedFolder = this.data.setupFolders.find((s: Setup) => s.name.startsWith(copySetupFolder)); 24 | } 25 | 26 | ngAfterViewInit(): void { 27 | this.setupList.nativeElement.querySelector('#selected')?.scrollIntoView({ block: 'center' }); 28 | } 29 | 30 | cancel(): void { 31 | this.dialog.close(); 32 | } 33 | 34 | copy(): void { 35 | this.dialog.close({ selectedFolder: this.selectedFolder, setupName: this.setupName }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/delete-confirm-popup/delete-confirm-popup.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Delete setup
4 |
5 | Are you sure you want to delete {{ data.setupName }}? 7 |
8 |
9 | 10 |
11 | 12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/delete-confirm-popup/delete-confirm-popup.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'rf-delete-confirm-popup', 6 | templateUrl: './delete-confirm-popup.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class DeleteConfirmPopupComponent { 10 | constructor(@Inject(MAT_DIALOG_DATA) public data: { setupName: string }) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/setup-tree/setup-tree.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 7 | 14 | 15 | 18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | File 27 | Status 28 | Modified 29 |
30 |
31 | 32 | 33 | 39 |
40 |
47 |
48 |
{{ node.displayName }}
49 |
50 | Current 51 | Compared 54 |
55 |
{{ node.modified }}
56 |
57 |
58 |
59 | 60 | 61 | 69 | 72 | 73 |
78 | {{ node.displayName }} 79 |
80 |
81 |
82 |
83 |
84 |
85 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/setup-tree/setup-tree.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | ElementRef, 6 | EventEmitter, 7 | Input, 8 | Output, 9 | ViewChild 10 | } from '@angular/core'; 11 | import { Setup } from '../../interfaces/setup'; 12 | import { ArrayDataSource } from '@angular/cdk/collections'; 13 | import { FlatTreeControl } from '@angular/cdk/tree'; 14 | 15 | export interface ExtendedSetup extends Setup { 16 | isDirectory: boolean; 17 | isExpanded?: boolean; 18 | displayName?: string; 19 | } 20 | 21 | @Component({ 22 | selector: 'rf-setup-tree', 23 | templateUrl: './setup-tree.component.html', 24 | changeDetection: ChangeDetectionStrategy.OnPush 25 | }) 26 | export class SetupTreeComponent { 27 | private _setups: ExtendedSetup[]; 28 | 29 | @ViewChild('treeList') 30 | tree: ElementRef; 31 | 32 | @Input() 33 | setupsLoading: boolean; 34 | 35 | @Input() 36 | activeSetupName: string; 37 | 38 | @Input() 39 | compareToSetup: string; 40 | 41 | @Input() 42 | currentTrackFolder: string; 43 | 44 | @Input() 45 | set setups(values: Setup[]) { 46 | values = values.filter((s: Setup) => s.name !== ''); 47 | 48 | const extendedSetups: ExtendedSetup[] = values 49 | .map((setup: Setup) => { 50 | const isDirectory: boolean = setup?.name.endsWith('\\') && setup?.modified === ''; 51 | return { 52 | ...setup, 53 | isDirectory, 54 | displayName: setup.name.split('\\')[isDirectory ? 0 : 1] 55 | } as ExtendedSetup; 56 | }) 57 | .filter((setup: ExtendedSetup, index: number, array: ExtendedSetup[]) => { 58 | // We need to keep leafs 59 | if (!setup.isDirectory) { 60 | return true; 61 | } 62 | 63 | if (index === array.length - 1) { 64 | return false; 65 | } 66 | 67 | // Remove empty directories 68 | return !array[index + 1]?.isDirectory; 69 | }); 70 | this.dataSource = new ArrayDataSource(extendedSetups); 71 | this._setups = extendedSetups; 72 | } 73 | 74 | @Output() 75 | selected: EventEmitter = new EventEmitter(); 76 | 77 | public dataSource: ArrayDataSource; 78 | public treeControl: FlatTreeControl = new FlatTreeControl( 79 | (node: ExtendedSetup) => (node.isDirectory ? 0 : 1), 80 | (node: ExtendedSetup) => node.isDirectory 81 | ); 82 | 83 | public selectedSetup: Setup; 84 | public filter: string = ''; 85 | public isDirectory = (idx: number, node: ExtendedSetup): boolean => node.isDirectory; 86 | 87 | constructor(private cd: ChangeDetectorRef) {} 88 | 89 | getParentNode(node: ExtendedSetup): ExtendedSetup | null { 90 | const nodeIndex: number = this._setups.indexOf(node); 91 | 92 | for (let i = nodeIndex - 1; i >= 0; i--) { 93 | if (this._setups[i].isDirectory && node.name.startsWith(this._setups[i].name)) { 94 | return this._setups[i]; 95 | } 96 | } 97 | 98 | return null; 99 | } 100 | 101 | shouldRenderLeaf(node: ExtendedSetup): boolean { 102 | let parent = this.getParentNode(node); 103 | while (parent) { 104 | if (!parent.isExpanded) { 105 | return false; 106 | } 107 | parent = this.getParentNode(parent); 108 | } 109 | return true; 110 | } 111 | 112 | shouldRenderNode(node: ExtendedSetup): boolean { 113 | const allowedByFilter: boolean = node.displayName.toLowerCase().includes(this.filter.toLowerCase()); 114 | const nodeIndex: number = this._setups.indexOf(node); 115 | 116 | // Last item 117 | if (nodeIndex === this._setups.length - 1) { 118 | return !node.isDirectory; 119 | } 120 | 121 | return !this._setups[nodeIndex + 1]?.isDirectory && allowedByFilter; 122 | } 123 | 124 | select(node: ExtendedSetup): void { 125 | this.selectedSetup = node; 126 | this.selected.emit(this.selectedSetup); 127 | } 128 | 129 | trackBy(index: number, item: ExtendedSetup): string { 130 | return item.name; 131 | } 132 | 133 | openActiveSetupFolder(): void { 134 | const active: ExtendedSetup = this._setups.find((s: ExtendedSetup) => s.name === this.activeSetupName); 135 | this.openSetupFolder(active); 136 | } 137 | 138 | openSetupFolder(setup: ExtendedSetup): void { 139 | if (!setup) return; 140 | const parent = this.getParentNode(setup); 141 | if (!parent) return; 142 | parent.isExpanded = true; 143 | this.treeControl.expand(parent); 144 | this.cd.markForCheck(); 145 | } 146 | 147 | scrollToCurrentFolder(): void { 148 | this.tree.nativeElement.querySelector('#currentFolder')?.scrollIntoView({ behavior: 'smooth' }); 149 | } 150 | 151 | scrollToCurrentFile(): void { 152 | this.openActiveSetupFolder(); 153 | setTimeout(() => this.tree.nativeElement.querySelector('#currentFile')?.scrollIntoView({ behavior: 'smooth' })); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/setups/setups.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Setups 4 | 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | {{ setupName$ | async }} 13 |
14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | 34 |
35 | 36 |
37 | 38 | 47 |
48 | 49 |
53 | 61 | 62 |
63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 |
75 | 76 | Show Only Relevant 77 |
78 | 79 |
80 | 83 | 84 | 87 |
88 |
89 | 99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /src/app/modules/garage/components/setups/setups.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostBinding, OnInit } from '@angular/core'; 2 | import { MatDialog, MatDialogRef } from '@angular/material/dialog'; 3 | import { select, Store } from '@ngrx/store'; 4 | import * as GarageActions from '../../state/garage.actions'; 5 | import { Setup } from '../../interfaces/setup'; 6 | import * as GarageSelectors from '../../state/garage.selectors'; 7 | import { Observable } from 'rxjs'; 8 | import { DeleteConfirmPopupComponent } from '../delete-confirm-popup/delete-confirm-popup.component'; 9 | import { CopySetupPopupComponent } from '../copy-setup-popup/copy-setup-popup.component'; 10 | 11 | @Component({ 12 | selector: 'rf-setups', 13 | templateUrl: './setups.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class SetupsComponent implements OnInit { 17 | @HostBinding('class') 18 | flexClass: string[] = ['flex', 'flex-col', 'h-full']; 19 | 20 | public setups$: Observable = this.store.pipe(select(GarageSelectors.selectSetups)); 21 | public setupsLoading$: Observable = this.store.pipe(select(GarageSelectors.selectSetupsLoading)); 22 | public setupName$: Observable = this.store.pipe(select(GarageSelectors.selectDisplayedSetupName)); 23 | public activeSetupName$: Observable = this.store.pipe(select(GarageSelectors.selectActiveSetupName)); 24 | public notes$: Observable = this.store.pipe(select(GarageSelectors.selectCurrentNotes)); 25 | public showingOnlyRelevant$: Observable = this.store.pipe(select(GarageSelectors.showingOnlyRelevant)); 26 | public compareToSetup$: Observable = this.store.pipe(select(GarageSelectors.selectCompareSetup)); 27 | public currentTrackFolder$: Observable = this.store.pipe(select(GarageSelectors.selectCurrentTrackFolder)); 28 | public setupFolders$: Observable = this.store.pipe(select(GarageSelectors.selectSetupFolders)); 29 | 30 | constructor(private dialogRef: MatDialogRef, private store: Store, private dialog: MatDialog) {} 31 | 32 | ngOnInit(): void { 33 | this.store.dispatch(GarageActions.loadSetups()); 34 | this.store.dispatch(GarageActions.loadSetupSummary()); 35 | this.store.dispatch(GarageActions.loadShowingRelevant()); 36 | this.store.dispatch(GarageActions.loadCurrentTrackFolder()); 37 | } 38 | 39 | loadSelected(): void { 40 | this.store.dispatch(GarageActions.loadSavedSetup()); 41 | this.close(); 42 | } 43 | 44 | close(): void { 45 | this.dialogRef.close(); 46 | } 47 | 48 | selectSetup(setup: Setup): void { 49 | this.store.dispatch(GarageActions.selectSetup({ setup })); 50 | } 51 | 52 | changeShowOnlyRelevant(show: boolean): void { 53 | this.store.dispatch(GarageActions.changeShowOnlyRelevant({ showOnlyRelevant: show })); 54 | } 55 | 56 | deleteSelected(setupName: string): void { 57 | this.dialog 58 | .open(DeleteConfirmPopupComponent, { 59 | panelClass: ['noDialogPadding', 'rfPanel'], 60 | data: { setupName } 61 | }) 62 | .afterClosed() 63 | .subscribe((confirmed: boolean) => { 64 | if (confirmed) { 65 | this.store.dispatch(GarageActions.deleteSelectedSetup()); 66 | } 67 | }); 68 | } 69 | 70 | compare(): void { 71 | this.store.dispatch(GarageActions.compareSelected()); 72 | this.dialogRef.close(); 73 | } 74 | 75 | setDefaultSetup(): void { 76 | this.store.dispatch(GarageActions.setDefaultSelected()); 77 | this.dialogRef.close(); 78 | } 79 | 80 | loadFactoryDefault(): void { 81 | this.store.dispatch(GarageActions.factoryDefault()); 82 | this.dialogRef.close(); 83 | } 84 | 85 | openCopyPopup(setupFolders: Setup[], setupName: string): void { 86 | this.dialog 87 | .open(CopySetupPopupComponent, { 88 | panelClass: ['noDialogPadding', 'rfPanel'], 89 | height: '70vh', 90 | maxHeight: '70vh', 91 | width: '40vw', 92 | data: { setupFolders, setupName } 93 | }) 94 | .afterClosed() 95 | .subscribe((result: { selectedFolder: Setup; setupName: string }) => { 96 | if (!result) return; 97 | this.store.dispatch( 98 | GarageActions.copySetup({ dest: `${result.selectedFolder.name}${result.setupName}`, src: setupName }) 99 | ); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app/modules/garage/garage.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { StoreModule } from '@ngrx/store'; 5 | import { EffectsModule } from '@ngrx/effects'; 6 | import { SetupsComponent } from './components/setups/setups.component'; 7 | import * as fromGarage from './state'; 8 | import { GarageEffects } from './state/garage.effects'; 9 | import { GarageService } from './services/garage.service'; 10 | import { UiComponentsModule } from '../ui-components/ui-components.module'; 11 | import { SetupTreeComponent } from './components/setup-tree/setup-tree.component'; 12 | import { CdkTreeModule } from '@angular/cdk/tree'; 13 | import { MatButtonModule } from '@angular/material/button'; 14 | import { DeleteConfirmPopupComponent } from './components/delete-confirm-popup/delete-confirm-popup.component'; 15 | import { MatDialogModule } from '@angular/material/dialog'; 16 | import { CopySetupPopupComponent } from './components/copy-setup-popup/copy-setup-popup.component'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | CommonModule, 21 | FormsModule, 22 | StoreModule.forFeature(fromGarage.garageFeatureKey, fromGarage.reducers, { metaReducers: fromGarage.metaReducers }), 23 | EffectsModule.forFeature([GarageEffects]), 24 | CdkTreeModule, 25 | UiComponentsModule, 26 | MatButtonModule, 27 | MatDialogModule 28 | ], 29 | declarations: [SetupsComponent, SetupTreeComponent, DeleteConfirmPopupComponent, CopySetupPopupComponent], 30 | providers: [GarageService], 31 | exports: [SetupsComponent] 32 | }) 33 | export class GarageModule {} 34 | -------------------------------------------------------------------------------- /src/app/modules/garage/interfaces/setup.ts: -------------------------------------------------------------------------------- 1 | export interface Setup { 2 | name: string; 3 | sameVehicleClass: boolean; 4 | created: string; // Date 5 | modified: string; // Date 6 | numDiffUpgrades: number; 7 | } 8 | 9 | export interface SetupSummary { 10 | settingSummaries: unknown; 11 | activeSetup: string; 12 | compareToSetup: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/modules/garage/services/garage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Setup, SetupSummary } from '../interfaces/setup'; 6 | 7 | @Injectable() 8 | export class GarageService { 9 | constructor(private http: HttpClient) {} 10 | 11 | public loadSetups(): Observable { 12 | return this.http.get('/rest/garage/setup'); 13 | } 14 | 15 | public loadSavedSetup(name: string): Observable { 16 | return this.http.put('/rest/garage/setup', name); 17 | } 18 | 19 | public loadSetupSummary(): Observable { 20 | return this.http.get('/rest/garage/summary'); 21 | } 22 | 23 | public compare(name: string): Observable { 24 | return this.http.post('/rest/garage/setup/compare', name); 25 | } 26 | 27 | public loadNotes(name: string): Observable { 28 | if (!name || name === '') { 29 | return of(''); 30 | } 31 | return this.http 32 | .get(`/rest/garage/setup/notes/${encodeURI(name)}`, { responseType: 'text' }) 33 | .pipe(map((message: string) => message.replace(/(ÿ)/gm, '\n').replace(/['"]+/g, '').replace('NOTES=', ''))); 34 | } 35 | 36 | public saveNotes(notes: string): Observable { 37 | return this.http.post('/rest/garage/setup/notes', notes); 38 | } 39 | 40 | public loadShowingOnlyRelevant(): Observable { 41 | return this.http.get('/rest/garage/showOnlyRelevantSetups'); 42 | } 43 | 44 | public changeShowingOnlyRelevant(showOnlyRelevant: boolean): Observable { 45 | return this.http.post('/rest/garage/showOnlyRelevantSetups', showOnlyRelevant); 46 | } 47 | 48 | public deleteSetup(name: string): Observable { 49 | return this.http.delete(`/rest/garage/setup/${encodeURI(name)}`); 50 | } 51 | 52 | public compareSetup(name: string): Observable { 53 | return this.http.post(`/rest/garage/setup/compare`, name); 54 | } 55 | 56 | public setDefault(name: string): Observable { 57 | return this.http.post('/rest/garage/setup/default', name); 58 | } 59 | 60 | public loadCurrentTrackFolder(): Observable { 61 | return this.http.get('/rest/garage/currentTrackFolder', { responseType: 'text' }); 62 | } 63 | 64 | public copySetup(dest: string, src: string): Observable { 65 | return this.http.post(`/rest/garage/setup/copysetup`, { dest, src }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/modules/garage/state/garage.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | import { Setup, SetupSummary } from '../interfaces/setup'; 3 | 4 | export const loadSetups = createAction('[Garage] Load Setups'); 5 | 6 | export const setupsLoaded = createAction('[Garage] Setups Loaded', props<{ setups: Setup[] }>()); 7 | 8 | export const loadSavedSetup = createAction('[Garage] Load Saved Setup'); 9 | 10 | export const updateView = createAction('[Garage] Update View'); 11 | 12 | export const loadSetupSummary = createAction('[Garage] Load Setup Summary'); 13 | 14 | export const setupSummaryLoaded = createAction('[Garage] Setup Summary Loaded', props()); 15 | 16 | export const selectSetup = createAction('[Garage] Select Setup', props<{ setup: Setup }>()); 17 | 18 | export const loadNotes = createAction('[Garage] Load Notes', props<{ setupName: string }>()); 19 | 20 | export const notesLoaded = createAction('[Garage] Notes Loaded', props<{ notes: string }>()); 21 | 22 | export const loadShowingRelevant = createAction('[Garage] Load Showing Relevant'); 23 | 24 | export const showingRelevantLoaded = createAction( 25 | '[Garage] Showing Relevant Loaded', 26 | props<{ showOnlyRelevant: boolean }>() 27 | ); 28 | 29 | export const changeShowOnlyRelevant = createAction( 30 | '[Garage] Change Show Only Relevant', 31 | props<{ showOnlyRelevant: boolean }>() 32 | ); 33 | 34 | export const deleteSelectedSetup = createAction('[Garage] Delete Selected Setup'); 35 | 36 | export const compareSelected = createAction('[Garage] Compare Selected'); 37 | 38 | export const setDefaultSelected = createAction('[Garage] Set Default Setup'); 39 | 40 | export const factoryDefault = createAction('[Garage] Factory Default'); 41 | 42 | export const loadCurrentTrackFolder = createAction('[Garage] Load Current Track Folder'); 43 | export const currentTrackFolderLoaded = createAction( 44 | '[Garage] Current Track Folder Loaded', 45 | props<{ currentTrackFolder: string }>() 46 | ); 47 | 48 | export const copySetup = createAction('[Garage] Copy Setup', props<{ dest: string; src: string }>()); 49 | -------------------------------------------------------------------------------- /src/app/modules/garage/state/garage.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; 3 | import { select, Store } from '@ngrx/store'; 4 | import { map, mergeMap, switchMap, tap } from 'rxjs/operators'; 5 | import * as GarageActions from './garage.actions'; 6 | import * as GarageSelectors from './garage.selectors'; 7 | import { GarageService } from '../services/garage.service'; 8 | import { Setup, SetupSummary } from '../interfaces/setup'; 9 | import { GarageState } from './garage.reducer'; 10 | 11 | interface SummaryService { 12 | initSummaryBoxes(data: unknown): void; 13 | } 14 | 15 | @Injectable() 16 | export class GarageEffects { 17 | loadSetups$ = createEffect(() => 18 | this.actions$.pipe( 19 | ofType(GarageActions.loadSetups), 20 | switchMap(() => this.service.loadSetups()), 21 | map((setups: Setup[]) => GarageActions.setupsLoaded({ setups })) 22 | ) 23 | ); 24 | 25 | loadSavedSetup$ = createEffect(() => 26 | this.actions$.pipe( 27 | ofType(GarageActions.loadSavedSetup), 28 | concatLatestFrom(() => this.store.pipe(select(GarageSelectors.selectDisplayedSetupName))), 29 | switchMap(([, setupName]: [never, string]) => this.service.loadSavedSetup(setupName)), 30 | map(() => GarageActions.updateView()) 31 | ) 32 | ); 33 | 34 | updateView$ = createEffect( 35 | () => 36 | this.actions$.pipe( 37 | ofType(GarageActions.updateView), 38 | map(() => angular.element('left-section').controller()), 39 | switchMap(controller => 40 | this.service.loadSetupSummary().pipe( 41 | tap((summary: SetupSummary) => { 42 | const summaryService: SummaryService = controller.summaryService; 43 | controller.settingsSummary = summary; 44 | summaryService.initSummaryBoxes(summary.settingSummaries); 45 | }), 46 | switchMap((summary: SetupSummary) => 47 | this.service.loadNotes(summary.activeSetup).pipe( 48 | tap((notes: string) => (controller.setupNotes = notes)), 49 | switchMap(() => this.service.loadNotes(summary.compareToSetup)), 50 | tap((notes: string) => (controller.comparedSetupNotes = notes)) 51 | ) 52 | ) 53 | ) 54 | ) 55 | ), 56 | { dispatch: false } 57 | ); 58 | 59 | loadSetupSummary$ = createEffect(() => 60 | this.actions$.pipe( 61 | ofType(GarageActions.loadSetupSummary), 62 | switchMap(() => this.service.loadSetupSummary()), 63 | mergeMap((summary: SetupSummary) => [ 64 | GarageActions.setupSummaryLoaded(summary), 65 | GarageActions.loadNotes({ setupName: summary.activeSetup }) 66 | ]) 67 | ) 68 | ); 69 | 70 | selectSetup$ = createEffect(() => 71 | this.actions$.pipe( 72 | ofType(GarageActions.selectSetup), 73 | map(action => GarageActions.loadNotes({ setupName: action.setup.name })) 74 | ) 75 | ); 76 | 77 | loadNotes$ = createEffect(() => 78 | this.actions$.pipe( 79 | ofType(GarageActions.loadNotes), 80 | switchMap((data: { setupName: string }) => this.service.loadNotes(data.setupName)), 81 | map((notes: string) => GarageActions.notesLoaded({ notes })) 82 | ) 83 | ); 84 | 85 | loadShowingOnlyRelevant$ = createEffect(() => 86 | this.actions$.pipe( 87 | ofType(GarageActions.loadShowingRelevant), 88 | switchMap(() => this.service.loadShowingOnlyRelevant()), 89 | map((showOnlyRelevant: boolean) => GarageActions.showingRelevantLoaded({ showOnlyRelevant })) 90 | ) 91 | ); 92 | 93 | changeShowOnlyRelevant$ = createEffect(() => 94 | this.actions$.pipe( 95 | ofType(GarageActions.changeShowOnlyRelevant), 96 | switchMap(action => this.service.changeShowingOnlyRelevant(action.showOnlyRelevant)), 97 | map(() => GarageActions.loadSetups()) 98 | ) 99 | ); 100 | 101 | deleteSetup$ = createEffect(() => 102 | this.actions$.pipe( 103 | ofType(GarageActions.deleteSelectedSetup), 104 | concatLatestFrom(() => this.store.pipe(select(GarageSelectors.selectDisplayedSetupName))), 105 | switchMap(([, setup]: [never, string]) => this.service.deleteSetup(setup)), 106 | map(() => GarageActions.loadSetups()) 107 | ) 108 | ); 109 | 110 | compare$ = createEffect(() => 111 | this.actions$.pipe( 112 | ofType(GarageActions.compareSelected), 113 | concatLatestFrom(() => this.store.pipe(select(GarageSelectors.selectDisplayedSetupName))), 114 | switchMap(([, setup]: [never, string]) => this.service.compareSetup(setup)), 115 | map(() => GarageActions.updateView()) 116 | ) 117 | ); 118 | 119 | setDefaultSelected$ = createEffect(() => 120 | this.actions$.pipe( 121 | ofType(GarageActions.setDefaultSelected), 122 | concatLatestFrom(() => this.store.pipe(select(GarageSelectors.selectDisplayedSetupName))), 123 | switchMap(([, setup]: [never, string]) => this.service.setDefault(setup)), 124 | map(() => GarageActions.updateView()) 125 | ) 126 | ); 127 | 128 | factoryDefault$ = createEffect(() => 129 | this.actions$.pipe( 130 | ofType(GarageActions.factoryDefault), 131 | switchMap(() => this.service.loadSavedSetup('')), 132 | map(() => GarageActions.updateView()) 133 | ) 134 | ); 135 | 136 | loadCurrentTrackFolder$ = createEffect(() => 137 | this.actions$.pipe( 138 | ofType(GarageActions.loadCurrentTrackFolder), 139 | switchMap(() => this.service.loadCurrentTrackFolder()), 140 | map((currentTrackFolder: string) => GarageActions.currentTrackFolderLoaded({ currentTrackFolder })) 141 | ) 142 | ); 143 | 144 | copySetup$ = createEffect(() => 145 | this.actions$.pipe( 146 | ofType(GarageActions.copySetup), 147 | switchMap((data: { dest: string; src: string }) => this.service.copySetup(data.dest, data.src)), 148 | map(() => GarageActions.loadSetups()) 149 | ) 150 | ); 151 | 152 | constructor(private actions$: Actions, private store: Store, private service: GarageService) {} 153 | } 154 | -------------------------------------------------------------------------------- /src/app/modules/garage/state/garage.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionReducer, createReducer, on } from '@ngrx/store'; 2 | import * as GarageActions from './garage.actions'; 3 | import { Setup, SetupSummary } from '../interfaces/setup'; 4 | 5 | export interface GarageState { 6 | setups: Setup[]; 7 | setupsLoading: boolean; 8 | currentSetupSummary: SetupSummary; 9 | selectedSetupName: string; 10 | currentNote: string; 11 | showOnlyRelevant: boolean; 12 | currentTrackFolder: string; 13 | } 14 | 15 | export const initialState: GarageState = { 16 | setups: [], 17 | setupsLoading: false, 18 | currentSetupSummary: undefined, 19 | selectedSetupName: undefined, 20 | currentNote: undefined, 21 | showOnlyRelevant: false, 22 | currentTrackFolder: undefined 23 | }; 24 | 25 | export const reducer: ActionReducer = createReducer( 26 | initialState, 27 | on(GarageActions.loadSetups, (state: GarageState) => ({ 28 | ...state, 29 | setups: [], 30 | setupsLoading: true 31 | })), 32 | 33 | on(GarageActions.setupsLoaded, (state: GarageState, { setups }) => ({ 34 | ...state, 35 | setups, 36 | setupsLoading: false 37 | })), 38 | 39 | on( 40 | GarageActions.setupSummaryLoaded, 41 | (state: GarageState, summary) => 42 | ({ 43 | ...state, 44 | currentSetupSummary: summary 45 | } as GarageState) 46 | ), 47 | 48 | on( 49 | GarageActions.selectSetup, 50 | (state: GarageState, { setup }) => 51 | ({ 52 | ...state, 53 | selectedSetupName: setup.name 54 | } as GarageState) 55 | ), 56 | 57 | on(GarageActions.notesLoaded, (state: GarageState, { notes }) => ({ 58 | ...state, 59 | currentNote: notes 60 | })), 61 | 62 | on(GarageActions.showingRelevantLoaded, (state: GarageState, { showOnlyRelevant }) => ({ 63 | ...state, 64 | showOnlyRelevant 65 | })), 66 | 67 | on(GarageActions.changeShowOnlyRelevant, (state: GarageState, { showOnlyRelevant }) => ({ 68 | ...state, 69 | showOnlyRelevant 70 | })), 71 | 72 | on(GarageActions.currentTrackFolderLoaded, (state: GarageState, { currentTrackFolder }) => ({ 73 | ...state, 74 | currentTrackFolder 75 | })) 76 | ); 77 | -------------------------------------------------------------------------------- /src/app/modules/garage/state/garage.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; 2 | import * as fromState from './index'; 3 | import { Setup, SetupSummary } from '../interfaces/setup'; 4 | import { GarageState } from './garage.reducer'; 5 | import { State } from './index'; 6 | 7 | const selectGarageFeatureState = createFeatureSelector(fromState.garageFeatureKey); 8 | 9 | export const selectGarageState = createSelector(selectGarageFeatureState, (state: State) => state.garage); 10 | 11 | export const selectSetups: MemoizedSelector = createSelector( 12 | selectGarageState, 13 | (state: GarageState) => state.setups 14 | ); 15 | 16 | export const selectSetupsLoading: MemoizedSelector = createSelector( 17 | selectGarageState, 18 | (state: GarageState) => state.setupsLoading 19 | ); 20 | 21 | export const selectCurrentSummary: MemoizedSelector = createSelector( 22 | selectGarageState, 23 | (state: GarageState) => state.currentSetupSummary 24 | ); 25 | 26 | export const selectDisplayedSetupName: MemoizedSelector = createSelector( 27 | selectGarageState, 28 | (state: GarageState) => state.selectedSetupName ?? state.currentSetupSummary?.activeSetup 29 | ); 30 | 31 | export const selectActiveSetupName: MemoizedSelector = createSelector( 32 | selectCurrentSummary, 33 | (summary: SetupSummary) => summary?.activeSetup 34 | ); 35 | 36 | export const selectCurrentNotes: MemoizedSelector = createSelector( 37 | selectGarageState, 38 | (state: GarageState) => state.currentNote 39 | ); 40 | 41 | export const showingOnlyRelevant: MemoizedSelector = createSelector( 42 | selectGarageState, 43 | (state: GarageState) => state.showOnlyRelevant 44 | ); 45 | 46 | export const selectCompareSetup: MemoizedSelector = createSelector( 47 | selectCurrentSummary, 48 | (summary: SetupSummary) => summary?.compareToSetup 49 | ); 50 | 51 | export const selectCurrentTrackFolder: MemoizedSelector = createSelector( 52 | selectGarageState, 53 | (state: GarageState) => state.currentTrackFolder 54 | ); 55 | 56 | export const selectSetupFolders: MemoizedSelector = createSelector( 57 | selectSetups, 58 | (setups: Setup[]) => setups?.filter((setup: Setup) => setup?.name.endsWith('\\') && setup?.modified === '') 59 | ); 60 | -------------------------------------------------------------------------------- /src/app/modules/garage/state/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap, MetaReducer } from '@ngrx/store'; 2 | import { environment } from '../../../../environments/environment'; 3 | import { GarageState, reducer } from './garage.reducer'; 4 | 5 | export const garageFeatureKey = 'garageFeature'; 6 | 7 | export interface State { 8 | garage: GarageState; 9 | } 10 | 11 | export const reducers: ActionReducerMap = { 12 | garage: reducer 13 | }; 14 | 15 | export const metaReducers: MetaReducer[] = !environment.production ? [] : []; 16 | -------------------------------------------------------------------------------- /src/app/modules/settings/components/settings-dialog/settings-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Better-UI Settings

4 | 5 |
6 | Disable setup popup 7 | 14 |
15 |
16 | 17 | 21 |
22 | -------------------------------------------------------------------------------- /src/app/modules/settings/components/settings-dialog/settings-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'rf-settings-dialog', 6 | templateUrl: './settings-dialog.component.html', 7 | styles: [], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class SettingsDialogComponent implements OnInit { 11 | disableSetup: boolean; 12 | 13 | constructor(private dialogRef: MatDialogRef) {} 14 | 15 | ngOnInit(): void { 16 | this.disableSetup = localStorage.getItem('betterUi.disableSetup') === 'true'; 17 | } 18 | 19 | save(): void { 20 | if (this.disableSetup) { 21 | localStorage.setItem('betterUi.disableSetup', 'true'); 22 | } else { 23 | localStorage.removeItem('betterUi.disableSetup'); 24 | } 25 | this.dialogRef.close(); 26 | } 27 | 28 | close(): void { 29 | this.dialogRef.close(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/modules/settings/components/settings-handler/settings-handler.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/app/modules/settings/components/settings-handler/settings-handler.component.html -------------------------------------------------------------------------------- /src/app/modules/settings/components/settings-handler/settings-handler.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { SettingsDialogComponent } from '../settings-dialog/settings-dialog.component'; 4 | 5 | @Component({ 6 | selector: 'rf-settings-handler', 7 | templateUrl: './settings-handler.component.html', 8 | styles: [], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class SettingsHandlerComponent implements OnInit, OnDestroy { 12 | constructor(private dialog: MatDialog) { 13 | console.log('Setting handler works'); 14 | } 15 | 16 | ngOnInit(): void { 17 | window.addEventListener('hashchange', this.addSettingsButton.bind(this)); 18 | 19 | this.addSettingsButton(); 20 | } 21 | 22 | ngOnDestroy(): void { 23 | window.removeEventListener('hashchange', this.addSettingsButton); 24 | } 25 | 26 | private addSettingsButton(): void { 27 | if (!location.hash.startsWith('#/summary')) { 28 | return; 29 | } 30 | const leftSection: HTMLElement = document.querySelector('section.left'); 31 | 32 | const settingsButton: HTMLButtonElement = document.createElement('button'); 33 | settingsButton.classList.add('primary'); 34 | settingsButton.onclick = () => { 35 | this.dialog.open(SettingsDialogComponent, { 36 | panelClass: ['noDialogPadding', 'rfPanel'] 37 | }); 38 | }; 39 | settingsButton.innerHTML = `Better-UI Settings`; 40 | 41 | leftSection.appendChild(settingsButton); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/modules/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/settings-handler/settings-handler.component'; 2 | -------------------------------------------------------------------------------- /src/app/modules/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SettingsHandlerComponent } from './components/settings-handler/settings-handler.component'; 4 | import { SettingsDialogComponent } from './components/settings-dialog/settings-dialog.component'; 5 | import { UiComponentsModule } from '../ui-components/ui-components.module'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, UiComponentsModule], 9 | declarations: [SettingsHandlerComponent, SettingsDialogComponent] 10 | }) 11 | export class SettingsModule {} 12 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/popup-header/popup-header.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

5 | 6 | 22 |
23 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/popup-header/popup-header.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'rf-popup-header', 5 | templateUrl: './popup-header.component.html', 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | export class PopupHeaderComponent { 9 | @Output() 10 | closeClicked: EventEmitter = new EventEmitter(); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/rf-button/rf-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/rf-button/rf-button.component.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../scss/common-styles' as common; 2 | 3 | .primary { 4 | background: #000000; 5 | } 6 | 7 | .danger { 8 | background: common.$colorRed; 9 | } 10 | 11 | .blue { 12 | background: common.$colorBlue; 13 | } 14 | 15 | .rfButton { 16 | @include common.skewElement(); 17 | 18 | line-height: 1.4; 19 | 20 | outline: 1px solid transparent; 21 | color: #ffff; 22 | 23 | border: 0; 24 | padding: 0.2rem 1.5rem 0.3rem; 25 | margin-bottom: 8px; 26 | margin-left: 0.45rem; 27 | text-transform: none; 28 | 29 | &:before { 30 | content: ''; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 100%; 36 | -webkit-box-shadow: 0.25rem 0.15rem 0.35rem rgba(0, 0, 0, 0.5); 37 | box-shadow: 0.25rem 0.15rem 0.35rem rgba(0, 0, 0, 0.5); 38 | opacity: 0; 39 | } 40 | 41 | &:after { 42 | content: ''; 43 | position: absolute; 44 | top: 0; 45 | right: 0; 46 | bottom: 0; 47 | left: 0; 48 | z-index: -1; 49 | background: common.$colorWhite; 50 | -webkit-transform: scale(0, 0); 51 | transform: scale(0, 0); 52 | -webkit-transform-origin: 0 0; 53 | transform-origin: 0 0; 54 | -webkit-transition: -webkit-transform 0.2s; 55 | transition: -webkit-transform 0.2s; 56 | transition: transform 0.2s, -webkit-transform 0.2s; 57 | } 58 | 59 | &:hover { 60 | transition: background 0.4s, color 0.2s; 61 | background: common.$colorWhite; 62 | color: #000000; 63 | 64 | &.danger { 65 | color: common.$colorRed; 66 | } 67 | 68 | &.blue { 69 | color: common.$colorBlue; 70 | } 71 | 72 | &:before { 73 | opacity: 1; 74 | -webkit-transition: opacity 0.4s; 75 | transition: opacity 0.4s; 76 | } 77 | 78 | &:after { 79 | -webkit-transform: scale(1, 1); 80 | transform: scale(1, 1); 81 | outline: 1px solid transparent; 82 | } 83 | } 84 | 85 | &:active { 86 | -webkit-transition: -webkit-transform linear 0.1s; 87 | transition: -webkit-transform linear 0.1s; 88 | transition: transform linear 0.1s, -webkit-transform linear 0.1s; 89 | // prettier-ignore 90 | -webkit-transform: scale(0.9, 0.9) skewX(- common.$skew); 91 | // prettier-ignore 92 | transform: scale(0.9, 0.9) skewX(- common.$skew); 93 | } 94 | 95 | .rfButtonText { 96 | display: inline-block; 97 | transform: skewX(common.$skew) translate3d(0, 0, 0); 98 | -webkit-transform: skewX(common.$skew) translate3d(0, 0, 0); 99 | font-size: 1rem; 100 | font-weight: 700; 101 | font-style: italic; 102 | text-transform: lowercase; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/rf-button/rf-button.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RfButtonComponent } from './rf-button.component'; 4 | 5 | describe('RfButtonComponent', () => { 6 | let component: RfButtonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RfButtonComponent] 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RfButtonComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/rf-button/rf-button.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostBinding, Input, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | // eslint-disable-next-line @angular-eslint/component-selector 5 | selector: 'button[rf-button]', 6 | templateUrl: './rf-button.component.html', 7 | styleUrls: ['./rf-button.component.scss'], 8 | encapsulation: ViewEncapsulation.None, 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class RfButtonComponent { 12 | @HostBinding('class.rfButton') 13 | rfButtonClass: boolean = true; 14 | 15 | @HostBinding('class') 16 | colorClass: string = 'primary'; 17 | 18 | @Input() 19 | set color(value: 'primary' | 'secondary' | 'danger' | 'blue') { 20 | this.colorClass = value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/spin-box/spin-box.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 10 |
11 | 12 |
13 |
{{ selected?.displayName }}
14 |
15 | 16 |
20 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/components/spin-box/spin-box.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { SpinBoxItem } from '../../model/spin-box-item'; 3 | 4 | @Component({ 5 | selector: 'rf-spin-box', 6 | templateUrl: './spin-box.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class SpinBoxComponent { 10 | @Input() 11 | items: SpinBoxItem[]; 12 | 13 | @Input() 14 | set selectedValue(value: unknown) { 15 | this.selectedIdx = this.items.findIndex((item: SpinBoxItem) => value === item.value); 16 | } 17 | 18 | get selected(): SpinBoxItem { 19 | return this.items[this.selectedIdx]; 20 | } 21 | 22 | @Output() 23 | selectedValueChange: EventEmitter = new EventEmitter(); 24 | 25 | public selectedIdx: number = -1; 26 | 27 | decrease(): void { 28 | if (this.selectedIdx === 0) { 29 | return; 30 | } 31 | this.selectedIdx--; 32 | this.selectedValueChange.emit(this.selected.value); 33 | } 34 | 35 | increase(): void { 36 | if (this.selectedIdx === this.items.length - 1) { 37 | return; 38 | } 39 | this.selectedIdx++; 40 | this.selectedValueChange.emit(this.selected.value); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model/spin-box-item'; 2 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/model/spin-box-item.ts: -------------------------------------------------------------------------------- 1 | export interface SpinBoxItem { 2 | displayName: string; 3 | value: unknown; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/modules/ui-components/ui-components.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RfButtonComponent } from './components/rf-button/rf-button.component'; 4 | import { SpinBoxComponent } from './components/spin-box/spin-box.component'; 5 | import { PopupHeaderComponent } from './components/popup-header/popup-header.component'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule], 9 | declarations: [RfButtonComponent, SpinBoxComponent, PopupHeaderComponent], 10 | exports: [RfButtonComponent, SpinBoxComponent, PopupHeaderComponent] 11 | }) 12 | export class UiComponentsModule {} 13 | -------------------------------------------------------------------------------- /src/app/services/race-button.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { RaceButtonService } from './race-button.service'; 4 | 5 | describe('RaceButtonService', () => { 6 | let service: RaceButtonService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(RaceButtonService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/services/race-button.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { waitForElement } from '../utils/utils'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class RaceButtonService { 9 | constructor(private http: HttpClient) {} 10 | 11 | public async addRaceButton(): Promise { 12 | const quitLi: HTMLLIElement = await waitForElement('nav ol.right li.fa-power-off', 1000); 13 | const ol: HTMLElement = quitLi.parentElement; 14 | if (ol) { 15 | const raceButton: HTMLButtonElement = document.createElement('button'); 16 | raceButton.classList.add('raceButton', 'fa', 'fa-play', 'fi-white'); 17 | raceButton.addEventListener('click', () => this.drive()); 18 | ol.appendChild(raceButton); 19 | } 20 | } 21 | 22 | private drive(): void { 23 | this.http.post('/rest/garage/drive', undefined).subscribe(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function waitForElement(selector: string, timeout: number = 30000): Promise { 2 | return new Promise((resolve: (element: T) => void, reject: () => void) => { 3 | if (document.querySelector(selector)) { 4 | return resolve(document.querySelector(selector)); 5 | } 6 | 7 | // after timeout give up 8 | const timeoutHandler = setTimeout(() => { 9 | observer.disconnect(); 10 | reject(); 11 | }, timeout); 12 | 13 | const observer = new MutationObserver(() => { 14 | if (document.querySelector(selector)) { 15 | resolve(document.querySelector(selector)); 16 | observer.disconnect(); 17 | clearInterval(timeoutHandler); 18 | } 19 | }); 20 | 21 | observer.observe(document.body, { 22 | childList: true, 23 | subtree: true 24 | }); 25 | }); 26 | } 27 | 28 | export function timeout(ms: number): Promise { 29 | return new Promise((resolve: () => void) => setTimeout(resolve, ms)); 30 | } 31 | 32 | export function arrowNavigation( 33 | event: KeyboardEvent, 34 | listItems: NodeListOf, 35 | selector: string = 'ol.tabnavigation:not(.bottom) li.selected' 36 | ): void { 37 | if (!['ArrowRight', 'ArrowLeft'].includes(event.key)) { 38 | return; 39 | } 40 | 41 | const selectedLi: HTMLLIElement = document.querySelector(selector); 42 | let selectedIndex: number = -1; 43 | listItems.forEach((node: HTMLLIElement, index: number) => { 44 | if (selectedLi === node) { 45 | selectedIndex = index; 46 | } 47 | }); 48 | 49 | switch (event.key) { 50 | case 'ArrowRight': 51 | selectedIndex = Math.min(selectedIndex + 1, listItems.length - 1); 52 | break; 53 | 54 | case 'ArrowLeft': 55 | selectedIndex = Math.max(selectedIndex - 1, 0); 56 | break; 57 | } 58 | 59 | listItems.forEach((node: HTMLLIElement, index: number) => { 60 | if (index === selectedIndex) { 61 | node.click(); 62 | } 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cselt/rf2-better-ui/3befd2a6177d55ed66d8ccb38b95ee7e9dd95aa2/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rf2BetterUi 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /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 { environment } from './environments/environment'; 6 | 7 | import 'zone.js'; // Included with Angular CLI. 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic() 14 | .bootstrapModule(AppModule) 15 | .catch(err => console.error(err)); 16 | -------------------------------------------------------------------------------- /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 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | // import 'zone.js'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | import 'document-register-element'; 64 | -------------------------------------------------------------------------------- /src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/rest/*": { 3 | "target": "http://localhost:5396", 4 | "secure": false, 5 | "logLevel": "debug" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use 'vars'; 2 | 3 | @mixin disabledState($bw: false) { 4 | opacity: vars.$opacityDisabled; 5 | pointer-events: none; 6 | cursor: default; 7 | 8 | @if ($bw == true) { 9 | -webkit-filter: grayScale(100%); 10 | filter: grayScale(100%); 11 | } 12 | } 13 | 14 | @mixin skewX($deg) { 15 | -webkit-transform: skewX($deg); 16 | -moz-transform: skewX($deg); 17 | -ms-transform: skewX($deg); 18 | transform: skewX($deg); 19 | } 20 | 21 | @mixin skewElement($toRight: true) { 22 | @if $toRight { 23 | // prettier-ignore 24 | @include skewX(- vars.$skew); 25 | } @else { 26 | @include skewX(vars.$skew); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scss/_vars.scss: -------------------------------------------------------------------------------- 1 | $colorDark: #262626; 2 | $colorRed: #ff2c00; 3 | $colorDarkRed: #cc2300; 4 | $colorBlue: #00c2f2; 5 | $colorOrange: #f90; 6 | $colorBlack: #333; 7 | $colorWhite: #fff; 8 | $colorBg: #e4e4e4; 9 | 10 | $colorGrey: #999; 11 | 12 | //OPACITY 13 | $opacityDisabled: 0.35; 14 | 15 | $skew: 22deg; 16 | -------------------------------------------------------------------------------- /src/scss/common-styles.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | @import 'mixins'; 3 | -------------------------------------------------------------------------------- /src/scss/rf-pages/event.scss: -------------------------------------------------------------------------------- 1 | html { 2 | .templ-col { 3 | margin: 0; 4 | } 5 | } 6 | 7 | .tab-camera { 8 | .camera-wrapper { 9 | flex: 1; 10 | 11 | .camera-controls { 12 | flex: 0 0 auto; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @use './scss/vars'; 3 | 4 | @import 'tailwindcss/base'; 5 | @import 'tailwindcss/components'; 6 | @import 'tailwindcss/utilities'; 7 | 8 | *, 9 | *:before, 10 | *:after { 11 | -webkit-box-sizing: border-box; 12 | -moz-box-sizing: border-box; 13 | box-sizing: border-box; 14 | } 15 | 16 | html, 17 | body { 18 | height: 100%; 19 | } 20 | 21 | .thumbnailwrapper { 22 | .thumbnails { 23 | .thumbnail { 24 | width: 8rem !important; 25 | } 26 | } 27 | } 28 | 29 | // disable animation 30 | body { 31 | opacity: 1 !important; 32 | } 33 | 34 | nav { 35 | ol.right { 36 | li { 37 | &.fa { 38 | cursor: pointer; 39 | padding: 0 1rem 0 1rem; 40 | } 41 | } 42 | } 43 | } 44 | 45 | .topbar.dark > ol.right { 46 | .fa { 47 | color: vars.$colorGrey; 48 | } 49 | } 50 | 51 | .fa-power-off:before { 52 | content: '\f011'; 53 | font-style: normal; 54 | font-weight: 400; 55 | font-size: 1.2em; 56 | line-height: 1; 57 | } 58 | 59 | .fa-chevron-right:before { 60 | content: '\f054'; 61 | } 62 | 63 | .fa-chevron-down:before { 64 | content: '\f078'; 65 | } 66 | 67 | .fa-search:before { 68 | content: '\f002'; 69 | } 70 | 71 | .fa-folder:before { 72 | content: '\f07b'; 73 | } 74 | 75 | .fa-file:before { 76 | content: '\f15b'; 77 | } 78 | 79 | .raceButton { 80 | margin: 0; 81 | background: vars.$colorRed; 82 | min-width: 3.5rem; 83 | max-width: 3.5rem; 84 | 85 | &:hover { 86 | transition: background-color 0.5s; 87 | background: vars.$colorDarkRed; 88 | } 89 | } 90 | 91 | .rfPanel .mat-dialog-container { 92 | border-radius: 0 !important; 93 | } 94 | 95 | .noDialogPadding .mat-dialog-container { 96 | padding: 0 !important; 97 | } 98 | -------------------------------------------------------------------------------- /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 { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | declare const require: { 8 | context( 9 | path: string, 10 | deep?: boolean, 11 | filter?: RegExp 12 | ): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare let angular: any; 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: { 4 | content: ['./src/**/*.{html,ts}'] 5 | }, 6 | darkMode: false, // or 'media' or 'class' 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'popup-title': '#b5b8ba', 11 | 'popup-footer': '#ccc' 12 | }, 13 | skew: { 14 | 22: '22deg', 15 | '-22': '-22deg' 16 | } 17 | } 18 | }, 19 | variants: { 20 | extend: { 21 | opacity: ['disabled'], 22 | scale: ['active'] 23 | } 24 | }, 25 | plugins: [], 26 | corePlugins: { 27 | preflight: false 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [], 7 | "resolveJsonModule": true 8 | }, 9 | "files": [ 10 | "src/main.ts", 11 | "src/polyfills.ts", 12 | "src/typings.d.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "allowSyntheticDefaultImports": true, 16 | "lib": [ 17 | "es2018", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /utils/builder.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { execSync } from 'child_process'; 4 | 5 | console.log("Build Angular application"); 6 | execSync("npm run build", {stdio: 'inherit'}); 7 | 8 | console.log('Build better-ui script'); 9 | execSync('npm run build.scripts', {stdio: 'inherit'}); 10 | 11 | enum FileTypes { 12 | main = 'main', 13 | polyFills = 'polyfills', 14 | runtime = 'runtime', 15 | styles = 'styles' 16 | } 17 | 18 | const interestedFiles: string[] = ['main', 'polyfills', 'runtime', 'styles']; 19 | 20 | const hashes: { [key: string]: string } = {}; 21 | 22 | readdirSync('./dist/rf2-better-ui').forEach((file: string) => { 23 | const splits: string[] = file.split('.'); 24 | switch (splits[0]) { 25 | case FileTypes.main: 26 | hashes[FileTypes.main] = splits[1]; 27 | break; 28 | 29 | case FileTypes.polyFills: 30 | hashes[FileTypes.polyFills] = splits[1]; 31 | break; 32 | 33 | case FileTypes.runtime: 34 | hashes[FileTypes.runtime] = splits[1]; 35 | break; 36 | 37 | case FileTypes.styles: 38 | hashes[FileTypes.styles] = splits[1]; 39 | break; 40 | } 41 | }); 42 | 43 | // Delete unnecessary files 44 | unlinkSync(join('./dist/rf2-better-ui/index.html')); 45 | unlinkSync(join('./dist/rf2-better-ui/favicon.ico')); 46 | 47 | console.log('Read hash numbers', hashes); 48 | let betterUi: string = readFileSync('./dist/scripts/better-ui.js', 'utf8'); 49 | 50 | Object.values(FileTypes).forEach((value: string) => { 51 | 52 | betterUi = betterUi.replace(`<${FileTypes.main}>`, hashes[FileTypes.main]); 53 | betterUi = betterUi.replace(`<${FileTypes.runtime}>`, hashes[FileTypes.runtime]); 54 | betterUi = betterUi.replace(`<${FileTypes.polyFills}>`, hashes[FileTypes.polyFills]); 55 | betterUi = betterUi.replace(`<${FileTypes.styles}>`, hashes[FileTypes.styles]); 56 | }); 57 | 58 | writeFileSync("./dist/scripts/better-ui.js", betterUi, {encoding: 'utf8'}); 59 | -------------------------------------------------------------------------------- /utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/scripts", 5 | "types": [ 6 | "node" 7 | ], 8 | "sourceMap": false, 9 | "typeRoots": [ 10 | "../node_modules/@types" 11 | ], 12 | "module": "CommonJS", 13 | }, 14 | "files": [ 15 | "builder.ts" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------