mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
refactor: remove ionic from remaining places (#2565)
This commit is contained in:
3
web/.gitignore
vendored
3
web/.gitignore
vendored
@@ -26,7 +26,6 @@ postprocess.js
|
||||
|
||||
/.angular
|
||||
/.idea
|
||||
/.ionic
|
||||
/.sass-cache
|
||||
/.sourcemaps
|
||||
/.versions
|
||||
@@ -39,4 +38,4 @@ postprocess.js
|
||||
/plugins
|
||||
|
||||
config.json
|
||||
proxy.conf.json
|
||||
proxy.conf.json
|
||||
|
||||
@@ -26,11 +26,6 @@
|
||||
"input": "projects/shared/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "node_modules/ionicons/dist/svg",
|
||||
"output": "./svg"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/monaco-editor",
|
||||
@@ -46,11 +41,6 @@
|
||||
"glob": "ngsw.json",
|
||||
"input": "dist/raw/ui",
|
||||
"output": "projects/ui/src"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@taiga-ui/icons/src",
|
||||
"output": "assets/taiga-ui/icons"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
|
||||
120
web/package-lock.json
generated
120
web/package-lock.json
generated
@@ -24,15 +24,15 @@
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||
"@taiga-ui/addon-charts": "3.65.0",
|
||||
"@taiga-ui/addon-commerce": "3.65.0",
|
||||
"@taiga-ui/addon-mobile": "3.65.0",
|
||||
"@taiga-ui/cdk": "3.65.0",
|
||||
"@taiga-ui/core": "3.65.0",
|
||||
"@taiga-ui/experimental": "3.65.0",
|
||||
"@taiga-ui/icons": "3.65.0",
|
||||
"@taiga-ui/kit": "3.65.0",
|
||||
"@taiga-ui/styles": "3.65.0",
|
||||
"@taiga-ui/addon-charts": "3.68.0",
|
||||
"@taiga-ui/addon-commerce": "3.68.0",
|
||||
"@taiga-ui/addon-mobile": "3.68.0",
|
||||
"@taiga-ui/cdk": "3.68.0",
|
||||
"@taiga-ui/core": "3.68.0",
|
||||
"@taiga-ui/experimental": "3.68.0",
|
||||
"@taiga-ui/icons": "3.68.0",
|
||||
"@taiga-ui/kit": "3.68.0",
|
||||
"@taiga-ui/styles": "3.68.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"@tinkoff/ng-event-plugins": "3.1.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
@@ -4672,9 +4672,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-charts": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.65.0.tgz",
|
||||
"integrity": "sha512-HNKUeK0ippIvLRF6wsuCiyJ4d98K4uIhkGwK1fWaTVOCN26Z+AnFKk9AryTyhocEZctyc4PMpJ7BP7h3CA4dZA==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.68.0.tgz",
|
||||
"integrity": "sha512-f19w8EikXSQuF2f/M8e3yZoXBzunugbLZlz/W0Fiw8ykGE2tZPWXmcX4VKHa2yuI/VPwSUClVcF/n7MgNque0w==",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
@@ -4682,15 +4682,15 @@
|
||||
"@angular/common": ">=12.0.0",
|
||||
"@angular/core": ">=12.0.0",
|
||||
"@ng-web-apis/common": "3.0.6",
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/core": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"@taiga-ui/core": "^3.68.0",
|
||||
"@tinkoff/ng-polymorpheus": "4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-commerce": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.65.0.tgz",
|
||||
"integrity": "sha512-D98M3nkPKVFz9TFiMxCmMtmJs9vDc69RlPv5M03ZF+qXHqbthfpVss/p2MSzs4Cr2vgoECaZWPLNcWBOO5mzCw==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.68.0.tgz",
|
||||
"integrity": "sha512-HyBqU9WRbty4mXloawhO+0E2tfNwwD7yZ1DOx8kEiRrGWwIT+11Io/PspkShZzY4mTyPt0iuoBkwe9rpdoxieA==",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
@@ -4702,18 +4702,18 @@
|
||||
"@maskito/core": "1.9.0",
|
||||
"@maskito/kit": "1.9.0",
|
||||
"@ng-web-apis/common": "3.0.6",
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/core": "^3.65.0",
|
||||
"@taiga-ui/i18n": "^3.65.0",
|
||||
"@taiga-ui/kit": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"@taiga-ui/core": "^3.68.0",
|
||||
"@taiga-ui/i18n": "^3.68.0",
|
||||
"@taiga-ui/kit": "^3.68.0",
|
||||
"@tinkoff/ng-polymorpheus": "4.3.0",
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-mobile": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-3.65.0.tgz",
|
||||
"integrity": "sha512-nKEf5Lb7yfR7vqkAIQQLoUEzSpKftdPpAsmco6FNfN4FDlvDFYTKE8MqqXAxzEqrXviDXv8/CKPv+nc6xd4VXg==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-3.68.0.tgz",
|
||||
"integrity": "sha512-ssX+dO+aPF7q49YiuW2X//N9X01XqkJB8NlTKM4kYPxOQi423JR4tewafmcHoRjdT9OnUo/pkq72GRE5UECEAg==",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
@@ -4722,17 +4722,17 @@
|
||||
"@angular/common": ">=12.0.0",
|
||||
"@angular/core": ">=12.0.0",
|
||||
"@ng-web-apis/common": "3.0.6",
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/core": "^3.65.0",
|
||||
"@taiga-ui/kit": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"@taiga-ui/core": "^3.68.0",
|
||||
"@taiga-ui/kit": "^3.68.0",
|
||||
"@tinkoff/ng-polymorpheus": "4.3.0",
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/cdk": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.65.0.tgz",
|
||||
"integrity": "sha512-hiFC9RlRng7pUv84YPZbqieKIYsFEzsMKCjMIckHBASBBU6qQ4OY6irKszFvTGqMe9KJgBh6sJU1hkQOBwFSaA==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.68.0.tgz",
|
||||
"integrity": "sha512-GB1wJaJGJMkK2+Njl3qePy3o4tu6w2MIBGcPkFE65sqgomIDNhM5jVB3ldQI5XtvTHsAAoV8m6UpyKRDQJdw6g==",
|
||||
"dependencies": {
|
||||
"@ng-web-apis/common": "3.0.6",
|
||||
"@ng-web-apis/mutation-observer": "3.1.0",
|
||||
@@ -4760,11 +4760,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@taiga-ui/core": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.65.0.tgz",
|
||||
"integrity": "sha512-zNctTTsrW73fhmYirWE/mZs32UUvv6gV5CoIFm0BzVos0X7ZkN+x7PLXd9R+3CEgL6Kv/OxY92p+pJRvqc5jHg==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.68.0.tgz",
|
||||
"integrity": "sha512-KBS7ZM8i/h0ReZcoD5xQP/AJYmrabg26f0n/fr4pZmDKgZGaaGvB54vwTxq6vh7QW1VOGc7LuJQ2MshEE9iOmQ==",
|
||||
"dependencies": {
|
||||
"@taiga-ui/i18n": "^3.65.0",
|
||||
"@taiga-ui/i18n": "^3.68.0",
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -4776,35 +4776,35 @@
|
||||
"@angular/router": ">=12.0.0",
|
||||
"@ng-web-apis/common": "3.0.6",
|
||||
"@ng-web-apis/mutation-observer": "3.1.0",
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/i18n": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"@taiga-ui/i18n": "^3.68.0",
|
||||
"@tinkoff/ng-event-plugins": "3.1.0",
|
||||
"@tinkoff/ng-polymorpheus": "4.3.0",
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/experimental": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.65.0.tgz",
|
||||
"integrity": "sha512-LZYR+XeJ2n+vE4AHBiIolzlqDrDGUx/bmE0ypmKO7dPgvHWu5Al8OXRrnhyqmAVO48FNpkSZ07YoqCG/aoxu6g==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.68.0.tgz",
|
||||
"integrity": "sha512-gZyD+S7af1Z11Sx0dQUZBGoyWsN2ykZsguTYCCtVF4iynoCiqwCsTTWhjaA7scNVuTl/H7ekwabw+U6+G3TPfg==",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=12.0.0",
|
||||
"@angular/core": ">=12.0.0",
|
||||
"@taiga-ui/addon-commerce": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/core": "^3.65.0",
|
||||
"@taiga-ui/kit": "^3.65.0",
|
||||
"@taiga-ui/addon-commerce": "^3.68.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"@taiga-ui/core": "^3.68.0",
|
||||
"@taiga-ui/kit": "^3.68.0",
|
||||
"@tinkoff/ng-polymorpheus": "4.3.0",
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/i18n": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.65.0.tgz",
|
||||
"integrity": "sha512-lHy9VDKc5IXbm40eJnnAyOlmm3vDgmWhGbr5woGe9bV/tTqsBBDATY7Rkhz7Bu1nbX7X+MI0TDfQh9ayoCCKRQ==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.68.0.tgz",
|
||||
"integrity": "sha512-AyJDYm3nD0mNfEnqXubGFgsHqUTCs8W8/P2Td/TF1JCsp7Zjo+qc9uocqWXzlf2Zd0w26d0oYbBsVjpnrMlVnw==",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
@@ -4815,20 +4815,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/icons": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.65.0.tgz",
|
||||
"integrity": "sha512-8iE6EuK+QBzcNiRM1ThZOOkZpal7V6dBouMXMj+QphRWiIp8Znj58mtY3L+uwQFpGnxt3DRs4p4eEA9ZuGFssw==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.68.0.tgz",
|
||||
"integrity": "sha512-krRHxz4I74hwYfD1/zOQRMUlzpYyAyXCxudYm9lTS8G2Yy7QeVzs0d7FkdpUvCYMqfQjBLqepckykxD5qPwoSw==",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@taiga-ui/cdk": "^3.65.0"
|
||||
"@taiga-ui/cdk": "^3.68.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/kit": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.65.0.tgz",
|
||||
"integrity": "sha512-Nh6pMSAFR7yScF7acj8WdCpKQUgDatW2jObqts0z4hy9BJ8gl9BAWRBgSlbp3Oen5c2WAIC316Gb9OcttC8nbw==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.68.0.tgz",
|
||||
"integrity": "sha512-s+GMHy9C5pX8FdyYdTrnue5H/OH41214qgIjXF1aPYzNveDsDTF1H1pFwJcDXYQO2wE3UEH0sV4nEyKXXyNjBA==",
|
||||
"dependencies": {
|
||||
"@maskito/angular": "1.9.0",
|
||||
"@maskito/core": "1.9.0",
|
||||
@@ -4845,19 +4845,19 @@
|
||||
"@ng-web-apis/common": "3.0.6",
|
||||
"@ng-web-apis/mutation-observer": "3.1.0",
|
||||
"@ng-web-apis/resize-observer": "3.0.6",
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/core": "^3.65.0",
|
||||
"@taiga-ui/i18n": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"@taiga-ui/core": "^3.68.0",
|
||||
"@taiga-ui/i18n": "^3.68.0",
|
||||
"@tinkoff/ng-polymorpheus": "4.3.0",
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/styles": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/styles/-/styles-3.65.0.tgz",
|
||||
"integrity": "sha512-HO2sZPxNOGj2BPQpWkrM6HgZV/QxaEMEemye3sJvsfuttvk6bmxoL8NF331I63tlp/Zx7woD8AusH5ATuUniqg==",
|
||||
"version": "3.68.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/styles/-/styles-3.68.0.tgz",
|
||||
"integrity": "sha512-7iC+T2ManhjCzPqw3e3H2vt3Bn9op555tio6WROhnsxakOdUrZbPiVZGaKcIlZ27ZjPbYKxL1NeCmaiviuphOw==",
|
||||
"peerDependencies": {
|
||||
"@taiga-ui/cdk": "^3.65.0",
|
||||
"@taiga-ui/cdk": "^3.68.0",
|
||||
"tslib": "2.6.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,15 +46,15 @@
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||
"@taiga-ui/addon-charts": "3.65.0",
|
||||
"@taiga-ui/addon-commerce": "3.65.0",
|
||||
"@taiga-ui/addon-mobile": "3.65.0",
|
||||
"@taiga-ui/cdk": "3.65.0",
|
||||
"@taiga-ui/core": "3.65.0",
|
||||
"@taiga-ui/experimental": "3.65.0",
|
||||
"@taiga-ui/icons": "3.65.0",
|
||||
"@taiga-ui/kit": "3.65.0",
|
||||
"@taiga-ui/styles": "3.65.0",
|
||||
"@taiga-ui/addon-charts": "3.68.0",
|
||||
"@taiga-ui/addon-commerce": "3.68.0",
|
||||
"@taiga-ui/addon-mobile": "3.68.0",
|
||||
"@taiga-ui/cdk": "3.68.0",
|
||||
"@taiga-ui/core": "3.68.0",
|
||||
"@taiga-ui/experimental": "3.68.0",
|
||||
"@taiga-ui/icons": "3.68.0",
|
||||
"@taiga-ui/kit": "3.68.0",
|
||||
"@taiga-ui/styles": "3.68.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"@tinkoff/ng-event-plugins": "3.1.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
:host {
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Directive, HostListener, Inject } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { debounce } from '../../util/misc.util'
|
||||
|
||||
@Directive({
|
||||
selector: '[appEnter]',
|
||||
})
|
||||
export class EnterDirective {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
@HostListener('document:keydown.enter')
|
||||
@debounce()
|
||||
handleKeyboardEvent() {
|
||||
const elems = this.document.querySelectorAll('.enter-click')
|
||||
const elem = elems[elems.length - 1] as HTMLButtonElement
|
||||
|
||||
if (elem && !elem.classList.contains('no-click') && !elem.disabled) {
|
||||
elem.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { EnterDirective } from './enter.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [EnterDirective],
|
||||
exports: [EnterDirective],
|
||||
})
|
||||
export class EnterModule {}
|
||||
@@ -18,8 +18,6 @@ export * from './components/drive.component'
|
||||
|
||||
export * from './directives/drag-scroller.directive'
|
||||
export * from './directives/safe-links.directive'
|
||||
export * from './directives/enter/enter.directive'
|
||||
export * from './directives/enter/enter.module'
|
||||
|
||||
export * from './mocks/get-setup-status'
|
||||
|
||||
@@ -42,11 +40,6 @@ export * from './services/http.service'
|
||||
export * from './services/setup.service'
|
||||
export * from './services/setup-logs.service'
|
||||
|
||||
export * from './themes/dark-theme/dark-theme.component'
|
||||
export * from './themes/dark-theme/dark-theme.module'
|
||||
export * from './themes/light-theme/light-theme.component'
|
||||
export * from './themes/light-theme/light-theme.module'
|
||||
|
||||
export * from './types/api'
|
||||
export * from './types/constructor'
|
||||
export * from './types/http.types'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../../styles/variables';
|
||||
@@ -1,15 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { AbstractTuiThemeSwitcher } from '@taiga-ui/cdk'
|
||||
|
||||
@Component({
|
||||
selector: 'dark-theme',
|
||||
template: '',
|
||||
styleUrls: ['./dark-theme.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DarkThemeComponent extends AbstractTuiThemeSwitcher {}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { DarkThemeComponent } from './dark-theme.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [DarkThemeComponent],
|
||||
exports: [DarkThemeComponent],
|
||||
})
|
||||
export class DarkThemeModule {}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Ionic Variables and Theming. For more info, please see:
|
||||
// http://ionicframework.com/docs/theming/
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
--ion-color-primary: #0075e1;
|
||||
--ion-color-primary-rgb: 66, 140, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80, 200, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106, 100, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47, 223, 117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffb506;
|
||||
--ion-color-warning-rgb: 255, 213, 52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd534;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255, 73, 97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
//--ion-color-light: #f4f5f8;
|
||||
//--ion-color-light-rgb: 244, 245, 248;
|
||||
//--ion-color-light-contrast: #000000;
|
||||
//--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
//--ion-color-light-shade: #d7d8da;
|
||||
//--ion-color-light-tint: #f5f6f9;
|
||||
//
|
||||
//--ion-color-medium: #f4f5f8;
|
||||
//--ion-color-medium-rgb: 244, 245, 248;
|
||||
//--ion-color-medium-contrast: #000000;
|
||||
//--ion-color-medium-contrast-rgb: 0, 0, 0;
|
||||
//--ion-color-medium-shade: #d7d8da;
|
||||
//--ion-color-medium-tint: #f5f6f9;
|
||||
//
|
||||
//--ion-color-dark: #92949c;
|
||||
//--ion-color-dark-rgb: 146, 148, 156;
|
||||
//--ion-color-dark-contrast: #ffffff;
|
||||
//--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
//--ion-color-dark-shade: #808289;
|
||||
//--ion-color-dark-tint: #9d9fa6;
|
||||
|
||||
--ion-color-step-50: #f2f2f2;
|
||||
--ion-color-step-100: #e6e6e6;
|
||||
--ion-color-step-150: #d9d9d9;
|
||||
--ion-color-step-200: #cccccc;
|
||||
--ion-color-step-250: #bfbfbf;
|
||||
--ion-color-step-300: #b3b3b3;
|
||||
--ion-color-step-350: #a6a6a6;
|
||||
--ion-color-step-400: #999999;
|
||||
--ion-color-step-450: #8c8c8c;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #737373;
|
||||
--ion-color-step-600: #666666;
|
||||
--ion-color-step-650: #595959;
|
||||
--ion-color-step-700: #4d4d4d;
|
||||
--ion-color-step-750: #404040;
|
||||
--ion-color-step-800: #333333;
|
||||
--ion-color-step-850: #262626;
|
||||
--ion-color-step-900: #191919;
|
||||
--ion-color-step-950: #0d0d0d;
|
||||
|
||||
--alt-red: #ff4961;
|
||||
--alt-orange: #f89248;
|
||||
--alt-yellow: #e5d53e;
|
||||
--alt-green: #3dcf6f;
|
||||
--alt-blue: #00a8a8;
|
||||
--alt-purple: #9747ff;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { AbstractTuiThemeSwitcher } from '@taiga-ui/cdk'
|
||||
|
||||
@Component({
|
||||
selector: 'light-theme',
|
||||
template: '',
|
||||
styleUrls: ['./light-theme.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightThemeComponent extends AbstractTuiThemeSwitcher {}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { LightThemeComponent } from './light-theme.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [LightThemeComponent],
|
||||
exports: [LightThemeComponent],
|
||||
})
|
||||
export class LightThemeModule {}
|
||||
@@ -1,101 +1,31 @@
|
||||
<svg class="definitions" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="round-corners">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur" />
|
||||
<feColorMatrix
|
||||
in="blur"
|
||||
type="matrix"
|
||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
|
||||
result="flt_tag"
|
||||
/>
|
||||
<feComposite in="SourceGraphic" in2="flt_tag" operator="atop" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
<tui-root
|
||||
*ngIf="widgetDrawer$ | async as drawer"
|
||||
tuiTheme="night"
|
||||
[tuiMode]="(theme$ | async) === 'Dark' ? 'onDark' : null"
|
||||
[style.--widgets-width.px]="drawer.open ? drawer.width : 0"
|
||||
>
|
||||
<ion-app appEnter>
|
||||
<svg-definitions />
|
||||
<!--TODO: Theme-->
|
||||
<tui-root tuiTheme="night" tuiMode="onDark">
|
||||
<ion-app>
|
||||
<ion-content>
|
||||
<ion-split-pane
|
||||
contentId="main-content"
|
||||
[disabled]="!(navigation$ | async)"
|
||||
(ionSplitPaneVisible)="splitPaneVisible($event)"
|
||||
<ion-router-outlet
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="offline$ | async"
|
||||
>
|
||||
<ion-menu
|
||||
*ngIf="navigation$ | async"
|
||||
contentId="main-content"
|
||||
type="overlay"
|
||||
side="start"
|
||||
class="left-menu"
|
||||
>
|
||||
<ion-content color="light" scrollY="false" class="menu">
|
||||
<app-menu *ngIf="authService.isVerified$ | async" />
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
|
||||
<ion-menu
|
||||
contentId="main-content"
|
||||
type="overlay"
|
||||
side="end"
|
||||
class="right-menu container"
|
||||
[class.container_offline]="offline$ | async"
|
||||
[class.right-menu_hidden]="!drawer.open"
|
||||
[style.--side-width.px]="drawer.width"
|
||||
>
|
||||
<div class="divider">
|
||||
<button
|
||||
class="widgets-button"
|
||||
[class.widgets-button_collapse]="drawer.width === 600"
|
||||
(click)="onResize(drawer)"
|
||||
></button>
|
||||
</div>
|
||||
</ion-menu>
|
||||
|
||||
<ion-router-outlet
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="offline$ | async"
|
||||
>
|
||||
<ion-content
|
||||
class="ion-padding with-widgets"
|
||||
style="pointer-events: none; opacity: 0"
|
||||
></ion-content>
|
||||
</ion-router-outlet>
|
||||
</ion-split-pane>
|
||||
|
||||
<section appPreloader></section>
|
||||
<ion-content
|
||||
class="ion-padding with-widgets"
|
||||
style="pointer-events: none; opacity: 0"
|
||||
></ion-content>
|
||||
</ion-router-outlet>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<footer appFooter></footer>
|
||||
</ion-footer>
|
||||
<ion-footer
|
||||
*ngIf="
|
||||
(navigation$ | async) &&
|
||||
(authService.isVerified$ | async) &&
|
||||
!(sidebarOpen$ | async)
|
||||
"
|
||||
>
|
||||
<connection-bar />
|
||||
</ion-footer>
|
||||
<toast-container />
|
||||
</ion-app>
|
||||
<toast-container />
|
||||
<sidebar-host ngProjectAs="tuiOverContent" />
|
||||
</tui-root>
|
||||
<ng-container
|
||||
*ngIf="authService.isVerified$ | async; else defaultTheme"
|
||||
[ngSwitch]="theme$ | async"
|
||||
>
|
||||
<ng-container *ngSwitchCase="'Dark'">
|
||||
<tui-theme-night />
|
||||
<dark-theme />
|
||||
</ng-container>
|
||||
<light-theme *ngSwitchCase="'Light'" />
|
||||
</ng-container>
|
||||
<ng-template #defaultTheme>
|
||||
|
||||
<!--TODO: Theme-->
|
||||
@if (auth.isVerified$ | async) {
|
||||
@switch (theme$ | async) {
|
||||
@case ('Dark') {
|
||||
<tui-theme-night />
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<tui-theme-night />
|
||||
<dark-theme />
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 705 B |
@@ -7,16 +7,6 @@ tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left-menu {
|
||||
--side-max-width: 280px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
:host-context(body[data-theme='Light']) & {
|
||||
--ion-color-base: #f4f4f5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
transition: filter 0.3s;
|
||||
@@ -24,110 +14,4 @@ tui-root {
|
||||
&_offline {
|
||||
filter: saturate(0.75) contrast(0.85);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991.499px) {
|
||||
--widgets-width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
--side-max-width: 600px;
|
||||
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
right: 0;
|
||||
left: auto;
|
||||
top: 74px;
|
||||
|
||||
// For some reason *ngIf is broken upon first login
|
||||
&_hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
background: #e2e2e2;
|
||||
|
||||
z-index: 10;
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -78px;
|
||||
left: 10px;
|
||||
width: 60px;
|
||||
height: 50px;
|
||||
border-bottom-left-radius: 14px;
|
||||
box-shadow: -14px 0 0 -1px #e2e2e2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
margin-top: 28px;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.widgets-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
font-size: 0;
|
||||
left: 100%;
|
||||
width: 16px;
|
||||
height: 60px;
|
||||
margin-top: -30px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
background: inherit;
|
||||
pointer-events: auto;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 3px;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
background: black;
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
margin-top: -5px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&_collapse:before {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&_collapse:after {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.definitions {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,36 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
import { ConnectionService } from './services/connection.service'
|
||||
import { Component, inject, OnInit } from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import {
|
||||
ClientStorageService,
|
||||
WidgetDrawer,
|
||||
} from './services/client-storage.service'
|
||||
import { ThemeSwitcherService } from './services/theme-switcher.service'
|
||||
import { THEME } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { ConnectionService } from './services/connection.service'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
import { slideInAnimation } from './route-animation'
|
||||
|
||||
function hasNavigation(url: string): boolean {
|
||||
return (
|
||||
!url.startsWith('/loading') &&
|
||||
!url.startsWith('/diagnostic') &&
|
||||
!url.startsWith('/portal')
|
||||
)
|
||||
}
|
||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
animations: [slideInAnimation],
|
||||
})
|
||||
export class AppComponent implements OnDestroy {
|
||||
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
export class AppComponent implements OnInit {
|
||||
private readonly title = inject(Title)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
|
||||
readonly auth = inject(AuthService)
|
||||
readonly theme$ = inject(THEME)
|
||||
// @TODO theres a bug here disabling the side menu from appearing on first login; refresh fixes
|
||||
readonly navigation$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.router.events.pipe(map(() => hasNavigation(this.router.url))),
|
||||
]).pipe(map(([isVerified, hasNavigation]) => isVerified && hasNavigation))
|
||||
readonly subscription = merge(
|
||||
inject(PatchDataService),
|
||||
inject(PatchMonitorService),
|
||||
)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe()
|
||||
|
||||
readonly offline$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.connection.connected$,
|
||||
inject(ConnectionService).connected$,
|
||||
this.auth.isVerified$,
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
@@ -56,37 +42,9 @@ export class AppComponent implements OnDestroy {
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly titleService: Title,
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
readonly authService: AuthService,
|
||||
readonly connection: ConnectionService,
|
||||
readonly clientStorageService: ClientStorageService,
|
||||
readonly themeSwitcher: ThemeSwitcherService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.patch
|
||||
.watch$('ui', 'name')
|
||||
.subscribe(name => this.titleService.setTitle(name || 'StartOS'))
|
||||
}
|
||||
|
||||
splitPaneVisible({ detail }: any) {
|
||||
this.splitPane.sidebarOpen$.next(detail.visible)
|
||||
}
|
||||
|
||||
onResize(drawer: WidgetDrawer) {
|
||||
this.clientStorageService.updateWidgetDrawer({
|
||||
...drawer,
|
||||
width: drawer.width === 400 ? 600 : 400,
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe()
|
||||
.subscribe(name => this.title.setTitle(name || 'StartOS'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,7 @@ import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import {
|
||||
DarkThemeModule,
|
||||
EnterModule,
|
||||
LightThemeModule,
|
||||
LoadingModule,
|
||||
MarkdownModule,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { LoadingModule } from '@start9labs/shared'
|
||||
import {
|
||||
TuiAlertModule,
|
||||
TuiDialogModule,
|
||||
@@ -19,17 +11,12 @@ import {
|
||||
TuiRootModule,
|
||||
TuiThemeNightModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { SidebarHostComponent } from 'src/app/common/sidebar-host.component'
|
||||
import { SvgDefinitionsComponent } from 'src/app/common/svg-definitions.component'
|
||||
import { ToastContainerComponent } from 'src/app/common/toast-container/toast-container.component'
|
||||
import { environment } from '../environments/environment'
|
||||
import { AppComponent } from './app.component'
|
||||
import { APP_PROVIDERS } from './app.providers'
|
||||
import { ConnectionBarComponentModule } from './app/connection-bar/connection-bar.component.module'
|
||||
import { FooterModule } from './app/footer/footer.module'
|
||||
import { MenuModule } from './app/menu/menu.module'
|
||||
import { PreloaderModule } from './app/preloader/preloader.module'
|
||||
import { SidebarHostComponent } from './app/sidebar-host.component'
|
||||
import { OSWelcomePageModule } from './common/os-welcome/os-welcome.module'
|
||||
import { QRComponentModule } from './common/qr/qr.module'
|
||||
import { ToastContainerModule } from './common/toast-container/toast-container.module'
|
||||
import { RoutingModule } from './routing.module'
|
||||
|
||||
@NgModule({
|
||||
@@ -41,23 +28,12 @@ import { RoutingModule } from './routing.module'
|
||||
mode: 'md',
|
||||
}),
|
||||
RoutingModule,
|
||||
MenuModule,
|
||||
PreloaderModule,
|
||||
FooterModule,
|
||||
EnterModule,
|
||||
OSWelcomePageModule,
|
||||
MarkdownModule,
|
||||
MonacoEditorModule,
|
||||
SharedPipesModule,
|
||||
ToastContainerModule,
|
||||
ConnectionBarComponentModule,
|
||||
ToastContainerComponent,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
TuiAlertModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
DarkThemeModule,
|
||||
LightThemeModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: environment.useServiceWorker,
|
||||
// Register the ServiceWorker as soon as the application is stable
|
||||
@@ -65,8 +41,8 @@ import { RoutingModule } from './routing.module'
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
LoadingModule,
|
||||
QRComponentModule,
|
||||
SidebarHostComponent,
|
||||
SvgDefinitionsComponent,
|
||||
],
|
||||
providers: APP_PROVIDERS,
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<ion-toolbar
|
||||
*ngIf="connection$ | async as connection"
|
||||
class="connection-toolbar"
|
||||
[color]="connection.color"
|
||||
>
|
||||
<div class="inline" slot="start">
|
||||
<ion-icon [name]="connection.icon" class="icon"></ion-icon>
|
||||
<p style="margin: 8px 0; font-weight: 600">{{ connection.message }}</p>
|
||||
<ion-spinner
|
||||
*ngIf="connection.dots"
|
||||
name="dots"
|
||||
color="light"
|
||||
class="ion-margin-start"
|
||||
></ion-spinner>
|
||||
</div>
|
||||
</ion-toolbar>
|
||||
@@ -1,11 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ConnectionBarComponent } from './connection-bar.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ConnectionBarComponent],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [ConnectionBarComponent],
|
||||
})
|
||||
export class ConnectionBarComponentModule {}
|
||||
@@ -1,9 +0,0 @@
|
||||
.connection-toolbar {
|
||||
padding: 0 24px;
|
||||
--min-height: 36px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 23px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, Observable, startWith } from 'rxjs'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'connection-bar',
|
||||
templateUrl: './connection-bar.component.html',
|
||||
styleUrls: ['./connection-bar.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ConnectionBarComponent {
|
||||
private readonly websocket$ = this.connectionService.websocketConnected$
|
||||
|
||||
readonly connection$: Observable<{
|
||||
message: string
|
||||
color: string
|
||||
icon: string
|
||||
dots: boolean
|
||||
}> = combineLatest([
|
||||
this.connectionService.networkConnected$,
|
||||
this.websocket$.pipe(startWith(false)),
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
]).pipe(
|
||||
map(([network, websocket, status]) => {
|
||||
if (!network)
|
||||
return {
|
||||
message: 'No Internet',
|
||||
color: 'danger',
|
||||
icon: 'cloud-offline-outline',
|
||||
dots: false,
|
||||
}
|
||||
if (!websocket)
|
||||
return {
|
||||
message: 'Connecting',
|
||||
color: 'warning',
|
||||
icon: 'cloud-offline-outline',
|
||||
dots: true,
|
||||
}
|
||||
if (status['shutting-down'])
|
||||
return {
|
||||
message: 'Shutting Down',
|
||||
color: 'dark',
|
||||
icon: 'power',
|
||||
dots: true,
|
||||
}
|
||||
if (status.restarting)
|
||||
return {
|
||||
message: 'Restarting',
|
||||
color: 'dark',
|
||||
icon: 'power',
|
||||
dots: true,
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Connected',
|
||||
color: 'success',
|
||||
icon: 'cloud-done',
|
||||
dots: false,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<ion-toolbar
|
||||
*ngIf="progress$ | async as progress"
|
||||
color="light"
|
||||
[@heightCollapse]="animation"
|
||||
>
|
||||
<ion-list class="list">
|
||||
<!-- show progress -->
|
||||
<ng-container *ngIf="progress.size !== null; else calculating">
|
||||
<ion-list-header>
|
||||
<ion-label
|
||||
>Downloading:
|
||||
{{ getProgress(progress.size, progress.downloaded) }}%</ion-label
|
||||
>
|
||||
</ion-list-header>
|
||||
<ion-progress-bar
|
||||
class="progress"
|
||||
color="secondary"
|
||||
[value]="getProgress(progress.size, progress.downloaded) / 100"
|
||||
></ion-progress-bar>
|
||||
</ng-container>
|
||||
<!-- show calculating -->
|
||||
<ng-template #calculating>
|
||||
<ion-list-header>
|
||||
<ion-label>Calculating download size</ion-label>
|
||||
</ion-list-header>
|
||||
<ion-progress-bar
|
||||
class="progress"
|
||||
color="secondary"
|
||||
type="indeterminate"
|
||||
></ion-progress-bar>
|
||||
</ng-template>
|
||||
</ion-list>
|
||||
</ion-toolbar>
|
||||
@@ -1,9 +0,0 @@
|
||||
.list {
|
||||
box-shadow: inset 0 1px var(--ion-color-dark);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: auto;
|
||||
margin: 0 16px 16px 16px;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { heightCollapse } from 'src/app/util/animations'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'footer[appFooter]',
|
||||
templateUrl: 'footer.component.html',
|
||||
styleUrls: ['footer.component.scss'],
|
||||
animations: [heightCollapse],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FooterComponent {
|
||||
readonly progress$ = this.patch
|
||||
.watch$('server-info', 'status-info', 'update-progress')
|
||||
.pipe(map(a => a && { ...a }))
|
||||
|
||||
readonly animation = {
|
||||
value: '',
|
||||
params: {
|
||||
duration: 1000,
|
||||
delay: 50,
|
||||
},
|
||||
}
|
||||
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
|
||||
getProgress(size: number, downloaded: number): number {
|
||||
return Math.round((100 * downloaded) / (size || 1))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { FooterComponent } from './footer.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule],
|
||||
declarations: [FooterComponent],
|
||||
exports: [FooterComponent],
|
||||
})
|
||||
export class FooterModule {}
|
||||
@@ -1,64 +0,0 @@
|
||||
<a class="logo" routerLink="/home">
|
||||
<img alt="StartOS" src="assets/img/icon.png" />
|
||||
</a>
|
||||
<ion-item-group class="menu">
|
||||
<ion-menu-toggle *ngFor="let page of pages" auto-hide="false">
|
||||
<ion-item
|
||||
button
|
||||
class="link"
|
||||
routerLinkActive="link_selected"
|
||||
color="transparent"
|
||||
routerDirection="root"
|
||||
lines="none"
|
||||
detail="false"
|
||||
[routerLink]="page.url"
|
||||
>
|
||||
<ion-icon
|
||||
slot="start"
|
||||
class="icon label"
|
||||
routerLinkActive="label_selected"
|
||||
[name]="page.icon"
|
||||
></ion-icon>
|
||||
<ion-label class="label montserrat" routerLinkActive="label_selected">
|
||||
{{ page.title }}
|
||||
</ion-label>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/system' && (warning$ | async)"
|
||||
color="warning"
|
||||
size="small"
|
||||
name="warning"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/system' && (showEOSUpdate$ | async)"
|
||||
color="success"
|
||||
size="small"
|
||||
name="rocket"
|
||||
></ion-icon>
|
||||
<ion-badge
|
||||
*ngIf="page.url === '/updates' && (updateCount$ | async) as updateCount"
|
||||
color="success"
|
||||
>
|
||||
{{ updateCount }}
|
||||
</ion-badge>
|
||||
<ion-badge
|
||||
*ngIf="
|
||||
page.url === '/notifications' &&
|
||||
(notificationCount$ | async) as notificaitonCount
|
||||
"
|
||||
color="danger"
|
||||
>
|
||||
{{ notificaitonCount }}
|
||||
</ion-badge>
|
||||
</ion-item>
|
||||
</ion-menu-toggle>
|
||||
</ion-item-group>
|
||||
<img
|
||||
appSnek
|
||||
class="snek"
|
||||
alt="Play Snake"
|
||||
src="assets/img/icons/snek.png"
|
||||
[appSnekHighScore]="(snekScore$ | async) || 0"
|
||||
/>
|
||||
<ion-footer *ngIf="sidebarOpen$ | async" class="bottom">
|
||||
<connection-bar></connection-bar>
|
||||
</ion-footer>
|
||||
@@ -1,49 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
width: 36%;
|
||||
margin: 0 auto;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
--border-radius: 0;
|
||||
|
||||
:host-context(body[data-theme='Light']) &_selected {
|
||||
--ion-color-base: #333;
|
||||
--ion-color-contrast: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--ion-color-dark-shade);
|
||||
|
||||
&_selected {
|
||||
color: var(--ion-color-dark);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.snek {
|
||||
position: absolute;
|
||||
bottom: 56px;
|
||||
right: 20px;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Inject,
|
||||
} from '@angular/core'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
combineLatest,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
Observable,
|
||||
pairwise,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { Emver, THEME } from '@start9labs/shared'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
templateUrl: 'menu.component.html',
|
||||
styleUrls: ['menu.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MenuComponent {
|
||||
readonly pages = [
|
||||
{
|
||||
title: 'Services',
|
||||
url: '/services',
|
||||
icon: 'grid-outline',
|
||||
},
|
||||
{
|
||||
title: 'Marketplace',
|
||||
url: '/marketplace',
|
||||
icon: 'storefront-outline',
|
||||
},
|
||||
{
|
||||
title: 'Updates',
|
||||
url: '/updates',
|
||||
icon: 'globe-outline',
|
||||
},
|
||||
{
|
||||
title: 'Backups',
|
||||
url: '/backups',
|
||||
icon: 'save-outline',
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
url: '/notifications',
|
||||
icon: 'notifications-outline',
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
url: '/system',
|
||||
icon: 'construct-outline',
|
||||
},
|
||||
]
|
||||
|
||||
readonly notificationCount$ = this.patch.watch$(
|
||||
'server-info',
|
||||
'unreadNotifications',
|
||||
'count',
|
||||
)
|
||||
|
||||
readonly snekScore$ = this.patch.watch$('ui', 'gaming', 'snake', 'high-score')
|
||||
|
||||
readonly showEOSUpdate$ = this.eosService.showUpdate$
|
||||
|
||||
private readonly local$ = this.connectionService.connected$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$('package-data').pipe(first())),
|
||||
switchMap(outer =>
|
||||
this.patch.watch$('package-data').pipe(
|
||||
pairwise(),
|
||||
filter(([prev, curr]) =>
|
||||
Object.values(prev).some(p => {
|
||||
const c = curr[p.manifest.id]
|
||||
return !c || (p['install-progress'] && !c['install-progress'])
|
||||
}),
|
||||
),
|
||||
map(([_, curr]) => curr),
|
||||
startWith(outer),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
readonly updateCount$: Observable<number> = combineLatest([
|
||||
this.marketplaceService.getMarketplace$(true),
|
||||
this.local$,
|
||||
]).pipe(
|
||||
map(([marketplace, local]) =>
|
||||
Object.entries(marketplace).reduce((list, [_, store]) => {
|
||||
store?.packages.forEach(({ manifest: { id, version } }) => {
|
||||
if (this.emver.compare(version, local[id]?.manifest.version) === 1)
|
||||
list.add(id)
|
||||
})
|
||||
return list
|
||||
}, new Set<string>()),
|
||||
),
|
||||
map(list => list.size),
|
||||
)
|
||||
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
readonly warning$ = this.patch
|
||||
.watch$('server-info', 'ntp-synced')
|
||||
.pipe(map(synced => !synced))
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly eosService: EOSService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MenuComponent } from './menu.component'
|
||||
import { SnekModule } from '../snek/snek.module'
|
||||
import { ConnectionBarComponentModule } from '../connection-bar/connection-bar.component.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule,
|
||||
SnekModule,
|
||||
ConnectionBarComponentModule,
|
||||
],
|
||||
declarations: [MenuComponent],
|
||||
exports: [MenuComponent],
|
||||
})
|
||||
export class MenuModule {}
|
||||
@@ -1,82 +0,0 @@
|
||||
<div style="display: none">
|
||||
<!-- Ionicons -->
|
||||
<ion-icon *ngFor="let icon of icons" [name]="icon"></ion-icon>
|
||||
|
||||
<!-- 3rd party components -->
|
||||
<qr-code value="hello"></qr-code>
|
||||
|
||||
<!-- Ionic components -->
|
||||
<ion-accordion></ion-accordion>
|
||||
<ion-accordion-group></ion-accordion-group>
|
||||
<ion-action-sheet></ion-action-sheet>
|
||||
<ion-alert></ion-alert>
|
||||
<ion-avatar></ion-avatar>
|
||||
<ion-back-button></ion-back-button>
|
||||
<ion-badge></ion-badge>
|
||||
<ion-button></ion-button>
|
||||
<ion-buttons></ion-buttons>
|
||||
<ion-card></ion-card>
|
||||
<ion-card-content></ion-card-content>
|
||||
<ion-card-header></ion-card-header>
|
||||
<ion-checkbox></ion-checkbox>
|
||||
<ion-content></ion-content>
|
||||
<ion-footer></ion-footer>
|
||||
<ion-grid></ion-grid>
|
||||
<ion-header></ion-header>
|
||||
<ion-popover></ion-popover>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed"></ion-refresher>
|
||||
<ion-refresher-content pullingContent="lines"></ion-refresher-content>
|
||||
<ion-infinite-scroll></ion-infinite-scroll>
|
||||
<ion-infinite-scroll-content
|
||||
loadingSpinner="lines"
|
||||
></ion-infinite-scroll-content>
|
||||
</ion-content>
|
||||
<ion-input></ion-input>
|
||||
<ion-item></ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item-group></ion-item-group>
|
||||
<ion-label></ion-label>
|
||||
<ion-label style="font-weight: bold"></ion-label>
|
||||
<ion-list></ion-list>
|
||||
<ion-loading></ion-loading>
|
||||
<ion-modal></ion-modal>
|
||||
<ion-menu-button></ion-menu-button>
|
||||
<ion-note></ion-note>
|
||||
<ion-progress-bar></ion-progress-bar>
|
||||
<ion-radio></ion-radio>
|
||||
<ion-row></ion-row>
|
||||
<ion-searchbar></ion-searchbar>
|
||||
<ion-segment></ion-segment>
|
||||
<ion-segment-button></ion-segment-button>
|
||||
<ion-select></ion-select>
|
||||
<ion-select-option></ion-select-option>
|
||||
<ion-spinner name="lines"></ion-spinner>
|
||||
<ion-text></ion-text>
|
||||
<ion-text><strong>load bold font</strong></ion-text>
|
||||
<ion-title></ion-title>
|
||||
<ion-toast></ion-toast>
|
||||
<ion-toggle></ion-toggle>
|
||||
<ion-toolbar></ion-toolbar>
|
||||
|
||||
<!-- images -->
|
||||
<img src="assets/img/icon.png" />
|
||||
<img src="assets/img/community-store.png" />
|
||||
<img src="assets/img/icons/snek.png" />
|
||||
<img src="assets/img/icons/wifi-1.png" />
|
||||
<img src="assets/img/icons/wifi-2.png" />
|
||||
<img src="assets/img/icons/wifi-3.png" />
|
||||
</div>
|
||||
|
||||
<div style="visibility: hidden; height: 0">
|
||||
<!-- fonts -->
|
||||
<p style="font-family: Courier New">a</p>
|
||||
<p style="font-family: Courier New; font-weight: bold">a</p>
|
||||
<p style="font-family: Montserrat">a</p>
|
||||
<p style="font-family: Montserrat; font-weight: bold">a</p>
|
||||
<p style="font-family: Montserrat; font-weight: 100">a</p>
|
||||
<p style="font-family: Open Sans">a</p>
|
||||
<p style="font-family: Open Sans; font-weight: bold">a</p>
|
||||
<p style="font-family: Open Sans; font-weight: 600">a</p>
|
||||
<p style="font-family: Open Sans; font-weight: 100">a</p>
|
||||
</div>
|
||||
@@ -1,106 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
// TODO: Turn into DI token if this is needed someplace else too
|
||||
const ICONS = [
|
||||
'add',
|
||||
'alarm-outline',
|
||||
'alert-outline',
|
||||
'alert-circle-outline',
|
||||
'aperture-outline',
|
||||
'archive-outline',
|
||||
'arrow-back',
|
||||
'arrow-forward',
|
||||
'arrow-up',
|
||||
'brush-outline',
|
||||
'bookmark-outline',
|
||||
'cellular-outline',
|
||||
'chatbubbles-outline',
|
||||
'checkmark',
|
||||
'chevron-down',
|
||||
'chevron-up',
|
||||
'chevron-forward',
|
||||
'close',
|
||||
'close-circle-outline',
|
||||
'cloud-outline',
|
||||
'cloud-done',
|
||||
'cloud-done-outline',
|
||||
'cloud-download-outline',
|
||||
'cloud-offline-outline',
|
||||
'cloud-upload-outline',
|
||||
'code-outline',
|
||||
'color-wand-outline',
|
||||
'construct-outline',
|
||||
'copy-outline',
|
||||
'desktop-outline',
|
||||
'download-outline',
|
||||
'duplicate-outline',
|
||||
'earth-outline',
|
||||
'ellipsis-horizontal',
|
||||
'eye-off-outline',
|
||||
'eye-outline',
|
||||
'file-tray-stacked-outline',
|
||||
'finger-print-outline',
|
||||
'flash-outline',
|
||||
'flask-outline',
|
||||
'flash-off-outline',
|
||||
'folder-open-outline',
|
||||
'globe-outline',
|
||||
'grid-outline',
|
||||
'hammer-outline',
|
||||
'help-circle-outline',
|
||||
'hammer-outline',
|
||||
'information-circle-outline',
|
||||
'key-outline',
|
||||
'list-outline',
|
||||
'log-out-outline',
|
||||
'logo-bitcoin',
|
||||
'mail-outline',
|
||||
'map-outline',
|
||||
'medkit-outline',
|
||||
'notifications-outline',
|
||||
'open-outline',
|
||||
'options-outline',
|
||||
'pencil',
|
||||
'phone-portrait-outline',
|
||||
'play-circle-outline',
|
||||
'play-outline',
|
||||
'power',
|
||||
'pricetag-outline',
|
||||
'pulse',
|
||||
'push-outline',
|
||||
'qr-code-outline',
|
||||
'radio-outline',
|
||||
'receipt-outline',
|
||||
'refresh',
|
||||
'reload',
|
||||
'remove',
|
||||
'remove-circle-outline',
|
||||
'remove-outline',
|
||||
'repeat-outline',
|
||||
'ribbon-outline',
|
||||
'rocket-outline',
|
||||
'save-outline',
|
||||
'server-outline',
|
||||
'settings-outline',
|
||||
'shield-outline',
|
||||
'shuffle-outline',
|
||||
'stop-outline',
|
||||
'stopwatch-outline',
|
||||
'storefront-outline',
|
||||
'swap-vertical',
|
||||
'terminal-outline',
|
||||
'trail-sign-outline',
|
||||
'trash',
|
||||
'trash-outline',
|
||||
'warning-outline',
|
||||
'wifi',
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'section[appPreloader]',
|
||||
templateUrl: 'preloader.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PreloaderComponent {
|
||||
readonly icons = ICONS
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
import { PreloaderComponent } from './preloader.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, QrCodeModule],
|
||||
declarations: [PreloaderComponent],
|
||||
exports: [PreloaderComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class PreloaderModule {}
|
||||
@@ -1,8 +0,0 @@
|
||||
<div class="canvas-center">
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<strong>Score: {{ score }}</strong>
|
||||
<span>High Score: {{ highScore }}</span>
|
||||
<button tuiButton (click)="dismiss()">Save and Quit</button>
|
||||
</footer>
|
||||
@@ -1,14 +0,0 @@
|
||||
.canvas-center {
|
||||
min-height: 50vh;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 32px;
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
HostListener,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
} from '@angular/core'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'snake',
|
||||
templateUrl: './snake.page.html',
|
||||
styleUrls: ['./snake.page.scss'],
|
||||
})
|
||||
export class SnakePage implements AfterViewInit, OnDestroy {
|
||||
highScore = this.dialog.data.highScore
|
||||
|
||||
score = 0
|
||||
|
||||
private readonly speed = 45
|
||||
private readonly width = 40
|
||||
private readonly height = 26
|
||||
private grid = NaN
|
||||
|
||||
private readonly startingLength = 4
|
||||
|
||||
private xDown?: number
|
||||
private yDown?: number
|
||||
private canvas!: HTMLCanvasElement
|
||||
private image!: HTMLImageElement
|
||||
private context!: CanvasRenderingContext2D
|
||||
|
||||
private snake: any
|
||||
private bitcoin: { x: number; y: number } = { x: NaN, y: NaN }
|
||||
|
||||
private moveQueue: String[] = []
|
||||
private destroyed = false
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly dialog: TuiDialogContext<number, { highScore: number }>,
|
||||
) {}
|
||||
|
||||
dismiss() {
|
||||
this.dialog.completeWith(this.highScore)
|
||||
}
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
keyEvent(e: KeyboardEvent) {
|
||||
this.moveQueue.push(e.key)
|
||||
}
|
||||
|
||||
@HostListener('touchstart', ['$event'])
|
||||
touchStart(e: TouchEvent) {
|
||||
this.handleTouchStart(e)
|
||||
}
|
||||
|
||||
@HostListener('touchmove', ['$event'])
|
||||
touchMove(e: TouchEvent) {
|
||||
this.handleTouchMove(e)
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
sizeChange() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroyed = true
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.init()
|
||||
|
||||
this.image = new Image()
|
||||
this.image.onload = () => {
|
||||
requestAnimationFrame(async () => await this.loop())
|
||||
}
|
||||
this.image.src = '../../../../../../assets/img/icons/bitcoin.svg'
|
||||
}
|
||||
|
||||
init() {
|
||||
this.canvas = this.document.querySelector('canvas#game')!
|
||||
this.canvas.style.border = '1px solid #e0e0e0'
|
||||
this.context = this.canvas.getContext('2d')!
|
||||
const container = this.document.querySelector('.canvas-center')!
|
||||
this.grid = Math.min(
|
||||
Math.floor(container.clientWidth / this.width),
|
||||
Math.floor(container.clientHeight / this.height),
|
||||
)
|
||||
this.snake = {
|
||||
x: this.grid * (Math.floor(this.width / 2) - this.startingLength),
|
||||
y: this.grid * Math.floor(this.height / 2),
|
||||
// snake velocity. moves one grid length every frame in either the x or y direction
|
||||
dx: this.grid,
|
||||
dy: 0,
|
||||
// keep track of all grids the snake body occupies
|
||||
cells: [],
|
||||
// length of the snake. grows when eating an bitcoin
|
||||
maxCells: this.startingLength,
|
||||
}
|
||||
this.bitcoin = {
|
||||
x: this.getRandomInt(0, this.width) * this.grid,
|
||||
y: this.getRandomInt(0, this.height) * this.grid,
|
||||
}
|
||||
|
||||
this.canvas.width = this.grid * this.width
|
||||
this.canvas.height = this.grid * this.height
|
||||
this.context.imageSmoothingEnabled = false
|
||||
}
|
||||
|
||||
getTouches(evt: TouchEvent) {
|
||||
return evt.touches
|
||||
}
|
||||
|
||||
handleTouchStart(evt: TouchEvent) {
|
||||
const firstTouch = this.getTouches(evt)[0]
|
||||
this.xDown = firstTouch.clientX
|
||||
this.yDown = firstTouch.clientY
|
||||
}
|
||||
|
||||
handleTouchMove(evt: TouchEvent) {
|
||||
if (!this.xDown || !this.yDown) {
|
||||
return
|
||||
}
|
||||
|
||||
var xUp = evt.touches[0].clientX
|
||||
var yUp = evt.touches[0].clientY
|
||||
|
||||
var xDiff = this.xDown - xUp
|
||||
var yDiff = this.yDown - yUp
|
||||
|
||||
if (Math.abs(xDiff) > Math.abs(yDiff)) {
|
||||
/*most significant*/
|
||||
if (xDiff > 0) {
|
||||
this.moveQueue.push('ArrowLeft')
|
||||
} else {
|
||||
this.moveQueue.push('ArrowRight')
|
||||
}
|
||||
} else {
|
||||
if (yDiff > 0) {
|
||||
this.moveQueue.push('ArrowUp')
|
||||
} else {
|
||||
this.moveQueue.push('ArrowDown')
|
||||
}
|
||||
}
|
||||
/* reset values */
|
||||
this.xDown = undefined
|
||||
this.yDown = undefined
|
||||
}
|
||||
|
||||
// game loop
|
||||
async loop() {
|
||||
if (this.destroyed) return
|
||||
|
||||
await pauseFor(this.speed)
|
||||
|
||||
requestAnimationFrame(async () => await this.loop())
|
||||
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
|
||||
// move snake by its velocity
|
||||
this.snake.x += this.snake.dx
|
||||
this.snake.y += this.snake.dy
|
||||
|
||||
if (this.moveQueue.length) {
|
||||
const move = this.moveQueue.shift()
|
||||
// left arrow key
|
||||
if (move === 'ArrowLeft' && this.snake.dx === 0) {
|
||||
this.snake.dx = -this.grid
|
||||
this.snake.dy = 0
|
||||
}
|
||||
// up arrow key
|
||||
else if (move === 'ArrowUp' && this.snake.dy === 0) {
|
||||
this.snake.dy = -this.grid
|
||||
this.snake.dx = 0
|
||||
}
|
||||
// right arrow key
|
||||
else if (move === 'ArrowRight' && this.snake.dx === 0) {
|
||||
this.snake.dx = this.grid
|
||||
this.snake.dy = 0
|
||||
}
|
||||
// down arrow key
|
||||
else if (move === 'ArrowDown' && this.snake.dy === 0) {
|
||||
this.snake.dy = this.grid
|
||||
this.snake.dx = 0
|
||||
}
|
||||
}
|
||||
|
||||
// edge death
|
||||
if (
|
||||
this.snake.x < 0 ||
|
||||
this.snake.y < 0 ||
|
||||
this.snake.x >= this.canvas.width ||
|
||||
this.snake.y >= this.canvas.height
|
||||
) {
|
||||
this.death()
|
||||
}
|
||||
|
||||
// keep track of where snake has been. front of the array is always the head
|
||||
this.snake.cells.unshift({ x: this.snake.x, y: this.snake.y })
|
||||
|
||||
// remove cells as we move away from them
|
||||
if (this.snake.cells.length > this.snake.maxCells) {
|
||||
this.snake.cells.pop()
|
||||
}
|
||||
|
||||
// draw bitcoin
|
||||
this.context.fillStyle = '#ff4961'
|
||||
this.context.drawImage(
|
||||
this.image,
|
||||
this.bitcoin.x - 1,
|
||||
this.bitcoin.y - 1,
|
||||
this.grid + 2,
|
||||
this.grid + 2,
|
||||
)
|
||||
|
||||
// draw snake one cell at a time
|
||||
this.context.fillStyle = '#2fdf75'
|
||||
|
||||
const firstCell = this.snake.cells[0]
|
||||
|
||||
for (let index = 0; index < this.snake.cells.length; index++) {
|
||||
const cell = this.snake.cells[index]
|
||||
|
||||
// drawing 1 px smaller than the grid creates a grid effect in the snake body so you can see how long it is
|
||||
this.context.fillRect(cell.x, cell.y, this.grid - 1, this.grid - 1)
|
||||
|
||||
// snake ate bitcoin
|
||||
if (cell.x === this.bitcoin.x && cell.y === this.bitcoin.y) {
|
||||
this.score++
|
||||
this.highScore = Math.max(this.score, this.highScore)
|
||||
this.snake.maxCells++
|
||||
|
||||
this.bitcoin.x = this.getRandomInt(0, this.width) * this.grid
|
||||
this.bitcoin.y = this.getRandomInt(0, this.height) * this.grid
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
// check collision with all cells after this one (modified bubble sort)
|
||||
// snake occupies same space as a body part. reset game
|
||||
if (
|
||||
firstCell.x === this.snake.cells[index].x &&
|
||||
firstCell.y === this.snake.cells[index].y
|
||||
) {
|
||||
this.death()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
death() {
|
||||
this.snake.x =
|
||||
this.grid * (Math.floor(this.width / 2) - this.startingLength)
|
||||
this.snake.y = this.grid * Math.floor(this.height / 2)
|
||||
this.snake.cells = []
|
||||
this.snake.maxCells = this.startingLength
|
||||
this.snake.dx = this.grid
|
||||
this.snake.dy = 0
|
||||
|
||||
this.bitcoin.x = this.getRandomInt(0, 25) * this.grid
|
||||
this.bitcoin.y = this.getRandomInt(0, 25) * this.grid
|
||||
this.score = 0
|
||||
}
|
||||
|
||||
getRandomInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Directive, HostListener, Input } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { SnakePage } from './snake.page'
|
||||
|
||||
@Directive({
|
||||
selector: 'img[appSnek]',
|
||||
})
|
||||
export class SnekDirective {
|
||||
@Input()
|
||||
appSnekHighScore = 0
|
||||
|
||||
constructor(
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly embassyApi: ApiService,
|
||||
) {}
|
||||
|
||||
@HostListener('click')
|
||||
async onClick() {
|
||||
this.dialogs
|
||||
.open<number>(new PolymorpheusComponent(SnakePage), {
|
||||
label: 'Snake!',
|
||||
closeable: false,
|
||||
dismissible: false,
|
||||
data: {
|
||||
highScore: this.appSnekHighScore,
|
||||
},
|
||||
})
|
||||
.pipe(filter(score => score > this.appSnekHighScore))
|
||||
.subscribe(async score => {
|
||||
const loader = this.loader.open('Saving high score...').subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.setDbValue<number>(
|
||||
['gaming', 'snake', 'high-score'],
|
||||
score,
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
|
||||
import { SnekDirective } from './snek.directive'
|
||||
import { SnakePage } from './snake.page'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, TuiButtonModule],
|
||||
declarations: [SnekDirective, SnakePage],
|
||||
exports: [SnekDirective, SnakePage],
|
||||
})
|
||||
export class SnekModule {}
|
||||
@@ -1,19 +1,20 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding: 32px;
|
||||
padding: 2rem;
|
||||
overflow: auto;
|
||||
background: var(--tui-base-01);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
font-size: calc(2vw + 14px);
|
||||
padding-bottom: 1.5rem;
|
||||
font-size: calc(2vw + 1rem);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
font-size: calc(1vw + 12px);
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
font-size: calc(1vw + 0.75rem);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -21,8 +22,8 @@
|
||||
display: block;
|
||||
color: var(--tui-success-fill);
|
||||
background: rgb(69, 69, 69);
|
||||
padding: 1px 16px;
|
||||
margin-bottom: 32px;
|
||||
padding: 1px 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning {
|
||||
@@ -31,5 +32,6 @@
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { IntersectionObserverModule } from '@ng-web-apis/intersection-observer'
|
||||
import { MutationObserverModule } from '@ng-web-apis/mutation-observer'
|
||||
import { TuiLoaderModule, TuiScrollbarModule } from '@taiga-ui/core'
|
||||
import { TuiBadgeModule, TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
import { LogsPage } from './logs.page'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
@@ -12,7 +16,17 @@ const ROUTES: Routes = [
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild(ROUTES)],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild(ROUTES),
|
||||
IntersectionObserverModule,
|
||||
MutationObserverModule,
|
||||
NgDompurifyModule,
|
||||
TuiBadgeModule,
|
||||
TuiButtonModule,
|
||||
TuiLoaderModule,
|
||||
TuiScrollbarModule,
|
||||
],
|
||||
declarations: [LogsPage],
|
||||
})
|
||||
export class LogsPageModule {}
|
||||
|
||||
@@ -1,57 +1,23 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Logs</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content
|
||||
[scrollEvents]="true"
|
||||
(ionScrollEnd)="scrollEnd()"
|
||||
class="ion-padding"
|
||||
<a
|
||||
routerLink="../"
|
||||
tuiButton
|
||||
iconLeft="tuiIconChevronLeft"
|
||||
appearance="icon"
|
||||
[style.align-self]="'flex-start'"
|
||||
>
|
||||
<ion-infinite-scroll
|
||||
id="scroller"
|
||||
*ngIf="!loading && needInfinite"
|
||||
position="top"
|
||||
threshold="0"
|
||||
(ionInfinite)="doInfinite($event)"
|
||||
Back
|
||||
</a>
|
||||
<tui-scrollbar childList subtree (waMutationObserver)="restoreScroll()">
|
||||
<section
|
||||
class="top"
|
||||
waIntersectionObserver
|
||||
(waIntersectionObservee)="onTop($event[0].isIntersecting)"
|
||||
>
|
||||
<ion-infinite-scroll-content
|
||||
loadingSpinner="lines"
|
||||
></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
<div id="container">
|
||||
<div id="template" style="white-space: pre-line"></div>
|
||||
</div>
|
||||
|
||||
<div id="bottom-div"></div>
|
||||
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'position': 'fixed',
|
||||
'bottom': '50px',
|
||||
'right': isOnBottom ? '-52px' : '30px',
|
||||
'border-radius': '100%',
|
||||
'transition': 'right 0.25s ease-out'
|
||||
}"
|
||||
>
|
||||
<ion-button
|
||||
style="
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
--padding-start: 0px;
|
||||
--padding-end: 0px;
|
||||
--border-radius: 100%;
|
||||
"
|
||||
color="dark"
|
||||
(click)="scrollToBottom()"
|
||||
strong
|
||||
>
|
||||
<ion-icon name="chevron-down"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
@if (loading) {
|
||||
<tui-loader textContent="Loading logs" />
|
||||
}
|
||||
</section>
|
||||
@for (log of logs; track log) {
|
||||
<pre [innerHTML]="log | dompurify"></pre>
|
||||
}
|
||||
</tui-scrollbar>
|
||||
|
||||
@@ -1,94 +1,79 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { ErrorService, toLocalIsoString } from '@start9labs/shared'
|
||||
import { DiagnosticService } from '../services/diagnostic.service'
|
||||
|
||||
const Convert = require('ansi-to-html')
|
||||
const convert = new Convert({
|
||||
bg: 'transparent',
|
||||
})
|
||||
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
||||
import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer'
|
||||
import { convertAnsi, ErrorService } from '@start9labs/shared'
|
||||
import { TuiScrollbarComponent } from '@taiga-ui/core'
|
||||
import { DiagnosticService } from 'src/app/apps/diagnostic/services/diagnostic.service'
|
||||
|
||||
@Component({
|
||||
selector: 'logs',
|
||||
templateUrl: './logs.page.html',
|
||||
styles: `
|
||||
:host {
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
background: var(--tui-base-01);
|
||||
}
|
||||
`,
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class LogsPage {
|
||||
@ViewChild(IonContent) private content?: IonContent
|
||||
loading = true
|
||||
needInfinite = true
|
||||
export class LogsPage implements OnInit {
|
||||
@ViewChild(TuiScrollbarComponent, { read: ElementRef })
|
||||
private readonly scrollbar?: ElementRef<HTMLElement>
|
||||
private readonly api = inject(DiagnosticService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
startCursor?: string
|
||||
limit = 200
|
||||
isOnBottom = true
|
||||
loading = false
|
||||
logs: string[] = []
|
||||
scrollTop = 0
|
||||
|
||||
constructor(
|
||||
private readonly api: DiagnosticService,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getLogs()
|
||||
this.loading = false
|
||||
ngOnInit() {
|
||||
this.getLogs()
|
||||
}
|
||||
|
||||
scrollEnd() {
|
||||
const bottomDiv = document.getElementById('bottom-div')
|
||||
this.isOnBottom =
|
||||
!!bottomDiv &&
|
||||
bottomDiv.getBoundingClientRect().top - 420 < window.innerHeight
|
||||
onTop(top: boolean) {
|
||||
if (top) this.getLogs()
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.content?.scrollToBottom(500)
|
||||
}
|
||||
restoreScroll() {
|
||||
if (this.loading || !this.scrollbar) return
|
||||
|
||||
async doInfinite(e: any): Promise<void> {
|
||||
await this.getLogs()
|
||||
e.target.complete()
|
||||
const scrollbar = this.scrollbar.nativeElement
|
||||
const offset = scrollbar.querySelector('pre')?.clientHeight || 0
|
||||
|
||||
scrollbar.scrollTop = this.scrollTop + offset
|
||||
}
|
||||
|
||||
private async getLogs() {
|
||||
if (this.loading) return
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const { 'start-cursor': startCursor, entries } = await this.api.getLogs({
|
||||
const response = await this.api.getLogs({
|
||||
cursor: this.startCursor,
|
||||
before: !!this.startCursor,
|
||||
limit: this.limit,
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
if (!entries.length) return
|
||||
if (!response.entries.length) return
|
||||
|
||||
this.startCursor = startCursor
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const newLogs = document.getElementById('template')?.cloneNode(true)
|
||||
|
||||
if (!(newLogs instanceof HTMLElement)) return
|
||||
|
||||
newLogs.innerHTML = entries
|
||||
.map(
|
||||
entry =>
|
||||
`<b>${toLocalIsoString(
|
||||
new Date(entry.timestamp),
|
||||
)}</b> ${convert.toHtml(entry.message)}`,
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
const beforeContainerHeight = container?.scrollHeight || 0
|
||||
container?.prepend(newLogs)
|
||||
const afterContainerHeight = container?.scrollHeight || 0
|
||||
|
||||
// scroll down
|
||||
setTimeout(() => {
|
||||
this.content?.scrollToPoint(
|
||||
0,
|
||||
afterContainerHeight - beforeContainerHeight,
|
||||
)
|
||||
}, 50)
|
||||
|
||||
if (entries.length < this.limit) {
|
||||
this.needInfinite = false
|
||||
}
|
||||
this.startCursor = response['start-cursor']
|
||||
this.logs = [convertAnsi(response.entries), ...this.logs]
|
||||
this.scrollTop = this.scrollbar?.nativeElement.scrollTop || 0
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
InitializingComponent,
|
||||
provideSetupLogsService,
|
||||
@@ -13,8 +13,8 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
template: `
|
||||
<app-initializing
|
||||
class="ion-page"
|
||||
(finished)="navCtrl.navigateForward('/login')"
|
||||
></app-initializing>
|
||||
(finished)="router.navigate(['login'])"
|
||||
/>
|
||||
`,
|
||||
providers: [
|
||||
provideSetupService(ApiService),
|
||||
@@ -23,5 +23,5 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
imports: [InitializingComponent],
|
||||
})
|
||||
export class LoadingPage {
|
||||
readonly navCtrl = inject(NavController)
|
||||
readonly router = inject(Router)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
overflow: visible;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: max(50%, 20rem);
|
||||
width: max(33%, 20rem);
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { getPlatforms } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { Router } from '@angular/router'
|
||||
@@ -45,7 +44,8 @@ export class LoginPage {
|
||||
}
|
||||
await this.api.login({
|
||||
password: this.password,
|
||||
metadata: { platforms: getPlatforms() },
|
||||
// TODO: get platforms metadata
|
||||
metadata: { platforms: [] },
|
||||
})
|
||||
|
||||
this.password = ''
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { Subject } from 'rxjs'
|
||||
import { HeaderMenuComponent } from './menu.component'
|
||||
import { HeaderNotificationsComponent } from './notifications.component'
|
||||
import { SidebarDirective } from '../../../../app/sidebar-host.component'
|
||||
import { SidebarDirective } from 'src/app/common/sidebar-host.component'
|
||||
import { NotificationService } from '../../services/notification.service'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TuiTitleModule,
|
||||
} from '@taiga-ui/experimental'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { QRComponent } from 'src/app/common/qr/qr.component'
|
||||
import { QRComponent } from 'src/app/common/qr.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
||||
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { InstallProgressPipeModule } from 'src/app/common/install-progress/install-progress.module'
|
||||
import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'service-status',
|
||||
@@ -18,7 +18,7 @@ import { InstallProgressPipeModule } from 'src/app/common/install-progress/insta
|
||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||
</strong>
|
||||
<ng-template #installing>
|
||||
<strong *ngIf="installProgress | installProgressDisplay as progress">
|
||||
<strong *ngIf="installProgress | installProgress as progress">
|
||||
Installing
|
||||
<span class="loading-dots"></span>
|
||||
{{ progress }}
|
||||
@@ -36,7 +36,7 @@ import { InstallProgressPipeModule } from 'src/app/common/install-progress/insta
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, InstallProgressPipeModule],
|
||||
imports: [CommonModule, InstallProgressPipe],
|
||||
})
|
||||
export class ServiceStatusComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService, SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiForModule } from '@taiga-ui/cdk'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { SkeletonListComponentModule } from 'src/app/common/skeleton-list/skeleton-list.component.module'
|
||||
import { ServiceCredentialComponent } from '../components/credential.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<skeleton-list *ngIf="loading$ | async; else loaded"></skeleton-list>
|
||||
<ng-template #loaded>
|
||||
<service-credential
|
||||
*ngFor="let cred of credentials | keyvalue : asIsOrder; empty: blank"
|
||||
[label]="cred.key"
|
||||
[value]="cred.value"
|
||||
></service-credential>
|
||||
</ng-template>
|
||||
<ng-template #blank>No credentials</ng-template>
|
||||
<button tuiButton icon="tuiIconRefreshCwLarge" (click)="refresh()">
|
||||
@if (loading$ | async) {
|
||||
<tui-loader />
|
||||
} @else {
|
||||
@for (cred of credentials | keyvalue: asIsOrder; track cred) {
|
||||
<service-credential [label]="cred.key" [value]="cred.value" />
|
||||
} @empty {
|
||||
No credentials
|
||||
}
|
||||
}
|
||||
<button tuiButton iconLeft="tuiIconRefreshCwLarge" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
`,
|
||||
@@ -36,11 +35,9 @@ import { ServiceCredentialComponent } from '../components/credential.component'
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiForModule,
|
||||
TuiButtonModule,
|
||||
SharedPipesModule,
|
||||
SkeletonListComponentModule,
|
||||
ServiceCredentialComponent,
|
||||
TuiLoaderModule,
|
||||
],
|
||||
})
|
||||
export class ServiceCredentialsModal {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { InstallProgress } from '../../services/patch-db/data-model'
|
||||
import { packageLoadingProgress } from '../../util/package-loading-progress'
|
||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
||||
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
|
||||
|
||||
@Pipe({
|
||||
name: 'installProgressDisplay',
|
||||
standalone: true,
|
||||
name: 'installProgress',
|
||||
})
|
||||
export class InstallProgressDisplayPipe implements PipeTransform {
|
||||
export class InstallProgressPipe implements PipeTransform {
|
||||
transform(installProgress?: InstallProgress): string {
|
||||
const totalProgress =
|
||||
packageLoadingProgress(installProgress)?.totalProgress || 0
|
||||
@@ -56,11 +56,12 @@ export class ToMenuPipe implements PipeTransform {
|
||||
name: 'Credentials',
|
||||
description: `Password, keys, or other credentials of interest`,
|
||||
action: () =>
|
||||
this.showDialog(
|
||||
`${manifest.title} credentials`,
|
||||
manifest.id,
|
||||
ServiceCredentialsModal,
|
||||
),
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(ServiceCredentialsModal), {
|
||||
label: `${manifest.title} credentials`,
|
||||
data: manifest.id,
|
||||
})
|
||||
.subscribe(),
|
||||
},
|
||||
{
|
||||
icon: 'tuiIconZapLarge',
|
||||
@@ -116,16 +117,6 @@ export class ToMenuPipe implements PipeTransform {
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private showDialog(label: string, data: any, modal: Type<any>) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(modal), {
|
||||
size: 'l',
|
||||
label,
|
||||
data,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private openConfig({ title, id }: Manifest) {
|
||||
this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
||||
label: `${title} configuration`,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { IpInfo } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'primaryIp',
|
||||
})
|
||||
export class PrimaryIpPipe implements PipeTransform {
|
||||
transform(ipInfo: IpInfo): string {
|
||||
return Object.values(ipInfo)
|
||||
.filter(iface => iface.ipv4)
|
||||
.sort((a, b) => (a.wireless ? -1 : 1))[0].ipv4!
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { TuiTextfieldControllerModule } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { PrimaryIpPipeModule } from 'src/app/common/primary-ip/primary-ip.module'
|
||||
import { RouterInfoComponent } from './info.component'
|
||||
import { PrimaryIpPipe } from './primary-ip.pipe'
|
||||
import { RouterPortComponent } from './table.component'
|
||||
|
||||
@Component({
|
||||
@@ -58,10 +58,10 @@ import { RouterPortComponent } from './table.component'
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
PrimaryIpPipeModule,
|
||||
RouterInfoComponent,
|
||||
RouterPortComponent,
|
||||
TuiTextfieldControllerModule,
|
||||
PrimaryIpPipe,
|
||||
],
|
||||
})
|
||||
export class SettingsRouterComponent {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AbstractMarketplaceService,
|
||||
StoreIconComponentModule,
|
||||
} from '@start9labs/marketplace'
|
||||
import { TuiForModule } from '@taiga-ui/cdk'
|
||||
import { TuiAvatarModule, TuiCellModule } from '@taiga-ui/experimental'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest } from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
@@ -12,66 +12,64 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { FilterUpdatesPipe } from './pipes/filter-updates.pipe'
|
||||
import { UpdatesItemComponent } from './components/item.component'
|
||||
import { SkeletonListComponent } from '../../../components/skeleton-list.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<section *ngFor="let host of data.hosts">
|
||||
@if (data$ | async; as data) {
|
||||
@for (host of data.hosts; track host) {
|
||||
<h3 class="g-title">
|
||||
<store-icon
|
||||
[url]="host.url"
|
||||
[marketplace]="config.marketplace"
|
||||
size="26px"
|
||||
></store-icon>
|
||||
<store-icon [url]="host.url" [marketplace]="mp" size="26px" />
|
||||
{{ host.name }}
|
||||
</h3>
|
||||
<p
|
||||
*ngIf="data.errors.includes(host.url)"
|
||||
[style.color]="'var(--tui-negative)'"
|
||||
>
|
||||
Request Failed
|
||||
</p>
|
||||
<updates-item
|
||||
*ngFor="
|
||||
let pkg of data.mp[host.url]?.packages | filterUpdates: data.local;
|
||||
else: loading;
|
||||
empty: blank
|
||||
"
|
||||
[marketplacePkg]="pkg"
|
||||
[localPkg]="data.local[pkg.manifest.id]"
|
||||
[url]="host.url"
|
||||
></updates-item>
|
||||
</section>
|
||||
</ng-container>
|
||||
<ng-template #blank><p>All services are up to date!</p></ng-template>
|
||||
<ng-template #loading>
|
||||
<skeleton-list [showAvatar]="true"></skeleton-list>
|
||||
</ng-template>
|
||||
@if (data.errors.includes(host.url)) {
|
||||
<p class="g-error">Request Failed</p>
|
||||
}
|
||||
@if (data.mp[host.url]?.packages | filterUpdates: data.local; as pkgs) {
|
||||
@for (pkg of pkgs; track pkg) {
|
||||
<updates-item
|
||||
[marketplacePkg]="pkg"
|
||||
[localPkg]="data.local[pkg.manifest.id]"
|
||||
[url]="host.url"
|
||||
/>
|
||||
} @empty {
|
||||
<p>All services are up to date!</p>
|
||||
}
|
||||
} @else {
|
||||
@for (i of [0, 1, 2]; track i) {
|
||||
<section tuiCell>
|
||||
<tui-avatar class="tui-skeleton" />
|
||||
<span class="tui-skeleton">Loading update item</span>
|
||||
<span class="tui-skeleton" [style.margin-left]="'auto'">
|
||||
Loading actions
|
||||
</span>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiForModule,
|
||||
TuiCellModule,
|
||||
TuiAvatarModule,
|
||||
StoreIconComponentModule,
|
||||
FilterUpdatesPipe,
|
||||
UpdatesItemComponent,
|
||||
SkeletonListComponent,
|
||||
],
|
||||
})
|
||||
export default class UpdatesComponent {
|
||||
private readonly marketplace = inject(
|
||||
private readonly service = inject(
|
||||
AbstractMarketplaceService,
|
||||
) as MarketplaceService
|
||||
|
||||
readonly config = inject(ConfigService)
|
||||
|
||||
readonly mp = inject(ConfigService).marketplace
|
||||
readonly data$ = combineLatest({
|
||||
hosts: this.marketplace.getKnownHosts$(true),
|
||||
mp: this.marketplace.getMarketplace$(),
|
||||
hosts: this.service.getKnownHosts$(true),
|
||||
mp: this.service.getMarketplace$(),
|
||||
local: inject(PatchDB<DataModel>).watch$('package-data'),
|
||||
errors: this.marketplace.getRequestErrors$(),
|
||||
errors: this.service.getRequestErrors$(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { InstallProgressDisplayPipe } from './install-progress.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [InstallProgressDisplayPipe],
|
||||
exports: [InstallProgressDisplayPipe],
|
||||
})
|
||||
export class InstallProgressPipeModule {}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PrimaryIpPipe } from './primary-ip.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [PrimaryIpPipe],
|
||||
exports: [PrimaryIpPipe],
|
||||
})
|
||||
export class PrimaryIpPipeModule {}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { IpInfo } from '../../services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
name: 'primaryIp',
|
||||
})
|
||||
export class PrimaryIpPipe implements PipeTransform {
|
||||
transform(ipInfo: IpInfo): string {
|
||||
return getPrimaryIp(ipInfo)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPrimaryIp(ipInfo: IpInfo): string {
|
||||
return Object.values(ipInfo)
|
||||
.filter(iface => iface.ipv4)
|
||||
.sort((a, b) => (a.wireless ? -1 : 1))[0].ipv4!
|
||||
}
|
||||
16
web/projects/ui/src/app/common/qr.component.ts
Normal file
16
web/projects/ui/src/app/common/qr.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'qr',
|
||||
template: '<qr-code [value]="context.data" size="400"></qr-code>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [QrCodeModule],
|
||||
})
|
||||
export class QRComponent {
|
||||
readonly context =
|
||||
inject<TuiDialogContext<void, string>>(POLYMORPHEUS_CONTEXT)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'qr',
|
||||
template: '<qr-code [value]="context.data" size="400"></qr-code>',
|
||||
})
|
||||
export class QRComponent {
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
readonly context: TuiDialogContext<void, string>,
|
||||
) {}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
import { QRComponent } from './qr.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [QRComponent],
|
||||
imports: [CommonModule, QrCodeModule],
|
||||
exports: [QRComponent],
|
||||
})
|
||||
export class QRComponentModule {}
|
||||
@@ -1,51 +0,0 @@
|
||||
<ng-container *ngIf="groups">
|
||||
<ion-item-group>
|
||||
<ng-container *ngFor="let g of groupsArr">
|
||||
<ion-item-divider>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 120px; height: 16px"
|
||||
></ion-skeleton-text>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let r of rowsArr">
|
||||
<ion-avatar *ngIf="showAvatar" slot="start">
|
||||
<ion-skeleton-text [animated]="true"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 200px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-label>
|
||||
<ion-note slot="end">
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 80px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!groups">
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let r of rowsArr">
|
||||
<ion-avatar *ngIf="showAvatar" slot="start">
|
||||
<ion-skeleton-text [animated]="true"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 200px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-label>
|
||||
<ion-note slot="end">
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 80px; height: 14px"
|
||||
></ion-skeleton-text>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { SkeletonListComponent } from './skeleton-list.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
|
||||
@NgModule({
|
||||
declarations: [SkeletonListComponent],
|
||||
imports: [CommonModule, IonicModule, RouterModule],
|
||||
exports: [SkeletonListComponent],
|
||||
})
|
||||
export class SkeletonListComponentModule {}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'skeleton-list',
|
||||
templateUrl: './skeleton-list.component.html',
|
||||
})
|
||||
export class SkeletonListComponent {
|
||||
@Input() groups = 0
|
||||
@Input() rows = 3
|
||||
@Input() showAvatar = false
|
||||
groupsArr: number[] = []
|
||||
rowsArr: number[] = []
|
||||
|
||||
ngOnInit() {
|
||||
this.groupsArr = Array(this.groups).fill(0)
|
||||
this.rowsArr = Array(this.rows).fill(0)
|
||||
}
|
||||
}
|
||||
32
web/projects/ui/src/app/common/svg-definitions.component.ts
Normal file
32
web/projects/ui/src/app/common/svg-definitions.component.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'svg-definitions',
|
||||
template: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="round-corners">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur" />
|
||||
<feColorMatrix
|
||||
in="blur"
|
||||
type="matrix"
|
||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
|
||||
result="flt_tag"
|
||||
/>
|
||||
<feComposite in="SourceGraphic" in2="flt_tag" operator="atop" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SvgDefinitionsComponent {}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TuiAlertModule } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable, Subject, merge, pairwise, map, endWith } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'notifications-toast',
|
||||
template: `
|
||||
<ng-template
|
||||
[tuiAlert]="!!(visible$ | async)"
|
||||
[tuiAlertOptions]="{ label: 'StartOS' }"
|
||||
(tuiAlertChange)="onDismiss()"
|
||||
>
|
||||
New notifications
|
||||
<a routerLink="/notifications" [queryParams]="{ toast: true }">View</a>
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiAlertModule, RouterLink, AsyncPipe],
|
||||
})
|
||||
export class NotificationsToastComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly visible$: Observable<boolean> = merge(
|
||||
this.dismiss$,
|
||||
inject(PatchDB<DataModel>)
|
||||
.watch$('server-info', 'unreadNotifications', 'count')
|
||||
.pipe(
|
||||
pairwise(),
|
||||
map(([prev, cur]) => cur > prev),
|
||||
endWith(false),
|
||||
),
|
||||
)
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<ng-template
|
||||
[tuiAlert]="!!(visible$ | async)"
|
||||
[tuiAlertOptions]="{ label: 'StartOS' }"
|
||||
(tuiAlertChange)="onDismiss()"
|
||||
>
|
||||
New notifications
|
||||
<a routerLink="/notifications" [queryParams]="{ toast: true }">View</a>
|
||||
</ng-template>
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { Observable, Subject, merge } from 'rxjs'
|
||||
|
||||
import { NotificationsToastService } from './notifications-toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'notifications-toast',
|
||||
templateUrl: './notifications-toast.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NotificationsToastComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly visible$: Observable<boolean> = merge(
|
||||
this.dismiss$,
|
||||
this.notifications$,
|
||||
)
|
||||
|
||||
constructor(
|
||||
@Inject(NotificationsToastService)
|
||||
private readonly notifications$: Observable<boolean>,
|
||||
) {}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { endWith, map, pairwise, Observable } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotificationsToastService extends Observable<boolean> {
|
||||
private readonly stream$ = this.patch
|
||||
.watch$('server-info', 'unreadNotifications', 'count')
|
||||
.pipe(
|
||||
pairwise(),
|
||||
map(([prev, cur]) => cur > prev),
|
||||
endWith(false),
|
||||
)
|
||||
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { SwUpdate } from '@angular/service-worker'
|
||||
import { Emver, LoadingService } from '@start9labs/shared'
|
||||
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
|
||||
import { TuiDialogModule } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { debounceTime, endWith, map, merge, Subject } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'refresh-alert',
|
||||
template: `
|
||||
<!-- <ng-template-->
|
||||
<!-- [tuiDialog]="show$ | async"-->
|
||||
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"-->
|
||||
<!-- (tuiDialogChange)="onDismiss()"-->
|
||||
<!-- >-->
|
||||
<!-- Your user interface is cached and out of date. Hard refresh the page to-->
|
||||
<!-- get the latest UI.-->
|
||||
<!-- <ul>-->
|
||||
<!-- <li>-->
|
||||
<!-- <b>On Mac</b>-->
|
||||
<!-- : cmd + shift + R-->
|
||||
<!-- </li>-->
|
||||
<!-- <li>-->
|
||||
<!-- <b>On Linux/Windows</b>-->
|
||||
<!-- : ctrl + shift + R-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
<!-- <button-->
|
||||
<!-- tuiButton-->
|
||||
<!-- tuiAutoFocus-->
|
||||
<!-- appearance="secondary"-->
|
||||
<!-- style="float: right"-->
|
||||
<!-- (click)="onDismiss()"-->
|
||||
<!-- >-->
|
||||
<!-- Ok-->
|
||||
<!-- </button>-->
|
||||
<!-- </ng-template>-->
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiDialogModule, AsyncPipe, TuiButtonModule, TuiAutoFocusModule],
|
||||
})
|
||||
export class RefreshAlertComponent {
|
||||
private readonly updates = inject(SwUpdate)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly emver = inject(Emver)
|
||||
private readonly config = inject(ConfigService)
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly show$ = merge(
|
||||
this.dismiss$,
|
||||
inject(PatchDB<DataModel>)
|
||||
.watch$('server-info', 'version')
|
||||
.pipe(
|
||||
map(version => !!this.emver.compare(this.config.version, version)),
|
||||
endWith(false),
|
||||
),
|
||||
).pipe(debounceTime(0))
|
||||
|
||||
// @TODO use this like we did on 0344
|
||||
onPwa = false
|
||||
|
||||
ngOnInit() {
|
||||
this.onPwa = window.matchMedia('(display-mode: standalone)').matches
|
||||
}
|
||||
|
||||
async pwaReload() {
|
||||
const loader = this.loader.open('Reloading PWA...').subscribe()
|
||||
|
||||
try {
|
||||
// attempt to update to the latest client version available
|
||||
await this.updates.activateUpdate()
|
||||
} catch (e) {
|
||||
console.error('Error activating update from service worker: ', e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
// always reload, as this resolves most out of sync cases
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<!--<ng-template-->
|
||||
<!-- [tuiDialog]="show$ | async"-->
|
||||
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"-->
|
||||
<!-- (tuiDialogChange)="onDismiss()"-->
|
||||
<!-->-->
|
||||
<!-- Your user interface is cached and out of date. Hard refresh the page to get-->
|
||||
<!-- the latest UI.-->
|
||||
<!-- <ul>-->
|
||||
<!-- <li>-->
|
||||
<!-- <b>On Mac</b>-->
|
||||
<!-- : cmd + shift + R-->
|
||||
<!-- </li>-->
|
||||
<!-- <li>-->
|
||||
<!-- <b>On Linux/Windows</b>-->
|
||||
<!-- : ctrl + shift + R-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
<!-- <button-->
|
||||
<!-- tuiButton-->
|
||||
<!-- tuiAutoFocus-->
|
||||
<!-- appearance="secondary"-->
|
||||
<!-- style="float: right"-->
|
||||
<!-- (click)="onDismiss()"-->
|
||||
<!-- >-->
|
||||
<!-- Ok-->
|
||||
<!-- </button>-->
|
||||
<!--</ng-template>-->
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { Observable, Subject, merge, debounceTime } from 'rxjs'
|
||||
|
||||
import { RefreshAlertService } from './refresh-alert.service'
|
||||
import { SwUpdate } from '@angular/service-worker'
|
||||
import { LoadingController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'refresh-alert',
|
||||
templateUrl: './refresh-alert.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RefreshAlertComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly show$ = merge(this.dismiss$, this.refresh$).pipe(debounceTime(0))
|
||||
|
||||
// @TODO use this like we did on 0344
|
||||
onPwa = false
|
||||
|
||||
constructor(
|
||||
@Inject(RefreshAlertService) private readonly refresh$: Observable<boolean>,
|
||||
private readonly updates: SwUpdate,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.onPwa = window.matchMedia('(display-mode: standalone)').matches
|
||||
}
|
||||
|
||||
async pwaReload() {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Reloading PWA...',
|
||||
})
|
||||
await loader.present()
|
||||
try {
|
||||
// attempt to update to the latest client version available
|
||||
await this.updates.activateUpdate()
|
||||
} catch (e) {
|
||||
console.error('Error activating update from service worker: ', e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
// always reload, as this resolves most out of sync cases
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { endWith, map, Observable } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RefreshAlertService extends Observable<boolean> {
|
||||
private readonly stream$ = this.patch.watch$('server-info', 'version').pipe(
|
||||
map(version => !!this.emver.compare(this.config.version, version)),
|
||||
endWith(false),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly emver: Emver,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<notifications-toast></notifications-toast>
|
||||
<refresh-alert></refresh-alert>
|
||||
<update-toast></update-toast>
|
||||
@@ -1,8 +1,21 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { NotificationsToastComponent } from './notifications-toast.component'
|
||||
import { RefreshAlertComponent } from './refresh-alert.component'
|
||||
import { UpdateToastComponent } from './update-toast.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'toast-container',
|
||||
templateUrl: './toast-container.component.html',
|
||||
template: `
|
||||
<notifications-toast />
|
||||
<refresh-alert />
|
||||
<update-toast />
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
NotificationsToastComponent,
|
||||
UpdateToastComponent,
|
||||
RefreshAlertComponent,
|
||||
],
|
||||
})
|
||||
export class ToastContainerComponent {}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
|
||||
import { TuiAlertModule, TuiDialogModule } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
|
||||
import { ToastContainerComponent } from './toast-container.component'
|
||||
import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component'
|
||||
import { RefreshAlertComponent } from './refresh-alert/refresh-alert.component'
|
||||
import { UpdateToastComponent } from './update-toast/update-toast.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
TuiDialogModule,
|
||||
TuiButtonModule,
|
||||
TuiAutoFocusModule,
|
||||
TuiAlertModule,
|
||||
],
|
||||
declarations: [
|
||||
ToastContainerComponent,
|
||||
NotificationsToastComponent,
|
||||
RefreshAlertComponent,
|
||||
UpdateToastComponent,
|
||||
],
|
||||
exports: [ToastContainerComponent],
|
||||
})
|
||||
export class ToastContainerModule {}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiAlertModule } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
endWith,
|
||||
filter,
|
||||
merge,
|
||||
Observable,
|
||||
Subject,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'update-toast',
|
||||
template: `
|
||||
<ng-template
|
||||
[tuiAlert]="!!(visible$ | async)"
|
||||
[tuiAlertOptions]="{
|
||||
label: 'StartOS download complete!',
|
||||
status: 'success',
|
||||
autoClose: false
|
||||
}"
|
||||
(tuiAlertChange)="onDismiss()"
|
||||
>
|
||||
Restart your server for these updates to take effect. It can take several
|
||||
minutes to come back online.
|
||||
<div>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
size="s"
|
||||
style="margin-top: 8px"
|
||||
(click)="restart()"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButtonModule, TuiAlertModule, AsyncPipe],
|
||||
})
|
||||
export class UpdateToastComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly visible$: Observable<boolean> = merge(
|
||||
this.dismiss$,
|
||||
inject(PatchDB<DataModel>)
|
||||
.watch$('server-info', 'status-info', 'updated')
|
||||
.pipe(distinctUntilChanged(), filter(Boolean), endWith(false)),
|
||||
)
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
this.onDismiss()
|
||||
|
||||
const loader = this.loader.open('Restarting...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.restartServer({})
|
||||
} catch (e: any) {
|
||||
await this.errorService.handleError(e)
|
||||
} finally {
|
||||
await loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<ng-template
|
||||
[tuiAlert]="!!(visible$ | async)"
|
||||
[tuiAlertOptions]="{
|
||||
label: 'StartOS download complete!',
|
||||
status: 'success',
|
||||
autoClose: false
|
||||
}"
|
||||
(tuiAlertChange)="onDismiss()"
|
||||
>
|
||||
Restart your server for these updates to take effect. It can take several
|
||||
minutes to come back online.
|
||||
<div>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
size="s"
|
||||
style="margin-top: 8px"
|
||||
(click)="restart()"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { Observable, Subject, merge } from 'rxjs'
|
||||
|
||||
import { UpdateToastService } from './update-toast.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'update-toast',
|
||||
templateUrl: './update-toast.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UpdateToastComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly visible$: Observable<boolean> = merge(this.dismiss$, this.update$)
|
||||
|
||||
constructor(
|
||||
@Inject(UpdateToastService) private readonly update$: Observable<boolean>,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
) {}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
this.onDismiss()
|
||||
|
||||
const loader = this.loader.open('Restarting...').subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
} catch (e: any) {
|
||||
await this.errorService.handleError(e)
|
||||
} finally {
|
||||
await loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { distinctUntilChanged, filter, endWith, Observable } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UpdateToastService extends Observable<boolean> {
|
||||
private readonly stream$ = this.patch
|
||||
.watch$('server-info', 'status-info', 'updated')
|
||||
.pipe(distinctUntilChanged(), filter(Boolean), endWith(false))
|
||||
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<ng-template #content>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
||||
<a
|
||||
*ngIf="externalLink; else internal"
|
||||
[href]="link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
<ng-template #internal>
|
||||
<a [routerLink]="link" [queryParams]="qp">
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
</ng-template>
|
||||
@@ -1,4 +0,0 @@
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'any-link',
|
||||
templateUrl: './any-link.component.html',
|
||||
styleUrls: ['./any-link.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AnyLinkComponent implements OnInit {
|
||||
@Input({ required: true }) link!: string
|
||||
@Input() qp?: Record<string, string>
|
||||
externalLink = false
|
||||
|
||||
ngOnInit() {
|
||||
try {
|
||||
const _ = new URL(this.link)
|
||||
this.externalLink = true
|
||||
} catch {
|
||||
this.externalLink = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<div
|
||||
class="outer-wrapper"
|
||||
#outerWrapper
|
||||
[ngStyle]="{ height: outerHeight, width: outerWidth }"
|
||||
>
|
||||
<div
|
||||
class="inner-wrapper"
|
||||
#innerWrapper
|
||||
[ngStyle]="{ transform: innerTransform }"
|
||||
>
|
||||
<ion-card>
|
||||
<any-link [link]="cardDetails.link" [qp]="cardDetails.qp">
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{ cardDetails.title }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-icon
|
||||
[name]="cardDetails.icon"
|
||||
[style.color]="cardDetails.color"
|
||||
></ion-icon>
|
||||
</ion-card-content>
|
||||
<ion-footer>
|
||||
<p>{{ cardDetails.description }}</p>
|
||||
</ion-footer>
|
||||
</any-link>
|
||||
</ion-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,68 +0,0 @@
|
||||
ion-card {
|
||||
background: rgba(70, 70, 70, 0.31);
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
margin: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
transition: all 350ms ease;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transition-property: transform;
|
||||
transform: scale(1.05);
|
||||
transition-delay: 40ms;
|
||||
}
|
||||
|
||||
ion-card-title {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 0.6rem;
|
||||
font-weight: 600;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
min-height: 8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: calc(90px + 0.4vw);
|
||||
--ionicon-stroke-width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
padding: 0 1rem;
|
||||
font-family: 'Open Sans';
|
||||
font-size: clamp(1rem, calc(12px + 0.5vw), 1.3rem);
|
||||
height: 4.5rem;
|
||||
width: clamp(13rem, 80%, 18rem);
|
||||
margin: 0 auto;
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-md::before {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
ion-footer {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
ion-footer {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-card',
|
||||
templateUrl: './widget-card.component.html',
|
||||
styleUrls: ['./widget-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WidgetCardComponent {
|
||||
@Input({ required: true }) cardDetails!: Card
|
||||
@Input({ required: true }) containerDimensions!: Dimension
|
||||
@ViewChild('outerWrapper') outerWrapper: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@ViewChild('innerWrapper') innerWrapper: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.resize()
|
||||
}
|
||||
maxHeight = 0
|
||||
maxWidth = 0
|
||||
innerTransform = ''
|
||||
outerWidth: any
|
||||
outerHeight: any
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.maxHeight = (<HTMLElement> (
|
||||
this.innerWrapper.nativeElement
|
||||
)).getBoundingClientRect().height
|
||||
this.maxWidth = (<HTMLElement> (
|
||||
this.innerWrapper.nativeElement
|
||||
)).getBoundingClientRect().width
|
||||
this.resize()
|
||||
}
|
||||
|
||||
resize() {
|
||||
const height = this.containerDimensions.height
|
||||
const width = this.containerDimensions.width
|
||||
const isMax = width >= this.maxWidth && height >= this.maxHeight
|
||||
const scale = Math.min(width / this.maxWidth, height / this.maxHeight)
|
||||
this.innerTransform = isMax ? '' : 'scale(' + scale + ')'
|
||||
this.outerWidth = isMax ? '' : this.maxWidth * scale
|
||||
this.outerHeight = isMax ? '' : this.maxHeight * scale
|
||||
}
|
||||
}
|
||||
|
||||
export interface Dimension {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
title: string
|
||||
icon: string
|
||||
color: string
|
||||
description: string
|
||||
link: string
|
||||
qp?: Record<string, string>
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<div #gridContent>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-justify-content-center ion-align-items-center">
|
||||
<ion-col *ngFor="let card of cards" sizeXs="12">
|
||||
<widget-card
|
||||
[cardDetails]="card"
|
||||
[containerDimensions]="containerDimensions"
|
||||
></widget-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { AnyLinkComponent } from './any-link/any-link.component'
|
||||
import { WidgetListComponent } from './widget-list.component'
|
||||
import { WidgetCardComponent } from './widget-card/widget-card.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [WidgetListComponent, WidgetCardComponent, AnyLinkComponent],
|
||||
imports: [CommonModule, IonicModule, RouterModule],
|
||||
exports: [WidgetListComponent],
|
||||
})
|
||||
export class WidgetListComponentModule {}
|
||||
@@ -1,19 +0,0 @@
|
||||
ion-col {
|
||||
max-width: 22rem !important;
|
||||
--ion-grid-column-padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1700px) {
|
||||
div {
|
||||
padding: 0 7%;
|
||||
}
|
||||
ion-col {
|
||||
max-width: 24rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
div {
|
||||
padding: 0 12%;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { Card, Dimension } from './widget-card/widget-card.component'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-list',
|
||||
templateUrl: './widget-list.component.html',
|
||||
styleUrls: ['./widget-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WidgetListComponent {
|
||||
@ViewChild('gridContent')
|
||||
gridContent: ElementRef<HTMLElement> = {} as ElementRef<HTMLElement>
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.setContainerDimensions()
|
||||
}
|
||||
|
||||
containerDimensions: Dimension = {} as Dimension
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.setContainerDimensions()
|
||||
}
|
||||
|
||||
setContainerDimensions() {
|
||||
this.containerDimensions.height = (<HTMLElement> (
|
||||
this.gridContent.nativeElement
|
||||
)).getBoundingClientRect().height
|
||||
this.containerDimensions.width = (<HTMLElement> (
|
||||
this.gridContent.nativeElement
|
||||
)).getBoundingClientRect().width
|
||||
}
|
||||
|
||||
cards: Card[] = [
|
||||
{
|
||||
title: 'Server Info',
|
||||
icon: 'information-circle-outline',
|
||||
color: 'var(--alt-green)',
|
||||
description: 'View information about your server',
|
||||
link: '/system/specs',
|
||||
},
|
||||
{
|
||||
title: 'Browse',
|
||||
icon: 'storefront-outline',
|
||||
color: 'var(--alt-purple)',
|
||||
description: 'Browse for services to install',
|
||||
link: '/marketplace',
|
||||
qp: { back: 'true' },
|
||||
},
|
||||
{
|
||||
title: 'Create Backup',
|
||||
icon: 'duplicate-outline',
|
||||
color: 'var(--alt-blue)',
|
||||
description: 'Back up StartOS and service data',
|
||||
link: '/system/backup',
|
||||
},
|
||||
{
|
||||
title: 'Monitor',
|
||||
icon: 'pulse-outline',
|
||||
color: 'var(--alt-orange)',
|
||||
description: `View your system resource usage`,
|
||||
link: '/system/metrics',
|
||||
},
|
||||
{
|
||||
title: 'User Manual',
|
||||
icon: 'map-outline',
|
||||
color: 'var(--alt-yellow)',
|
||||
description: 'Discover what StartOS can do',
|
||||
link: 'https://docs.start9.com/0.3.5.x/user-manual/index',
|
||||
},
|
||||
{
|
||||
title: 'Contact Support',
|
||||
icon: 'chatbubbles-outline',
|
||||
color: 'var(--alt-red)',
|
||||
description: 'Get help from the Start9 community',
|
||||
link: 'https://start9.com/contact',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import {
|
||||
animate,
|
||||
group,
|
||||
query,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations'
|
||||
export const slideInAnimation = trigger('routeAnimations', [
|
||||
transition('* => *', [
|
||||
query(':enter, :leave', style({ position: 'fixed', width: '100%' }), {
|
||||
optional: true,
|
||||
}),
|
||||
group([
|
||||
query(
|
||||
':enter',
|
||||
[
|
||||
style({ transform: 'translateX(-100%)' }),
|
||||
animate('1s ease-in-out', style({ transform: 'translateX(0%)' })),
|
||||
],
|
||||
{ optional: true },
|
||||
),
|
||||
query(
|
||||
':leave',
|
||||
[
|
||||
style({ transform: 'translateX(0%)' }),
|
||||
animate('1s ease-in-out', style({ transform: 'translateX(100%)' })),
|
||||
],
|
||||
{ optional: true },
|
||||
),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
Reference in New Issue
Block a user