├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── build_release.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── _config.yml ├── angular-electron-readme.md ├── angular.json ├── angular.webpack.js ├── e2e ├── common-setup.ts ├── main.e2e.ts └── tsconfig.e2e.json ├── electron-builder.json ├── main.ts ├── package.json ├── screenshots ├── items.png └── timeline.png ├── src ├── app │ ├── alert-list │ │ ├── alert-list.component.html │ │ ├── alert-list.component.scss │ │ ├── alert-list.component.spec.ts │ │ ├── alert-list.component.ts │ │ └── alert-list.module.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── core.module.ts │ │ └── services │ │ │ ├── electron │ │ │ ├── electron.service.spec.ts │ │ │ └── electron.service.ts │ │ │ └── index.ts │ ├── data │ │ ├── clock.ts │ │ ├── common.ts │ │ ├── data-analyzer.ts │ │ ├── data-store.ts │ │ └── metadata-store.ts │ ├── day-view-dialog │ │ ├── day-view-dialog.component.html │ │ ├── day-view-dialog.component.scss │ │ ├── day-view-dialog.component.spec.ts │ │ ├── day-view-dialog.component.ts │ │ └── day-view-dialog.module.ts │ ├── day-view │ │ ├── day-view.component.html │ │ ├── day-view.component.scss │ │ ├── day-view.component.spec.ts │ │ ├── day-view.component.ts │ │ └── day-view.module.ts │ ├── detail │ │ ├── detail-routing.module.ts │ │ ├── detail.component.html │ │ ├── detail.component.scss │ │ ├── detail.component.spec.ts │ │ ├── detail.component.ts │ │ └── detail.module.ts │ ├── goto-item │ │ ├── goto-item.component.html │ │ ├── goto-item.component.scss │ │ ├── goto-item.component.spec.ts │ │ ├── goto-item.component.ts │ │ └── goto-item.module.ts │ ├── home │ │ ├── home-routing.module.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ ├── home.component.ts │ │ └── home.module.ts │ ├── item-details │ │ ├── item-details.component.html │ │ ├── item-details.component.scss │ │ ├── item-details.component.spec.ts │ │ ├── item-details.component.ts │ │ └── item-details.module.ts │ ├── item │ │ ├── item.component.html │ │ ├── item.component.scss │ │ ├── item.component.spec.ts │ │ ├── item.component.ts │ │ └── item.module.ts │ ├── items │ │ ├── items.component.html │ │ ├── items.component.scss │ │ ├── items.component.spec.ts │ │ ├── items.component.ts │ │ └── items.module.ts │ ├── month-day-view │ │ ├── month-day-view.component.html │ │ ├── month-day-view.component.scss │ │ ├── month-day-view.component.spec.ts │ │ ├── month-day-view.component.ts │ │ └── month-day-view.module.ts │ ├── queue │ │ ├── queue.component.html │ │ ├── queue.component.scss │ │ ├── queue.component.spec.ts │ │ ├── queue.component.ts │ │ └── queue.module.ts │ ├── quick-quota-edit │ │ ├── quick-quota-edit.component.html │ │ ├── quick-quota-edit.component.scss │ │ ├── quick-quota-edit.component.spec.ts │ │ ├── quick-quota-edit.component.ts │ │ └── quick-quota-edit.module.ts │ ├── quota-list │ │ ├── quota-list.component.html │ │ ├── quota-list.component.scss │ │ ├── quota-list.component.spec.ts │ │ ├── quota-list.component.ts │ │ └── quota-list.module.ts │ ├── quota-rule-details │ │ ├── quota-rule-details.component.html │ │ ├── quota-rule-details.component.scss │ │ ├── quota-rule-details.component.spec.ts │ │ ├── quota-rule-details.component.ts │ │ └── quota-rule-details.module.ts │ ├── quota-rule │ │ ├── quota-rule.component.html │ │ ├── quota-rule.component.scss │ │ ├── quota-rule.component.spec.ts │ │ ├── quota-rule.component.ts │ │ └── quota-rule.module.ts │ ├── session-chip │ │ ├── session-chip.component.html │ │ ├── session-chip.component.scss │ │ ├── session-chip.component.spec.ts │ │ ├── session-chip.component.ts │ │ └── session-chip.module.ts │ ├── session-details │ │ ├── session-details.component.html │ │ ├── session-details.component.scss │ │ ├── session-details.component.spec.ts │ │ ├── session-details.component.ts │ │ └── session-details.module.ts │ ├── settings-dialog │ │ ├── settings-dialog.component.html │ │ ├── settings-dialog.component.scss │ │ ├── settings-dialog.component.spec.ts │ │ ├── settings-dialog.component.ts │ │ └── settings-dialog.module.ts │ ├── shared │ │ ├── components │ │ │ ├── index.ts │ │ │ └── page-not-found │ │ │ │ ├── page-not-found.component.html │ │ │ │ ├── page-not-found.component.scss │ │ │ │ ├── page-not-found.component.spec.ts │ │ │ │ └── page-not-found.component.ts │ │ ├── directives │ │ │ ├── droptarget.directive.spec.ts │ │ │ ├── droptarget.directive.ts │ │ │ ├── index.ts │ │ │ └── webview │ │ │ │ ├── webview.directive.spec.ts │ │ │ │ └── webview.directive.ts │ │ └── shared.module.ts │ ├── start-dialog │ │ ├── start-dialog.component.html │ │ ├── start-dialog.component.scss │ │ ├── start-dialog.component.spec.ts │ │ ├── start-dialog.component.ts │ │ └── start-dialog.module.ts │ ├── timeline │ │ ├── timeline.component.html │ │ ├── timeline.component.scss │ │ ├── timeline.component.spec.ts │ │ ├── timeline.component.ts │ │ └── timeline.module.ts │ ├── timer │ │ ├── timer.component.html │ │ ├── timer.component.scss │ │ ├── timer.component.spec.ts │ │ ├── timer.component.ts │ │ └── timer.module.ts │ └── util │ │ ├── fs-util.ts │ │ ├── serialization.ts │ │ ├── time-util.ts │ │ └── util.ts ├── assets │ ├── .gitkeep │ ├── background.jpg │ ├── i18n │ │ └── en.json │ └── icons │ │ ├── angular │ │ ├── favicon.256x256.png │ │ ├── favicon.512x512.png │ │ ├── favicon.icns │ │ ├── favicon.ico │ │ └── favicon.png │ │ ├── favicon.256x256.png │ │ ├── favicon.512x512.png │ │ └── favicon.png ├── environments │ ├── environment.dev.ts │ ├── environment.prod.ts │ ├── environment.ts │ └── environment.web.ts ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills-test.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tsconfig.serve.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "es2017": true 7 | }, 8 | "overrides": [ 9 | { 10 | "files": ["*.ts"], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 10, 20 | "project": [ 21 | "./tsconfig.serve.json", 22 | "./src/tsconfig.app.json", 23 | "./src/tsconfig.spec.json", 24 | "./e2e/tsconfig.e2e.json" 25 | ], 26 | "sourceType": "module", 27 | "ecmaFeatures": { 28 | "modules": true 29 | } 30 | }, 31 | "plugins": [ 32 | "@typescript-eslint", 33 | "@angular-eslint/eslint-plugin" 34 | ], 35 | "rules": { 36 | "@typescript-eslint/indent": [ 37 | "error", 2, { 38 | "SwitchCase": 1, 39 | "CallExpression": {"arguments": "first"}, 40 | "FunctionExpression": {"parameters": "first"}, 41 | "FunctionDeclaration": {"parameters": "first"} 42 | } 43 | ], 44 | "@typescript-eslint/no-empty-function": 0, 45 | "@typescript-eslint/no-explicit-any": 0, 46 | "@typescript-eslint/no-var-requires": 0, 47 | "@typescript-eslint/no-unsafe-call": 0, 48 | "@typescript-eslint/no-unsafe-member-access": 0, 49 | "@typescript-eslint/no-unsafe-assignment": 0, 50 | "@typescript-eslint/no-unsafe-return": 0, 51 | "@typescript-eslint/no-floating-promises": 0, 52 | "@typescript-eslint/semi": "error", 53 | "@angular-eslint/use-injectable-provided-in": "error", 54 | "@angular-eslint/no-attribute-decorator": "error" 55 | } 56 | }, 57 | { 58 | "files": ["*.component.html"], 59 | "parser": "@angular-eslint/template-parser", 60 | "plugins": ["@angular-eslint/template"], 61 | "rules": { 62 | "@angular-eslint/template/banana-in-box": "error", 63 | "@angular-eslint/template/no-negated-async": "error" 64 | } 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: Release/Linux, Windows, Mac 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: "Install Node.js, NPM and Yarn" 17 | uses: actions/setup-node@v1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | node-version: 14 22 | 23 | - name: Install dependencies & Build release 24 | run: | 25 | npm i --legacy-peer-deps 26 | npm run electron:build 27 | 28 | - name: Created files 29 | run: ls -l ./release 30 | 31 | - name: Upload release 32 | uses: xresloader/upload-to-github-release@master 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | file: "release/*.exe;LICENSE.md;release/*.deb;release/*.AppImage;release/*.dmg" 37 | tag_name: continuous_releases 38 | overwrite: true 39 | verbose: true 40 | draft: false 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /app-builds 8 | /release 9 | main.js 10 | src/**/*.js 11 | !src/karma.conf.js 12 | *.js.map 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | .vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | 33 | # misc 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | testem.log 40 | /typings 41 | package-lock.json 42 | 43 | # e2e 44 | /e2e/*.js 45 | !/e2e/protractor.conf.js 46 | /e2e/*.map 47 | 48 | # System Files 49 | .DS_Store 50 | Thumbs.db 51 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | - windows 5 | language: node_js 6 | node_js: 7 | - 'lts/*' 8 | services: 9 | - xvfb 10 | before_script: 11 | - export DISPLAY=:99.0 12 | install: 13 | - npm set progress=false 14 | - npm install 15 | script: 16 | - ng lint 17 | - if [ "$TRAVIS_OS_NAME" != "windows" ]; then npm run test ; fi 18 | - if [ "$TRAVIS_OS_NAME" != "windows" ]; then npm run e2e ; fi 19 | - npm run build 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Electron Main Renderer", 9 | "type": "node", 10 | "request": "launch", 11 | "protocol": "inspector", 12 | // Prelaunch task compiles main.ts for Electron & starts Angular dev server. 13 | "preLaunchTask": "Build.All", 14 | "cwd": "${workspaceFolder}", 15 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 16 | "runtimeArgs": [ 17 | "--serve", 18 | ".", 19 | "--remote-debugging-port=9222" 20 | ], 21 | "windows": { 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 23 | } 24 | }, { 25 | "name": "Karma Attach Chrome", 26 | "type": "chrome", 27 | "request": "attach", 28 | "port": 9222, 29 | "webRoot": "${workspaceFolder}/", 30 | "sourceMaps": true, 31 | "timeout": 30000, 32 | "trace": true 33 | } 34 | 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build.All", 6 | "type": "shell", 7 | "command": "npm run electron:serve-tsc && ng serve", 8 | "isBackground": true, 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": { 14 | "owner": "typescript", 15 | "source": "ts", 16 | "applyTo": "closedDocuments", 17 | "fileLocation": ["relative", "${cwd}"], 18 | "pattern": "$tsc", 19 | "background": { 20 | "activeOnStart": true, 21 | "beginsPattern": "^.*", 22 | "endsPattern": "^.*Compiled successfully.*" 23 | } 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 - Maxime GRIS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIR 2 | 3 | ![travis badge](https://travis-ci.com/TommyX12/VIR.svg?branch=master) 4 | 5 | **VIR** is an open-source intelligent time-management tool designed to tame the 6 | stress. By simply listing your tasks and schedules, VIR can automatically 7 | generate plans around your available time, and alert you of potential conflicts, 8 | so you'll never have to worry about unrealistic deadlines or todo-list overload. 9 | 10 | *** 11 | 12 | ![timeline screenshot](screenshots/timeline.png) 13 | 14 | ![items screenshot](screenshots/items.png) 15 | 16 | *** 17 | 18 | ## Getting Started 19 | 20 | VIR is built 21 | with [angular-electron](https://github.com/maximegris/angular-electron). Windows, MacOS, and Linux are all supported. 22 | Grab your binary from the [releases section](https://github.com/TommyX12/VIR/releases). 23 | 24 | ### Building the Binary Yourself 25 | 26 | 1. Clone the repository: 27 | ```bash 28 | git clone https://github.com/TommyX12/VIR.git 29 | ``` 30 | 2. Enter the directory and install dependencies: 31 | ```bash 32 | cd VIR 33 | npm install --legacy-peer-deps 34 | ``` 35 | 3. Build the binary: 36 | ```bash 37 | npm run electron:build 38 | ``` 39 | The built binary will be in `./release`. 40 | - **Mac**: Go to `./release`, and open the `.dmg` file. 41 | - **Windows**: Go to `./release`, and open the `.exe` file. 42 | 43 | ### Development Build 44 | 45 | To run live-reload debug build: 46 | 47 | ```bash 48 | npm start 49 | ``` 50 | 51 | ## Wiki 52 | 53 | See the project [wiki](https://github.com/TommyX12/VIR/wiki). Check out 54 | the [Basics](https://github.com/TommyX12/VIR/wiki/Basics) guide for an 55 | introduction. 56 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-electron": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-builders/custom-webpack:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets" 21 | ], 22 | "styles": [ 23 | "src/styles.scss" 24 | ], 25 | "scripts": [], 26 | "customWebpackConfig": { 27 | "path": "./angular.webpack.js" 28 | } 29 | }, 30 | "configurations": { 31 | "dev": { 32 | "optimization": false, 33 | "outputHashing": "all", 34 | "sourceMap": true, 35 | "extractCss": true, 36 | "namedChunks": false, 37 | "aot": false, 38 | "extractLicenses": true, 39 | "vendorChunk": false, 40 | "buildOptimizer": false, 41 | "fileReplacements": [ 42 | { 43 | "replace": "src/environments/environment.ts", 44 | "with": "src/environments/environment.dev.ts" 45 | } 46 | ] 47 | }, 48 | "web": { 49 | "optimization": false, 50 | "outputHashing": "all", 51 | "sourceMap": true, 52 | "extractCss": true, 53 | "namedChunks": false, 54 | "aot": false, 55 | "extractLicenses": true, 56 | "vendorChunk": false, 57 | "buildOptimizer": false, 58 | "fileReplacements": [ 59 | { 60 | "replace": "src/environments/environment.ts", 61 | "with": "src/environments/environment.web.ts" 62 | } 63 | ] 64 | }, 65 | "production": { 66 | "optimization": true, 67 | "outputHashing": "all", 68 | "sourceMap": false, 69 | "extractCss": true, 70 | "namedChunks": false, 71 | "aot": true, 72 | "extractLicenses": true, 73 | "vendorChunk": false, 74 | "buildOptimizer": true, 75 | "fileReplacements": [ 76 | { 77 | "replace": "src/environments/environment.ts", 78 | "with": "src/environments/environment.prod.ts" 79 | } 80 | ] 81 | } 82 | } 83 | }, 84 | "serve": { 85 | "builder": "@angular-builders/custom-webpack:dev-server", 86 | "options": { 87 | "browserTarget": "angular-electron:build" 88 | }, 89 | "configurations": { 90 | "dev": { 91 | "browserTarget": "angular-electron:build:dev" 92 | }, 93 | "web": { 94 | "browserTarget": "angular-electron:build:web" 95 | }, 96 | "production": { 97 | "browserTarget": "angular-electron:build:production" 98 | } 99 | } 100 | }, 101 | "extract-i18n": { 102 | "builder": "@angular-devkit/build-angular:extract-i18n", 103 | "options": { 104 | "browserTarget": "angular-electron:build" 105 | } 106 | }, 107 | "test": { 108 | "builder": "@angular-builders/custom-webpack:karma", 109 | "options": { 110 | "main": "src/test.ts", 111 | "polyfills": "src/polyfills-test.ts", 112 | "tsConfig": "src/tsconfig.spec.json", 113 | "karmaConfig": "src/karma.conf.js", 114 | "scripts": [], 115 | "styles": [ 116 | "src/styles.scss" 117 | ], 118 | "assets": [ 119 | "src/assets" 120 | ], 121 | "customWebpackConfig": { 122 | "path": "./angular.webpack.js" 123 | } 124 | } 125 | }, 126 | "lint": { 127 | "builder": "@angular-eslint/builder:lint", 128 | "options": { 129 | "eslintConfig": ".eslintrc.json", 130 | "lintFilePatterns": [ 131 | "src/**.ts", 132 | "main.ts" 133 | ] 134 | } 135 | } 136 | } 137 | }, 138 | "angular-electron-e2e": { 139 | "root": "e2e", 140 | "projectType": "application", 141 | "architect": { 142 | "lint": { 143 | "builder": "@angular-eslint/builder:lint", 144 | "options": { 145 | "eslintConfig": ".eslintrc.json", 146 | "lintFilePatterns": [ 147 | "e2e/**.ts" 148 | ] 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "defaultProject": "angular-electron", 155 | "schematics": { 156 | "@schematics/angular:component": { 157 | "prefix": "app", 158 | "style": "scss" 159 | }, 160 | "@schematics/angular:directive": { 161 | "prefix": "app" 162 | } 163 | }, 164 | "cli": { 165 | "analytics": false 166 | } 167 | } -------------------------------------------------------------------------------- /angular.webpack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom angular webpack configuration 3 | */ 4 | 5 | module.exports = (config, options) => { 6 | config.target = 'electron-renderer'; 7 | 8 | 9 | if (options.fileReplacements) { 10 | for(let fileReplacement of options.fileReplacements) { 11 | if (fileReplacement.replace !== 'src/environments/environment.ts') { 12 | continue; 13 | } 14 | 15 | let fileReplacementParts = fileReplacement['with'].split('.'); 16 | if (fileReplacementParts.length > 1 && ['web'].indexOf(fileReplacementParts[1]) >= 0) { 17 | config.target = 'web'; 18 | } 19 | break; 20 | } 21 | } 22 | 23 | return config; 24 | } 25 | -------------------------------------------------------------------------------- /e2e/common-setup.ts: -------------------------------------------------------------------------------- 1 | const Application = require('spectron').Application; 2 | const electronPath = require('electron'); // Require Electron from the binaries included in node_modules. 3 | const path = require('path'); 4 | 5 | export default function setup(): void { 6 | beforeEach(async function () { 7 | this.app = new Application({ 8 | // Your electron path can be any binary 9 | // i.e for OSX an example path could be '/Applications/MyApp.app/Contents/MacOS/MyApp' 10 | // But for the sake of the example we fetch it from our node_modules. 11 | path: electronPath, 12 | 13 | // Assuming you have the following directory structure 14 | 15 | // |__ my project 16 | // |__ ... 17 | // |__ main.js 18 | // |__ package.json 19 | // |__ index.html 20 | // |__ ... 21 | // |__ test 22 | // |__ spec.js <- You are here! ~ Well you should be. 23 | 24 | // The following line tells spectron to look and use the main.js file 25 | // and the package.json located 1 level above. 26 | args: [path.join(__dirname, '..')], 27 | webdriverOptions: {} 28 | }); 29 | 30 | await this.app.start(); 31 | }); 32 | 33 | afterEach(async function () { 34 | if (this.app && this.app.isRunning()) { 35 | await this.app.stop(); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /e2e/main.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { SpectronClient } from 'spectron'; 3 | 4 | import commonSetup from './common-setup'; 5 | 6 | describe('angular-electron App', function () { 7 | 8 | commonSetup.apply(this); 9 | 10 | let client: SpectronClient; 11 | 12 | beforeEach(function() { 13 | client = this.app.client; 14 | }); 15 | 16 | it('creates initial windows', async function () { 17 | const count = await client.getWindowCount(); 18 | expect(count).to.equal(1); 19 | }); 20 | 21 | it('should display message saying App works !', async function () { 22 | const elem = await client.$('app-home h1'); 23 | const text = await elem.getText(); 24 | expect(text).to.equal('App works !'); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "types": [ 7 | "mocha", 8 | "node" 9 | ] 10 | }, 11 | "include": [ 12 | "**.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "VIR", 3 | "directories": { 4 | "output": "release/" 5 | }, 6 | "files": [ 7 | "**/*", 8 | "!**/*.ts", 9 | "!*.code-workspace", 10 | "!LICENSE.md", 11 | "!package.json", 12 | "!package-lock.json", 13 | "!src/", 14 | "!e2e/", 15 | "!hooks/", 16 | "!angular.json", 17 | "!_config.yml", 18 | "!karma.conf.js", 19 | "!tsconfig.json", 20 | "!tslint.json" 21 | ], 22 | "win": { 23 | "icon": "dist/assets/icons", 24 | "target": [ 25 | "portable" 26 | ] 27 | }, 28 | "mac": { 29 | "icon": "dist/assets/icons", 30 | "target": [ 31 | "dmg" 32 | ] 33 | }, 34 | "linux": { 35 | "icon": "dist/assets/icons", 36 | "target": [ 37 | "AppImage" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, screen } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | 5 | let win: BrowserWindow = null; 6 | const args = process.argv.slice(1), 7 | serve = args.some(val => val === '--serve'); 8 | 9 | function createWindow(): BrowserWindow { 10 | 11 | const electronScreen = screen; 12 | const size = electronScreen.getPrimaryDisplay().workAreaSize; 13 | 14 | // Create the browser window. 15 | win = new BrowserWindow({ 16 | x: 0, 17 | y: 0, 18 | width: size.width, 19 | height: size.height, 20 | webPreferences: { 21 | nodeIntegration: true, 22 | allowRunningInsecureContent: (serve) ? true : false, 23 | contextIsolation: false, // false if you want to run 2e2 test with Spectron 24 | enableRemoteModule : true // true if you want to run 2e2 test with Spectron or use remote module in renderer context (ie. Angular) 25 | }, 26 | }); 27 | 28 | if (serve) { 29 | 30 | win.webContents.openDevTools(); 31 | 32 | require('electron-reload')(__dirname, { 33 | electron: require(`${__dirname}/node_modules/electron`) 34 | }); 35 | win.loadURL('http://localhost:4200'); 36 | 37 | } else { 38 | win.loadURL(url.format({ 39 | pathname: path.join(__dirname, 'dist/index.html'), 40 | protocol: 'file:', 41 | slashes: true 42 | })); 43 | } 44 | 45 | // Emitted when the window is closed. 46 | win.on('closed', () => { 47 | // Dereference the window object, usually you would store window 48 | // in an array if your app supports multi windows, this is the time 49 | // when you should delete the corresponding element. 50 | win = null; 51 | }); 52 | 53 | return win; 54 | } 55 | 56 | try { 57 | // This method will be called when Electron has finished 58 | // initialization and is ready to create browser windows. 59 | // Some APIs can only be used after this event occurs. 60 | // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947 61 | app.on('ready', () => setTimeout(createWindow, 400)); 62 | 63 | // Quit when all windows are closed. 64 | app.on('window-all-closed', () => { 65 | // On OS X it is common for applications and their menu bar 66 | // to stay active until the user quits explicitly with Cmd + Q 67 | if (process.platform !== 'darwin') { 68 | app.quit(); 69 | } 70 | }); 71 | 72 | app.on('activate', () => { 73 | // On OS X it's common to re-create a window in the app when the 74 | // dock icon is clicked and there are no other windows open. 75 | if (win === null) { 76 | createWindow(); 77 | } 78 | }); 79 | 80 | } catch (e) { 81 | // Catch Error 82 | // throw e; 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vir", 3 | "version": "0.0.2", 4 | "description": "A powerful time manager.", 5 | "homepage": "https://github.com/TommyX12/VIR", 6 | "author": { 7 | "name": "Maxime GRIS", 8 | "email": "maxime.gris@gmail.com" 9 | }, 10 | "keywords": [ 11 | "angular", 12 | "angular 11", 13 | "electron", 14 | "nodejs", 15 | "typescript", 16 | "spectron", 17 | "eslint", 18 | "sass", 19 | "windows", 20 | "mac", 21 | "linux" 22 | ], 23 | "main": "main.js", 24 | "private": true, 25 | "scripts": { 26 | "postinstall": "electron-builder install-app-deps", 27 | "ng": "ng", 28 | "start": "npm-run-all -p electron:serve ng:serve", 29 | "build": "npm run electron:serve-tsc && ng build --base-href ./", 30 | "build:dev": "npm run build -- -c dev", 31 | "build:prod": "npm run build -- -c production", 32 | "ng:serve": "ng serve -c web -o", 33 | "electron:serve-tsc": "tsc -p tsconfig.serve.json", 34 | "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve", 35 | "electron:local": "npm run build:prod && npx electron .", 36 | "electron:build": "npm run build:prod && electron-builder build --publish never", 37 | "test": "ng test --watch=false", 38 | "test:watch": "ng test", 39 | "e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts", 40 | "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", 41 | "lint": "ng lint" 42 | }, 43 | "dependencies": { 44 | "@angular/animations": "11.0.2", 45 | "@angular/cdk": "11.0.1", 46 | "@angular/forms": "11.0.2", 47 | "@angular/material": "11.0.1", 48 | "angular-material-dynamic-themes": "1.0.4", 49 | "color": "3.1.3", 50 | "color-convert": "2.0.1", 51 | "deepcopy": "2.1.0", 52 | "fuse.js": "6.4.3", 53 | "immer": "8.0.0", 54 | "json-stable-stringify": "1.0.1", 55 | "material-design-icons": "3.0.1", 56 | "ngx-color-picker": "10.1.0" 57 | }, 58 | "devDependencies": { 59 | "@angular-builders/custom-webpack": "10.0.1", 60 | "@angular-devkit/build-angular": "0.1100.2", 61 | "@angular-eslint/builder": "0.8.0-beta.0", 62 | "@angular-eslint/eslint-plugin": "0.8.0-beta.0", 63 | "@angular-eslint/eslint-plugin-template": "0.8.0-beta.0", 64 | "@angular-eslint/schematics": "0.8.0-beta.0", 65 | "@angular-eslint/template-parser": "0.8.0-beta.0", 66 | "@angular/cli": "11.0.2", 67 | "@angular/common": "11.0.2", 68 | "@angular/compiler": "11.0.2", 69 | "@angular/compiler-cli": "11.0.2", 70 | "@angular/core": "11.0.2", 71 | "@angular/forms": "11.0.2", 72 | "@angular/language-service": "11.0.2", 73 | "@angular/platform-browser": "11.0.2", 74 | "@angular/platform-browser-dynamic": "11.0.2", 75 | "@angular/router": "11.0.2", 76 | "@ngx-translate/core": "13.0.0", 77 | "@ngx-translate/http-loader": "6.0.0", 78 | "@types/jasmine": "3.6.1", 79 | "@types/jasminewd2": "2.0.8", 80 | "@types/mocha": "8.0.4", 81 | "@types/node": "16.11.9", 82 | "@typescript-eslint/eslint-plugin": "4.7.0", 83 | "@typescript-eslint/eslint-plugin-tslint": "4.7.0", 84 | "@typescript-eslint/parser": "4.7.0", 85 | "chai": "4.2.0", 86 | "conventional-changelog-cli": "2.1.1", 87 | "core-js": "3.6.5", 88 | "cross-env": "7.0.2", 89 | "electron": "12.2.3", 90 | "electron-builder": "22.9.1", 91 | "electron-reload": "1.5.0", 92 | "eslint": "7.13.0", 93 | "eslint-plugin-import": "2.22.1", 94 | "eslint-plugin-jsdoc": "30.7.8", 95 | "eslint-plugin-prefer-arrow": "1.2.2", 96 | "jasmine-core": "3.6.0", 97 | "jasmine-spec-reporter": "6.0.0", 98 | "karma": "~5.1.0", 99 | "karma-coverage-istanbul-reporter": "3.0.3", 100 | "karma-electron": "6.3.1", 101 | "karma-jasmine": "4.0.1", 102 | "karma-jasmine-html-reporter": "1.5.4", 103 | "mocha": "8.2.1", 104 | "npm-run-all": "4.1.5", 105 | "rxjs": "6.6.3", 106 | "spectron": "15.0.0", 107 | "ts-node": "9.0.0", 108 | "tslib": "2.0.3", 109 | "typescript": "4.0.5", 110 | "wait-on": "5.0.1", 111 | "webdriver-manager": "12.1.7", 112 | "zone.js": "0.10.3" 113 | }, 114 | "engines": { 115 | "node": ">=10.13.0" 116 | }, 117 | "browser": { 118 | "fs": false, 119 | "os": false, 120 | "path": false 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /screenshots/items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/screenshots/items.png -------------------------------------------------------------------------------- /screenshots/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/screenshots/timeline.png -------------------------------------------------------------------------------- /src/app/alert-list/alert-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
12 |
14 | 16 | {{node.alert.icon}} 17 | 18 | 20 | 21 |
22 | 23 | 24 | 29 | 30 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/app/alert-list/alert-list.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .alert-list-container { 10 | flex: 1; 11 | width: 100%; 12 | } 13 | 14 | .alert-list-viewport { 15 | max-height: 1200px; 16 | height: 100%; 17 | width: 100%; 18 | overflow-x: hidden; 19 | overflow-y: auto; 20 | } 21 | 22 | .leaf-padding { 23 | width: 20px; 24 | } 25 | 26 | .item-content { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .hfill { 32 | flex: 1; 33 | } 34 | 35 | .right-margin { 36 | margin-right: 5px; 37 | } 38 | 39 | .left-margin { 40 | margin-left: 5px; 41 | } 42 | 43 | .flex-child { 44 | flex: 1; 45 | } 46 | 47 | .toolbar { 48 | width: 100%; 49 | height: 40px; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | padding-left: 5px; 54 | padding-right: 5px; 55 | box-sizing: border-box; 56 | } 57 | 58 | .search-input { 59 | } 60 | 61 | .divider { 62 | width: 100%; 63 | } 64 | 65 | .label-text { 66 | font-size: 0.85em; 67 | } 68 | 69 | .toggle-icon { 70 | position: relative; 71 | top: 2px; 72 | } 73 | 74 | .entry-container { 75 | width: 100%; 76 | box-sizing: border-box; 77 | } 78 | 79 | .entry { 80 | box-sizing: border-box; 81 | width: 100%; 82 | height: 100%; 83 | border: 1px solid #88888844; 84 | background-color: #88888822; 85 | display: flex; 86 | flex-direction: row; 87 | align-items: center; 88 | justify-content: center; 89 | padding: 0 10px; 90 | cursor: pointer; 91 | } 92 | 93 | .entry:hover { 94 | background-color: #88888844; 95 | } 96 | 97 | .entry-icon { 98 | margin-right: 10px; 99 | } 100 | 101 | .entry-text { 102 | flex: 1; 103 | font-size: 0.9em; 104 | } 105 | 106 | .entry-text ::ng-deep b { 107 | color: #55bbea; 108 | } 109 | -------------------------------------------------------------------------------- /src/app/alert-list/alert-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AlertListComponent } from './alert-list.component'; 4 | 5 | describe('AlertListComponent', () => { 6 | let component: AlertListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AlertListComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AlertListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/alert-list/alert-list.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {AlertListComponent} from './alert-list.component' 4 | import {MatTreeModule} from '@angular/material/tree' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatButtonModule} from '@angular/material/button' 7 | import {ItemModule} from '../item/item.module' 8 | import {MatDialogModule} from '@angular/material/dialog' 9 | import {ItemDetailsModule} from '../item-details/item-details.module' 10 | import {MatTooltipModule} from '@angular/material/tooltip' 11 | import {MatInputModule} from '@angular/material/input' 12 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 13 | import {MatDividerModule} from '@angular/material/divider' 14 | import {FormsModule} from '@angular/forms' 15 | import {DragDropModule} from '@angular/cdk/drag-drop' 16 | import {ScrollingModule} from '@angular/cdk/scrolling' 17 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 18 | import {QuotaRuleModule} from '../quota-rule/quota-rule.module' 19 | import {QuotaRuleDetailsModule} from '../quota-rule-details/quota-rule-details.module' 20 | import {MatMenuModule} from '@angular/material/menu' 21 | 22 | @NgModule({ 23 | declarations: [AlertListComponent], 24 | imports: [ 25 | CommonModule, 26 | MatTreeModule, 27 | MatIconModule, 28 | MatButtonModule, 29 | MatDialogModule, 30 | ItemDetailsModule, 31 | ItemModule, 32 | MatTooltipModule, 33 | MatInputModule, 34 | MatSlideToggleModule, 35 | MatDividerModule, 36 | FormsModule, 37 | DragDropModule, 38 | ScrollingModule, 39 | MatButtonToggleModule, 40 | QuotaRuleModule, 41 | QuotaRuleDetailsModule, 42 | MatMenuModule, 43 | ], 44 | exports: [ 45 | AlertListComponent, 46 | ], 47 | }) 48 | export class AlertListModule { 49 | } 50 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { PageNotFoundComponent } from './shared/components'; 4 | 5 | import { HomeRoutingModule } from './home/home-routing.module'; 6 | import { DetailRoutingModule } from './detail/detail-routing.module'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | redirectTo: 'home', 12 | pathMatch: 'full' 13 | }, 14 | { 15 | path: '**', 16 | component: PageNotFoundComponent 17 | } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [ 22 | RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' }), 23 | HomeRoutingModule, 24 | DetailRoutingModule 25 | ], 26 | exports: [RouterModule] 27 | }) 28 | export class AppRoutingModule { } 29 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | 3 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { ElectronService } from './core/services'; 6 | 7 | describe('AppComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [AppComponent], 11 | providers: [ElectronService], 12 | imports: [RouterTestingModule, TranslateModule.forRoot()] 13 | }).compileComponents(); 14 | })); 15 | 16 | it('should create the app', waitForAsync(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | })); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ElectronService } from './core/services'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { AppConfig } from '../environments/environment'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent { 12 | constructor( 13 | private electronService: ElectronService, 14 | private translate: TranslateService 15 | ) { 16 | this.translate.setDefaultLang('en'); 17 | console.log('AppConfig', AppConfig); 18 | 19 | if (electronService.isElectron) { 20 | console.log(process.env); 21 | console.log('Run in electron'); 22 | console.log('Electron ipcRenderer', this.electronService.ipcRenderer); 23 | console.log('NodeJS childProcess', this.electronService.childProcess); 24 | } else { 25 | console.log('Run in browser'); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser' 2 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations' 3 | import {NgModule} from '@angular/core' 4 | import {FormsModule} from '@angular/forms' 5 | import {HttpClient, HttpClientModule} from '@angular/common/http' 6 | import {CoreModule} from './core/core.module' 7 | import {SharedModule} from './shared/shared.module' 8 | 9 | import {AppRoutingModule} from './app-routing.module' 10 | 11 | // NG Translate 12 | import {TranslateLoader, TranslateModule} from '@ngx-translate/core' 13 | import {TranslateHttpLoader} from '@ngx-translate/http-loader' 14 | 15 | import {HomeModule} from './home/home.module' 16 | import {DetailModule} from './detail/detail.module' 17 | 18 | import {AppComponent} from './app.component' 19 | import {DataStore} from './data/data-store' 20 | import {DataAnalyzer} from './data/data-analyzer' 21 | import {MetadataStore} from './data/metadata-store' 22 | import {FsUtil} from './util/fs-util' 23 | import {Clock} from './data/clock' 24 | 25 | // AoT requires an exported function for factories 26 | export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { 27 | return new TranslateHttpLoader(http, './assets/i18n/', '.json') 28 | } 29 | 30 | @NgModule({ 31 | declarations: [AppComponent], 32 | imports: [ 33 | BrowserModule, 34 | BrowserAnimationsModule, 35 | FormsModule, 36 | HttpClientModule, 37 | CoreModule, 38 | SharedModule, 39 | HomeModule, 40 | DetailModule, 41 | AppRoutingModule, 42 | TranslateModule.forRoot({ 43 | loader: { 44 | provide: TranslateLoader, 45 | useFactory: HttpLoaderFactory, 46 | deps: [HttpClient], 47 | }, 48 | }), 49 | ], 50 | providers: [MetadataStore, DataStore, DataAnalyzer, FsUtil, Clock], 51 | bootstrap: [AppComponent], 52 | }) 53 | export class AppModule { 54 | } 55 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @NgModule({ 5 | declarations: [], 6 | imports: [ 7 | CommonModule 8 | ] 9 | }) 10 | export class CoreModule { } 11 | -------------------------------------------------------------------------------- /src/app/core/services/electron/electron.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ElectronService } from './electron.service'; 4 | 5 | describe('ElectronService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ElectronService = TestBed.get(ElectronService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/core/services/electron/electron.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core' 2 | 3 | // If you import a module but never use any of the imported values other than 4 | // as TypeScript types, the resulting javascript file will look as if you never 5 | // imported the module at all. 6 | import {ipcRenderer, remote, webFrame} from 'electron' 7 | import * as childProcess from 'child_process' 8 | import * as fs from 'fs' 9 | import * as os from 'os' 10 | import * as path from 'path' 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class ElectronService { 16 | // @ts-ignore 17 | ipcRenderer: typeof ipcRenderer 18 | // @ts-ignore 19 | webFrame: typeof webFrame 20 | // @ts-ignore 21 | remote: typeof remote 22 | // @ts-ignore 23 | childProcess: typeof childProcess 24 | // @ts-ignore 25 | fs: typeof fs 26 | // @ts-ignore 27 | os: typeof os 28 | // @ts-ignore 29 | path: typeof path 30 | 31 | get isElectron(): boolean { 32 | return !!(window && window.process && window.process.type) 33 | } 34 | 35 | constructor() { 36 | // Conditional imports 37 | if (this.isElectron) { 38 | this.ipcRenderer = window.require('electron').ipcRenderer 39 | this.webFrame = window.require('electron').webFrame 40 | 41 | // If you wan to use remote object, pleanse set enableRemoteModule to 42 | // true in main.ts 43 | this.remote = window.require('electron').remote 44 | 45 | this.childProcess = window.require('child_process') 46 | this.fs = window.require('fs') 47 | this.os = window.require('os') 48 | this.path = window.require('path') 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/core/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './electron/electron.service'; 2 | -------------------------------------------------------------------------------- /src/app/data/clock.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core' 2 | import {BehaviorSubject} from 'rxjs' 3 | 4 | /** 5 | * Provide time in minute granularity. 6 | */ 7 | @Injectable() 8 | export class Clock { 9 | value: BehaviorSubject 10 | private lastMinute: number = new Date().getMinutes() 11 | 12 | constructor() { 13 | const date = new Date() 14 | this.value = new BehaviorSubject(date) 15 | this.lastMinute = date.getMinutes() 16 | 17 | setInterval(() => { 18 | const date = new Date() 19 | if (date.getMinutes() !== this.lastMinute || 20 | (date.getTime() - this.value.value.getTime()) >= 60000) { 21 | this.value.next(date) 22 | this.lastMinute = date.getMinutes() 23 | } 24 | }, 1000) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/data/metadata-store.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core' 2 | import {FsUtil} from '../util/fs-util' 3 | import {stableStringify} from '../util/util' 4 | 5 | const METADATA_FILE_NAME = 'vir-metadata.json' 6 | const TEMP_METADATA_FILE_NAME_1 = 'vir-metadata.tmp1.json' 7 | const TEMP_METADATA_FILE_NAME_2 = 'vir-metadata.tmp2.json' 8 | 9 | @Injectable() 10 | export class MetadataStore { 11 | dataDir: string 12 | saveFilePath: string 13 | increasePostponementEffort: boolean 14 | 15 | constructor( 16 | private readonly fsUtil: FsUtil, 17 | ) { 18 | const HOME_DIR = fsUtil.homeDir() 19 | const METADATA_PATH = fsUtil.path.join(HOME_DIR, METADATA_FILE_NAME) 20 | const DEFAULT_DATA_DIR = fsUtil.path.join(HOME_DIR, 'vir-data') 21 | this.dataDir = DEFAULT_DATA_DIR 22 | 23 | this.saveFilePath = METADATA_PATH 24 | this.increasePostponementEffort = false 25 | 26 | this.load() 27 | this.save() 28 | } 29 | 30 | getSaveFilePath() { 31 | return this.saveFilePath 32 | } 33 | 34 | save() { 35 | const filePath = this.getSaveFilePath() 36 | const text = stableStringify({ 37 | dataDir: this.dataDir, 38 | increasePostponementEffort: this.increasePostponementEffort, 39 | }) 40 | this.fsUtil.safeWriteFileSync( 41 | filePath, text, TEMP_METADATA_FILE_NAME_1, TEMP_METADATA_FILE_NAME_2) 42 | } 43 | 44 | /** 45 | * Return if load successful 46 | */ 47 | load() { 48 | const filePath = this.getSaveFilePath() 49 | try { 50 | const text = this.fsUtil.readFileTextSync(filePath) 51 | if (text === undefined) return false 52 | const data = JSON.parse(text) 53 | this.dataDir = data.dataDir 54 | this.increasePostponementEffort = !!data.increasePostponementEffort 55 | } catch (e) { 56 | console.log(e) 57 | return false 58 | } 59 | return true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/day-view-dialog/day-view-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 | 9 |
10 | -------------------------------------------------------------------------------- /src/app/day-view-dialog/day-view-dialog.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .day-view { 9 | height: 60vh; 10 | width: 100%; 11 | } 12 | 13 | .fill-parent { 14 | height: 100%; 15 | width: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/day-view-dialog/day-view-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DayViewDialogComponent } from './day-view-dialog.component'; 4 | 5 | describe('DayViewDialogComponent', () => { 6 | let component: DayViewDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DayViewDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DayViewDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/day-view-dialog/day-view-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core' 2 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog' 3 | import {DayID} from '../data/common' 4 | import {HomeComponent} from '../home/home.component' 5 | 6 | export interface DayViewDialogConfig { 7 | home?: HomeComponent 8 | dayID: DayID 9 | } 10 | 11 | @Component({ 12 | selector: 'app-day-view-dialog', 13 | templateUrl: './day-view-dialog.component.html', 14 | styleUrls: ['./day-view-dialog.component.scss'], 15 | }) 16 | export class DayViewDialogComponent implements OnInit { 17 | static readonly DIALOG_WIDTH = '500px' 18 | 19 | dayID: DayID 20 | home?: HomeComponent 21 | 22 | constructor( 23 | public dialogRef: MatDialogRef, 24 | @Inject(MAT_DIALOG_DATA) public data: DayViewDialogConfig) { 25 | this.dayID = data.dayID 26 | this.home = data.home 27 | } 28 | 29 | ngOnInit(): void { 30 | } 31 | 32 | close() { 33 | this.dialogRef.close() 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/day-view-dialog/day-view-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {DayViewDialogComponent} from './day-view-dialog.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {DayViewModule} from '../day-view/day-view.module' 7 | 8 | 9 | @NgModule({ 10 | declarations: [DayViewDialogComponent], 11 | imports: [ 12 | CommonModule, 13 | MatButtonModule, 14 | MatIconModule, 15 | DayViewModule, 16 | ], 17 | exports: [DayViewDialogComponent], 18 | }) 19 | export class DayViewDialogModule { 20 | } 21 | -------------------------------------------------------------------------------- /src/app/day-view/day-view.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | position: relative; 7 | } 8 | 9 | .content { 10 | width: 100%; 11 | flex: 1; 12 | margin-top: 10px; 13 | overflow-x: hidden; 14 | overflow-y: auto; 15 | } 16 | 17 | .color-block { 18 | width: 5px; 19 | height: 100%; 20 | } 21 | 22 | .header { 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | height: 35px; 27 | background-color: #88888844; 28 | padding: 0 5px; 29 | } 30 | 31 | .background { 32 | pointer-events: none; 33 | position: absolute; 34 | width: 100%; 35 | height: 100%; 36 | box-sizing: border-box; 37 | } 38 | 39 | .session-group { 40 | width: 100%; 41 | } 42 | 43 | .session-group-header { 44 | height: 25px; 45 | vertical-align: center; 46 | display: flex; 47 | justify-content: flex-start; 48 | align-items: center; 49 | font-size: 0.8em; 50 | font-weight: bold; 51 | //background-color: #88888822; 52 | //border: 1px solid #88888844; 53 | padding: 0 5px; 54 | cursor: pointer; 55 | } 56 | 57 | .session-group-header-text { 58 | opacity: 0.65; 59 | } 60 | 61 | .right-margin { 62 | margin-right: 5px; 63 | } 64 | 65 | .session-group-header:hover { 66 | background-color: #88888844; 67 | } 68 | 69 | .session { 70 | height: 32px; 71 | font-size: 0.9em; 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | box-sizing: border-box; 76 | border: 1px solid #88888833; 77 | cursor: pointer; 78 | } 79 | 80 | .container-faded { 81 | opacity: 0.75; 82 | } 83 | 84 | .faded { 85 | opacity: 0.75; 86 | } 87 | 88 | .session-icon { 89 | vertical-align: center; 90 | position: relative; 91 | top: 0.5em; 92 | } 93 | 94 | .session:hover { 95 | outline: 2px solid #55bbeaaa; 96 | outline-offset: -2px; 97 | } 98 | 99 | .toolbar { 100 | width: 100%; 101 | height: 40px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | padding-left: 5px; 106 | padding-right: 5px; 107 | box-sizing: border-box; 108 | } 109 | 110 | .quota-text { 111 | box-sizing: border-box; 112 | } 113 | 114 | .quota-text ::ng-deep b { 115 | color: #27b727; 116 | } 117 | 118 | .header.is-today { 119 | background-color: #55bbea44; 120 | } 121 | 122 | .header.is-past { 123 | background-color: transparent; 124 | } 125 | 126 | .progress-bar { 127 | width: 100%; 128 | height: 2px; 129 | background-color: #88888811; 130 | position: relative; 131 | } 132 | 133 | .progress-bar-filled { 134 | width: 0; 135 | background-color: #55bbea88; 136 | position: absolute; 137 | left: 0; 138 | top: 0; 139 | bottom: 0; 140 | } 141 | 142 | .progress-bar-overfilled { 143 | background-color: #ff000088; 144 | } 145 | 146 | .progress-bar-done-filled { 147 | width: 0; 148 | background-color: #46ca11; 149 | position: absolute; 150 | left: 0; 151 | top: 0; 152 | bottom: 0; 153 | } 154 | 155 | .warning-icon { 156 | color: #b78215; 157 | } 158 | 159 | .item-icon { 160 | vertical-align: center; 161 | position: relative; 162 | top: 0.6em; 163 | } 164 | 165 | .date.is-past { 166 | color: #888888; 167 | } 168 | 169 | .date.is-today { 170 | font-weight: bold; 171 | } 172 | 173 | .date.is-start-of-month { 174 | color: #4488ff; 175 | } 176 | 177 | .date { 178 | margin-left: 5px; 179 | font-size: 0.9em; 180 | } 181 | 182 | .hfill { 183 | flex: 1; 184 | pointer-events: none; 185 | } 186 | 187 | .checkbox { 188 | color: #309aea; 189 | position: relative; 190 | bottom: 1px; 191 | right: 2px; 192 | } 193 | 194 | .checkbox:hover { 195 | color: #55bbea; 196 | } 197 | 198 | .checked { 199 | color: #88888899; 200 | } 201 | 202 | .checked:hover { 203 | color: #888888; 204 | } 205 | 206 | .no-pointer-event { 207 | pointer-events: none; 208 | } 209 | 210 | .session-group-list-container { 211 | margin-bottom: 10px; 212 | } 213 | 214 | .session-name { 215 | font-size: 0.95em; 216 | } 217 | 218 | .left-margin { 219 | margin-left: 5px; 220 | } 221 | 222 | .date-picker-input { 223 | font-size: 0.75em; 224 | } 225 | 226 | .progress-chip { 227 | font-size: 0.85em; 228 | padding: 2px 4px; 229 | border-radius: 5px; 230 | background-color: #88888866; 231 | margin-right: 10px; 232 | } 233 | 234 | .progress-chip ::ng-deep b { 235 | color: #3cb73c; 236 | } 237 | -------------------------------------------------------------------------------- /src/app/day-view/day-view.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DayViewComponent } from './day-view.component'; 4 | 5 | describe('DayViewComponent', () => { 6 | let component: DayViewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DayViewComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DayViewComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/day-view/day-view.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {DayViewComponent} from './day-view.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatMenuModule} from '@angular/material/menu' 7 | import {MatTooltipModule} from '@angular/material/tooltip' 8 | import {SessionDetailsModule} from '../session-details/session-details.module' 9 | import {SharedModule} from '../shared/shared.module' 10 | import {MatSnackBarModule} from '@angular/material/snack-bar' 11 | import {MatDividerModule} from '@angular/material/divider' 12 | import {QuickQuotaEditModule} from '../quick-quota-edit/quick-quota-edit.module' 13 | import {SessionChipModule} from '../session-chip/session-chip.module' 14 | 15 | 16 | @NgModule({ 17 | declarations: [DayViewComponent], 18 | imports: [ 19 | CommonModule, 20 | MatButtonModule, 21 | MatIconModule, 22 | MatMenuModule, 23 | SessionDetailsModule, 24 | SharedModule, 25 | MatTooltipModule, 26 | MatSnackBarModule, 27 | MatDividerModule, 28 | QuickQuotaEditModule, 29 | SessionChipModule, 30 | ], 31 | exports: [DayViewComponent], 32 | }) 33 | export class DayViewModule { 34 | } 35 | -------------------------------------------------------------------------------- /src/app/detail/detail-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | import { DetailComponent } from './detail.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'detail', 9 | component: DetailComponent 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | declarations: [], 15 | imports: [CommonModule, RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class DetailRoutingModule {} 19 | -------------------------------------------------------------------------------- /src/app/detail/detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ 'PAGES.DETAIL.TITLE' | translate }} 4 |

5 | 6 | {{ 'PAGES.DETAIL.BACK_TO_HOME' | translate }} 7 |
8 | -------------------------------------------------------------------------------- /src/app/detail/detail.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | 3 | } -------------------------------------------------------------------------------- /src/app/detail/detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { DetailComponent } from './detail.component'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | 8 | describe('DetailComponent', () => { 9 | let component: DetailComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [DetailComponent], 15 | imports: [TranslateModule.forRoot(), RouterTestingModule] 16 | }).compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(DetailComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | 29 | it('should render title in a h1 tag', waitForAsync(() => { 30 | const compiled = fixture.debugElement.nativeElement; 31 | expect(compiled.querySelector('h1').textContent).toContain( 32 | 'PAGES.DETAIL.TITLE' 33 | ); 34 | })); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-detail', 5 | templateUrl: './detail.component.html', 6 | styleUrls: ['./detail.component.scss'] 7 | }) 8 | export class DetailComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/detail/detail.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { DetailRoutingModule } from './detail-routing.module'; 5 | 6 | import { DetailComponent } from './detail.component'; 7 | import { SharedModule } from '../shared/shared.module'; 8 | 9 | @NgModule({ 10 | declarations: [DetailComponent], 11 | imports: [CommonModule, SharedModule, DetailRoutingModule] 12 | }) 13 | export class DetailModule {} 14 | -------------------------------------------------------------------------------- /src/app/goto-item/goto-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | Goto item 3 |
4 | 7 |
8 |
9 |
10 |
11 | 12 | Item 13 | 15 | 16 | 18 | {{option}} 19 | 20 | 21 | 25 | 26 |
27 |
28 |
29 | 36 | -------------------------------------------------------------------------------- /src/app/goto-item/goto-item.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .footer { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | height: 40px; 13 | margin-top: 10px; 14 | } 15 | 16 | .content { 17 | width: 100%; 18 | } 19 | 20 | .form { 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .full-width { 29 | width: 100%; 30 | } 31 | 32 | .full-width-field { 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: flex-start; 37 | vertical-align: middle; 38 | } 39 | 40 | .hfill { 41 | flex: 1; 42 | } 43 | 44 | .field-label { 45 | margin-right: 10px; 46 | } 47 | 48 | .right-margin { 49 | margin-right: 10px; 50 | } 51 | 52 | .flex-row { 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | } 57 | 58 | .flex-child { 59 | flex: 1; 60 | } 61 | 62 | .id-text { 63 | font-size: 0.75em; 64 | } 65 | 66 | .key-hint-text { 67 | font-size: 0.75em; 68 | opacity: 0.8; 69 | } 70 | 71 | .color-container { 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | } 76 | -------------------------------------------------------------------------------- /src/app/goto-item/goto-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GotoItemComponent } from './goto-item.component'; 4 | 5 | describe('GotoItemComponent', () => { 6 | let component: GotoItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ GotoItemComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GotoItemComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/goto-item/goto-item.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ElementRef, 5 | Inject, 6 | OnInit, 7 | ViewChild, 8 | } from '@angular/core' 9 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog' 10 | import {DataStore, DataStoreAutoCompleter} from '../data/data-store' 11 | import {BehaviorSubject} from 'rxjs' 12 | 13 | export interface GotoItemConfig { 14 | } 15 | 16 | @Component({ 17 | selector: 'app-goto-item', 18 | templateUrl: './goto-item.component.html', 19 | styleUrls: ['./goto-item.component.scss'], 20 | }) 21 | export class GotoItemComponent implements OnInit, AfterViewInit { 22 | static readonly DIALOG_WIDTH = '500px' 23 | 24 | // @ts-ignore 25 | @ViewChild('itemInput') itemInput: ElementRef 26 | 27 | private _itemKey = '' 28 | filteredKeys = new BehaviorSubject([]) 29 | autoCompleter: DataStoreAutoCompleter 30 | 31 | constructor( 32 | public dialogRef: MatDialogRef, 33 | @Inject(MAT_DIALOG_DATA) public data: GotoItemConfig, 34 | private readonly dataStore: DataStore) { 35 | this.autoCompleter = dataStore.createAutoCompleter() 36 | } 37 | 38 | ngOnInit(): void { 39 | } 40 | 41 | get itemKey() { 42 | return this._itemKey 43 | } 44 | 45 | set itemKey(value: string) { 46 | this._itemKey = value 47 | this.filteredKeys.next(this.autoCompleter.queryKeys(value, 10)) 48 | } 49 | 50 | ngAfterViewInit() { 51 | setTimeout(() => { 52 | this.itemInput?.nativeElement?.focus() 53 | this.itemInput?.nativeElement?.select() 54 | }) 55 | } 56 | 57 | close() { 58 | this.dialogRef.close() 59 | } 60 | 61 | go() { 62 | const itemID = this.autoCompleter.keyToID(this.itemKey) 63 | if (itemID === undefined) { 64 | this.errorItemNotFound() 65 | return 66 | } 67 | this.dialogRef.close(itemID) 68 | } 69 | 70 | errorItemNotFound() { 71 | alert('Error: Item not found') 72 | } 73 | 74 | onFormKeyDown(event: KeyboardEvent) { 75 | if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { 76 | this.go() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/goto-item/goto-item.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {GotoItemComponent} from './goto-item.component' 4 | import {MatIconModule} from '@angular/material/icon' 5 | import {MatFormFieldModule} from '@angular/material/form-field' 6 | import {FormsModule} from '@angular/forms' 7 | import {MatInputModule} from '@angular/material/input' 8 | import {MatButtonModule} from '@angular/material/button' 9 | import {MatAutocompleteModule} from '@angular/material/autocomplete' 10 | 11 | 12 | @NgModule({ 13 | declarations: [GotoItemComponent], 14 | imports: [ 15 | CommonModule, 16 | MatIconModule, 17 | MatFormFieldModule, 18 | FormsModule, 19 | MatInputModule, 20 | MatButtonModule, 21 | MatAutocompleteModule, 22 | ], 23 | }) 24 | export class GotoItemModule { 25 | } 26 | -------------------------------------------------------------------------------- /src/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | import { HomeComponent } from './home.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'home', 9 | component: HomeComponent 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | declarations: [], 15 | imports: [CommonModule, RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class HomeRoutingModule {} 19 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | camera 5 | VIR 6 | 7 | 8 | {{clock.value | async | date:'shortTime'}} 9 | 10 | 11 | 12 | {{clock.value | async | date:'mediumDate'}} 13 | 14 | 15 | 16 |
17 | 18 | 19 | Last saved: {{dataStore.lastSavedMs | async | date:'shortTime'}} 20 | 21 | 22 | 27 | 32 |
33 | 34 | brightness_4 35 | 36 | 37 | 40 | 43 |
44 |
45 | 46 | 47 | 79 | 80 | 81 | 84 | 85 | 86 | 87 | today 88 | Timeline 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | apps 98 | Items 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | playlist_play 108 | Queue 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | timelapse 118 | Quota 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | error_outline 128 | Alerts 129 | 131 | {{alertsCount.get('primary')}} 132 | 133 | 135 | {{alertsCount.get('accent')}} 136 | 137 | 139 | {{alertsCount.get('warn')}} 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
150 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | overflow: hidden; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .header-content { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | width: 100%; 13 | } 14 | 15 | .content-container { 16 | flex: 1; 17 | } 18 | 19 | .side-bar { 20 | width: 320px; 21 | } 22 | 23 | .side-bar-content { 24 | width: 100%; 25 | height: 100%; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | .side-bar-header { 33 | width: 100%; 34 | height: 50px; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | padding: 0 6px; 39 | box-sizing: border-box; 40 | } 41 | 42 | .right-margin { 43 | margin-right: 10px; 44 | } 45 | 46 | .large-right-margin { 47 | margin-right: 20px; 48 | } 49 | 50 | .hfill { 51 | flex: 1; 52 | } 53 | 54 | .flex-child { 55 | flex: 1; 56 | min-width: 0; 57 | } 58 | 59 | .date-picker { 60 | width: 100px; 61 | } 62 | 63 | .full-width { 64 | width: 100%; 65 | } 66 | 67 | .tab-group { 68 | height: 100%; 69 | } 70 | 71 | // Make tab fill height 72 | .tab-group ::ng-deep .mat-tab-body-wrapper { 73 | flex-grow: 1; 74 | } 75 | 76 | .tab-group ::ng-deep .mat-tab-body.mat-tab-body-active { 77 | height: 100%; 78 | } 79 | 80 | .title { 81 | } 82 | 83 | .clock-time { 84 | font-size: 0.8em; 85 | font-weight: normal; 86 | } 87 | 88 | .clock-date { 89 | font-size: 0.8em; 90 | font-weight: normal; 91 | opacity: 0.6; 92 | } 93 | 94 | .small-space { 95 | width: 10px; 96 | } 97 | 98 | .vir-icon { 99 | margin-left: 10px; 100 | margin-right: 20px; 101 | } 102 | 103 | .dark-mode-icon { 104 | position: relative; 105 | top: 2px; 106 | margin-left: 5px; 107 | } 108 | 109 | .tab-label { 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .tab-label-icon { 116 | margin-right: 5px; 117 | position: relative; 118 | top: 1px; 119 | } 120 | 121 | .vertical-divider { 122 | height: 30px; 123 | margin: 0 15px; 124 | } 125 | 126 | .last-saved-time { 127 | margin-right: 5px; 128 | font-size: 0.65em; 129 | font-weight: normal; 130 | opacity: 0.3; 131 | } 132 | 133 | .alert-count-chip { 134 | color: #ffffff; 135 | font-size: 0.7em; 136 | border-radius: 100px; 137 | padding: 2px 5px; 138 | margin-left: 5px; 139 | } 140 | 141 | .primary-alert-count { 142 | background-color: #27b727; 143 | } 144 | 145 | .accent-alert-count { 146 | background-color: #4488ff; 147 | } 148 | 149 | .warn-alert-count { 150 | background-color: #cb3838; 151 | } 152 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | 7 | describe('HomeComponent', () => { 8 | let component: HomeComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [HomeComponent], 14 | imports: [TranslateModule.forRoot(), RouterTestingModule] 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(HomeComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | 28 | it('should render title in a h1 tag', waitForAsync(() => { 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain( 31 | 'PAGES.HOME.TITLE' 32 | ); 33 | })); 34 | }); 35 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | 4 | import {HomeRoutingModule} from './home-routing.module' 5 | 6 | import {HomeComponent} from './home.component' 7 | import {SharedModule} from '../shared/shared.module' 8 | 9 | import {ItemsModule} from '../items/items.module' 10 | 11 | import {MatSidenavModule} from '@angular/material/sidenav' 12 | import {MatTabsModule} from '@angular/material/tabs' 13 | import {MatButtonModule} from '@angular/material/button' 14 | import {MatIconModule} from '@angular/material/icon' 15 | import {MatToolbarModule} from '@angular/material/toolbar' 16 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 17 | import {MatTooltipModule} from '@angular/material/tooltip' 18 | import {TimelineModule} from '../timeline/timeline.module' 19 | import {DayViewModule} from '../day-view/day-view.module' 20 | import {MatFormFieldModule} from '@angular/material/form-field' 21 | import {MatDatepickerModule} from '@angular/material/datepicker' 22 | import {MatInputModule} from '@angular/material/input' 23 | import {QueueModule} from '../queue/queue.module' 24 | import {QuotaListModule} from '../quota-list/quota-list.module' 25 | import {MatDividerModule} from '@angular/material/divider' 26 | import {SettingsDialogModule} from '../settings-dialog/settings-dialog.module' 27 | import {StartDialogModule} from '../start-dialog/start-dialog.module' 28 | import {AlertListModule} from '../alert-list/alert-list.module' 29 | import {TimerModule} from '../timer/timer.module' 30 | 31 | @NgModule({ 32 | declarations: [HomeComponent], 33 | imports: [ 34 | CommonModule, SharedModule, HomeRoutingModule, MatButtonModule, 35 | MatToolbarModule, MatIconModule, MatSlideToggleModule, MatSidenavModule, 36 | MatTabsModule, ItemsModule, 37 | MatTooltipModule, TimelineModule, DayViewModule, MatFormFieldModule, 38 | MatDatepickerModule, MatInputModule, QueueModule, QuotaListModule, 39 | MatDividerModule, SettingsDialogModule, StartDialogModule, AlertListModule, 40 | TimerModule, 41 | ], 42 | }) 43 | export class HomeModule { 44 | } 45 | -------------------------------------------------------------------------------- /src/app/item-details/item-details.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .footer { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | height: 40px; 13 | margin-top: 10px; 14 | } 15 | 16 | .content { 17 | width: 100%; 18 | } 19 | 20 | .form { 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .full-width { 29 | width: 100%; 30 | } 31 | 32 | .full-width-field { 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | vertical-align: middle; 38 | } 39 | 40 | .smaller-text { 41 | font-size: 0.9em; 42 | } 43 | 44 | .color-picker { 45 | width: 40px; 46 | height: 20px; 47 | border-radius: 3px; 48 | border: 2px solid #88888888; 49 | } 50 | 51 | .color-picker-container { 52 | width: 50px; 53 | height: 25px; 54 | } 55 | 56 | .hfill { 57 | flex: 1; 58 | } 59 | 60 | .field-label { 61 | margin-right: 10px; 62 | } 63 | 64 | .right-margin { 65 | margin-right: 10px; 66 | } 67 | 68 | .flex-row { 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | } 73 | 74 | .flex-child { 75 | flex: 1; 76 | min-width: 0; 77 | } 78 | 79 | .id-text { 80 | font-size: 0.75em; 81 | } 82 | 83 | .key-hint-text { 84 | font-size: 0.75em; 85 | opacity: 0.8; 86 | } 87 | 88 | .color-container { 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | } 93 | 94 | .repeat-interval-input { 95 | text-align: right; 96 | } 97 | 98 | .repeat-type-select { 99 | width: 80px; 100 | } 101 | 102 | .repeat-interval-field { 103 | min-width: 0; 104 | width: 40px; 105 | } 106 | 107 | .help-icon { 108 | margin-left: 5px; 109 | } 110 | 111 | .bottom-margin { 112 | margin-bottom: 10px; 113 | } 114 | 115 | .defer-offset-input { 116 | min-width: 0; 117 | } 118 | -------------------------------------------------------------------------------- /src/app/item-details/item-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemDetailsComponent } from './item-details.component'; 4 | 5 | describe('ItemDetailsComponent', () => { 6 | let component: ItemDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ItemDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ItemDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/item-details/item-details.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {ItemDetailsComponent} from './item-details.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatInputModule} from '@angular/material/input' 7 | import {MatFormFieldModule} from '@angular/material/form-field' 8 | import {FormsModule, ReactiveFormsModule} from '@angular/forms' 9 | import {ColorPickerModule} from 'ngx-color-picker' 10 | import {MatCheckboxModule} from '@angular/material/checkbox' 11 | import {MatAutocompleteModule} from '@angular/material/autocomplete' 12 | import {MatTooltipModule} from '@angular/material/tooltip' 13 | import {MatDatepickerModule} from '@angular/material/datepicker' 14 | import {MatSelectModule} from '@angular/material/select' 15 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 16 | 17 | 18 | @NgModule({ 19 | declarations: [ItemDetailsComponent], 20 | imports: [ 21 | CommonModule, 22 | MatButtonModule, 23 | MatIconModule, 24 | MatInputModule, 25 | MatFormFieldModule, 26 | FormsModule, 27 | ColorPickerModule, 28 | MatCheckboxModule, 29 | MatAutocompleteModule, 30 | ReactiveFormsModule, 31 | MatTooltipModule, 32 | MatDatepickerModule, 33 | MatSelectModule, 34 | MatButtonToggleModule, 35 | ], 36 | entryComponents: [ItemDetailsComponent], 37 | exports: [ItemDetailsComponent], 38 | }) 39 | export class ItemDetailsModule { 40 | } 41 | -------------------------------------------------------------------------------- /src/app/item/item.component.html: -------------------------------------------------------------------------------- 1 |
8 |
11 |
12 |
15 |
16 | 17 | 18 | 22 | 27 | 31 | 32 | 33 |
34 |
35 | 36 | 41 |
44 | 46 | {{node.name}} 47 | 48 | 51 | Defer: ({{getEffectiveDeferDateDeltaText()}}) 52 | {{getEffectiveDeferDate() | date:'MMM d'}} 53 | 54 | 58 | Due: ({{getEffectiveDueDateDeltaText()}}) 59 | {{getEffectiveDueDate() | date:'MMM d'}} 60 | 61 | 65 | {{node.problem === problemType.IMPOSSIBLE_BY_QUEUE ? 'Conflict' : 'Impossible'}} 66 | 67 | 72 | Est. ({{getEstimatedDoneDateDeltaText()}}) 73 | {{getEstimatedDoneDate() | date:'MMM d'}} 74 | 75 | 77 | 78 |
79 | 80 |
81 |
82 | -------------------------------------------------------------------------------- /src/app/item/item.component.scss: -------------------------------------------------------------------------------- 1 | .item-content { 2 | width: 100%; 3 | height: 100%; 4 | box-sizing: border-box; 5 | border: 1px solid #88888833; 6 | position: relative; 7 | } 8 | 9 | .decoration { 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | box-sizing: border-box; 14 | cursor: pointer; 15 | } 16 | 17 | .decoration:hover { 18 | outline: #4488ff solid 2px; 19 | outline-offset: -2px; 20 | } 21 | 22 | .inner-content { 23 | width: 100%; 24 | height: 100%; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | box-sizing: border-box; 29 | } 30 | 31 | .color-block { 32 | width: 5px; 33 | height: 100%; 34 | } 35 | 36 | .item-body { 37 | flex: 1; 38 | margin-right: 10px; 39 | height: 100%; 40 | display: flex; 41 | align-items: center; 42 | justify-content: flex-start; 43 | pointer-events: none; 44 | min-width: 0; 45 | } 46 | 47 | .item-name { 48 | flex: 1; 49 | font-size: 0.9em; 50 | } 51 | 52 | //.checkbox { 53 | // box-sizing: border-box; 54 | // border: 3px solid #44aeff88; 55 | // border-radius: 100%; 56 | // width: 20px; 57 | // height: 20px; 58 | // margin-right: 10px; 59 | // cursor: pointer; 60 | //} 61 | // 62 | //.checkbox:hover { 63 | // border-color: #44aeff; 64 | //} 65 | // 66 | //.checked { 67 | // border-width: 0; 68 | // background-color: #88888888; 69 | //} 70 | // 71 | //.checked:hover { 72 | // background-color: #888888; 73 | //} 74 | 75 | .right-margin { 76 | margin-right: 10px; 77 | } 78 | 79 | .large-right-margin { 80 | margin-right: 15px; 81 | } 82 | 83 | .checkbox { 84 | color: #309aea; 85 | position: relative; 86 | bottom: 1px; 87 | right: 2px; 88 | } 89 | 90 | .checkbox:hover { 91 | color: #55bbea; 92 | } 93 | 94 | .checked { 95 | color: #88888899; 96 | } 97 | 98 | .checked:hover { 99 | color: #888888; 100 | } 101 | 102 | .faded { 103 | opacity: 0.75; 104 | } 105 | 106 | .is-indirect { 107 | opacity: 0.5; 108 | } 109 | 110 | .deferred-name { 111 | opacity: 0.5; 112 | } 113 | 114 | .container-faded { 115 | opacity: 0.55; 116 | } 117 | 118 | .defer-date-text { 119 | font-size: 0.75em; 120 | color: #847ac6; 121 | } 122 | 123 | .due-date-text { 124 | font-size: 0.75em; 125 | color: #479ec6; 126 | } 127 | 128 | .eta-date-text { 129 | font-size: 0.75em; 130 | opacity: 0.75; 131 | } 132 | 133 | .problem-text { 134 | font-size: 0.75em; 135 | font-weight: bold; 136 | } 137 | 138 | .warning-lv-1 { 139 | color: #b78057; 140 | } 141 | 142 | .warning-lv-2 { 143 | color: #db5a5e; 144 | } 145 | 146 | .cost-text { 147 | font-size: 0.95em; 148 | } 149 | 150 | .cost-text ::ng-deep b { 151 | color: #27b727; 152 | } 153 | 154 | .text-chip { 155 | padding: 2px 5px; 156 | border-radius: 3px; 157 | background-color: #88888844; 158 | } 159 | -------------------------------------------------------------------------------- /src/app/item/item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemComponent } from './item.component'; 4 | 5 | describe('ItemComponent', () => { 6 | let component: ItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ItemComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ItemComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/item/item.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {ItemComponent} from './item.component' 4 | import {MatIconModule} from '@angular/material/icon' 5 | import {MatButtonModule} from '@angular/material/button' 6 | import {SessionChipModule} from '../session-chip/session-chip.module' 7 | import {MatCheckboxModule} from '@angular/material/checkbox' 8 | import {FormsModule} from '@angular/forms' 9 | import {SharedModule} from '../shared/shared.module' 10 | import {MatSnackBarModule} from '@angular/material/snack-bar' 11 | import {MatTooltipModule} from '@angular/material/tooltip' 12 | import {MatMenuModule} from '@angular/material/menu' 13 | 14 | 15 | @NgModule({ 16 | declarations: [ItemComponent], 17 | imports: [ 18 | CommonModule, 19 | MatIconModule, 20 | MatButtonModule, 21 | SessionChipModule, 22 | MatCheckboxModule, 23 | FormsModule, 24 | SharedModule, 25 | MatSnackBarModule, 26 | MatTooltipModule, 27 | MatMenuModule, 28 | ], 29 | exports: [ItemComponent], 30 | }) 31 | export class ItemModule { 32 | } 33 | -------------------------------------------------------------------------------- /src/app/items/items.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 | 11 |
12 | 16 |
17 | 18 |
19 | 23 | search 24 | 25 | 26 | 30 | 31 | 32 | Show Deferred 33 | 34 | 36 | Active 37 | All 38 | Completed 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 | 56 |
64 | 70 | 71 | 79 | 82 | 83 |
84 | 93 | 96 | 97 | 102 | 108 |
109 |
110 |
111 |
112 |
113 |
114 | -------------------------------------------------------------------------------- /src/app/items/items.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .tree-container { 10 | flex: 1; 11 | width: 100%; 12 | } 13 | 14 | .tree-viewport { 15 | max-height: 1200px; 16 | height: 100%; 17 | width: 100%; 18 | overflow-x: hidden; 19 | overflow-y: auto; 20 | } 21 | 22 | .leaf-padding { 23 | width: 20px; 24 | } 25 | 26 | .item { 27 | } 28 | 29 | .show-deferred-checkbox { 30 | font-size: 0.8em; 31 | margin: 0 10px; 32 | opacity: 0.8; 33 | } 34 | 35 | .item-content { 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .hfill { 41 | flex: 1; 42 | } 43 | 44 | .right-margin { 45 | margin-right: 5px; 46 | } 47 | 48 | .flex-child { 49 | flex: 1; 50 | } 51 | 52 | .toolbar { 53 | width: 100%; 54 | height: 40px; 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | padding-left: 5px; 59 | padding-right: 5px; 60 | box-sizing: border-box; 61 | } 62 | 63 | .filter-bar { 64 | width: 100%; 65 | height: 45px; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | padding-left: 5px; 70 | padding-right: 5px; 71 | box-sizing: border-box; 72 | } 73 | 74 | .search-input { 75 | } 76 | 77 | .divider { 78 | width: 100%; 79 | } 80 | 81 | .label-text { 82 | font-size: 0.85em; 83 | } 84 | 85 | .toggle-icon { 86 | position: relative; 87 | top: 2px; 88 | } 89 | -------------------------------------------------------------------------------- /src/app/items/items.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemsComponent } from './items.component'; 4 | 5 | describe('ItemsComponent', () => { 6 | let component: ItemsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ItemsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ItemsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/items/items.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {ItemsComponent} from './items.component' 4 | import {MatTreeModule} from '@angular/material/tree' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatButtonModule} from '@angular/material/button' 7 | import {ItemModule} from '../item/item.module' 8 | import {MatDialogModule} from '@angular/material/dialog' 9 | import {ItemDetailsModule} from '../item-details/item-details.module' 10 | import {MatTooltipModule} from '@angular/material/tooltip' 11 | import {MatInputModule} from '@angular/material/input' 12 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 13 | import {MatDividerModule} from '@angular/material/divider' 14 | import {FormsModule} from '@angular/forms' 15 | import {DragDropModule} from '@angular/cdk/drag-drop' 16 | import {ScrollingModule} from '@angular/cdk/scrolling' 17 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 18 | import {MatCheckboxModule} from '@angular/material/checkbox' 19 | import {GotoItemModule} from '../goto-item/goto-item.module' 20 | 21 | @NgModule({ 22 | declarations: [ItemsComponent], 23 | imports: [ 24 | CommonModule, 25 | MatTreeModule, 26 | MatIconModule, 27 | MatButtonModule, 28 | MatDialogModule, 29 | ItemDetailsModule, 30 | ItemModule, 31 | MatTooltipModule, 32 | MatInputModule, 33 | GotoItemModule, 34 | MatSlideToggleModule, 35 | MatDividerModule, 36 | FormsModule, 37 | DragDropModule, 38 | ScrollingModule, 39 | MatButtonToggleModule, 40 | MatCheckboxModule, 41 | ], 42 | exports: [ 43 | ItemsComponent, 44 | ], 45 | }) 46 | export class ItemsModule { 47 | } 48 | -------------------------------------------------------------------------------- /src/app/month-day-view/month-day-view.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 |
6 |
7 |

11 | {{getDate() | date:getDateFormat()}} 12 |

13 |
14 | 16 | 17 | 19 | add 20 | 21 | 22 | 23 | 27 | 31 | 35 | 36 | 37 |
38 |
39 |
42 |
44 |
45 |
46 |
53 |
55 | check_circle 57 | 58 | priority_high 62 | 63 | 65 | {{session.item.name}} 66 | 67 |
68 | {{session.count}} 69 | 70 | {{getSessionTypeIcon(session.type)}} 71 | 72 | 73 | 74 | 79 | 84 | 89 | 94 | 99 | 104 | 109 | 110 | 111 |
112 |
113 |
114 | -------------------------------------------------------------------------------- /src/app/month-day-view/month-day-view.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | position: relative; 7 | } 8 | 9 | .content { 10 | width: 100%; 11 | flex: 1; 12 | min-height: 0; 13 | overflow-x: hidden; 14 | overflow-y: auto; 15 | } 16 | 17 | .color-block { 18 | width: 5px; 19 | height: 100%; 20 | margin-right: 5px; 21 | } 22 | 23 | .header { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | height: 25px; 28 | //background-color: #88888822; 29 | cursor: pointer; 30 | position: relative; 31 | } 32 | 33 | .background { 34 | pointer-events: none; 35 | position: absolute; 36 | width: 100%; 37 | height: 100%; 38 | box-sizing: border-box; 39 | } 40 | 41 | .session { 42 | height: 23px; 43 | font-size: 0.7em; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | box-sizing: border-box; 48 | border: 1px solid #88888833; 49 | padding-right: 5px; 50 | cursor: pointer; 51 | } 52 | 53 | .container-faded { 54 | opacity: 0.55; 55 | } 56 | 57 | .faded { 58 | opacity: 0.75; 59 | } 60 | 61 | .session-icon { 62 | margin-left: 4px; 63 | vertical-align: center; 64 | position: relative; 65 | top: 0.5em; 66 | } 67 | 68 | .item-icon { 69 | vertical-align: center; 70 | position: relative; 71 | top: 0.5em; 72 | margin-right: 3px; 73 | } 74 | 75 | .warning-icon { 76 | color: #b78215; 77 | } 78 | 79 | .session:hover { 80 | outline: 2px solid #55bbeaaa; 81 | outline-offset: -2px; 82 | } 83 | 84 | .header.is-today { 85 | background-color: #55bbea44; 86 | } 87 | 88 | .header.is-past { 89 | //background-color: transparent; 90 | } 91 | 92 | .header:hover { 93 | outline: 2px solid #55bbeaaa; 94 | outline-offset: -2px; 95 | } 96 | 97 | .progress-bar { 98 | width: 100%; 99 | height: 2px; 100 | background-color: #88888811; 101 | position: relative; 102 | } 103 | 104 | .progress-bar-filled { 105 | width: 0; 106 | background-color: #55bbea88; 107 | position: absolute; 108 | left: 0; 109 | top: 0; 110 | bottom: 0; 111 | } 112 | 113 | .progress-bar-overfilled { 114 | background-color: #ff000088; 115 | } 116 | 117 | .progress-bar-done-filled { 118 | width: 0; 119 | background-color: #46ca11; 120 | position: absolute; 121 | left: 0; 122 | top: 0; 123 | bottom: 0; 124 | } 125 | 126 | .date { 127 | margin-left: 5px; 128 | font-size: 0.8em; 129 | font-weight: bold; 130 | } 131 | 132 | .date.is-past { 133 | color: #888888; 134 | font-weight: normal; 135 | } 136 | 137 | .date.is-today { 138 | } 139 | 140 | .date.is-start-of-month { 141 | color: #309aea; 142 | } 143 | 144 | .quota-text { 145 | font-size: 0.8em; 146 | box-sizing: border-box; 147 | padding: 0 5px; 148 | opacity: 0.7; 149 | } 150 | 151 | .quota-text:hover { 152 | outline: 2px solid #309aea; 153 | } 154 | 155 | .quota-text ::ng-deep b { 156 | color: #27b727; 157 | } 158 | 159 | .icon-button { 160 | font-size: 25px; 161 | cursor: pointer; 162 | } 163 | 164 | .hfill { 165 | flex: 1; 166 | pointer-events: none; 167 | } 168 | 169 | .no-pointer-event { 170 | pointer-events: none; 171 | } 172 | 173 | .header-background { 174 | display: flex; 175 | align-items: center; 176 | justify-content: flex-start; 177 | flex: 1; 178 | } 179 | -------------------------------------------------------------------------------- /src/app/month-day-view/month-day-view.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MonthDayViewComponent } from './month-day-view.component'; 4 | 5 | describe('MonthDayViewComponent', () => { 6 | let component: MonthDayViewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ MonthDayViewComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MonthDayViewComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/month-day-view/month-day-view.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {MonthDayViewComponent} from './month-day-view.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatMenuModule} from '@angular/material/menu' 7 | import {SessionDetailsModule} from '../session-details/session-details.module' 8 | import {SharedModule} from '../shared/shared.module' 9 | import {DayViewDialogModule} from '../day-view-dialog/day-view-dialog.module' 10 | import {QuickQuotaEditModule} from '../quick-quota-edit/quick-quota-edit.module' 11 | import {MatTooltipModule} from '@angular/material/tooltip' 12 | 13 | 14 | @NgModule({ 15 | declarations: [MonthDayViewComponent], 16 | imports: [ 17 | CommonModule, 18 | MatButtonModule, 19 | MatIconModule, 20 | MatMenuModule, 21 | SessionDetailsModule, 22 | SharedModule, 23 | DayViewDialogModule, 24 | QuickQuotaEditModule, 25 | MatTooltipModule, 26 | ], 27 | exports: [MonthDayViewComponent], 28 | }) 29 | export class MonthDayViewModule { 30 | } 31 | -------------------------------------------------------------------------------- /src/app/queue/queue.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 | search 8 | 9 | 10 | 14 | 15 | 16 | Show Deferred 17 | 18 | 23 |
24 | 25 |
26 | 29 |
36 | 41 |
42 |
43 |
44 | 45 | 50 | 56 |
57 |
58 |
59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /src/app/queue/queue.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .queue-container { 10 | flex: 1; 11 | width: 100%; 12 | } 13 | 14 | .queue-viewport { 15 | max-height: 1200px; 16 | height: 100%; 17 | width: 100%; 18 | overflow-x: hidden; 19 | overflow-y: auto; 20 | } 21 | 22 | .leaf-padding { 23 | width: 20px; 24 | } 25 | 26 | .item { 27 | } 28 | 29 | .item-content { 30 | width: 100%; 31 | height: 100%; 32 | } 33 | 34 | .hfill { 35 | flex: 1; 36 | } 37 | 38 | .right-margin { 39 | margin-right: 5px; 40 | } 41 | 42 | .left-margin { 43 | margin-left: 5px; 44 | } 45 | 46 | .flex-child { 47 | flex: 1; 48 | } 49 | 50 | .show-deferred-checkbox { 51 | font-size: 0.8em; 52 | margin: 0 10px; 53 | opacity: 0.8; 54 | } 55 | 56 | .toolbar { 57 | width: 100%; 58 | height: 40px; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | padding-left: 5px; 63 | padding-right: 5px; 64 | box-sizing: border-box; 65 | } 66 | 67 | .filter-bar { 68 | width: 100%; 69 | height: 45px; 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | padding-left: 5px; 74 | padding-right: 5px; 75 | box-sizing: border-box; 76 | } 77 | 78 | .search-input { 79 | } 80 | 81 | .divider { 82 | width: 100%; 83 | } 84 | 85 | .label-text { 86 | font-size: 0.85em; 87 | } 88 | 89 | .toggle-icon { 90 | position: relative; 91 | top: 2px; 92 | } 93 | -------------------------------------------------------------------------------- /src/app/queue/queue.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { QueueComponent } from './queue.component'; 4 | 5 | describe('QueueComponent', () => { 6 | let component: QueueComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ QueueComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(QueueComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/queue/queue.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {QueueComponent} from './queue.component' 4 | import {MatTreeModule} from '@angular/material/tree' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatButtonModule} from '@angular/material/button' 7 | import {ItemModule} from '../item/item.module' 8 | import {MatDialogModule} from '@angular/material/dialog' 9 | import {ItemDetailsModule} from '../item-details/item-details.module' 10 | import {MatTooltipModule} from '@angular/material/tooltip' 11 | import {MatInputModule} from '@angular/material/input' 12 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 13 | import {MatDividerModule} from '@angular/material/divider' 14 | import {FormsModule} from '@angular/forms' 15 | import {DragDropModule} from '@angular/cdk/drag-drop' 16 | import {ScrollingModule} from '@angular/cdk/scrolling' 17 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 18 | import {MatCheckboxModule} from '@angular/material/checkbox' 19 | 20 | @NgModule({ 21 | declarations: [QueueComponent], 22 | imports: [ 23 | CommonModule, 24 | MatTreeModule, 25 | MatIconModule, 26 | MatButtonModule, 27 | MatDialogModule, 28 | ItemDetailsModule, 29 | ItemModule, 30 | MatTooltipModule, 31 | MatInputModule, 32 | MatSlideToggleModule, 33 | MatDividerModule, 34 | FormsModule, 35 | DragDropModule, 36 | ScrollingModule, 37 | MatButtonToggleModule, 38 | MatCheckboxModule, 39 | ], 40 | exports: [ 41 | QueueComponent, 42 | ], 43 | }) 44 | export class QueueModule { 45 | } 46 | -------------------------------------------------------------------------------- /src/app/quick-quota-edit/quick-quota-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 | Edit Quota 3 |
4 | 7 |
8 |
9 |
10 |
11 | 12 | Quota 13 | 14 | 15 |
16 |
17 |
18 | 25 | -------------------------------------------------------------------------------- /src/app/quick-quota-edit/quick-quota-edit.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .footer { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | height: 40px; 13 | margin-top: 10px; 14 | } 15 | 16 | .content { 17 | width: 100%; 18 | } 19 | 20 | .form { 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .full-width { 29 | width: 100%; 30 | } 31 | 32 | .full-width-field { 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: flex-start; 37 | vertical-align: middle; 38 | } 39 | 40 | .hfill { 41 | flex: 1; 42 | } 43 | 44 | .field-label { 45 | margin-right: 10px; 46 | } 47 | 48 | .right-margin { 49 | margin-right: 10px; 50 | } 51 | 52 | .flex-row { 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | } 57 | 58 | .flex-child { 59 | flex: 1; 60 | } 61 | 62 | .id-text { 63 | font-size: 0.75em; 64 | } 65 | 66 | .key-hint-text { 67 | font-size: 0.75em; 68 | opacity: 0.8; 69 | } 70 | 71 | .color-container { 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | } 76 | -------------------------------------------------------------------------------- /src/app/quick-quota-edit/quick-quota-edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { QuickQuotaEditComponent } from './quick-quota-edit.component'; 4 | 5 | describe('QuickQuotaEditComponent', () => { 6 | let component: QuickQuotaEditComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ QuickQuotaEditComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(QuickQuotaEditComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/quick-quota-edit/quick-quota-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ElementRef, 5 | Inject, 6 | OnInit, 7 | ViewChild, 8 | } from '@angular/core' 9 | import {DayID} from '../data/common' 10 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog' 11 | import {DataStore} from '../data/data-store' 12 | 13 | export interface QuickQuotaEditConfig { 14 | dayID: DayID 15 | initialValue: number 16 | } 17 | 18 | @Component({ 19 | selector: 'app-quick-quota-edit', 20 | templateUrl: './quick-quota-edit.component.html', 21 | styleUrls: ['./quick-quota-edit.component.scss'], 22 | }) 23 | export class QuickQuotaEditComponent implements OnInit, AfterViewInit { 24 | static readonly DIALOG_WIDTH = '500px' 25 | 26 | // @ts-ignore 27 | @ViewChild('quotaInput') quotaInput: ElementRef 28 | 29 | dayID: DayID 30 | value: number 31 | 32 | constructor( 33 | public dialogRef: MatDialogRef, 34 | @Inject(MAT_DIALOG_DATA) public data: QuickQuotaEditConfig, 35 | private readonly dataStore: DataStore) { 36 | this.dayID = data.dayID 37 | this.value = data.initialValue 38 | } 39 | 40 | ngOnInit(): void { 41 | } 42 | 43 | ngAfterViewInit() { 44 | setTimeout(() => { 45 | this.quotaInput?.nativeElement?.focus() 46 | this.quotaInput?.nativeElement?.select() 47 | }) 48 | } 49 | 50 | close() { 51 | this.dialogRef.close() 52 | } 53 | 54 | save() { 55 | if (this.value < 0) { 56 | this.errorInvalidValue() 57 | return 58 | } 59 | this.dataStore.quickEditQuotaRule(this.dayID, this.value) 60 | this.close() 61 | } 62 | 63 | get valueString() { 64 | return this.value.toString() 65 | } 66 | 67 | set valueString(value: string) { 68 | let v = Number(value) 69 | if (isNaN(v) || v < 0) { 70 | v = 0 71 | } 72 | this.value = v 73 | } 74 | 75 | onFormKeyDown(event: KeyboardEvent) { 76 | if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { 77 | this.save() 78 | } 79 | } 80 | 81 | private errorInvalidValue() { 82 | alert('Error: invalid quota value') 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/quick-quota-edit/quick-quota-edit.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {QuickQuotaEditComponent} from './quick-quota-edit.component' 4 | import {MatIconModule} from '@angular/material/icon' 5 | import {MatFormFieldModule} from '@angular/material/form-field' 6 | import {FormsModule} from '@angular/forms' 7 | import {MatInputModule} from '@angular/material/input' 8 | import {MatButtonModule} from '@angular/material/button' 9 | 10 | 11 | @NgModule({ 12 | declarations: [QuickQuotaEditComponent], 13 | imports: [ 14 | CommonModule, 15 | MatIconModule, 16 | MatFormFieldModule, 17 | FormsModule, 18 | MatInputModule, 19 | MatButtonModule, 20 | ], 21 | }) 22 | export class QuickQuotaEditModule { 23 | } 24 | -------------------------------------------------------------------------------- /src/app/quota-list/quota-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 9 |
10 | 11 |
12 | 15 |
21 | 25 | 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/app/quota-list/quota-list.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .quota-list-container { 10 | flex: 1; 11 | width: 100%; 12 | } 13 | 14 | .quota-list-viewport { 15 | max-height: 1200px; 16 | height: 100%; 17 | width: 100%; 18 | overflow-x: hidden; 19 | overflow-y: auto; 20 | } 21 | 22 | .leaf-padding { 23 | width: 20px; 24 | } 25 | 26 | .item-content { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .hfill { 32 | flex: 1; 33 | } 34 | 35 | .right-margin { 36 | margin-right: 5px; 37 | } 38 | 39 | .left-margin { 40 | margin-left: 5px; 41 | } 42 | 43 | .flex-child { 44 | flex: 1; 45 | } 46 | 47 | .toolbar { 48 | width: 100%; 49 | height: 40px; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | padding-left: 5px; 54 | padding-right: 5px; 55 | box-sizing: border-box; 56 | } 57 | 58 | .search-input { 59 | } 60 | 61 | .divider { 62 | width: 100%; 63 | } 64 | 65 | .label-text { 66 | font-size: 0.85em; 67 | } 68 | 69 | .toggle-icon { 70 | position: relative; 71 | top: 2px; 72 | } 73 | -------------------------------------------------------------------------------- /src/app/quota-list/quota-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed} from '@angular/core/testing' 2 | 3 | import {QuotaListComponent} from './quota-list.component' 4 | 5 | describe('QuotaListComponent', () => { 6 | let component: QuotaListComponent 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [QuotaListComponent], 12 | }) 13 | .compileComponents() 14 | }) 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(QuotaListComponent) 18 | component = fixture.componentInstance 19 | fixture.detectChanges() 20 | }) 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/app/quota-list/quota-list.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {QuotaListComponent} from './quota-list.component' 4 | import {MatTreeModule} from '@angular/material/tree' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatButtonModule} from '@angular/material/button' 7 | import {ItemModule} from '../item/item.module' 8 | import {MatDialogModule} from '@angular/material/dialog' 9 | import {ItemDetailsModule} from '../item-details/item-details.module' 10 | import {MatTooltipModule} from '@angular/material/tooltip' 11 | import {MatInputModule} from '@angular/material/input' 12 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 13 | import {MatDividerModule} from '@angular/material/divider' 14 | import {FormsModule} from '@angular/forms' 15 | import {DragDropModule} from '@angular/cdk/drag-drop' 16 | import {ScrollingModule} from '@angular/cdk/scrolling' 17 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 18 | import {QuotaRuleModule} from '../quota-rule/quota-rule.module' 19 | import {QuotaRuleDetailsModule} from '../quota-rule-details/quota-rule-details.module' 20 | 21 | @NgModule({ 22 | declarations: [QuotaListComponent], 23 | imports: [ 24 | CommonModule, 25 | MatTreeModule, 26 | MatIconModule, 27 | MatButtonModule, 28 | MatDialogModule, 29 | ItemDetailsModule, 30 | ItemModule, 31 | MatTooltipModule, 32 | MatInputModule, 33 | MatSlideToggleModule, 34 | MatDividerModule, 35 | FormsModule, 36 | DragDropModule, 37 | ScrollingModule, 38 | MatButtonToggleModule, 39 | QuotaRuleModule, 40 | QuotaRuleDetailsModule, 41 | ], 42 | exports: [ 43 | QuotaListComponent, 44 | ], 45 | }) 46 | export class QuotaListModule { 47 | } 48 | -------------------------------------------------------------------------------- /src/app/quota-rule-details/quota-rule-details.component.html: -------------------------------------------------------------------------------- 1 |
2 | Quota Rule 3 |
4 | 7 |
8 |
9 |
10 |
11 | 12 | 13 | {{option.displayName}} 15 | 16 | 17 |
18 | 19 | Date 20 | 23 | 27 | 29 | 30 | 31 | 34 | 36 | {{option.dowDate | date:'E'}} 37 | 38 | 39 |
40 |
41 | 42 | 43 | {{option.displayName}} 45 | 46 | 47 |
48 | 49 | Date 50 | 53 | 57 | 59 | 60 | 61 | 62 | Date Range 63 | 64 | 67 | 70 | 71 | 75 | 77 | 78 | 79 |
80 |
81 | 82 | Quota 83 | 84 | 85 |
86 |
87 |
88 | 98 | -------------------------------------------------------------------------------- /src/app/quota-rule-details/quota-rule-details.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .footer { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | height: 40px; 13 | margin-top: 10px; 14 | } 15 | 16 | .content { 17 | width: 100%; 18 | } 19 | 20 | .form { 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .full-width { 29 | width: 100%; 30 | } 31 | 32 | .full-width-field { 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | vertical-align: middle; 38 | } 39 | 40 | .color-picker { 41 | width: 40px; 42 | height: 20px; 43 | border-radius: 3px; 44 | border: 2px solid #88888888; 45 | } 46 | 47 | .color-picker-container { 48 | width: 50px; 49 | height: 25px; 50 | } 51 | 52 | .hfill { 53 | flex: 1; 54 | } 55 | 56 | .field-label { 57 | margin-right: 10px; 58 | } 59 | 60 | .right-margin { 61 | margin-right: 10px; 62 | } 63 | 64 | .flex-row { 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | } 69 | 70 | .flex-child { 71 | flex: 1; 72 | min-width: 0; 73 | } 74 | 75 | .bottom-margin { 76 | margin-bottom: 10px; 77 | } 78 | 79 | .id-text { 80 | font-size: 0.75em; 81 | } 82 | 83 | .key-hint-text { 84 | font-size: 0.75em; 85 | opacity: 0.8; 86 | } 87 | 88 | .color-container { 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | } 93 | 94 | .range-type-select { 95 | width: 140px; 96 | } 97 | 98 | .primary-type-select { 99 | width: 150px; 100 | } 101 | -------------------------------------------------------------------------------- /src/app/quota-rule-details/quota-rule-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { QuotaRuleDetailsComponent } from './quota-rule-details.component'; 4 | 5 | describe('QuotaRuleDetailsComponent', () => { 6 | let component: QuotaRuleDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ QuotaRuleDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(QuotaRuleDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/quota-rule-details/quota-rule-details.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {QuotaRuleDetailsComponent} from './quota-rule-details.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatInputModule} from '@angular/material/input' 7 | import {MatFormFieldModule} from '@angular/material/form-field' 8 | import {FormsModule, ReactiveFormsModule} from '@angular/forms' 9 | import {MatCheckboxModule} from '@angular/material/checkbox' 10 | import {MatAutocompleteModule} from '@angular/material/autocomplete' 11 | import {MatTooltipModule} from '@angular/material/tooltip' 12 | import {MatSelectModule} from '@angular/material/select' 13 | import {MatDatepickerModule} from '@angular/material/datepicker' 14 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 15 | 16 | 17 | @NgModule({ 18 | declarations: [QuotaRuleDetailsComponent], 19 | imports: [ 20 | CommonModule, 21 | MatButtonModule, 22 | MatIconModule, 23 | MatInputModule, 24 | MatFormFieldModule, 25 | FormsModule, 26 | MatCheckboxModule, 27 | MatAutocompleteModule, 28 | ReactiveFormsModule, 29 | MatTooltipModule, 30 | MatSelectModule, 31 | MatDatepickerModule, 32 | MatButtonToggleModule, 33 | ], 34 | entryComponents: [QuotaRuleDetailsComponent], 35 | exports: [QuotaRuleDetailsComponent], 36 | }) 37 | export class QuotaRuleDetailsModule { 38 | } 39 | -------------------------------------------------------------------------------- /src/app/quota-rule/quota-rule.component.html: -------------------------------------------------------------------------------- 1 |
6 |
8 |
9 |
10 |
11 | timelapse 12 | 13 | 14 | {{node.value}} 15 | 16 | 17 | 18 | 21 | 22 |
23 |
24 | 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/app/quota-rule/quota-rule.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | box-sizing: border-box; 5 | border: 1px solid #88888844; 6 | background-color: #88888822; 7 | position: relative; 8 | } 9 | 10 | .decoration { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | box-sizing: border-box; 15 | cursor: pointer; 16 | } 17 | 18 | .decoration:hover { 19 | outline: #4488ff solid 2px; 20 | outline-offset: -2px; 21 | } 22 | 23 | .inner-content { 24 | width: 100%; 25 | height: 100%; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | box-sizing: border-box; 30 | } 31 | 32 | .hfill { 33 | flex: 1; 34 | } 35 | 36 | .body { 37 | flex: 1; 38 | margin: 0 10px; 39 | height: 100%; 40 | display: flex; 41 | align-items: center; 42 | justify-content: flex-start; 43 | pointer-events: none; 44 | min-width: 0; 45 | } 46 | 47 | .description { 48 | flex: 1; 49 | } 50 | 51 | .description ::ng-deep b { 52 | color: #309aea; 53 | font-weight: normal; 54 | } 55 | 56 | .right-margin { 57 | margin-right: 10px; 58 | } 59 | 60 | .large-right-margin { 61 | margin-right: 15px; 62 | } 63 | 64 | .large-left-margin { 65 | margin-left: 15px; 66 | } 67 | 68 | .date-text { 69 | font-size: 0.75em; 70 | opacity: 0.75; 71 | } 72 | 73 | .value { 74 | min-width: 65px; 75 | text-align: right; 76 | } 77 | -------------------------------------------------------------------------------- /src/app/quota-rule/quota-rule.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { QuotaRuleComponent } from './quota-rule.component'; 4 | 5 | describe('QuotaRuleComponent', () => { 6 | let component: QuotaRuleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ QuotaRuleComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(QuotaRuleComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/quota-rule/quota-rule.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | NgZone, 7 | OnInit, 8 | Output, 9 | ViewChild, 10 | } from '@angular/core' 11 | import {DataStore} from '../data/data-store' 12 | import {MatSnackBar} from '@angular/material/snack-bar' 13 | import {QuotaRuleID} from '../data/common' 14 | import {QuotaRuleNode} from '../quota-list/quota-list.component' 15 | 16 | export enum QuotaRuleDroppedInsertionType { 17 | ABOVE, 18 | BELOW, 19 | } 20 | 21 | export interface QuotaRuleDroppedEvent { 22 | draggedQuotaRuleID: QuotaRuleID 23 | receiverQuotaRuleID: QuotaRuleID 24 | insertionType: QuotaRuleDroppedInsertionType 25 | } 26 | 27 | @Component({ 28 | selector: 'app-quota-rule', 29 | templateUrl: './quota-rule.component.html', 30 | styleUrls: ['./quota-rule.component.scss'], 31 | }) 32 | export class QuotaRuleComponent implements OnInit { 33 | @ViewChild('decorationContainer') decorationContainerRef?: ElementRef 34 | 35 | // @ts-ignore 36 | @Input() node: QuotaRuleNode 37 | 38 | /** 39 | * This is for correctly computing drag and drop 40 | */ 41 | @Input() itemHeight: number = 35 42 | 43 | @Output() bodyClicked = new EventEmitter() 44 | @Output() quotaRuleDropped = new EventEmitter() 45 | 46 | constructor( 47 | private readonly dataStore: DataStore, 48 | private readonly zone: NgZone, 49 | private readonly snackBar: MatSnackBar, 50 | ) { 51 | } 52 | 53 | ngOnInit(): void { 54 | } 55 | 56 | onBodyClicked() { 57 | this.bodyClicked.emit() 58 | } 59 | 60 | onDrop(event: DragEvent) { 61 | const data = event.dataTransfer?.getData('text') 62 | if (!data || !data.startsWith('quotaRuleID ')) return 63 | 64 | const quotaRuleID = Number(data.substring(12)) 65 | 66 | // @ts-ignore 67 | const rect = event.target.getBoundingClientRect() 68 | const x = event.clientX - rect.left 69 | const y = event.clientY - rect.top 70 | const yPercent = y / this.itemHeight 71 | let insertionType: QuotaRuleDroppedInsertionType 72 | if (yPercent < 0.5) { 73 | insertionType = QuotaRuleDroppedInsertionType.ABOVE 74 | } else { 75 | insertionType = QuotaRuleDroppedInsertionType.BELOW 76 | } 77 | 78 | this.quotaRuleDropped.emit({ 79 | draggedQuotaRuleID: quotaRuleID, 80 | receiverQuotaRuleID: this.node.id, 81 | insertionType, 82 | }) 83 | } 84 | 85 | onDragReact = (event: DragEvent) => { 86 | // @ts-ignore 87 | const rect = event.target.getBoundingClientRect() 88 | const x = event.clientX - rect.left 89 | const y = event.clientY - rect.top 90 | const yPercent = y / this.itemHeight 91 | const element = this.decorationContainerRef?.nativeElement 92 | if (element) { 93 | element.style.borderTop = '' 94 | element.style.borderBottom = '' 95 | if (yPercent < 0.5) { 96 | element.style.borderTop = '5px solid #4488ff' 97 | } else { 98 | element.style.borderBottom = '5px solid #4488ff' 99 | } 100 | } 101 | } 102 | 103 | clearDragReact = () => { 104 | const element = this.decorationContainerRef?.nativeElement 105 | if (element) { 106 | element.style.borderTop = '' 107 | element.style.borderBottom = '' 108 | } 109 | } 110 | 111 | deleteRule() { 112 | this.dataStore.removeQuotaRule(this.node.id) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/app/quota-rule/quota-rule.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {QuotaRuleComponent} from './quota-rule.component' 4 | import {SharedModule} from '../shared/shared.module' 5 | import {MatButtonModule} from '@angular/material/button' 6 | import {MatIconModule} from '@angular/material/icon' 7 | import {SessionChipModule} from '../session-chip/session-chip.module' 8 | 9 | 10 | @NgModule({ 11 | declarations: [QuotaRuleComponent], 12 | imports: [ 13 | CommonModule, 14 | SharedModule, 15 | MatButtonModule, 16 | MatIconModule, 17 | SessionChipModule, 18 | ], 19 | exports: [ 20 | QuotaRuleComponent, 21 | ], 22 | }) 23 | export class QuotaRuleModule { 24 | } 25 | -------------------------------------------------------------------------------- /src/app/session-chip/session-chip.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | schedule 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/session-chip/session-chip.component.scss: -------------------------------------------------------------------------------- 1 | .icon-container { 2 | display: flex; 3 | } 4 | 5 | .icon { 6 | margin-left: 5px; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/session-chip/session-chip.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionChipComponent } from './session-chip.component'; 4 | 5 | describe('SessionChipComponent', () => { 6 | let component: SessionChipComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionChipComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SessionChipComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/session-chip/session-chip.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'app-session-chip', 5 | templateUrl: './session-chip.component.html', 6 | styleUrls: ['./session-chip.component.scss'], 7 | }) 8 | export class SessionChipComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/session-chip/session-chip.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {SessionChipComponent} from './session-chip.component' 4 | import {MatIconModule} from '@angular/material/icon' 5 | 6 | 7 | @NgModule({ 8 | declarations: [SessionChipComponent], 9 | imports: [ 10 | CommonModule, 11 | MatIconModule, 12 | ], 13 | exports: [ 14 | SessionChipComponent, 15 | ], 16 | }) 17 | export class SessionChipModule { 18 | } 19 | -------------------------------------------------------------------------------- /src/app/session-details/session-details.component.html: -------------------------------------------------------------------------------- 1 |
2 | Sessions 3 |
4 | 7 |
8 |
9 |
10 |
11 | 12 | Item 13 | 16 | 17 | 19 | {{option}} 20 | 21 | 22 | 26 | 27 |
28 |
29 | 30 | Count 31 | 32 | 33 | 34 | Type 35 | 36 | {{option.displayName}} 38 | 39 | 40 |
41 |
42 |
43 | 53 | -------------------------------------------------------------------------------- /src/app/session-details/session-details.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .footer { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | height: 40px; 13 | margin-top: 10px; 14 | } 15 | 16 | .content { 17 | width: 100%; 18 | } 19 | 20 | .form { 21 | width: 100%; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .full-width { 29 | width: 100%; 30 | } 31 | 32 | .full-width-field { 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | justify-content: flex-start; 37 | vertical-align: middle; 38 | } 39 | 40 | .color-picker { 41 | width: 40px; 42 | height: 20px; 43 | border-radius: 3px; 44 | border: 2px solid #88888888; 45 | } 46 | 47 | .color-picker-container { 48 | width: 50px; 49 | height: 25px; 50 | } 51 | 52 | .hfill { 53 | flex: 1; 54 | } 55 | 56 | .field-label { 57 | margin-right: 10px; 58 | } 59 | 60 | .right-margin { 61 | margin-right: 10px; 62 | } 63 | 64 | .flex-row { 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | } 69 | 70 | .flex-child { 71 | flex: 1; 72 | } 73 | 74 | .id-text { 75 | font-size: 0.75em; 76 | } 77 | 78 | .key-hint-text { 79 | font-size: 0.75em; 80 | opacity: 0.8; 81 | } 82 | 83 | .color-container { 84 | display: flex; 85 | align-items: center; 86 | justify-content: center; 87 | } 88 | -------------------------------------------------------------------------------- /src/app/session-details/session-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SessionDetailsComponent } from './session-details.component'; 4 | 5 | describe('SessionDetailsComponent', () => { 6 | let component: SessionDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SessionDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SessionDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/session-details/session-details.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | ElementRef, 5 | Inject, 6 | OnInit, 7 | ViewChild, 8 | } from '@angular/core' 9 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog' 10 | import {DataStore, DataStoreAutoCompleter} from '../data/data-store' 11 | import {BehaviorSubject} from 'rxjs' 12 | import {DayID, ItemID, SessionType} from '../data/common' 13 | 14 | export interface SessionDetailsConfig { 15 | isEditing?: boolean 16 | itemID?: ItemID 17 | dayID: DayID 18 | count?: number 19 | type?: SessionType 20 | } 21 | 22 | const SESSION_TYPE_OPTIONS = [ 23 | { 24 | type: SessionType.COMPLETED, 25 | displayName: 'Completed', 26 | }, 27 | { 28 | type: SessionType.SCHEDULED, 29 | displayName: 'Scheduled', 30 | }, 31 | ] 32 | 33 | @Component({ 34 | selector: 'app-session-details', 35 | templateUrl: './session-details.component.html', 36 | styleUrls: ['./session-details.component.scss'], 37 | }) 38 | export class SessionDetailsComponent implements OnInit, AfterViewInit { 39 | static readonly DIALOG_WIDTH = '500px' 40 | @ViewChild('itemKeyInput') itemKeyInput?: ElementRef 41 | @ViewChild('countInput') countInput?: ElementRef 42 | 43 | type: SessionType 44 | count: number = 1 45 | 46 | private autoCompleter: DataStoreAutoCompleter 47 | filteredItemKeys = new BehaviorSubject([]) 48 | private _itemKey = '' 49 | 50 | sessionTypeOptions = SESSION_TYPE_OPTIONS 51 | 52 | dayID: number 53 | 54 | isEditing: boolean 55 | 56 | originalItemID?: number 57 | originalItemKey?: string 58 | originalType?: SessionType 59 | originalCount?: number 60 | 61 | constructor( 62 | public dialogRef: MatDialogRef, 63 | private readonly dataStore: DataStore, 64 | @Inject(MAT_DIALOG_DATA) public data: SessionDetailsConfig, 65 | ) { 66 | this.dayID = data.dayID 67 | this.type = data.type === undefined ? SessionType.SCHEDULED : data.type 68 | this.count = data.count === undefined ? 1 : data.count 69 | this.isEditing = !!data.isEditing 70 | 71 | this.autoCompleter = dataStore.createAutoCompleter() 72 | if (this.isEditing) { 73 | if (data.itemID === undefined) { 74 | throw new Error('Item ID not given when isEditing is true') 75 | } 76 | this.originalItemID = data.itemID 77 | this.originalItemKey = 78 | this.autoCompleter.idToKey(this.originalItemID) 79 | if (this.originalItemKey === undefined) { 80 | throw new Error('Item key not found') 81 | } 82 | this._itemKey = this.originalItemKey 83 | this.originalType = data.type 84 | this.originalCount = data.count 85 | } 86 | } 87 | 88 | ngOnInit(): void { 89 | } 90 | 91 | ngAfterViewInit() { 92 | setTimeout(() => { 93 | const inputElement = this.isEditing ? this.countInput?.nativeElement : 94 | this.itemKeyInput?.nativeElement 95 | inputElement?.focus() 96 | inputElement?.select() 97 | }) 98 | } 99 | 100 | get countString() { 101 | return this.count.toString() 102 | } 103 | 104 | set countString(value: string) { 105 | let v = Number(value) 106 | if (isNaN(v) || v < 0) { 107 | v = 0 108 | } 109 | this.count = v 110 | } 111 | 112 | close() { 113 | this.dialogRef.close() 114 | } 115 | 116 | save() { 117 | // Validation 118 | // TODO refactor: move this 119 | if (!this.itemKey) { 120 | this.errorInvalidItemKey() 121 | return 122 | } 123 | const itemID = (this.originalItemKey !== undefined && 124 | this.itemKey === this.originalItemKey) ? 125 | this.originalItemID : 126 | this.autoCompleter.keyToID(this._itemKey) 127 | if (itemID === undefined) { 128 | this.errorItemNotFound() 129 | return 130 | } 131 | if (this.count < 0) { 132 | this.errorInvalidCount() 133 | return 134 | } 135 | 136 | // Finalize 137 | if (this.isEditing) { 138 | this.dataStore.batchEdit((it) => { 139 | it.removeSession( 140 | this.dayID, this.originalType!, this.originalItemID!, 141 | this.originalCount!, 142 | ) 143 | it.addSession(this.dayID, this.type, itemID, this.count) 144 | }) 145 | } else { 146 | this.dataStore.addSession(this.dayID, this.type, itemID, this.count) 147 | } 148 | this.close() 149 | } 150 | 151 | private errorInvalidCount() { 152 | alert('Error: Invalid count') 153 | } 154 | 155 | get itemKey() { 156 | return this._itemKey 157 | } 158 | 159 | set itemKey(value: string) { 160 | this._itemKey = value 161 | this.filteredItemKeys.next(this.autoCompleter.queryKeys(value, 10)) 162 | } 163 | 164 | onFormKeyDown(event: KeyboardEvent) { 165 | if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { 166 | this.save() 167 | } 168 | } 169 | 170 | private errorInvalidItemKey() { 171 | alert('Error: Invalid item') 172 | } 173 | 174 | private errorItemNotFound() { 175 | alert('Error: Item not found') 176 | } 177 | 178 | delete() { 179 | if (this.originalType === undefined || this.originalItemID === undefined) { 180 | throw new Error('Trying to delete but nothing is being edited') 181 | } 182 | this.dataStore.removeSession( 183 | this.dayID, this.originalType, this.originalItemID, this.originalCount!) 184 | this.close() 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/app/session-details/session-details.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {SessionDetailsComponent} from './session-details.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatInputModule} from '@angular/material/input' 7 | import {MatFormFieldModule} from '@angular/material/form-field' 8 | import {FormsModule, ReactiveFormsModule} from '@angular/forms' 9 | import {MatCheckboxModule} from '@angular/material/checkbox' 10 | import {MatAutocompleteModule} from '@angular/material/autocomplete' 11 | import {MatTooltipModule} from '@angular/material/tooltip' 12 | import {MatSelectModule} from '@angular/material/select' 13 | 14 | 15 | @NgModule({ 16 | declarations: [SessionDetailsComponent], 17 | imports: [ 18 | CommonModule, 19 | MatButtonModule, 20 | MatIconModule, 21 | MatInputModule, 22 | MatFormFieldModule, 23 | FormsModule, 24 | MatCheckboxModule, 25 | MatAutocompleteModule, 26 | ReactiveFormsModule, 27 | MatTooltipModule, 28 | MatSelectModule, 29 | ], 30 | entryComponents: [SessionDetailsComponent], 31 | exports: [SessionDetailsComponent], 32 | }) 33 | export class SessionDetailsModule { 34 | } 35 | -------------------------------------------------------------------------------- /src/app/settings-dialog/settings-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 | Settings 3 |
4 | 7 |
8 |
9 |
10 |
11 | Data Path 12 | 13 | 15 | 16 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 38 | -------------------------------------------------------------------------------- /src/app/settings-dialog/settings-dialog.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | height: 40px; 6 | } 7 | 8 | .fill-parent { 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | .footer { 14 | display: flex; 15 | align-items: center; 16 | justify-content: flex-end; 17 | height: 40px; 18 | margin-top: 20px; 19 | } 20 | 21 | .content { 22 | width: 100%; 23 | } 24 | 25 | .form { 26 | width: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .full-width { 34 | width: 100%; 35 | } 36 | 37 | .full-width-field { 38 | width: 100%; 39 | display: flex; 40 | align-items: center; 41 | justify-content: flex-start; 42 | vertical-align: middle; 43 | } 44 | 45 | .color-picker { 46 | width: 40px; 47 | height: 20px; 48 | border-radius: 3px; 49 | border: 2px solid #88888888; 50 | } 51 | 52 | .color-picker-container { 53 | width: 50px; 54 | height: 25px; 55 | } 56 | 57 | .hfill { 58 | flex: 1; 59 | } 60 | 61 | .field-label { 62 | margin-right: 10px; 63 | } 64 | 65 | .right-margin { 66 | margin-right: 10px; 67 | } 68 | 69 | .flex-row { 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | } 74 | 75 | .flex-child { 76 | flex: 1; 77 | min-width: 0; 78 | } 79 | 80 | .id-text { 81 | font-size: 0.75em; 82 | } 83 | 84 | .key-hint-text { 85 | font-size: 0.75em; 86 | opacity: 0.8; 87 | } 88 | 89 | .color-container { 90 | display: flex; 91 | align-items: center; 92 | justify-content: center; 93 | } 94 | -------------------------------------------------------------------------------- /src/app/settings-dialog/settings-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsDialogComponent } from './settings-dialog.component'; 4 | 5 | describe('SettingsDialogComponent', () => { 6 | let component: SettingsDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SettingsDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SettingsDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/settings-dialog/settings-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core' 2 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog' 3 | import {MetadataStore} from '../data/metadata-store' 4 | import {DataStore} from '../data/data-store' 5 | import {FsUtil} from '../util/fs-util' 6 | 7 | export interface SettingsDialogConfig { 8 | } 9 | 10 | @Component({ 11 | selector: 'app-settings-dialog', 12 | templateUrl: './settings-dialog.component.html', 13 | styleUrls: ['./settings-dialog.component.scss'], 14 | }) 15 | export class SettingsDialogComponent implements OnInit { 16 | static readonly DIALOG_WIDTH = '600px' 17 | 18 | dataDir: string 19 | increasePostponementEffort: boolean 20 | 21 | constructor(public dialogRef: MatDialogRef, 22 | private readonly metadataStore: MetadataStore, 23 | private readonly dataStore: DataStore, 24 | private readonly fsUtil: FsUtil, 25 | @Inject(MAT_DIALOG_DATA) public data: SettingsDialogConfig) { 26 | this.dataDir = metadataStore.dataDir 27 | this.increasePostponementEffort = metadataStore.increasePostponementEffort 28 | } 29 | 30 | ngOnInit(): void { 31 | } 32 | 33 | close() { 34 | this.dialogRef.close() 35 | } 36 | 37 | save() { 38 | this.metadataStore.dataDir = this.dataDir 39 | this.metadataStore.increasePostponementEffort = 40 | this.increasePostponementEffort 41 | this.metadataStore.save() 42 | this.close() 43 | } 44 | 45 | changeDataDir() { 46 | const path = this.fsUtil.readDirPathSync(this.dataDir) 47 | if (path !== undefined) { 48 | this.dataDir = path 49 | } 50 | } 51 | 52 | reloadData() { 53 | if (confirm(`Reload data from "${this.dataDir}" and set as data path?`)) { 54 | const oldDataDir = this.metadataStore.dataDir 55 | this.metadataStore.dataDir = this.dataDir 56 | if (this.dataStore.load()) { 57 | this.metadataStore.save() 58 | this.close() 59 | } else { 60 | alert('Error: Load failed') 61 | this.metadataStore.dataDir = oldDataDir 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/settings-dialog/settings-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {SettingsDialogComponent} from './settings-dialog.component' 4 | import {MatIconModule} from '@angular/material/icon' 5 | import {MatButtonModule} from '@angular/material/button' 6 | import {MatFormFieldModule} from '@angular/material/form-field' 7 | import {FormsModule} from '@angular/forms' 8 | import {MatInputModule} from '@angular/material/input' 9 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 10 | 11 | 12 | @NgModule({ 13 | declarations: [SettingsDialogComponent], 14 | imports: [ 15 | CommonModule, 16 | MatIconModule, 17 | MatButtonModule, 18 | MatSlideToggleModule, 19 | MatFormFieldModule, 20 | FormsModule, 21 | MatInputModule, 22 | ], 23 | }) 24 | export class SettingsDialogModule { 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page-not-found/page-not-found.component'; 2 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |

2 | page-not-found works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/app/shared/components/page-not-found/page-not-found.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { PageNotFoundComponent } from './page-not-found.component'; 4 | 5 | describe('PageNotFoundComponent', () => { 6 | let component: PageNotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PageNotFoundComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PageNotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/directives/droptarget.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { DroptargetDirective } from './droptarget.directive'; 2 | 3 | describe('DroptargetDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new DroptargetDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/droptarget.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Directive, 4 | ElementRef, 5 | HostListener, 6 | Input, 7 | NgZone, 8 | OnDestroy, 9 | } from '@angular/core' 10 | 11 | const DRAG_REACTION_DELAY = 100 12 | 13 | const NO_OP = () => { 14 | } 15 | 16 | @Directive({ 17 | selector: '[appDroptarget]', 18 | }) 19 | export class DroptargetDirective implements AfterViewInit, OnDestroy { 20 | @Input() onDragReact: (DragEvent) => void = NO_OP 21 | @Input() clearDragReact: (DragEvent) => void = NO_OP 22 | 23 | private dragReactionDelayHandle?: any 24 | 25 | private onDragEnter = (event: DragEvent) => { 26 | event.preventDefault() 27 | } 28 | 29 | private onDragOver = (event: DragEvent) => { 30 | event.preventDefault() 31 | 32 | if (this.dragReactionDelayHandle !== undefined) return 33 | 34 | this.dragReactionDelayHandle = setTimeout(() => { 35 | this.onDragReact(event) 36 | this.dragReactionDelayHandle = undefined 37 | }, DRAG_REACTION_DELAY) 38 | } 39 | 40 | private onDragLeave = (event: DragEvent) => { 41 | if (this.dragReactionDelayHandle !== undefined) { 42 | clearTimeout(this.dragReactionDelayHandle) 43 | this.dragReactionDelayHandle = undefined 44 | } 45 | this.clearDragReact(event) 46 | } 47 | 48 | @HostListener('drop', ['$event']) onDrop(event: DragEvent) { 49 | this.onDragLeave(event) 50 | } 51 | 52 | constructor(private readonly el: ElementRef, private readonly zone: NgZone) { 53 | } 54 | 55 | ngAfterViewInit() { 56 | this.zone.runOutsideAngular(() => { 57 | this.el.nativeElement?.addEventListener('dragenter', this.onDragEnter) 58 | this.el.nativeElement?.addEventListener('dragover', this.onDragOver) 59 | this.el.nativeElement?.addEventListener('dragleave', this.onDragLeave) 60 | }) 61 | } 62 | 63 | ngOnDestroy() { 64 | this.el.nativeElement?.removeEventListener('dragenter', this.onDragEnter) 65 | this.el.nativeElement?.removeEventListener('dragover', this.onDragOver) 66 | this.el.nativeElement?.removeEventListener('dragleave', this.onDragLeave) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/shared/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './webview/webview.directive'; 2 | -------------------------------------------------------------------------------- /src/app/shared/directives/webview/webview.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { WebviewDirective } from './webview.directive'; 2 | 3 | describe('WebviewDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new WebviewDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/webview/webview.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'webview' 5 | }) 6 | export class WebviewDirective { 7 | constructor() { } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | 4 | import {TranslateModule} from '@ngx-translate/core' 5 | 6 | import {PageNotFoundComponent} from './components/' 7 | import {WebviewDirective} from './directives/' 8 | import {FormsModule} from '@angular/forms' 9 | import {DroptargetDirective} from './directives/droptarget.directive' 10 | 11 | @NgModule({ 12 | declarations: [PageNotFoundComponent, WebviewDirective, DroptargetDirective], 13 | imports: [CommonModule, TranslateModule, FormsModule], 14 | exports: [TranslateModule, WebviewDirective, FormsModule, 15 | DroptargetDirective], 16 | }) 17 | export class SharedModule { 18 | } 19 | -------------------------------------------------------------------------------- /src/app/start-dialog/start-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome

3 |
4 |
5 |
6 |
7 | Data Path 8 | 9 | 12 | 13 | 16 |
17 |
18 |
19 | 24 | -------------------------------------------------------------------------------- /src/app/start-dialog/start-dialog.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 40px; 6 | margin-bottom: 40px; 7 | } 8 | 9 | .fill-parent { 10 | height: 100%; 11 | width: 100%; 12 | } 13 | 14 | .footer { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | height: 40px; 19 | margin-top: 50px; 20 | } 21 | 22 | .content { 23 | width: 100%; 24 | } 25 | 26 | .form { 27 | width: 100%; 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | justify-content: center; 32 | } 33 | 34 | .full-width { 35 | width: 100%; 36 | } 37 | 38 | .full-width-field { 39 | width: 100%; 40 | display: flex; 41 | align-items: center; 42 | justify-content: flex-start; 43 | vertical-align: middle; 44 | } 45 | 46 | .color-picker { 47 | width: 40px; 48 | height: 20px; 49 | border-radius: 3px; 50 | border: 2px solid #88888888; 51 | } 52 | 53 | .color-picker-container { 54 | width: 50px; 55 | height: 25px; 56 | } 57 | 58 | .hfill { 59 | flex: 1; 60 | } 61 | 62 | .field-label { 63 | margin-right: 10px; 64 | } 65 | 66 | .right-margin { 67 | margin-right: 10px; 68 | } 69 | 70 | .flex-row { 71 | display: flex; 72 | justify-content: center; 73 | align-items: center; 74 | } 75 | 76 | .flex-child { 77 | flex: 1; 78 | min-width: 0; 79 | } 80 | 81 | .id-text { 82 | font-size: 0.75em; 83 | } 84 | 85 | .key-hint-text { 86 | font-size: 0.75em; 87 | opacity: 0.8; 88 | } 89 | 90 | .color-container { 91 | display: flex; 92 | align-items: center; 93 | justify-content: center; 94 | } 95 | -------------------------------------------------------------------------------- /src/app/start-dialog/start-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { StartDialogComponent } from './start-dialog.component'; 4 | 5 | describe('StartDialogComponent', () => { 6 | let component: StartDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ StartDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(StartDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/start-dialog/start-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core' 2 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog' 3 | import {MetadataStore} from '../data/metadata-store' 4 | import {FsUtil} from '../util/fs-util' 5 | import {DataStore} from '../data/data-store' 6 | 7 | export interface StartDialogConfig { 8 | 9 | } 10 | 11 | @Component({ 12 | selector: 'app-start-dialog', 13 | templateUrl: './start-dialog.component.html', 14 | styleUrls: ['./start-dialog.component.scss'], 15 | }) 16 | export class StartDialogComponent implements OnInit { 17 | static readonly DIALOG_WIDTH = '600px' 18 | 19 | constructor(public dialogRef: MatDialogRef, 20 | @Inject(MAT_DIALOG_DATA) public data: StartDialogConfig, 21 | private readonly fsUtil: FsUtil, 22 | readonly metadataStore: MetadataStore, 23 | private readonly dataStore: DataStore) { 24 | } 25 | 26 | ngOnInit(): void { 27 | } 28 | 29 | changeDataDir() { 30 | const path = this.fsUtil.readDirPathSync(this.metadataStore.dataDir) 31 | if (path !== undefined) { 32 | this.metadataStore.dataDir = path 33 | } 34 | } 35 | 36 | start() { 37 | this.metadataStore.save() 38 | if (!this.dataStore.load()) { 39 | this.dataStore.save() 40 | } 41 | this.dataStore.startAutoSave() 42 | this.dialogRef.close() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/start-dialog/start-dialog.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {StartDialogComponent} from './start-dialog.component' 4 | import {MatFormFieldModule} from '@angular/material/form-field' 5 | import {MatInputModule} from '@angular/material/input' 6 | import {MatButtonModule} from '@angular/material/button' 7 | import {FormsModule} from '@angular/forms' 8 | 9 | 10 | @NgModule({ 11 | declarations: [StartDialogComponent], 12 | imports: [ 13 | CommonModule, 14 | MatFormFieldModule, 15 | MatInputModule, 16 | MatButtonModule, 17 | FormsModule, 18 | ], 19 | }) 20 | export class StartDialogModule { 21 | } 22 | -------------------------------------------------------------------------------- /src/app/timeline/timeline.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 12 | 18 |

Week of

19 | 20 | 23 | 25 | 26 | 27 | 29 | Month 30 | Week 31 | 32 |
33 | 37 |
38 | 39 |
40 |

41 | Max free time in {{getFreeTimeQuotaPeriod()}} days: 42 |

43 |

46 | 47 |
48 |
50 |
51 |

Last week daily completion:

52 |

53 | 54 | {{lastWeekDailyCompletion | number:'1.0-1'}} 55 | 56 |

57 | 58 |
59 | 62 |
63 | hourglass_empty 64 |
65 |
66 |
67 | 68 |
69 | 70 | 71 | 72 | 75 | 76 | 77 |
73 | {{d | date:'E'}} 74 |
78 |
79 |
80 |
83 |
86 | 95 | 96 |
97 |
98 |
99 |
100 | -------------------------------------------------------------------------------- /src/app/timeline/timeline.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | overflow: hidden; 9 | } 10 | 11 | .today-date { 12 | font-weight: bold; 13 | } 14 | 15 | .flex-row { 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .toolbar { 22 | width: 100%; 23 | height: 40px; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | padding: 0 5px; 28 | box-sizing: border-box; 29 | } 30 | 31 | .warning { 32 | color: #cb3838; 33 | } 34 | 35 | .dow-header { 36 | width: 100%; 37 | height: 30px; 38 | } 39 | 40 | .dow-table { 41 | width: 100%; 42 | height: 100%; 43 | table-layout: fixed; 44 | border-collapse: collapse; 45 | border-spacing: 0; 46 | } 47 | 48 | .dow-cell { 49 | text-align: center; 50 | } 51 | 52 | .divider { 53 | width: 100%; 54 | } 55 | 56 | .right-margin { 57 | margin-right: 10px; 58 | } 59 | 60 | .hfill { 61 | flex: 1; 62 | } 63 | 64 | .content { 65 | flex: 1; 66 | width: 100%; 67 | overflow: hidden; 68 | } 69 | 70 | .date-input { 71 | width: 120px; 72 | } 73 | 74 | .calendar { 75 | width: 100%; 76 | height: 100%; 77 | table-layout: fixed; 78 | border-collapse: collapse; 79 | border-spacing: 0; 80 | } 81 | 82 | .calendar td { 83 | border: 1px solid #88888866; 84 | } 85 | 86 | .left-margin { 87 | margin-left: 15px; 88 | } 89 | 90 | .calendar-row { 91 | width: 100%; 92 | display: flex; 93 | flex-direction: row; 94 | align-items: stretch; 95 | } 96 | 97 | .calendar-column { 98 | height: 100%; 99 | border-right: 1px solid #88888866; 100 | border-top: 1px solid #88888866; 101 | box-sizing: border-box; 102 | } 103 | 104 | .free-time-text ::ng-deep b { 105 | color: #55bbea; 106 | } 107 | 108 | .free-time-label { 109 | font-size: 0.85em; 110 | opacity: 0.8; 111 | margin-right: 15px; 112 | } 113 | 114 | .completion-text { 115 | color: #27b727; 116 | } 117 | 118 | .free-time-progress-bar { 119 | margin-left: 10px; 120 | width: 45px; 121 | height: 15px; 122 | border-radius: 5px; 123 | border: 1px solid #88888888; 124 | overflow: hidden; 125 | position: relative; 126 | } 127 | 128 | .free-time-progress-bar-fill { 129 | position: absolute; 130 | top: 0; 131 | bottom: 0; 132 | left: 0; 133 | background-color: #55bbea; 134 | } 135 | -------------------------------------------------------------------------------- /src/app/timeline/timeline.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TimelineComponent } from './timeline.component'; 4 | 5 | describe('TimelineComponent', () => { 6 | let component: TimelineComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TimelineComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TimelineComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/timeline/timeline.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {TimelineComponent} from './timeline.component' 4 | import {MatDividerModule} from '@angular/material/divider' 5 | import {FormsModule} from '@angular/forms' 6 | import {MatButtonToggleModule} from '@angular/material/button-toggle' 7 | import {MatButtonModule} from '@angular/material/button' 8 | import {MonthDayViewModule} from '../month-day-view/month-day-view.module' 9 | import {MatIconModule} from '@angular/material/icon' 10 | import {MatTooltipModule} from '@angular/material/tooltip' 11 | import {MatDatepickerModule} from '@angular/material/datepicker' 12 | import {MatFormFieldModule} from '@angular/material/form-field' 13 | import {MatInputModule} from '@angular/material/input' 14 | import {MatNativeDateModule} from '@angular/material/core' 15 | import {MatSlideToggleModule} from '@angular/material/slide-toggle' 16 | import {SessionChipModule} from '../session-chip/session-chip.module' 17 | 18 | 19 | @NgModule({ 20 | declarations: [TimelineComponent], 21 | imports: [ 22 | CommonModule, 23 | MatDividerModule, 24 | FormsModule, 25 | MatButtonToggleModule, 26 | MatButtonModule, 27 | MatTooltipModule, 28 | MonthDayViewModule, 29 | MatIconModule, 30 | MatDatepickerModule, 31 | MatFormFieldModule, 32 | MatInputModule, 33 | MatNativeDateModule, 34 | MatSlideToggleModule, 35 | SessionChipModule, 36 | ], 37 | exports: [TimelineComponent], 38 | }) 39 | export class TimelineModule { 40 | } 41 | -------------------------------------------------------------------------------- /src/app/timer/timer.component.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 | 16 | 22 |
23 | 24 | {{elapsedText | async}} 25 | 26 | 27 | Started: {{startedDate | async | date:'shortTime'}} 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/app/timer/timer.component.scss: -------------------------------------------------------------------------------- 1 | .flex-row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .elapsed-text { 8 | margin-left: 5px; 9 | margin-right: 12px; 10 | font-size: 18px; 11 | font-weight: normal; 12 | } 13 | 14 | .started-text { 15 | font-size: 13px; 16 | opacity: 0.4; 17 | font-weight: normal; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/timer/timer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TimerComponent } from './timer.component'; 4 | 5 | describe('TimerComponent', () => { 6 | let component: TimerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TimerComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TimerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/timer/timer.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core' 2 | import {BehaviorSubject} from 'rxjs' 3 | import {map} from 'rxjs/operators' 4 | 5 | @Component({ 6 | selector: 'app-timer', 7 | templateUrl: './timer.component.html', 8 | styleUrls: ['./timer.component.scss'], 9 | }) 10 | export class TimerComponent implements OnInit { 11 | timerEnabled = false 12 | startedDate = new BehaviorSubject(new Date()) 13 | elapsedMs = new BehaviorSubject(0) 14 | elapsedText = this.elapsedMs.pipe(map(value => this.getText(value))) 15 | startedMs = -1 16 | pausedMs = -1 17 | started = false 18 | running = false 19 | intervalID: any = undefined 20 | 21 | updateCallback = () => { 22 | if (!this.running) { 23 | return 24 | } 25 | this.elapsedMs.next(Date.now() - this.startedMs) 26 | } 27 | 28 | constructor() { 29 | } 30 | 31 | ngOnInit(): void { 32 | } 33 | 34 | toggleTimer() { 35 | this.timerEnabled = !this.timerEnabled 36 | } 37 | 38 | reset() { 39 | if (!this.started) return 40 | const shouldRestart = this.running 41 | this.startedMs = -1 42 | this.pausedMs = -1 43 | this.started = false 44 | this.running = false 45 | this.elapsedMs.next(0) 46 | clearInterval(this.intervalID) 47 | if (shouldRestart) { 48 | this.startOrPause() 49 | } 50 | } 51 | 52 | getText(elapsedTotalMs: number) { 53 | let elapsedSeconds = Math.floor(elapsedTotalMs / 1000) 54 | const elapsedHours = Math.floor(elapsedSeconds / 3600) 55 | elapsedSeconds -= elapsedHours * 3600 56 | const elapsedMinutes = Math.floor(elapsedSeconds / 60) 57 | elapsedSeconds -= elapsedMinutes * 60 58 | if (elapsedHours > 0) { 59 | return `${elapsedHours}:${elapsedMinutes.toString(10) 60 | .padStart(2, '0')}:${elapsedSeconds.toString(10).padStart(2, '0')}` 61 | } 62 | return `${elapsedMinutes.toString(10) 63 | .padStart(2, '0')}:${elapsedSeconds.toString(10).padStart(2, '0')}` 64 | } 65 | 66 | startOrPause() { 67 | if (!this.started) { // start 68 | this.started = true 69 | this.running = true 70 | this.startedDate.next(new Date()) 71 | this.startedMs = this.startedDate.value.getTime() 72 | this.intervalID = setInterval(this.updateCallback, 1000) 73 | } else { 74 | if (this.running) { // pause 75 | this.running = false 76 | this.pausedMs = Date.now() 77 | clearInterval(this.intervalID) 78 | } else { // resume 79 | this.running = true 80 | this.startedMs = Date.now() - (this.pausedMs - this.startedMs) 81 | this.pausedMs = -1 82 | this.intervalID = setInterval(this.updateCallback, 1000) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/timer/timer.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {CommonModule} from '@angular/common' 3 | import {TimerComponent} from './timer.component' 4 | import {MatButtonModule} from '@angular/material/button' 5 | import {MatIconModule} from '@angular/material/icon' 6 | import {MatTooltipModule} from '@angular/material/tooltip' 7 | 8 | 9 | @NgModule({ 10 | declarations: [TimerComponent], 11 | exports: [ 12 | TimerComponent, 13 | ], 14 | imports: [ 15 | CommonModule, 16 | MatButtonModule, 17 | MatIconModule, 18 | MatTooltipModule, 19 | ], 20 | }) 21 | export class TimerModule { 22 | } 23 | -------------------------------------------------------------------------------- /src/app/util/fs-util.ts: -------------------------------------------------------------------------------- 1 | import {ElectronService} from '../core/services' 2 | import {Injectable} from '@angular/core' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | 6 | @Injectable() 7 | export class FsUtil { 8 | fs: typeof fs 9 | path: typeof path 10 | 11 | constructor(private readonly els: ElectronService) { 12 | this.fs = els.fs 13 | this.path = els.path 14 | } 15 | 16 | homeDir() { 17 | return this.els.os.homedir() 18 | } 19 | 20 | readDirPathSync(defaultPath?: string) { 21 | const p = this.els.remote.dialog.showOpenDialogSync({ 22 | defaultPath: defaultPath, 23 | properties: ['openDirectory'], 24 | }) 25 | if (p === undefined) return undefined 26 | return p[0] 27 | } 28 | 29 | ensureParentDirExistsSync(filePath: string) { 30 | this.els.fs.mkdirSync(this.els.path.dirname(filePath), { 31 | recursive: true, 32 | }) 33 | } 34 | 35 | readFileTextSync(filePath: string): string | undefined { 36 | if (this.els.fs.existsSync(filePath)) { 37 | return this.els.fs.readFileSync(filePath, {encoding: 'utf8'}) 38 | } else { 39 | return undefined 40 | } 41 | } 42 | 43 | safeWriteFileSync(filePath: string, text: string, 44 | tempFileName1: string, 45 | tempFileName2: string) { 46 | this.ensureParentDirExistsSync(filePath) 47 | if (this.els.fs.existsSync(filePath)) { 48 | const dir = this.els.path.dirname(filePath) 49 | const tempFilePath1 = this.els.path.join(dir, tempFileName1) 50 | if (this.els.fs.existsSync(tempFilePath1)) { 51 | alert( 52 | `Unable to write file: temp file (${tempFilePath1}) already exists`) 53 | } 54 | const tempFilePath2 = this.els.path.join(dir, tempFileName2) 55 | if (this.els.fs.existsSync(tempFilePath2)) { 56 | alert( 57 | `Unable to write file: temp file (${tempFilePath2}) already exists`) 58 | } 59 | this.els.fs.writeFileSync(tempFilePath1, text) 60 | this.els.fs.renameSync(filePath, tempFilePath2) 61 | this.els.fs.renameSync(tempFilePath1, filePath) 62 | this.els.fs.unlinkSync(tempFilePath2) 63 | } else { 64 | this.els.fs.writeFileSync(filePath, text) 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /src/app/util/serialization.ts: -------------------------------------------------------------------------------- 1 | import {stableStringify} from './util' 2 | 3 | export type ObjectMap = { [key: string]: V } 4 | 5 | export type SerializedObjectMap = { readonly [key: string]: SerializedObject | undefined } 6 | 7 | export type SerializedObject = 8 | number 9 | | null 10 | | string 11 | | boolean 12 | | SerializedObjectMap 13 | | SerializedObject[] 14 | 15 | export function quickDeserialize(value: SerializedObject | undefined, 16 | defaultValue: T) { 17 | return value === undefined ? defaultValue : (value as unknown as T) 18 | } 19 | 20 | export function quickDeserializeRequired(value: SerializedObject | undefined) { 21 | if (value === undefined) { 22 | throw new Error('Failed to deserialize required field') 23 | } 24 | return value as unknown as T 25 | } 26 | 27 | export function serializePrimitiveToString(value: number | null | string | boolean): string { 28 | return `${value}` 29 | } 30 | 31 | export function deserializeNumberFromString(str: string): number { 32 | return Number(str) 33 | } 34 | 35 | export function serializePrimitiveToObject(value: number | null | string | boolean): SerializedObject { 36 | return value 37 | } 38 | 39 | export function deserializeNumberFromObject(obj: SerializedObject): number { 40 | return obj as unknown as number 41 | } 42 | 43 | export function serializeMapToObject(map: Map, 44 | keyToString: (key: K) => string, 45 | valueToObject: (value: V) => SerializedObject): SerializedObject { 46 | const result: ObjectMap = {} 47 | map.forEach((value, key) => { 48 | result[keyToString(key)] = valueToObject(value) 49 | }) 50 | return result 51 | } 52 | 53 | export function deserializeMapFromObject(obj: SerializedObject, 54 | keyFromString: (key: string) => K, 55 | valueFromObject: (obj: SerializedObject) => V): Map { 56 | const result = new Map() 57 | for (let key in (obj as SerializedObjectMap)) { 58 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 59 | const value = (obj as SerializedObjectMap)[key] 60 | if (value !== undefined) { 61 | result.set( 62 | keyFromString(key), valueFromObject(value)) 63 | } 64 | } 65 | } 66 | return result 67 | } 68 | 69 | export function serializeArrayToObject(array: V[], 70 | valueToObject: (value: V) => SerializedObject): SerializedObject { 71 | const result: SerializedObject[] = [] 72 | array.forEach(value => { 73 | result.push(valueToObject(value)) 74 | }) 75 | return result 76 | } 77 | 78 | export function deserializeArrayFromObject(obj: SerializedObject, 79 | valueFromObject: (obj: SerializedObject) => V): V[] { 80 | const result: V[] = []; 81 | (obj as SerializedObject[]).forEach(value => { 82 | result.push(valueFromObject(value)) 83 | }) 84 | return result 85 | } 86 | 87 | export function stringifySerializedObject(obj: SerializedObject): string { 88 | return stableStringify(obj) 89 | } 90 | 91 | export function parseSerializedObject(str: string): SerializedObject { 92 | return JSON.parse(str) 93 | } 94 | 95 | -------------------------------------------------------------------------------- /src/app/util/time-util.ts: -------------------------------------------------------------------------------- 1 | import {DayID} from '../data/common' 2 | import {MatDatepickerInputEvent} from '@angular/material/datepicker' 3 | 4 | export const MS_PER_DAY = 86400000 5 | 6 | export function dayIDToDate(dayID: DayID): Date { 7 | const date = new Date(dayID * MS_PER_DAY) 8 | return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) 9 | } 10 | 11 | /** 12 | * Converts a JavaScript date to DayID. 13 | * @param date The JavaScript date. 14 | * @param offsetMinutes The number of minutes after midnight when a new day 15 | * starts. For example, if this is 60, then any time before 1AM is still 16 | * counted as the previous day. 17 | */ 18 | export function dateToDayID(date: Date, offsetMinutes: number = 0): DayID { 19 | return Math.floor(Date.UTC( 20 | date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), 21 | date.getMinutes() - offsetMinutes, date.getSeconds(), 22 | ) / 23 | MS_PER_DAY) 24 | } 25 | 26 | /** 27 | * @param offsetMinutes See {@link dateToDayID}. 28 | */ 29 | export function dayIDNow(offsetMinutes: number = 0): DayID { 30 | return dateToDayID(new Date(), offsetMinutes) 31 | } 32 | 33 | export function dateAddDay(date: Date, delta: number) { 34 | return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta) 35 | } 36 | 37 | export function startOfWeek(date: Date): Date { 38 | return new Date( 39 | date.getFullYear(), date.getMonth(), date.getDate() - date.getDay()) 40 | } 41 | 42 | export function dowOfDayID(dayID: DayID): number { 43 | return (dayID + 4) % 7 44 | } 45 | 46 | export function startOfWeekDayID(dayID: DayID): DayID { 47 | return dayID - dowOfDayID(dayID) 48 | } 49 | 50 | /** 51 | * NOTE: month is zero-based to be consistent with JavaScript dates. 52 | */ 53 | export function daysInMonth(year: number, month: number) { 54 | return new Date(year, month + 1, 0).getDate() 55 | } 56 | 57 | const SPECIAL_DATE_KEYWORD_PARSERS: { [key: string]: (todayDayID: DayID) => DayID } = { 58 | 'today': t => t, 59 | 'tmr': t => t + 1, 60 | 'next': t => t + 1, 61 | 'next week': t => t + 7, 62 | 'tomorrow': t => t + 1, 63 | 'yesterday': t => t - 1, 64 | } 65 | 66 | export function parseSpecialDate(text: string, 67 | todayDayID: DayID): DayID | undefined { 68 | text = text.trim().toLowerCase() 69 | 70 | // Keyword rules 71 | const parser = SPECIAL_DATE_KEYWORD_PARSERS[text] 72 | if (parser) { 73 | return parser(todayDayID) 74 | } 75 | 76 | // Other rules 77 | if (text.startsWith('+')) { 78 | const delta = Number(text.substring(1)) 79 | if (isNaN(delta)) { 80 | return undefined 81 | } else { 82 | return todayDayID + delta 83 | } 84 | } 85 | if (text.startsWith('-')) { 86 | const delta = Number(text.substring(1)) 87 | if (isNaN(delta)) { 88 | return undefined 89 | } else { 90 | return todayDayID - delta 91 | } 92 | } 93 | 94 | // TODO add more features here 95 | 96 | return undefined 97 | } 98 | 99 | export function parseMatDatePicker(event: MatDatepickerInputEvent, 100 | currentDayID: DayID): DayID | undefined { 101 | let dayID = parseSpecialDate( 102 | (event.targetElement as any).value || '', currentDayID) 103 | if (dayID === undefined) { 104 | const date = event.value 105 | if (date) { 106 | dayID = dateToDayID(date as Date) 107 | } 108 | } 109 | return dayID 110 | } 111 | 112 | const DOW_TO_SHORT_DISPLAY_NAME = [ 113 | 'Sun', 114 | 'Mon', 115 | 'Tue', 116 | 'Wed', 117 | 'Thu', 118 | 'Fri', 119 | 'Sat', 120 | ] 121 | 122 | export function getShowrtDOWDisplayName(dow: number) { 123 | if (dow >= 0 && dow < DOW_TO_SHORT_DISPLAY_NAME.length) { 124 | return DOW_TO_SHORT_DISPLAY_NAME[dow] 125 | } 126 | return '' 127 | } 128 | 129 | export function getLongDateDisplayName(date: Date) { 130 | return date.toLocaleDateString(undefined, { 131 | year: 'numeric', month: 'long', day: 'numeric', 132 | }) 133 | } 134 | 135 | export function getShortDateDisplayName(date: Date) { 136 | return date.toLocaleDateString(undefined, { 137 | year: 'numeric', month: 'numeric', day: 'numeric', 138 | }) 139 | } 140 | 141 | export function getLongDayIDDisplayName(dayID: DayID) { 142 | return getLongDateDisplayName(dayIDToDate(dayID)) 143 | } 144 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/background.jpg -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "PAGES": { 3 | "HOME": { 4 | "TITLE": "App works !", 5 | "GO_TO_DETAIL": "Go to Detail" 6 | }, 7 | "DETAIL": { 8 | "TITLE": "Detail page !", 9 | "BACK_TO_HOME": "Back to Home" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/angular/favicon.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/angular/favicon.256x256.png -------------------------------------------------------------------------------- /src/assets/icons/angular/favicon.512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/angular/favicon.512x512.png -------------------------------------------------------------------------------- /src/assets/icons/angular/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/angular/favicon.icns -------------------------------------------------------------------------------- /src/assets/icons/angular/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/angular/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/angular/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/angular/favicon.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/favicon.256x256.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/favicon.512x512.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TommyX12/VIR/c34dfebad30ff26141b710468a7953d59e80ebd1/src/assets/icons/favicon.png -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'DEV' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: true, 3 | environment: 'PROD' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'LOCAL' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.web.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'WEB' 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Electron 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-electron'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: [ 'html', 'lcovonly' ], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | browsers: ['AngularElectron'], 28 | customLaunchers: { 29 | AngularElectron: { 30 | base: 'Electron', 31 | flags: [ 32 | '--remote-debugging-port=9222' 33 | ], 34 | browserWindowOptions: { 35 | webPreferences: { 36 | nodeIntegration: true, 37 | nodeIntegrationInSubFrames: true, 38 | allowRunningInsecureContent: true, 39 | enableRemoteModule: true 40 | } 41 | } 42 | } 43 | } 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core' 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic' 3 | 4 | import {AppModule} from './app/app.module' 5 | import {AppConfig} from './environments/environment' 6 | import {enableMapSet} from 'immer' 7 | 8 | enableMapSet() 9 | 10 | if (AppConfig.production) { 11 | enableProdMode() 12 | } 13 | 14 | platformBrowserDynamic() 15 | .bootstrapModule(AppModule, { 16 | preserveWhitespaces: false, 17 | }) 18 | .catch(err => console.error(err)) 19 | -------------------------------------------------------------------------------- /src/polyfills-test.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es/reflect'; 2 | import 'zone.js/dist/zone'; 3 | -------------------------------------------------------------------------------- /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 | /** IE10 and 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.ts'; 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/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | @import '~material-design-icons/iconfont/material-icons.css'; 3 | 4 | @include mat-core(); 5 | 6 | // Add your desired themes to this map. 7 | $themes-map: ( 8 | main: ( 9 | primary-base: $mat-green, 10 | accent-base: $mat-light-blue, 11 | ), 12 | ); 13 | 14 | // Import the module and do the job: 15 | @import '~angular-material-dynamic-themes/themes-core'; 16 | @include make-stylesheets($themes-map); 17 | 18 | /* You can add global styles to this file, and also import other style files */ 19 | html, body { 20 | margin: 0; 21 | padding: 0; 22 | 23 | user-select: none; 24 | 25 | height: 100%; 26 | font-family: Arial, Helvetica, sans-serif; 27 | } 28 | 29 | .app-single-line-text { 30 | text-overflow: ellipsis; 31 | white-space: nowrap; 32 | min-width: 0; 33 | overflow: hidden; 34 | } 35 | 36 | .app-single-line-text-no-shrink { 37 | white-space: nowrap; 38 | overflow: visible; 39 | } 40 | -------------------------------------------------------------------------------- /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/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "strictNullChecks": true, 6 | "strictPropertyInitialization": true, 7 | "skipDefaultLibCheck": true, 8 | "skipLibCheck": true, 9 | "outDir": "../out-tsc/app", 10 | "module": "es2015", 11 | "baseUrl": "", 12 | "types": [] 13 | }, 14 | "include": [ 15 | "main.ts", 16 | "polyfill.ts" 17 | ], 18 | "exclude": [ 19 | "**/*.spec.ts", 20 | "**/*.service.ts" 21 | ], 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true, 25 | "preserveWhitespaces": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills-test.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ], 19 | "exclude": [ 20 | "dist", 21 | "release", 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare const nodeModule: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | interface Window { 7 | process: any; 8 | require: any; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "es2020", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "es2016", 18 | "es2015", 19 | "dom" 20 | ] 21 | }, 22 | "files": [ 23 | "src/main.ts", 24 | "src/polyfills.ts" 25 | ], 26 | "include": [ 27 | "src/**/*.d.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es5", 9 | "types": [ 10 | "node" 11 | ], 12 | "lib": [ 13 | "es2017", 14 | "es2016", 15 | "es2015", 16 | "dom" 17 | ] 18 | }, 19 | "files": [ 20 | "main.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------