mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Feat/domains
update FE types and unify sideload page with marketplace show begin popover for UI launch select update node version for github workflows fix type errors eager load more components fix mocks for types recalculate updates bad on pkg uninstall chore: break form-object file structure files for config finish file upload API and implement for config chore: break down form-object by type, part 1 remove NEW from config comment entire setTimeout for new generic form options chore: break down form-object by type, part 2 headers for enums and unions implement select and multiselect for config update union types and camel case for specs implement textarea config value inputspec and required instead of nullable remove subtype from list spec update start-sdk bump start-sdk feat: use Taiga UI for config modal (#2250) * feat: use Taiga UI for config modal * chore: finish remaining changes * chore: address comments * bump sdk version --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> update package lock update to sdk 20 and fix types chore: update Taiga UI and migrate some more forms (#2252) update form to latest sdk validate length for textarea too chore: accommodate new changes to the specs (#2254) * chore: accommodate new changes to the specs * chore: fix error * chore: fix error feat: add input color (#2257) * feat: add input color * patterns will always be there --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> chore: properly type pattern error update to latest sdk Add sans-serif font fallback (#2263) * Add sans-serif font fallback * Update frontend readme start scripts feat: add datetime spec support (#2264) Wifi optional (#2249) * begin work * allow enable and disable wifi * nice styling * done except for popover not dismissing * update wifi.ts * address comments Feat/automated backups (#2142) * initial restructuring * very cool * new structure in place * delete unnecessary T * down the rabbit hole * getting better * dont like it * nice * very nice * sessions select all * nice * backup runs * fix targets and more * small improvements * mostly working * address PR comments * fix error * delete issue with merge * fix checkboxes and add API for deleting backup runs * better styling for checkboxes * small button in ssh kpage too * complete multiple UI launcher * fix actions * present error toast too * fix target forms Add logs window to setup wizard loading screen (#2076) * add logs window to setup wizard loading screen * fix type error * Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> --------- Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> statically type server metrics and use websocket (#2124) Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Feat/external-smtp (#1791) * UI for EOS smtp, missing API layer * implement api * fix errors * switch to external smtp creds * fix things up * fix types * update types for new forms * feat: add new form to emails and marketplace (#2268) * import tuilet module * feat: get rid of old form completely (#2270) * move to builder spec and delete developer menu * update sdk * tiny * getting better * working * done * feat: add step to number config * chore: small fixes * update SDK and step for numbers --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> latest sdk, fix build update SDK for better disabled props feat: implement `disabled`, `immutable` and `generate` (#2280) * feat: implement `disabled`, `immutable` and `generate` * chore: remove unnecessary code * chore: add generate to textarea and implement immutable * no generate for textarea --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> update lockfile refactor: extract loading status to shared library (#2282) * refactor: extract loading status to shared library * chore: remove inline style refactor: break routing down to apps level (#2285) closes #2212 and closes #2214 Feat/credentials (#2290) add credentials and remove properties refactor: break ui up further down (#2292) * refactor: break ui up further down * permit loading even when authed --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> update patchdb for package compatability fixes fix file structure WIP finish rebase mvp complete port forwards mvp looking good cleaner system page move experimental features manual port overrides better info headers for jobs pages refactor: move diagnostic-ui app under ui route (#2306) * refactor: move diagnostic-ui app under ui route * chore: hide navigation * chore: remove ionic from diagnostic * fix navbar showing on login --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> chore: partially remove ionic modals and loaders (#2308) * chore: partially remove ionic modals and loaders * change to snake --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> better session data fetching abstract store icon component to shared marketplace project (#2311) * abstract store icon component to shared marketplace project * better than using a pipe * minor cleanup * chore: fix missing node types in libraries * typo --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: waterplea <alexander@inkin.ru> refactor: continue to get rid of ionic infrastructure (#2325) refactor: finish removing ionic entities: (#2333) * refactor: finish removing ionic entities: ToastController ErrorToastService ModalController AlertController LoadingController * chore: rollback testing code * chore: fix comments * minor form change * chore: fix comments * update clearnet address parts * move around patchDB * chore: fix comments --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> fixup after rebase
This commit is contained in:
committed by
Aiden McClelland
parent
c03778ec8b
commit
38c2c47789
@@ -1,25 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule)
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadChildren: () => import('./pages/logs/logs.module').then( m => m.LogsPageModule)
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
useHash: true,
|
||||
})
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -1,5 +0,0 @@
|
||||
<tui-root>
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
@@ -1,8 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||
import { TuiRootModule } from '@taiga-ui/core'
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { MockApiService } from './services/api/mock-api.service'
|
||||
import { LiveApiService } from './services/api/live-api.service'
|
||||
import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
IonicModule.forRoot({
|
||||
mode: 'md',
|
||||
}),
|
||||
AppRoutingModule,
|
||||
TuiRootModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { HomePage } from './home.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class HomePageRoutingModule {}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { HomePageRoutingModule } from './home-routing.module'
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
HomePageRoutingModule
|
||||
],
|
||||
declarations: [HomePage]
|
||||
})
|
||||
export class HomePageModule {}
|
||||
@@ -1,81 +0,0 @@
|
||||
<ion-content>
|
||||
<div style="padding: 48px">
|
||||
<ng-container *ngIf="!restarted; else refresh">
|
||||
<h1
|
||||
class="ion-text-center"
|
||||
style="padding-bottom: 36px; font-size: calc(2vw + 14px)"
|
||||
>
|
||||
StartOS - Diagnostic Mode
|
||||
</h1>
|
||||
|
||||
<ng-container *ngIf="error">
|
||||
<h2
|
||||
style="
|
||||
padding-bottom: 16px;
|
||||
font-size: calc(1vw + 14px);
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
StartOS launch error:
|
||||
</h2>
|
||||
<div class="code-block">
|
||||
<code>
|
||||
<ion-text color="warning">{{ error.problem }}</ion-text>
|
||||
<span *ngIf="error.details">
|
||||
<br />
|
||||
<br />
|
||||
<ion-text color="warning">{{ error.details }}</ion-text>
|
||||
</span>
|
||||
</code>
|
||||
</div>
|
||||
<ion-button routerLink="logs">View Logs</ion-button>
|
||||
<h2
|
||||
style="
|
||||
padding: 32px 0 16px 0;
|
||||
font-size: calc(1vw + 12px);
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
Possible solutions:
|
||||
</h2>
|
||||
<div class="code-block">
|
||||
<code><ion-text color="success">{{ error.solution }}</ion-text></code>
|
||||
</div>
|
||||
<ion-button (click)="restart()">Restart Server</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-start"
|
||||
*ngIf="error.code === 15 || error.code === 25"
|
||||
(click)="forgetDrive()"
|
||||
>
|
||||
{{ error.code === 15 ? 'Setup Current Drive' : 'Enter Recovery Mode'
|
||||
}}
|
||||
</ion-button>
|
||||
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentAlertSystemRebuild()" color="warning">
|
||||
System Rebuild
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentAlertRepairDisk()" color="danger">
|
||||
Repair Drive
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #refresh>
|
||||
<h1
|
||||
class="ion-text-center"
|
||||
style="padding-bottom: 36px; font-size: calc(2vw + 12px)"
|
||||
>
|
||||
Server is restarting
|
||||
</h1>
|
||||
<h2 style="padding-bottom: 16px; font-size: calc(1vw + 12px)">
|
||||
Wait for the server to restart, then refresh this page.
|
||||
</h2>
|
||||
<ion-button (click)="refreshPage()">Refresh</ion-button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -1,5 +0,0 @@
|
||||
.code-block {
|
||||
background-color: rgb(69, 69, 69);
|
||||
padding: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
}
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>StartOS Diagnostic UI</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/icon/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { AppModule } from './app/app.module'
|
||||
import { environment } from './environments/environment'
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err))
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
import './zone-flags'
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
@@ -1,41 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf');
|
||||
}
|
||||
|
||||
/** Ionic CSS Variables overrides **/
|
||||
:root {
|
||||
--ion-font-family: 'Montserrat', sans-serif;
|
||||
|
||||
--ion-color-primary: #0075e1;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
|
||||
--ion-item-background: #2b2b2b;
|
||||
--ion-toolbar-background: #2b2b2b;
|
||||
--ion-card-background: #2b2b2b;
|
||||
|
||||
--ion-background-color: #282828;
|
||||
--ion-background-color-rgb: 30,30,30;
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
--ion-text-color-rgb: var(--ion-color-dark-rgb);
|
||||
}
|
||||
|
||||
.loader {
|
||||
--spinner-color: var(--ion-color-warning) !important;
|
||||
z-index: 40000 !important;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Prevents Angular change detection from
|
||||
* running with certain Web Component callbacks
|
||||
*/
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(window as any).__Zone_disable_customElements = true
|
||||
@@ -1,9 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<tui-root>
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<tui-root tuiMode="onDark">
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
|
||||
@@ -2,14 +2,23 @@ import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||
import { TuiRootModule } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiRootModule,
|
||||
TuiThemeNightModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { MockApiService } from './services/api/mock-api.service'
|
||||
import { LiveApiService } from './services/api/live-api.service'
|
||||
import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared'
|
||||
import {
|
||||
LoadingModule,
|
||||
RELATIVE_URL,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -26,6 +35,10 @@ const {
|
||||
}),
|
||||
AppRoutingModule,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
LoadingModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, IonicSlides, LoadingController } from '@ionic/angular'
|
||||
import { IonicSlides } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { DiskInfo } from '@start9labs/shared'
|
||||
import { DiskInfo, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
|
||||
@@ -18,9 +21,9 @@ export class HomePage {
|
||||
error = ''
|
||||
|
||||
constructor(
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -55,10 +58,7 @@ export class HomePage {
|
||||
}
|
||||
|
||||
private async install(overwrite: boolean) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Installing StartOS...',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader.open('Installing StartOS...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.install({
|
||||
@@ -69,56 +69,52 @@ export class HomePage {
|
||||
} catch (e: any) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertDanger() {
|
||||
private presentAlertDanger() {
|
||||
const { vendor, model } = this.selectedDisk!
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: `This action will COMPLETELY erase the disk ${
|
||||
vendor || 'Unknown Vendor'
|
||||
} - ${model || 'Unknown Model'} and install StartOS in its place`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `This action will COMPLETELY erase the disk ${
|
||||
vendor || 'Unknown Vendor'
|
||||
} - ${model || 'Unknown Model'} and install StartOS in its place`,
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
this.install(true)
|
||||
},
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-danger-message',
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
this.install(true)
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertReboot() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Install Success',
|
||||
message:
|
||||
this.dialogs
|
||||
.open(
|
||||
'Remove the USB stick and reboot your device to begin using your new Start9 server',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Reboot',
|
||||
handler: () => {
|
||||
this.reboot()
|
||||
},
|
||||
label: 'Install Success',
|
||||
closeable: false,
|
||||
dismissible: false,
|
||||
size: 's',
|
||||
data: { button: 'Reboot' },
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-success-message',
|
||||
})
|
||||
await alert.present()
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
this.reboot()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async reboot() {
|
||||
const loader = await this.loadingCtrl.create()
|
||||
await loader.present()
|
||||
const loader = this.loader.open('').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.reboot()
|
||||
@@ -126,16 +122,16 @@ export class HomePage {
|
||||
} catch (e: any) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertComplete() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Rebooting',
|
||||
message: 'Please wait for StartOS to restart, then refresh this page',
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
private presentAlertComplete() {
|
||||
this.dialogs
|
||||
.open('Please wait for StartOS to restart, then refresh this page', {
|
||||
label: 'Rebooting',
|
||||
size: 's',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "@start9labs/marketplace",
|
||||
"version": "0.3.11",
|
||||
"version": "0.3.12",
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=13.2.0",
|
||||
"@angular/core": ">=13.2.0",
|
||||
"@ionic/angular": ">=6.0.0",
|
||||
"@start9labs/shared": ">=0.3.0",
|
||||
"@taiga-ui/cdk": ">=3.0.0",
|
||||
"@tinkoff/ng-dompurify": ">=4.0.0",
|
||||
"fuse.js": "^6.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<img
|
||||
*ngIf="url | getIcon as icon; else noIcon"
|
||||
*ngIf="icon; else noIcon"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt=""
|
||||
alt="Service Icon"
|
||||
/>
|
||||
<ng-template #noIcon>
|
||||
<ion-icon name="storefront-outline" [style.font-size]="size"></ion-icon>
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { GetIconPipe, StoreIconComponent } from './store-icon.component'
|
||||
import { StoreIconComponent } from './store-icon.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [StoreIconComponent, GetIconPipe],
|
||||
declarations: [StoreIconComponent],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [StoreIconComponent],
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'store-icon',
|
||||
templateUrl: './store-icon.component.html',
|
||||
styleUrls: ['./store-icon.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StoreIconComponent {
|
||||
@Input()
|
||||
url = ''
|
||||
@Input()
|
||||
size?: string
|
||||
@Input()
|
||||
marketplace!: MarketplaceConfig
|
||||
|
||||
get icon() {
|
||||
const { start9, community } = this.marketplace
|
||||
|
||||
if (sameUrl(this.url, start9)) {
|
||||
return 'assets/img/icon_transparent.png'
|
||||
} else if (sameUrl(this.url, community)) {
|
||||
return 'assets/img/community-store.png'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-content class="with-widgets">
|
||||
<ng-container *ngIf="notes$ | async as notes; else loading">
|
||||
<div *ngFor="let note of notes | keyvalue: asIsOrder">
|
||||
<div *ngFor="let note of notes | keyvalue : asIsOrder">
|
||||
<ion-button
|
||||
expand="full"
|
||||
color="light"
|
||||
@@ -20,7 +20,8 @@
|
||||
>
|
||||
<ion-text
|
||||
id="release-notes"
|
||||
[innerHTML]="note.value | markdown"
|
||||
safeLinks
|
||||
[innerHTML]="note.value | markdown | dompurify"
|
||||
></ion-text>
|
||||
</ion-card>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,11 @@ import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiElementModule } from '@taiga-ui/cdk'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
|
||||
import { ReleaseNotesComponent } from './release-notes.component'
|
||||
|
||||
@@ -18,6 +20,8 @@ import { ReleaseNotesComponent } from './release-notes.component'
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
TuiElementModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksModule,
|
||||
],
|
||||
declarations: [ReleaseNotesComponent],
|
||||
exports: [ReleaseNotesComponent],
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div [innerHTML]="pkg.manifest['release-notes'] | markdown"></div>
|
||||
<div
|
||||
safeLinks
|
||||
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
|
||||
></div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-button routerLink="notes" fill="clear" strong>
|
||||
|
||||
@@ -2,7 +2,12 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksModule,
|
||||
} from '@start9labs/shared'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
|
||||
import { AboutComponent } from './about.component'
|
||||
|
||||
@@ -13,6 +18,8 @@ import { AboutComponent } from './about.component'
|
||||
IonicModule,
|
||||
MarkdownPipeModule,
|
||||
EmverPipesModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksModule,
|
||||
],
|
||||
declarations: [AboutComponent],
|
||||
exports: [AboutComponent],
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*ngIf="manifest['git-hash'] as gitHash; else noHash"
|
||||
button
|
||||
detail="false"
|
||||
(click)="copy(gitHash)"
|
||||
(click)="copyService.copy(gitHash)"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Git Hash</h2>
|
||||
@@ -34,14 +34,39 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
<ion-item button detail="false" (click)="presentAlertVersions()">
|
||||
<ion-item button detail="false" (click)="presentAlertVersions(version)">
|
||||
<ion-label>
|
||||
<h2>Other Versions</h2>
|
||||
<p>Click to view other versions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
<ng-template #version let-data="data" let-completeWith="completeWith">
|
||||
<tui-radio-list
|
||||
class="radio"
|
||||
size="l"
|
||||
[items]="data.items"
|
||||
[itemContent]="displayEmver | tuiStringifyContent"
|
||||
[(ngModel)]="data.value"
|
||||
></tui-radio-list>
|
||||
<footer class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(null)"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(data.value)"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</footer>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
<ion-item button detail="false" (click)="presentModalMd('license')">
|
||||
<ion-item button detail="false" (click)="presentModalMd('License')">
|
||||
<ion-label>
|
||||
<h2>License</h2>
|
||||
<p>{{ manifest.license }}</p>
|
||||
@@ -51,7 +76,7 @@
|
||||
<ion-item
|
||||
button
|
||||
detail="false"
|
||||
(click)="presentModalMd('instructions')"
|
||||
(click)="presentModalMd('Instructions')"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Instructions</h2>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.radio {
|
||||
display: block;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -4,25 +4,30 @@ import {
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef,
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import {
|
||||
AlertController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
TuiAlertService,
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
CopyService,
|
||||
copyToClipboard,
|
||||
displayEmver,
|
||||
Emver,
|
||||
MarkdownComponent,
|
||||
} from '@start9labs/shared'
|
||||
import { filter } from 'rxjs'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional',
|
||||
templateUrl: 'additional.component.html',
|
||||
styleUrls: ['additional.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AdditionalComponent {
|
||||
@@ -34,68 +39,46 @@ export class AdditionalComponent {
|
||||
|
||||
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
|
||||
|
||||
readonly displayEmver = displayEmver
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
readonly copyService: CopyService,
|
||||
private readonly alerts: TuiAlertService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly emver: Emver,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
async copy(address: string): Promise<void> {
|
||||
const success = await copyToClipboard(address)
|
||||
const message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
presentAlertVersions(version: TemplateRef<TuiDialogContext>) {
|
||||
this.dialogs
|
||||
.open<string>(version, {
|
||||
label: 'Versions',
|
||||
size: 's',
|
||||
data: {
|
||||
value: this.pkg.manifest.version,
|
||||
items: this.pkg.versions.sort(
|
||||
(a, b) => -1 * (this.emver.compare(a, b) || 0),
|
||||
),
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(version => this.version.emit(version))
|
||||
}
|
||||
|
||||
async presentAlertVersions() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Versions',
|
||||
inputs: this.pkg.versions
|
||||
.sort((a, b) => -1 * (this.emver.compare(a, b) || 0))
|
||||
.map(v => ({
|
||||
name: v, // for CSS
|
||||
type: 'radio',
|
||||
label: displayEmver(v), // appearance on screen
|
||||
value: v, // literal SEM version value
|
||||
checked: this.pkg.manifest.version === v,
|
||||
})),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
presentModalMd(label: string) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(MarkdownComponent), {
|
||||
label,
|
||||
size: 'l',
|
||||
data: {
|
||||
content: this.marketplaceService.fetchStatic$(
|
||||
this.pkg.manifest.id,
|
||||
label.toLowerCase(),
|
||||
this.url,
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: (version: string) => this.version.emit(version),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentModalMd(title: string) {
|
||||
const content = this.marketplaceService.fetchStatic$(
|
||||
this.pkg.manifest.id,
|
||||
title,
|
||||
this.url,
|
||||
)
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { title, content },
|
||||
component: MarkdownComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,24 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { MarkdownModule, ResponsiveColModule } from '@start9labs/shared'
|
||||
|
||||
import { AdditionalComponent } from './additional.component'
|
||||
import {
|
||||
TuiRadioListModule,
|
||||
TuiStringifyContentPipeModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiButtonModule } from '@taiga-ui/core'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, MarkdownModule, ResponsiveColModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
MarkdownModule,
|
||||
ResponsiveColModule,
|
||||
TuiRadioListModule,
|
||||
FormsModule,
|
||||
TuiStringifyContentPipeModule,
|
||||
TuiButtonModule,
|
||||
],
|
||||
declarations: [AdditionalComponent],
|
||||
exports: [AdditionalComponent],
|
||||
})
|
||||
|
||||
@@ -24,6 +24,8 @@ export * from './pages/show/package/package.module'
|
||||
export * from './pipes/filter-packages.pipe'
|
||||
export * from './pipes/mime-type.pipe'
|
||||
|
||||
export * from './components/store-icon/store-icon.component.module'
|
||||
|
||||
export * from './services/marketplace.service'
|
||||
|
||||
export * from './types'
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface Manifest {
|
||||
long: string
|
||||
}
|
||||
assets: {
|
||||
icon: string // ie. icon.png
|
||||
icon: Url // filename
|
||||
}
|
||||
replaces?: string[]
|
||||
'release-notes': string
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
"inlineSources": true
|
||||
},
|
||||
"exclude": ["src/test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<tui-root>
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<tui-root tuiMode="onDark">
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared'
|
||||
export class AppComponent {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly navCtrl: NavController,
|
||||
) {}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class AppComponent {
|
||||
|
||||
await this.navCtrl.navigateForward(route)
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { TuiRootModule } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiAlertModule,
|
||||
tuiButtonOptionsProvider,
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiRootModule,
|
||||
TuiThemeNightModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { MockApiService } from './services/api/mock-api.service'
|
||||
import { LiveApiService } from './services/api/live-api.service'
|
||||
@@ -19,6 +26,7 @@ import { LoadingPageModule } from './pages/loading/loading.module'
|
||||
import { RecoverPageModule } from './pages/recover/recover.module'
|
||||
import { TransferPageModule } from './pages/transfer/transfer.module'
|
||||
import {
|
||||
LoadingModule,
|
||||
provideSetupLogsService,
|
||||
provideSetupService,
|
||||
RELATIVE_URL,
|
||||
@@ -46,10 +54,16 @@ const {
|
||||
RecoverPageModule,
|
||||
TransferPageModule,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
TuiAlertModule,
|
||||
LoadingModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
],
|
||||
providers: [
|
||||
provideSetupService(ApiService),
|
||||
provideSetupLogsService(ApiService),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{
|
||||
provide: ApiService,
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiFieldErrorPipeModule,
|
||||
TuiInputModule,
|
||||
TuiInputPasswordModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { CifsModal } from './cifs-modal.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CifsModal,
|
||||
],
|
||||
declarations: [CifsModal],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
CifsModal,
|
||||
TuiButtonModule,
|
||||
TuiInputModule,
|
||||
TuiErrorModule,
|
||||
ReactiveFormsModule,
|
||||
TuiFieldErrorPipeModule,
|
||||
TuiInputPasswordModule,
|
||||
],
|
||||
exports: [CifsModal],
|
||||
})
|
||||
export class CifsModalModule { }
|
||||
export class CifsModalModule {}
|
||||
|
||||
@@ -1,94 +1,39 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title> Connect Network Folder </ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<tui-input formControlName="hostname">
|
||||
Hostname
|
||||
<input tuiTextfield placeholder="'My Computer' OR 'my-computer.local'" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="hostname"
|
||||
[error]="['required'] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<form (ngSubmit)="submit()" #cifsForm="ngForm">
|
||||
<p>Hostname *</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="hostname"
|
||||
required
|
||||
[(ngModel)]="cifs.hostname"
|
||||
name="hostname"
|
||||
#hostname="ngModel"
|
||||
placeholder="e.g. 'My Computer' OR 'my-computer.local'"
|
||||
pattern="^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<p [hidden]="hostname.valid || hostname.pristine">
|
||||
<ion-text color="danger"
|
||||
>Hostname is required. e.g. 'My Computer' OR
|
||||
'my-computer.local'</ion-text
|
||||
>
|
||||
</p>
|
||||
<tui-input formControlName="path" class="input">
|
||||
Path
|
||||
<input tuiTextfield placeholder="/Desktop/my-folder'" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="path"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
|
||||
<p>Path *</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="path"
|
||||
required
|
||||
[(ngModel)]="cifs.path"
|
||||
name="path"
|
||||
#path="ngModel"
|
||||
placeholder="ex. /Desktop/my-folder'"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<p [hidden]="path.valid || path.pristine">
|
||||
<ion-text color="danger">Path is required</ion-text>
|
||||
</p>
|
||||
<tui-input formControlName="username" class="input">
|
||||
Username
|
||||
<input tuiTextfield placeholder="Enter username" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="username"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
|
||||
<p>Username *</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="username"
|
||||
required
|
||||
[(ngModel)]="cifs.username"
|
||||
name="username"
|
||||
#username="ngModel"
|
||||
placeholder="Enter username"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<p [hidden]="username.valid || username.pristine">
|
||||
<ion-text color="danger">Username is required</ion-text>
|
||||
</p>
|
||||
<tui-input-password formControlName="password" class="input">
|
||||
Password
|
||||
</tui-input-password>
|
||||
|
||||
<p>Password</p>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
id="password"
|
||||
type="password"
|
||||
[(ngModel)]="cifs.password"
|
||||
name="password"
|
||||
#password="ngModel"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<button hidden type="submit"></button>
|
||||
</form>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button
|
||||
class="ion-padding-end"
|
||||
slot="end"
|
||||
color="warning"
|
||||
(click)="cancel()"
|
||||
>
|
||||
<footer class="modal-buttons">
|
||||
<button tuiButton appearance="secondary" type="button" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-end"
|
||||
slot="end"
|
||||
color="primary"
|
||||
strong="true"
|
||||
[disabled]="!cifsForm.form.valid"
|
||||
(click)="submit()"
|
||||
>
|
||||
Verify
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</button>
|
||||
<button tuiButton [disabled]="form.invalid">Verify</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
.item-interactive {
|
||||
--highlight-background: var(--ion-color-dark) !important;
|
||||
.input {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
|
||||
&:hover {
|
||||
transition-property: transform;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.item-has-focus {
|
||||
--background: var(--ion-color-dark-tint) !important;
|
||||
}
|
||||
@@ -1,94 +1,117 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms'
|
||||
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
|
||||
import { LoadingService, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
ApiService,
|
||||
CifsBackupTarget,
|
||||
CifsRecoverySource,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { PASSWORD } from '../password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'cifs-modal',
|
||||
templateUrl: 'cifs-modal.page.html',
|
||||
styleUrls: ['cifs-modal.page.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
useValue: {
|
||||
required: 'This field is required',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CifsModal {
|
||||
cifs = {
|
||||
type: 'cifs' as 'cifs',
|
||||
hostname: '',
|
||||
path: '',
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
readonly form = new FormGroup({
|
||||
hostname: new FormControl('', {
|
||||
validators: [
|
||||
Validators.required,
|
||||
Validators.pattern(/^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$/),
|
||||
],
|
||||
nonNullable: true,
|
||||
}),
|
||||
path: new FormControl('', {
|
||||
validators: [Validators.required],
|
||||
nonNullable: true,
|
||||
}),
|
||||
username: new FormControl('', {
|
||||
validators: [Validators.required],
|
||||
nonNullable: true,
|
||||
}),
|
||||
password: new FormControl(),
|
||||
})
|
||||
|
||||
constructor(
|
||||
private readonly modalController: ModalController,
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<{
|
||||
cifs: CifsRecoverySource
|
||||
recoveryPassword: string
|
||||
}>,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoadingService,
|
||||
) {}
|
||||
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Connecting to shared folder...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader
|
||||
.open('Connecting to shared folder...')
|
||||
.subscribe()
|
||||
|
||||
try {
|
||||
const diskInfo = await this.api.verifyCifs({
|
||||
...this.cifs,
|
||||
password: this.cifs.password
|
||||
? await this.api.encrypt(this.cifs.password)
|
||||
...this.form.getRawValue(),
|
||||
type: 'cifs',
|
||||
password: this.form.value.password
|
||||
? await this.api.encrypt(String(this.form.value.password))
|
||||
: null,
|
||||
})
|
||||
|
||||
await loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
|
||||
this.presentModalPassword(diskInfo)
|
||||
} catch (e) {
|
||||
await loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
this.presentAlertFailed()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalPassword(diskInfo: StartOSDiskInfo): Promise<void> {
|
||||
private presentModalPassword(diskInfo: StartOSDiskInfo) {
|
||||
const target: CifsBackupTarget = {
|
||||
...this.cifs,
|
||||
...this.form.getRawValue(),
|
||||
mountable: true,
|
||||
'embassy-os': diskInfo,
|
||||
}
|
||||
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { target },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.modalController.dismiss(
|
||||
{
|
||||
cifs: this.cifs,
|
||||
recoveryPassword: res.data.password,
|
||||
},
|
||||
'success',
|
||||
)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Unlock Drive',
|
||||
size: 's',
|
||||
data: { target },
|
||||
})
|
||||
.subscribe(recoveryPassword => {
|
||||
this.context.completeWith({
|
||||
cifs: { ...this.form.getRawValue(), type: 'cifs' },
|
||||
recoveryPassword,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertFailed(): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Connection Failed',
|
||||
message:
|
||||
private presentAlertFailed() {
|
||||
this.dialogs
|
||||
.open(
|
||||
'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.',
|
||||
buttons: ['OK'],
|
||||
})
|
||||
alert.present()
|
||||
{
|
||||
label: 'Connection Failed',
|
||||
size: 's',
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core'
|
||||
import { TuiInputPasswordModule } from '@taiga-ui/kit'
|
||||
import { PasswordPage } from './password.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
PasswordPage,
|
||||
],
|
||||
declarations: [PasswordPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
PasswordPage,
|
||||
TuiButtonModule,
|
||||
TuiInputPasswordModule,
|
||||
TuiErrorModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
exports: [PasswordPage],
|
||||
})
|
||||
export class PasswordPageModule { }
|
||||
export class PasswordPageModule {}
|
||||
|
||||
@@ -1,91 +1,35 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ storageDrive ? 'Set Password' : 'Unlock Drive' }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<p *ngIf="!storageDrive else choose">
|
||||
Enter the password that was used to encrypt this drive.
|
||||
</p>
|
||||
<ng-template #choose>
|
||||
<p>
|
||||
Choose a password for your server.
|
||||
<i>Make it good. Write it down.</i>
|
||||
</p>
|
||||
</ng-template>
|
||||
|
||||
<ion-content>
|
||||
<div style="padding: 8px 24px">
|
||||
<p *ngIf="!storageDrive else choose">
|
||||
Enter the password that was used to encrypt this drive.
|
||||
</p>
|
||||
<ng-template #choose>
|
||||
<p>
|
||||
Choose a password for your server.
|
||||
<i>Make it good. Write it down.</i>
|
||||
</p>
|
||||
</ng-template>
|
||||
|
||||
<form (ngSubmit)="storageDrive ? submitPw() : verifyPw()">
|
||||
<ion-item
|
||||
[class]="pwError ? 'error-border' : password && storageDrive ? 'success-border' : ''"
|
||||
>
|
||||
<ion-input
|
||||
#focusInput
|
||||
[(ngModel)]="password"
|
||||
[ngModelOptions]="{'standalone': true}"
|
||||
[type]="!unmasked1 ? 'password' : 'text'"
|
||||
placeholder="Enter Password"
|
||||
(ionChange)="validate()"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button fill="clear" color="light" (click)="unmasked1 = !unmasked1">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked1 ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<p *ngIf="pwError" class="error-message">{{ pwError }}</p>
|
||||
<ng-container *ngIf="storageDrive">
|
||||
<ion-item
|
||||
[class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''"
|
||||
>
|
||||
<ion-input
|
||||
[(ngModel)]="passwordVer"
|
||||
[ngModelOptions]="{'standalone': true}"
|
||||
[type]="!unmasked2 ? 'password' : 'text'"
|
||||
placeholder="Retype Password"
|
||||
(ionChange)="checkVer()"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="unmasked2 = !unmasked2"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked2 ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<p *ngIf="verError" class="error-message">{{ verError }}</p>
|
||||
</ng-container>
|
||||
<input type="submit" style="display: none" />
|
||||
</form>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button
|
||||
class="ion-padding-end"
|
||||
slot="end"
|
||||
color="warning"
|
||||
(click)="cancel()"
|
||||
>
|
||||
<form (ngSubmit)="storageDrive ? submitPw() : verifyPw()">
|
||||
<tui-input-password [formControl]="password">
|
||||
Enter Password
|
||||
<input tuiTextfield maxlength="64" />
|
||||
</tui-input-password>
|
||||
<tui-error [error]="passwordError"></tui-error>
|
||||
<ng-container *ngIf="storageDrive">
|
||||
<tui-input-password style="margin-top: 16px" [formControl]="confirm">
|
||||
Retype Password
|
||||
<input tuiTextfield maxlength="64" />
|
||||
</tui-input-password>
|
||||
<tui-error [error]="confirmError"></tui-error>
|
||||
</ng-container>
|
||||
<footer class="modal-buttons">
|
||||
<button tuiButton appearance="secondary" type="button" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-end"
|
||||
slot="end"
|
||||
strong="true"
|
||||
(click)="storageDrive ? submitPw() : verifyPw()"
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!password.value || !!confirmError || !!passwordError"
|
||||
>
|
||||
{{ storageDrive ? 'Finish' : 'Unlock' }}
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
.item-interactive {
|
||||
--highlight-background: var(--ion-color-dark) !important;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
&:hover {
|
||||
transition-property: transform;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-has-focus {
|
||||
--background: var(--ion-color-dark-tint) !important;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--ion-color-danger) !important;
|
||||
font-size: .9rem !important;
|
||||
margin-left: 36px;
|
||||
margin-top: -16px;
|
||||
}
|
||||
@@ -1,81 +1,77 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, ModalController } from '@ionic/angular'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import {
|
||||
PolymorpheusComponent,
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
|
||||
interface DialogData {
|
||||
target?: CifsBackupTarget | DiskBackupTarget
|
||||
storageDrive?: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-password',
|
||||
templateUrl: 'password.page.html',
|
||||
styleUrls: ['password.page.scss'],
|
||||
})
|
||||
export class PasswordPage {
|
||||
@ViewChild('focusInput') elem?: IonInput
|
||||
@Input() target?: CifsBackupTarget | DiskBackupTarget
|
||||
@Input() storageDrive = false
|
||||
readonly target = this.context.data.target
|
||||
readonly storageDrive = this.context.data.storageDrive
|
||||
readonly password = new FormControl('', { nonNullable: true })
|
||||
readonly confirm = new FormControl('', { nonNullable: true })
|
||||
|
||||
pwError = ''
|
||||
password = ''
|
||||
unmasked1 = false
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<string, DialogData>,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
|
||||
verError = ''
|
||||
passwordVer = ''
|
||||
unmasked2 = false
|
||||
get passwordError(): string | null {
|
||||
if (!this.password.touched || this.target) return null
|
||||
|
||||
constructor(private modalController: ModalController) {}
|
||||
if (!this.storageDrive && !this.target?.['embassy-os'])
|
||||
return 'No recovery target' // unreachable
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.elem?.setFocus(), 400)
|
||||
if (this.password.value.length < 12)
|
||||
return 'Must be 12 characters or greater'
|
||||
|
||||
if (this.password.value.length > 64)
|
||||
return 'Must be less than 65 characters'
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async verifyPw() {
|
||||
if (!this.target || !this.target['embassy-os'])
|
||||
this.pwError = 'No recovery target' // unreachable
|
||||
get confirmError(): string | null {
|
||||
return this.confirm.touched && this.password.value !== this.confirm.value
|
||||
? 'Passwords do not match'
|
||||
: null
|
||||
}
|
||||
|
||||
verifyPw() {
|
||||
try {
|
||||
const passwordHash = this.target!['embassy-os']?.['password-hash'] || ''
|
||||
|
||||
argon2.verify(passwordHash, this.password)
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
argon2.verify(passwordHash, this.password.value)
|
||||
this.context.completeWith(this.password.value)
|
||||
} catch (e) {
|
||||
this.pwError = 'Incorrect password provided'
|
||||
this.errorService.handleError('Incorrect password provided')
|
||||
}
|
||||
}
|
||||
|
||||
async submitPw() {
|
||||
this.validate()
|
||||
if (this.password !== this.passwordVer) {
|
||||
this.verError = '*passwords do not match'
|
||||
}
|
||||
|
||||
if (this.pwError || this.verError) return
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!!this.target) return (this.pwError = '')
|
||||
|
||||
if (this.passwordVer) {
|
||||
this.checkVer()
|
||||
}
|
||||
|
||||
if (this.password.length < 12) {
|
||||
this.pwError = 'Must be 12 characters or greater'
|
||||
} else if (this.password.length > 64) {
|
||||
this.pwError = 'Must be less than 65 characters'
|
||||
} else {
|
||||
this.pwError = ''
|
||||
}
|
||||
}
|
||||
|
||||
checkVer() {
|
||||
this.verError =
|
||||
this.password !== this.passwordVer ? 'Passwords do not match' : ''
|
||||
submitPw() {
|
||||
this.context.completeWith(this.password.value)
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
}
|
||||
|
||||
export const PASSWORD = new PolymorpheusComponent(PasswordPage)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { DiskInfo, ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { DiskInfo, ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from 'src/app/modals/password/password.page'
|
||||
import { PASSWORD, PasswordPage } from 'src/app/modals/password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-attach',
|
||||
@@ -21,10 +18,10 @@ export class AttachPage {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly stateService: StateService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -41,38 +38,34 @@ export class AttachPage {
|
||||
try {
|
||||
this.drives = await this.apiService.getDrives()
|
||||
} catch (e: any) {
|
||||
this.errToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async select(guid: string) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { storageDrive: true },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.attachDrive(guid, res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
select(guid: string) {
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Set Password',
|
||||
size: 's',
|
||||
data: { storageDrive: true },
|
||||
})
|
||||
.subscribe(password => {
|
||||
this.attachDrive(guid, password)
|
||||
})
|
||||
}
|
||||
|
||||
private async attachDrive(guid: string, password: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Connecting to drive...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader.open('Connecting to drive...').subscribe()
|
||||
|
||||
try {
|
||||
await this.stateService.importDrive(guid, password)
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} catch (e: any) {
|
||||
this.errToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
DiskInfo,
|
||||
ErrorService,
|
||||
GuidPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import {
|
||||
ApiService,
|
||||
BackupRecoverySource,
|
||||
DiskRecoverySource,
|
||||
DiskMigrateSource,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
import { PASSWORD, PasswordPage } from '../../modals/password/password.page'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { filter, of, switchMap } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-embassy',
|
||||
@@ -28,11 +31,10 @@ export class EmbassyPage {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly stateService: StateService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly guidPipe: GuidPipe,
|
||||
) {}
|
||||
|
||||
@@ -77,87 +79,71 @@ export class EmbassyPage {
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async chooseDrive(drive: DiskInfo) {
|
||||
if (
|
||||
this.guidPipe.transform(drive) ||
|
||||
!!drive.partitions.find(p => p.used)
|
||||
) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
subHeader: 'Drive contains data!',
|
||||
message: 'All data stored on this drive will be permanently deleted.',
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
// for backup recoveries
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(
|
||||
drive.logicalname,
|
||||
this.stateService.recoveryPassword,
|
||||
)
|
||||
} else {
|
||||
// for migrations and fresh setups
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
chooseDrive(drive: DiskInfo) {
|
||||
of(!this.guidPipe.transform(drive) && !drive.partitions.some(p => p.used))
|
||||
.pipe(
|
||||
switchMap(unused =>
|
||||
unused
|
||||
? of(true)
|
||||
: this.dialogs.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content:
|
||||
'<strong>Drive contains data!</strong><p>All data stored on this drive will be permanently deleted.</p>',
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
// for backup recoveries
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(
|
||||
drive.logicalname,
|
||||
this.stateService.recoveryPassword,
|
||||
)
|
||||
} else {
|
||||
// for migrations and fresh setups
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
// for backup recoveries
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
|
||||
} else {
|
||||
// for migrations and fresh setups
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalPassword(logicalname: string): Promise<void> {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: {
|
||||
storageDrive: true,
|
||||
},
|
||||
})
|
||||
modal.onDidDismiss().then(async ret => {
|
||||
if (!ret.data || !ret.data.password) return
|
||||
this.setupEmbassy(logicalname, ret.data.password)
|
||||
})
|
||||
await modal.present()
|
||||
private presentModalPassword(logicalname: string) {
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Set Password',
|
||||
size: 's',
|
||||
data: { storageDrive: true },
|
||||
})
|
||||
.subscribe(password => {
|
||||
this.setupEmbassy(logicalname, password)
|
||||
})
|
||||
}
|
||||
|
||||
private async setupEmbassy(
|
||||
logicalname: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Connecting to drive...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader.open('Connecting to drive...').subscribe()
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy(logicalname, password)
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { IonicSlides } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
@@ -19,7 +19,7 @@ export class HomePage {
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class HomePage {
|
||||
await this.api.getPubKey()
|
||||
} catch (e: any) {
|
||||
this.error = true
|
||||
this.errToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { LoadingModule } from '@start9labs/shared'
|
||||
import { InitializingModule } from '@start9labs/shared'
|
||||
import { LoadingPage } from './loading.page'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -11,7 +11,7 @@ const routes: Routes = [
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [LoadingModule, RouterModule.forChild(routes)],
|
||||
imports: [InitializingModule, RouterModule.forChild(routes)],
|
||||
declarations: [LoadingPage],
|
||||
})
|
||||
export class LoadingPageModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<app-loading
|
||||
<app-initializing
|
||||
class="ion-page"
|
||||
[setupType]="stateService.setupType"
|
||||
(finished)="navCtrl.navigateForward('/success')"
|
||||
></app-loading>
|
||||
></app-initializing>
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, NavController } from '@ionic/angular'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
|
||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
CifsRecoverySource,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
import { PASSWORD } from '../../modals/password/password.page'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
|
||||
@Component({
|
||||
selector: 'app-recover',
|
||||
@@ -18,9 +25,8 @@ export class RecoverPage {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
@@ -62,34 +68,28 @@ export class RecoverPage {
|
||||
})
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalCifs(): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: CifsModal,
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
const { hostname, path, username, password } = res.data.cifs
|
||||
presentModalCifs() {
|
||||
this.dialogs
|
||||
.open<{ cifs: CifsRecoverySource; recoveryPassword: string }>(
|
||||
new PolymorpheusComponent(CifsModal),
|
||||
{
|
||||
label: 'Connect Network Folder',
|
||||
},
|
||||
)
|
||||
.subscribe(({ cifs, recoveryPassword }) => {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'backup',
|
||||
target: {
|
||||
type: 'cifs',
|
||||
hostname,
|
||||
path,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
target: cifs,
|
||||
}
|
||||
this.stateService.recoveryPassword = res.data.recoveryPassword
|
||||
this.stateService.recoveryPassword = recoveryPassword
|
||||
this.navCtrl.navigateForward('/storage')
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
})
|
||||
}
|
||||
|
||||
async select(target: DiskBackupTarget) {
|
||||
@@ -97,17 +97,16 @@ export class RecoverPage {
|
||||
|
||||
if (!logicalname) return
|
||||
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data?.password) {
|
||||
this.selectRecoverySource(logicalname, res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Unlock Drive',
|
||||
size: 's',
|
||||
data: { target },
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(password => {
|
||||
this.selectRecoverySource(logicalname, password)
|
||||
})
|
||||
}
|
||||
|
||||
private async selectRecoverySource(logicalname: string, password?: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Component, ElementRef, Inject, NgZone, ViewChild } from '@angular/core'
|
||||
import { DownloadHTMLService, ErrorToastService } from '@start9labs/shared'
|
||||
import { DownloadHTMLService, ErrorService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@@ -12,7 +12,8 @@ import { StateService } from 'src/app/services/state.service'
|
||||
})
|
||||
export class SuccessPage {
|
||||
@ViewChild('canvas', { static: true })
|
||||
private canvas: ElementRef<HTMLCanvasElement> = {} as ElementRef<HTMLCanvasElement>
|
||||
private canvas: ElementRef<HTMLCanvasElement> =
|
||||
{} as ElementRef<HTMLCanvasElement>
|
||||
private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D
|
||||
|
||||
torAddress?: string
|
||||
@@ -28,7 +29,7 @@ export class SuccessPage {
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly errCtrl: ErrorToastService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly stateService: StateService,
|
||||
private readonly api: ApiService,
|
||||
private readonly downloadHtml: DownloadHTMLService,
|
||||
@@ -55,7 +56,7 @@ export class SuccessPage {
|
||||
await this.api.exit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
await this.errCtrl.present(e)
|
||||
await this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, NavController } from '@ionic/angular'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { DiskInfo, ErrorToastService } from '@start9labs/shared'
|
||||
import { DiskInfo, ErrorService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-transfer',
|
||||
@@ -16,8 +19,8 @@ export class TransferPage {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
@@ -35,34 +38,31 @@ export class TransferPage {
|
||||
try {
|
||||
this.drives = await this.apiService.getDrives()
|
||||
} catch (e: any) {
|
||||
this.errToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async select(guid: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message:
|
||||
'After transferring data from this drive, <b>do not</b> attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.',
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'Cancel',
|
||||
select(guid: string) {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content:
|
||||
'After transferring data from this drive, <b>do not</b> attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.',
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'migrate',
|
||||
guid,
|
||||
}
|
||||
this.navCtrl.navigateForward(`/storage`)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'migrate',
|
||||
guid,
|
||||
}
|
||||
this.navCtrl.navigateForward(`/storage`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -10,6 +10,8 @@
|
||||
"@ng-web-apis/resize-observer": ">=2.0.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@taiga-ui/cdk": ">=3.0.0",
|
||||
"@taiga-ui/core": ">=3.0.0",
|
||||
"@tinkoff/ng-dompurify": ">=4.0.0",
|
||||
"ansi-to-html": "^0.7.2"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Directive, ElementRef, Input } from '@angular/core'
|
||||
import { AlertButton } from '@ionic/angular'
|
||||
|
||||
@Directive({
|
||||
selector: `button[alertButton], a[alertButton]`,
|
||||
})
|
||||
export class AlertButtonDirective implements AlertButton {
|
||||
@Input()
|
||||
icon?: string
|
||||
|
||||
@Input()
|
||||
role?: 'cancel' | 'destructive' | string
|
||||
|
||||
handler = () => {
|
||||
this.elementRef.nativeElement.click()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
|
||||
|
||||
get text(): string {
|
||||
return this.elementRef.nativeElement.textContent?.trim() || ''
|
||||
}
|
||||
|
||||
get cssClass(): string[] {
|
||||
return Array.from(this.elementRef.nativeElement.classList)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Directive, ElementRef, Input } from '@angular/core'
|
||||
import { AlertInput } from '@ionic/angular'
|
||||
|
||||
@Directive({
|
||||
selector: `input[alertInput], textarea[alertInput]`,
|
||||
})
|
||||
export class AlertInputDirective<T> implements AlertInput {
|
||||
@Input()
|
||||
value?: T
|
||||
|
||||
@Input()
|
||||
label?: string
|
||||
|
||||
constructor(private readonly elementRef: ElementRef<HTMLInputElement>) {}
|
||||
|
||||
get checked(): boolean {
|
||||
return this.elementRef.nativeElement.checked
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.elementRef.nativeElement.name
|
||||
}
|
||||
|
||||
get type(): AlertInput['type'] {
|
||||
return this.elementRef.nativeElement.type as AlertInput['type']
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { AlertController, AlertOptions, IonicSafeString } from '@ionic/angular'
|
||||
import { OverlayEventDetail } from '@ionic/core'
|
||||
import { AlertButtonDirective } from './alert-button.directive'
|
||||
import { AlertInputDirective } from './alert-input.directive'
|
||||
|
||||
@Component({
|
||||
selector: 'alert',
|
||||
template: `
|
||||
<div #message><ng-content></ng-content></div>
|
||||
<ng-content select="[alertInput]"></ng-content>
|
||||
<ng-content select="[alertButton]"></ng-content>
|
||||
`,
|
||||
styles: [':host { display: none !important; }'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AlertComponent<T> implements AfterViewInit, OnDestroy {
|
||||
@Output()
|
||||
readonly dismiss = new EventEmitter<OverlayEventDetail<T>>()
|
||||
|
||||
@Input()
|
||||
header = ''
|
||||
|
||||
@Input()
|
||||
subHeader = ''
|
||||
|
||||
@Input()
|
||||
backdropDismiss = true
|
||||
|
||||
@ViewChild('message', { static: true })
|
||||
private readonly content?: ElementRef<HTMLElement>
|
||||
|
||||
@ContentChildren(AlertButtonDirective)
|
||||
private readonly buttons: QueryList<AlertButtonDirective> = new QueryList()
|
||||
|
||||
@ContentChildren(AlertInputDirective)
|
||||
private readonly inputs: QueryList<AlertInputDirective<any>> = new QueryList()
|
||||
|
||||
private alert?: HTMLIonAlertElement
|
||||
|
||||
constructor(
|
||||
private readonly elementRef: ElementRef<HTMLElement>,
|
||||
private readonly controller: AlertController,
|
||||
) {}
|
||||
|
||||
get cssClass(): string[] {
|
||||
return Array.from(this.elementRef.nativeElement.classList)
|
||||
}
|
||||
|
||||
get message(): IonicSafeString {
|
||||
return new IonicSafeString(this.content?.nativeElement.innerHTML || '')
|
||||
}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
this.alert = await this.controller.create(this.getOptions())
|
||||
this.alert.onDidDismiss().then(event => {
|
||||
this.dismiss.emit(event)
|
||||
})
|
||||
|
||||
await this.alert.present()
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
await this.alert?.dismiss()
|
||||
}
|
||||
|
||||
private getOptions(): AlertOptions {
|
||||
const {
|
||||
header,
|
||||
subHeader,
|
||||
message,
|
||||
cssClass,
|
||||
buttons,
|
||||
inputs,
|
||||
backdropDismiss,
|
||||
} = this
|
||||
return {
|
||||
header,
|
||||
subHeader,
|
||||
message,
|
||||
cssClass,
|
||||
backdropDismiss,
|
||||
buttons: buttons.toArray(),
|
||||
inputs: inputs.toArray(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { AlertComponent } from './alert.component'
|
||||
import { AlertButtonDirective } from './alert-button.directive'
|
||||
import { AlertInputDirective } from './alert-input.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AlertComponent, AlertButtonDirective, AlertInputDirective],
|
||||
exports: [AlertComponent, AlertButtonDirective, AlertInputDirective],
|
||||
})
|
||||
export class AlertModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
ion-card-title {
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
max-width: 700px;
|
||||
padding-bottom: 20px;
|
||||
margin: auto auto 40px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
margin-top: 24px;
|
||||
height: 280px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 31px;
|
||||
margin-inline: 10px;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, inject, Input, Output } from '@angular/core'
|
||||
import { delay, filter } from 'rxjs'
|
||||
import { SetupService } from '../../services/setup.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-initializing',
|
||||
templateUrl: 'initializing.component.html',
|
||||
styleUrls: ['initializing.component.scss'],
|
||||
})
|
||||
export class InitializingComponent {
|
||||
readonly progress$ = inject(SetupService)
|
||||
|
||||
@Input()
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
|
||||
@Output()
|
||||
readonly finished = this.progress$.pipe(
|
||||
filter(progress => progress === 1),
|
||||
delay(500),
|
||||
)
|
||||
|
||||
getMessage(progress: number | null): string {
|
||||
if (['fresh', 'attach'].includes(this.setupType || '')) {
|
||||
return 'Setting up your server'
|
||||
}
|
||||
|
||||
if (!progress) {
|
||||
return 'Preparing data. This can take a while'
|
||||
} else if (progress < 1) {
|
||||
return 'Copying data'
|
||||
} else {
|
||||
return 'Finalizing'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
|
||||
import { LogsWindowComponent } from './logs-window/logs-window.component'
|
||||
import { InitializingComponent } from './initializing.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, TuiLetModule],
|
||||
declarations: [InitializingComponent, LogsWindowComponent],
|
||||
exports: [InitializingComponent],
|
||||
})
|
||||
export class InitializingModule {}
|
||||
@@ -6,8 +6,8 @@ import { SetupLogsService } from '../../../services/setup-logs.service'
|
||||
import { Log } from '../../../types/api'
|
||||
import { toLocalIsoString } from '../../../util/to-local-iso-string'
|
||||
|
||||
var Convert = require('ansi-to-html')
|
||||
var convert = new Convert({
|
||||
const Convert = require('ansi-to-html')
|
||||
const convert = new Convert({
|
||||
bg: 'transparent',
|
||||
})
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
ion-card-title {
|
||||
font-size: 42px;
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
@include shadow(3);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 80%;
|
||||
margin: auto;
|
||||
padding: 1.5rem;
|
||||
background: var(--tui-elevation-01);
|
||||
border-radius: var(--tui-radius-m);
|
||||
|
||||
--tui-primary: var(--tui-warning-fill);
|
||||
}
|
||||
|
||||
.progress {
|
||||
max-width: 700px;
|
||||
padding-bottom: 20px;
|
||||
margin: auto auto 40px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
margin-top: 24px;
|
||||
height: 280px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 31px;
|
||||
margin-inline: 10px;
|
||||
tui-loader {
|
||||
flex-shrink: 0;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
import { Component, inject, Input, Output } from '@angular/core'
|
||||
import { delay, filter } from 'rxjs'
|
||||
import { SetupService } from '../../services/setup.service'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusContent,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading',
|
||||
templateUrl: 'loading.component.html',
|
||||
styleUrls: ['loading.component.scss'],
|
||||
template: `
|
||||
<tui-loader [textContent]="content"></tui-loader>
|
||||
`,
|
||||
styleUrls: ['./loading.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LoadingComponent {
|
||||
readonly progress$ = inject(SetupService)
|
||||
|
||||
@Input()
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
|
||||
@Output()
|
||||
readonly finished = this.progress$.pipe(
|
||||
filter(progress => progress === 1),
|
||||
delay(500),
|
||||
)
|
||||
|
||||
getMessage(progress: number | null): string {
|
||||
if (['fresh', 'attach'].includes(this.setupType || '')) {
|
||||
return 'Setting up your server'
|
||||
}
|
||||
|
||||
if (!progress) {
|
||||
return 'Preparing data. This can take a while'
|
||||
} else if (progress < 1) {
|
||||
return 'Copying data'
|
||||
} else {
|
||||
return 'Finalizing'
|
||||
}
|
||||
}
|
||||
readonly content: PolymorpheusContent =
|
||||
inject(POLYMORPHEUS_CONTEXT)['content']
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
|
||||
import { LogsWindowComponent } from './logs-window/logs-window.component'
|
||||
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||
import { tuiAsDialog } from '@taiga-ui/cdk'
|
||||
import { LoadingComponent } from './loading.component'
|
||||
import { LoadingService } from './loading.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, TuiLetModule],
|
||||
declarations: [LoadingComponent, LogsWindowComponent],
|
||||
imports: [TuiLoaderModule],
|
||||
declarations: [LoadingComponent],
|
||||
exports: [LoadingComponent],
|
||||
providers: [tuiAsDialog(LoadingService)],
|
||||
})
|
||||
export class LoadingModule {}
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ title | titlecase }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-item *ngIf="error$ | async as error">
|
||||
<ion-label>
|
||||
<ion-text safeLinks color="danger">{{ error }}</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-content>
|
||||
<ion-item *ngIf="error$ | async as error">
|
||||
<ion-label>
|
||||
<ion-text safeLinks color="danger">{{ error }}</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div
|
||||
*ngIf="content$ | async as result; else loading"
|
||||
safeLinks
|
||||
class="content-padding"
|
||||
[innerHTML]="result | markdown | dompurify"
|
||||
></div>
|
||||
|
||||
<div
|
||||
*ngIf="content$ | async as result; else loading"
|
||||
safeLinks
|
||||
class="content-padding"
|
||||
[innerHTML]="result | markdown"
|
||||
></div>
|
||||
|
||||
<ng-template #loading>
|
||||
<text-spinner [text]="'Loading ' + title | titlecase"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
<ng-template #loading>
|
||||
<text-spinner [text]="'Loading ' + title | titlecase"></text-spinner>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
|
||||
import { MarkdownPipeModule } from '../../pipes/markdown/markdown.module'
|
||||
import { SafeLinksModule } from '../../directives/safe-links/safe-links.module'
|
||||
@@ -15,6 +16,7 @@ import { MarkdownComponent } from './markdown.component'
|
||||
MarkdownPipeModule,
|
||||
TextSpinnerComponentModule,
|
||||
SafeLinksModule,
|
||||
NgDompurifyModule,
|
||||
],
|
||||
exports: [MarkdownComponent],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
catchError,
|
||||
ignoreElements,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
of,
|
||||
} from 'rxjs'
|
||||
|
||||
import { getErrorMessage } from '../../services/error-toast.service'
|
||||
import { getErrorMessage } from '../../services/error.service'
|
||||
|
||||
@Component({
|
||||
selector: 'markdown',
|
||||
@@ -18,11 +19,10 @@ import { getErrorMessage } from '../../services/error-toast.service'
|
||||
styleUrls: ['./markdown.component.scss'],
|
||||
})
|
||||
export class MarkdownComponent {
|
||||
@Input() content!: string | Observable<string>
|
||||
@Input() title!: string
|
||||
|
||||
readonly content$ = defer(() =>
|
||||
isObservable(this.content) ? this.content : of(this.content),
|
||||
isObservable(this.context.data.content)
|
||||
? this.context.data.content
|
||||
: of(this.context.data.content),
|
||||
).pipe(share())
|
||||
|
||||
readonly error$ = this.content$.pipe(
|
||||
@@ -30,9 +30,15 @@ export class MarkdownComponent {
|
||||
catchError(e => of(getErrorMessage(e))),
|
||||
)
|
||||
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<
|
||||
void,
|
||||
{ content: string | Observable<string> }
|
||||
>,
|
||||
) {}
|
||||
|
||||
async dismiss() {
|
||||
return this.modalCtrl.dismiss(true)
|
||||
get title(): string {
|
||||
return this.context.label || ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Directive, ElementRef, Input } from '@angular/core'
|
||||
import { ToastButton } from '@ionic/angular'
|
||||
|
||||
@Directive({
|
||||
selector: `button[toastButton], a[toastButton]`,
|
||||
})
|
||||
export class ToastButtonDirective implements ToastButton {
|
||||
@Input()
|
||||
icon?: string
|
||||
|
||||
@Input()
|
||||
side?: 'start' | 'end'
|
||||
|
||||
@Input()
|
||||
role?: 'cancel' | string
|
||||
|
||||
handler = () => {
|
||||
this.elementRef.nativeElement.click()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
|
||||
|
||||
get text(): string | undefined {
|
||||
return this.elementRef.nativeElement.textContent?.trim() || undefined
|
||||
}
|
||||
|
||||
get cssClass(): string[] {
|
||||
return Array.from(this.elementRef.nativeElement.classList)
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { IonicSafeString, ToastController, ToastOptions } from '@ionic/angular'
|
||||
import { OverlayEventDetail } from '@ionic/core'
|
||||
import { ToastButtonDirective } from './toast-button.directive'
|
||||
|
||||
@Component({
|
||||
selector: 'toast',
|
||||
template: `
|
||||
<div #message><ng-content></ng-content></div>
|
||||
<ng-content select="[toastButton]"></ng-content>
|
||||
`,
|
||||
styles: [':host { display: none !important; }'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ToastComponent<T> implements AfterViewInit, OnDestroy {
|
||||
@Output()
|
||||
readonly dismiss = new EventEmitter<OverlayEventDetail<T>>()
|
||||
|
||||
@Input()
|
||||
header = ''
|
||||
|
||||
@Input()
|
||||
duration = 0
|
||||
|
||||
@Input()
|
||||
position: 'top' | 'bottom' | 'middle' = 'bottom'
|
||||
|
||||
@ViewChild('message', { static: true })
|
||||
private readonly content?: ElementRef<HTMLElement>
|
||||
|
||||
@ContentChildren(ToastButtonDirective)
|
||||
private readonly buttons: QueryList<ToastButtonDirective> = new QueryList()
|
||||
|
||||
private toast?: HTMLIonToastElement
|
||||
|
||||
constructor(
|
||||
private readonly elementRef: ElementRef<HTMLElement>,
|
||||
private readonly controller: ToastController,
|
||||
) {}
|
||||
|
||||
get cssClass(): string[] {
|
||||
return Array.from(this.elementRef.nativeElement.classList)
|
||||
}
|
||||
|
||||
get message(): IonicSafeString {
|
||||
return new IonicSafeString(this.content?.nativeElement.innerHTML || '')
|
||||
}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
this.toast = await this.controller.create(this.getOptions())
|
||||
this.toast.onDidDismiss().then(event => {
|
||||
this.dismiss.emit(event)
|
||||
})
|
||||
|
||||
await this.toast.present()
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
await this.toast?.dismiss()
|
||||
}
|
||||
|
||||
private getOptions(): ToastOptions {
|
||||
const { header, message, duration, position, cssClass, buttons } = this
|
||||
return {
|
||||
header,
|
||||
message,
|
||||
duration,
|
||||
position,
|
||||
cssClass,
|
||||
buttons: buttons.toArray(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { ToastComponent } from './toast.component'
|
||||
import { ToastButtonDirective } from './toast-button.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ToastComponent, ToastButtonDirective],
|
||||
exports: [ToastComponent, ToastButtonDirective],
|
||||
})
|
||||
export class ToastModule {}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Directive } from '@angular/core'
|
||||
import {
|
||||
AbstractTuiDialogDirective,
|
||||
AbstractTuiDialogService,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { TuiAlertOptions, TuiAlertService } from '@taiga-ui/core'
|
||||
|
||||
// TODO: Move to Taiga UI
|
||||
@Directive({
|
||||
selector: 'ng-template[tuiAlert]',
|
||||
providers: [
|
||||
{
|
||||
provide: AbstractTuiDialogService,
|
||||
useExisting: TuiAlertService,
|
||||
},
|
||||
],
|
||||
inputs: ['options: tuiAlertOptions', 'open: tuiAlert'],
|
||||
outputs: ['openChange: tuiAlertChange'],
|
||||
})
|
||||
export class TuiAlertDirective<T> extends AbstractTuiDialogDirective<
|
||||
TuiAlertOptions<T>
|
||||
> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { TuiAlertDirective } from './alert.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [TuiAlertDirective],
|
||||
exports: [TuiAlertDirective],
|
||||
})
|
||||
export class TuiAlertModule {}
|
||||
@@ -1,28 +1,11 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { marked } from 'marked'
|
||||
import * as DOMPurify from 'dompurify'
|
||||
|
||||
@Pipe({
|
||||
name: 'markdown',
|
||||
})
|
||||
export class MarkdownPipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (value && value.length > 0) {
|
||||
// convert markdown to html
|
||||
const html = marked(value)
|
||||
// sanitize html
|
||||
const sanitized = DOMPurify.sanitize(html)
|
||||
// parse html to find all links
|
||||
let parser = new DOMParser()
|
||||
const doc = parser.parseFromString(sanitized, 'text/html')
|
||||
const links = Array.from(doc.getElementsByTagName('a'))
|
||||
// add target="_blank" to every link
|
||||
links.forEach(link => {
|
||||
link.setAttribute('target', '_blank')
|
||||
})
|
||||
// return new html string
|
||||
return doc.documentElement.innerHTML
|
||||
}
|
||||
return value
|
||||
return value?.length ? marked(value) : ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,23 +5,21 @@
|
||||
export * from './classes/http-error'
|
||||
export * from './classes/rpc-error'
|
||||
|
||||
export * from './components/alert/alert.component'
|
||||
export * from './components/alert/alert.module'
|
||||
export * from './components/alert/alert-button.directive'
|
||||
export * from './components/alert/alert-input.directive'
|
||||
export * from './components/loading/logs-window/logs-window.component'
|
||||
export * from './components/loading/loading.module'
|
||||
export * from './components/initializing/logs-window/logs-window.component'
|
||||
export * from './components/initializing/initializing.module'
|
||||
export * from './components/initializing/initializing.component'
|
||||
export * from './components/loading/loading.component'
|
||||
export * from './components/loading/loading.module'
|
||||
export * from './components/loading/loading.service'
|
||||
export * from './components/markdown/markdown.component'
|
||||
export * from './components/markdown/markdown.component.module'
|
||||
export * from './components/text-spinner/text-spinner.component'
|
||||
export * from './components/text-spinner/text-spinner.component.module'
|
||||
export * from './components/ticker/ticker.component'
|
||||
export * from './components/ticker/ticker.module'
|
||||
export * from './components/toast/toast.component'
|
||||
export * from './components/toast/toast.module'
|
||||
export * from './components/toast/toast-button.directive'
|
||||
|
||||
export * from './directives/alert/alert.directive'
|
||||
export * from './directives/alert/alert.module'
|
||||
export * from './directives/responsive-col/responsive-col.directive'
|
||||
export * from './directives/responsive-col/responsive-col.module'
|
||||
export * from './directives/responsive-col/responsive-col-viewport.directive'
|
||||
@@ -43,10 +41,10 @@ export * from './pipes/shared/trust.pipe'
|
||||
export * from './pipes/unit-conversion/unit-conversion.module'
|
||||
export * from './pipes/unit-conversion/unit-conversion.pipe'
|
||||
|
||||
export * from './services/copy.service'
|
||||
export * from './services/download-html.service'
|
||||
export * from './services/emver.service'
|
||||
export * from './services/error.service'
|
||||
export * from './services/error-toast.service'
|
||||
export * from './services/http.service'
|
||||
export * from './services/setup.service'
|
||||
export * from './services/setup-logs.service'
|
||||
|
||||
16
frontend/projects/shared/src/services/copy.service.ts
Normal file
16
frontend/projects/shared/src/services/copy.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { TuiAlertService } from '@taiga-ui/core'
|
||||
import { copyToClipboard } from '../util/copy-to-clipboard'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CopyService {
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
|
||||
async copy(text: string) {
|
||||
const success = await copyToClipboard(text)
|
||||
|
||||
this.alerts
|
||||
.open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.')
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { IonicSafeString, ToastController } from '@ionic/angular'
|
||||
import { HttpError } from '../classes/http-error'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorToastService {
|
||||
private toast?: HTMLIonToastElement
|
||||
|
||||
constructor(private readonly toastCtrl: ToastController) {}
|
||||
|
||||
async present(e: HttpError | string, link?: string): Promise<void> {
|
||||
console.error(e)
|
||||
|
||||
if (this.toast) return
|
||||
|
||||
this.toast = await this.toastCtrl.create({
|
||||
header: 'Error',
|
||||
message: getErrorMessage(e, link),
|
||||
duration: 0,
|
||||
position: 'top',
|
||||
cssClass: 'error-toast',
|
||||
buttons: [
|
||||
{
|
||||
side: 'end',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
this.dismiss()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await this.toast.present()
|
||||
}
|
||||
|
||||
async dismiss(): Promise<void> {
|
||||
if (this.toast) {
|
||||
await this.toast.dismiss()
|
||||
this.toast = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(
|
||||
e: HttpError | string,
|
||||
link?: string,
|
||||
): string | IonicSafeString {
|
||||
let message = ''
|
||||
|
||||
if (typeof e === 'string') {
|
||||
message = e
|
||||
} else if (e.code === 0) {
|
||||
message =
|
||||
'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again'
|
||||
} else if (!e.message) {
|
||||
message = 'Unknown Error'
|
||||
link = 'https://docs.start9.com/latest/support/faq'
|
||||
} else {
|
||||
message = e.message
|
||||
}
|
||||
|
||||
if (link) {
|
||||
return new IonicSafeString(
|
||||
`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`,
|
||||
)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export class ErrorService extends ErrorHandler {
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(e: HttpError | string, link?: string): string {
|
||||
export function getErrorMessage(e: HttpError | string, link?: string): string {
|
||||
let message = ''
|
||||
|
||||
if (typeof e === 'string') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, StaticClassProvider, Type } from '@angular/core'
|
||||
import { inject, StaticClassProvider } from '@angular/core'
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
takeWhile,
|
||||
} from 'rxjs'
|
||||
import { SetupStatus } from '../types/api'
|
||||
import { ErrorToastService } from './error-toast.service'
|
||||
import { Constructor } from '../types/constructor'
|
||||
import { ErrorService } from './error.service'
|
||||
|
||||
export function provideSetupService(
|
||||
api: Constructor<ConstructorParameters<typeof SetupService>[0]>,
|
||||
@@ -26,12 +26,12 @@ export function provideSetupService(
|
||||
}
|
||||
|
||||
export class SetupService extends Observable<number> {
|
||||
private readonly errorToastService = inject(ErrorToastService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly progress$ = interval(500).pipe(
|
||||
exhaustMap(() =>
|
||||
from(this.api.getSetupStatus()).pipe(
|
||||
catchError(e => {
|
||||
this.errorToastService.present(e)
|
||||
this.errorService.handleError(e)
|
||||
|
||||
return EMPTY
|
||||
}),
|
||||
|
||||
@@ -4,19 +4,21 @@ export type WorkspaceConfig = {
|
||||
gitHash: string
|
||||
useMocks: boolean
|
||||
enableWidgets: boolean
|
||||
// each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard, diagnostic-ui
|
||||
// each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard
|
||||
ui: {
|
||||
api: {
|
||||
url: string
|
||||
version: string
|
||||
}
|
||||
marketplace: {
|
||||
start9: 'https://registry.start9.com/'
|
||||
community: 'https://community-registry.start9.com/'
|
||||
}
|
||||
marketplace: MarketplaceConfig
|
||||
mocks: {
|
||||
maskAs: 'tor' | 'lan'
|
||||
skipStartupAlerts: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MarketplaceConfig {
|
||||
start9: 'https://registry.start9.com/'
|
||||
community: 'https://community-registry.start9.com/'
|
||||
}
|
||||
|
||||
@@ -160,3 +160,10 @@ a {
|
||||
color: aqua;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
49
frontend/projects/shared/styles/taiga.scss
Normal file
49
frontend/projects/shared/styles/taiga.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
/* stylelint-disable order/order */
|
||||
[tuiWrapper][data-appearance='secondary-warning'] {
|
||||
background: var(--tui-warning-bg);
|
||||
color: var(--tui-warning-fill);
|
||||
|
||||
&[data-mode='onDark'] {
|
||||
background: var(--tui-warning-bg-night);
|
||||
color: var(--tui-warning-fill-night);
|
||||
|
||||
@include wrapper-hover {
|
||||
background: var(--tui-warning-bg-night-hover);
|
||||
}
|
||||
|
||||
@include wrapper-active {
|
||||
background: var(--tui-warning-bg-night-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@include wrapper-hover {
|
||||
background: var(--tui-warning-bg-hover);
|
||||
}
|
||||
|
||||
@include wrapper-active {
|
||||
background: var(--tui-warning-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
tui-dialog {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
tui-opt-group[data-label^='⚠️']:before {
|
||||
color: var(--tui-warning-fill);
|
||||
}
|
||||
|
||||
tui-hint[data-appearance='onDark'] {
|
||||
background: white !important;
|
||||
color: #222 !important;
|
||||
}
|
||||
|
||||
[tuiLink] {
|
||||
color: var(--tui-link) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--tui-link-hover) !important;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<ion-content>
|
||||
<ion-split-pane
|
||||
contentId="main-content"
|
||||
[disabled]="!(authService.isVerified$ | async)"
|
||||
[disabled]="!(navigation$ | async)"
|
||||
(ionSplitPaneVisible)="splitPaneVisible($event)"
|
||||
>
|
||||
<ion-menu
|
||||
@@ -75,7 +75,7 @@
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
<ng-container
|
||||
*ngIf="authService.isVerified$ | async"
|
||||
*ngIf="authService.isVerified$ | async; else defaultTheme"
|
||||
[ngSwitch]="theme$ | async"
|
||||
>
|
||||
<ng-container *ngSwitchCase="'Dark'">
|
||||
@@ -84,3 +84,7 @@
|
||||
</ng-container>
|
||||
<light-theme *ngSwitchCase="'Light'"></light-theme>
|
||||
</ng-container>
|
||||
<ng-template #defaultTheme>
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<dark-theme></dark-theme>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core'
|
||||
import { merge } from 'rxjs'
|
||||
import { Router } from '@angular/router'
|
||||
import { combineLatest, map, merge } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
@@ -15,6 +16,10 @@ import { THEME } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
|
||||
function hasNavigation(url: string): boolean {
|
||||
return !url.startsWith('/loading') && !url.startsWith('/diagnostic')
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
@@ -25,8 +30,13 @@ export class AppComponent implements OnDestroy {
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
readonly theme$ = inject(THEME)
|
||||
readonly navigation$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.router.events.pipe(map(() => hasNavigation(this.router.url))),
|
||||
]).pipe(map(([isVerified, hasNavigation]) => isVerified && hasNavigation))
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly titleService: Title,
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
LightThemeModule,
|
||||
LoadingModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
@@ -32,7 +33,6 @@ import { ConnectionBarComponentModule } from './app/connection-bar/connection-ba
|
||||
import { WidgetsPageModule } from 'src/app/apps/ui/pages/widgets/widgets.module'
|
||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||
import { environment } from '../environments/environment'
|
||||
import { LoadingModule } from './common/loading/loading.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
<img
|
||||
appSnek
|
||||
class="snek"
|
||||
alt="Play Snek"
|
||||
alt="Play Snake"
|
||||
src="assets/img/icons/snek.png"
|
||||
[appSnekHighScore]="snekScore$ | async"
|
||||
[appSnekHighScore]="(snekScore$ | async) || 0"
|
||||
/>
|
||||
<ion-footer *ngIf="sidebarOpen$ | async" class="bottom">
|
||||
<connection-bar></connection-bar>
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
<img src="assets/img/icons/bitcoin.svg" />
|
||||
<img src="assets/img/icon.png" />
|
||||
<img src="assets/img/logo.png" />
|
||||
<img src="assets/img/icon.png" />
|
||||
<img src="assets/img/icon_transparent.png" />
|
||||
<img src="assets/img/community-store.png" />
|
||||
<img src="assets/img/icons/snek.png" />
|
||||
@@ -83,5 +82,4 @@
|
||||
<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>
|
||||
<p style="font-family: Redacted">a</p>
|
||||
</div>
|
||||
|
||||
@@ -82,9 +82,11 @@ const ICONS = [
|
||||
'settings-outline',
|
||||
'shield-checkmark-outline',
|
||||
'stop-outline',
|
||||
'stopwatch-outline',
|
||||
'storefront-outline',
|
||||
'swap-vertical',
|
||||
'terminal-outline',
|
||||
'trail-sign-outline',
|
||||
'trash',
|
||||
'trash-outline',
|
||||
'warning-outline',
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Play Snek!</ion-title>
|
||||
<ion-title slot="end">Score: {{ score }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="canvas-center" style="width: 100%; height: 100%">
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title slot="start">High Score: {{ highScore }}</ion-title>
|
||||
<ion-buttons slot="end" class="ion-padding-end">
|
||||
<ion-button
|
||||
fill="solid"
|
||||
color="primary"
|
||||
(click)="dismiss()"
|
||||
class="enter-click btn-128"
|
||||
>
|
||||
Save and Quit
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
<div class="canvas-center">
|
||||
<canvas id="game"></canvas>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<strong>Score: {{ score }}</strong>
|
||||
<span>High Score: {{ highScore }}</span>
|
||||
<button tuiButton (click)="dismiss()">Save and Quit</button>
|
||||
</footer>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
.canvas-center {
|
||||
min-height: 50vh;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Component, HostListener, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { pauseFor } from '../../../../../shared/src/public-api'
|
||||
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 {
|
||||
@Input()
|
||||
highScore = 0
|
||||
export class SnakePage implements AfterViewInit, OnDestroy {
|
||||
highScore = this.dialog.data.highScore
|
||||
|
||||
score = 0
|
||||
|
||||
@@ -30,11 +37,16 @@ export class SnakePage {
|
||||
private bitcoin: { x: number; y: number } = { x: NaN, y: NaN }
|
||||
|
||||
private moveQueue: String[] = []
|
||||
private destroyed = false
|
||||
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly dialog: TuiDialogContext<number, { highScore: number }>,
|
||||
) {}
|
||||
|
||||
async dismiss() {
|
||||
return this.modalCtrl.dismiss({ highScore: this.highScore })
|
||||
dismiss() {
|
||||
this.dialog.completeWith(this.highScore)
|
||||
}
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
@@ -57,7 +69,11 @@ export class SnakePage {
|
||||
this.init()
|
||||
}
|
||||
|
||||
ionViewDidEnter() {
|
||||
ngOnDestroy() {
|
||||
this.destroyed = true
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.init()
|
||||
|
||||
this.image = new Image()
|
||||
@@ -68,10 +84,10 @@ export class SnakePage {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.canvas = document.querySelector('canvas#game')!
|
||||
this.canvas = this.document.querySelector('canvas#game')!
|
||||
this.canvas.style.border = '1px solid #e0e0e0'
|
||||
this.context = this.canvas.getContext('2d')!
|
||||
const container = document.getElementsByClassName('canvas-center')[0]
|
||||
const container = this.document.querySelector('.canvas-center')!
|
||||
this.grid = Math.min(
|
||||
Math.floor(container.clientWidth / this.width),
|
||||
Math.floor(container.clientHeight / this.height),
|
||||
@@ -139,13 +155,15 @@ export class SnakePage {
|
||||
|
||||
// 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 it's velocity
|
||||
// move snake by its velocity
|
||||
this.snake.x += this.snake.dx
|
||||
this.snake.y += this.snake.dy
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Directive, HostListener, Input } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
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({
|
||||
@@ -9,45 +11,40 @@ import { SnakePage } from './snake.page'
|
||||
})
|
||||
export class SnekDirective {
|
||||
@Input()
|
||||
appSnekHighScore: number | null = null
|
||||
appSnekHighScore = 0
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly embassyApi: ApiService,
|
||||
) {}
|
||||
|
||||
@HostListener('click')
|
||||
async onClick() {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: SnakePage,
|
||||
cssClass: 'snake-modal',
|
||||
backdropDismiss: false,
|
||||
componentProps: { highScore: this.appSnekHighScore || 0 },
|
||||
})
|
||||
|
||||
modal.onDidDismiss().then(async ({ data }) => {
|
||||
if (data?.highScore <= (this.appSnekHighScore || 0)) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving high score...',
|
||||
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()
|
||||
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.setDbValue<number>(
|
||||
['gaming', 'snake', 'high-score'],
|
||||
data.highScore,
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loadingCtrl.dismiss()
|
||||
}
|
||||
})
|
||||
|
||||
modal.present()
|
||||
try {
|
||||
await this.embassyApi.setDbValue<number>(
|
||||
['gaming', 'snake', 'high-score'],
|
||||
score,
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { SnekDirective } from './snek.directive'
|
||||
import { SnakePage } from './snake.page'
|
||||
import { TuiButtonModule } from '@taiga-ui/core'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule],
|
||||
imports: [CommonModule, IonicModule, TuiButtonModule],
|
||||
declarations: [SnekDirective, SnakePage],
|
||||
exports: [SnekDirective, SnakePage],
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user