refactor: remove ionic from remaining places (#2565)

This commit is contained in:
Alex Inkin
2024-02-28 00:15:32 +04:00
committed by GitHub
parent 7b41b295b7
commit a5b1b4e103
91 changed files with 585 additions and 2582 deletions

3
web/.gitignore vendored
View File

@@ -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

View File

@@ -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
View File

@@ -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"
}
},

View File

@@ -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",

View File

@@ -1,4 +1,5 @@
:host {
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -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()
}
}
}

View File

@@ -1,9 +0,0 @@
import { NgModule } from '@angular/core'
import { EnterDirective } from './enter.directive'
@NgModule({
declarations: [EnterDirective],
exports: [EnterDirective],
})
export class EnterModule {}

View File

@@ -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'

View File

@@ -1 +0,0 @@
@import '../../../styles/variables';

View File

@@ -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 {}

View File

@@ -1,9 +0,0 @@
import { NgModule } from '@angular/core'
import { DarkThemeComponent } from './dark-theme.component'
@NgModule({
declarations: [DarkThemeComponent],
exports: [DarkThemeComponent],
})
export class DarkThemeModule {}

View File

@@ -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;
}

View File

@@ -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 {}

View File

@@ -1,9 +0,0 @@
import { NgModule } from '@angular/core'
import { LightThemeComponent } from './light-theme.component'
@NgModule({
declarations: [LightThemeComponent],
exports: [LightThemeComponent],
})
export class LightThemeModule {}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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'))
}
}

View File

@@ -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],

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -1,9 +0,0 @@
.connection-toolbar {
padding: 0 24px;
--min-height: 36px;
}
.icon {
font-size: 23px;
padding-right: 12px;
}

View File

@@ -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>,
) {}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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))
}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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,
) {}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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()
}
})
}
}

View File

@@ -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 {}

View File

@@ -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;
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -9,7 +9,7 @@
overflow: visible;
align-items: center;
text-align: center;
width: max(50%, 20rem);
width: max(33%, 20rem);
}
.logo {

View File

@@ -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 = ''

View File

@@ -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({

View File

@@ -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,

View File

@@ -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 })

View File

@@ -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 {

View File

@@ -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

View File

@@ -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`,

View File

@@ -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!
}
}

View File

@@ -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 {

View File

@@ -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$(),
})
}

View File

@@ -1,8 +0,0 @@
import { NgModule } from '@angular/core'
import { InstallProgressDisplayPipe } from './install-progress.pipe'
@NgModule({
declarations: [InstallProgressDisplayPipe],
exports: [InstallProgressDisplayPipe],
})
export class InstallProgressPipeModule {}

View File

@@ -1,8 +0,0 @@
import { NgModule } from '@angular/core'
import { PrimaryIpPipe } from './primary-ip.pipe'
@NgModule({
declarations: [PrimaryIpPipe],
exports: [PrimaryIpPipe],
})
export class PrimaryIpPipeModule {}

View File

@@ -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!
}

View 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)
}

View File

@@ -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>,
) {}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -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)
}
}

View 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 {}

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -1,27 +0,0 @@
<!--<ng-template-->
<!-- [tuiDialog]="show$ | async"-->
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"-->
<!-- (tuiDialogChange)="onDismiss()"-->
<!--&gt;-->
<!-- 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>-->

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -1,3 +0,0 @@
<notifications-toast></notifications-toast>
<refresh-alert></refresh-alert>
<update-toast></update-toast>

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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()
}
}
}

View File

@@ -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>

View File

@@ -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()
}
}
}

View File

@@ -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))
}
}

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
a {
text-decoration: none;
color: unset;
}

View File

@@ -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
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -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%;
}
}

View File

@@ -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',
},
]
}

View File

@@ -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 },
),
]),
]),
])