├── .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 | 
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 | 
13 |
14 | 
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 |
6 |
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 |
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 |
8 |
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 |
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 |
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 |
38 |
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 |
8 |
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 |
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 |
8 |
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 |
8 |
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 |
8 |
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 |
4 |
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 |
38 |
39 |
67 |
68 |
79 |
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 |
--------------------------------------------------------------------------------