refactor: refactor sideload page (#2475)

* refactor: refactor sideload page

* chore: improve ux

* chore: update

* chore: update
This commit is contained in:
Alex Inkin
2023-11-06 21:05:05 +04:00
committed by GitHub
parent b195e3435f
commit 06207145af
58 changed files with 952 additions and 229 deletions

View File

@@ -1,11 +1,9 @@
<div class="header montserrat">
<img class="logo" alt="" [src]="pkg | mimeType | trustUrl" />
<div class="text">
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<p *ngIf="pkg['published-at'] as published" class="published">
Released: {{ published | date : 'medium' }}
</p>
<ng-content></ng-content>
</div>
<img class="logo" alt="" [src]="pkg | mimeType | trustUrl" />
<div class="text">
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<p *ngIf="pkg['published-at'] as published" class="published">
Released: {{ published | date : 'medium' }}
</p>
<ng-content></ng-content>
</div>

View File

@@ -1,7 +1,8 @@
.header {
:host {
display: flex;
align-items: flex-start;
padding: 16px;
line-height: 2;
}
.text {

View File

@@ -6,6 +6,8 @@ import { MarketplacePkg } from '../types'
})
export class MimeTypePipe implements PipeTransform {
transform(pkg: MarketplacePkg): string {
if (pkg.icon.startsWith('data:')) return pkg.icon
if (pkg.manifest.assets.icon) {
switch (pkg.manifest.assets.icon.split('.').pop()) {
case 'png':

View File

@@ -39,6 +39,13 @@
color: var(--tui-error-fill);
}
[tuiWrapper][data-appearance='input-file'] {
&:hover,
&:active {
background: transparent !important;
}
}
tui-dialog {
transform: translate3d(0, 0, 0);
}

View File

@@ -4,10 +4,10 @@ import { Router, RouteReuseStrategy } from '@angular/router'
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
import { TUI_DATE_FORMAT, TUI_DATE_SEPARATOR } from '@taiga-ui/cdk'
import {
tuiButtonOptionsProvider,
tuiNumberFormatProvider,
tuiTextfieldOptionsProvider,
} from '@taiga-ui/core'
import { tuiButtonOptionsProvider } from '@taiga-ui/experimental'
import {
TUI_DATE_TIME_VALUE_TRANSFORMER,
TUI_DATE_VALUE_TRANSFORMER,

View File

@@ -1,10 +1,10 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { SnekDirective } from './snek.directive'
import { SnakePage } from './snake.page'
import { TuiButtonModule } from '@taiga-ui/core'
@NgModule({
imports: [CommonModule, IonicModule, TuiButtonModule],

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { RouterModule, Routes } from '@angular/router'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { HomePage } from './home.page'
const ROUTES: Routes = [

View File

@@ -24,9 +24,9 @@
<button
tuiIconButton
appearance="outline"
shape="rounded"
size="xs"
icon="tuiIconMoreHorizontal"
iconLeft="tuiIconMoreHorizontal"
[style.border-radius.%]="100"
>
Actions
</button>

View File

@@ -9,11 +9,11 @@ import {
import {
TuiBadgedContentModule,
TuiBadgeNotificationModule,
TuiButtonModule,
} from '@taiga-ui/experimental'
import { RouterLink } from '@angular/router'
import { TickerModule } from '@start9labs/shared'
import {
TuiButtonModule,
TuiDataListModule,
TuiHostedDropdownModule,
TuiSvgModule,

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
TuiButtonModule,
TuiDataListModule,
TuiHostedDropdownModule,
TuiSvgModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { AuthService } from 'src/app/services/auth.service'

View File

@@ -1,10 +1,11 @@
<ng-content></ng-content>
<div class="toolbar">
<button tuiIconButton icon="tuiIconCloudLarge" appearance="success">
<button tuiIconButton iconLeft="tuiIconCloudLarge" appearance="success">
Connection
</button>
<tui-badged-content size="m" [contentBottom]="4">
<button tuiIconButton icon="tuiIconBellLarge" appearance="warning">
<tui-badged-content [style.--tui-radius.%]="50">
<tui-badge-notification tuiSlot="bottom" size="s">4</tui-badge-notification>
<button tuiIconButton iconLeft="tuiIconBellLarge" appearance="warning">
Notifications
</button>
</tui-badged-content>

View File

@@ -1,11 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiBadgedContentModule } from '@taiga-ui/kit'
import {
TuiButtonModule,
TuiDataListModule,
TuiHostedDropdownModule,
TuiSvgModule,
} from '@taiga-ui/core'
import {
TuiBadgedContentModule,
TuiBadgeNotificationModule,
TuiButtonModule,
} from '@taiga-ui/experimental'
import { HeaderMenuComponent } from './header-menu/header-menu.component'
@Component({
@@ -16,6 +19,7 @@ import { HeaderMenuComponent } from './header-menu/header-menu.component'
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiBadgedContentModule,
TuiBadgeNotificationModule,
TuiButtonModule,
TuiHostedDropdownModule,
TuiDataListModule,

View File

@@ -25,7 +25,7 @@
<button
tuiIconButton
size="xs"
icon="tuiIconClose"
iconLeft="tuiIconClose"
appearance="icon"
class="close"
(click.stop.prevent)="removeTab(tab, rla.isActive)"

View File

@@ -1,7 +1,8 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { Router, RouterModule } from '@angular/router'
import { TuiButtonModule, TuiSvgModule } from '@taiga-ui/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { NavigationService } from '../../services/navigation.service'
import { NavigationItem } from '../../types/navigation-item'

View File

@@ -8,17 +8,9 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
icon: 'tuiIconGlobeLarge',
title: 'Updates',
},
'/portal/system/devices': {
icon: 'assets/img/icon_transparent.png',
title: 'Devices',
},
'/portal/system/metrics': {
icon: 'assets/img/icon_transparent.png',
title: 'Metrics',
},
'/portal/system/manual': {
icon: 'assets/img/icon_transparent.png',
title: 'Manual',
'/portal/system/sideload': {
icon: 'tuiIconUploadLarge',
title: 'Sideload',
},
'/portal/system/snek': {
icon: 'assets/img/icon_transparent.png',

View File

@@ -1,7 +1,8 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { CopyService } from '@start9labs/shared'
import { TuiButtonModule, TuiDialogContext } from '@taiga-ui/core'
import { TuiDialogContext } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { QrCodeModule } from 'ng-qrcode'
import { ActionResponse } from 'src/app/services/api/api.types'
@@ -21,7 +22,7 @@ import { ActionResponse } from 'src/app/services/api/api.types'
*ngIf="context.data.copyable"
tuiIconButton
appearance="flat"
icon="tuiIconCopyLarge"
iconLeft="tuiIconCopyLarge"
(click)="copyService.copy(context.data.value)"
>
Copy

View File

@@ -1,7 +1,8 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiButtonModule, TuiDialogService } from '@taiga-ui/core'
import { TuiDialogService } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { tuiPure } from '@taiga-ui/cdk'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client'

View File

@@ -6,7 +6,8 @@ import {
} from '@angular/core'
import { CopyService } from '@start9labs/shared'
import { mask } from 'src/app/util/mask'
import { TuiButtonModule, TuiLabelModule } from '@taiga-ui/core'
import { TuiLabelModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
@Component({
selector: 'service-credential',
@@ -17,7 +18,7 @@ import { TuiButtonModule, TuiLabelModule } from '@taiga-ui/core'
<button
tuiIconButton
appearance="flat"
[icon]="masked ? 'tuiIconEyeLarge' : 'tuiIconEyeOffLarge'"
[iconLeft]="masked ? 'tuiIconEyeLarge' : 'tuiIconEyeOffLarge'"
(click)="masked = !masked"
>
Toggle
@@ -25,7 +26,7 @@ import { TuiButtonModule, TuiLabelModule } from '@taiga-ui/core'
<button
tuiIconButton
appearance="flat"
icon="tuiIconCopyLarge"
iconLeft="tuiIconCopyLarge"
(click)="copyService.copy(value)"
>
Copy

View File

@@ -5,7 +5,8 @@ import {
inject,
Input,
} from '@angular/core'
import { TuiButtonModule, TuiSvgModule } from '@taiga-ui/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { ConfigService } from 'src/app/services/config.service'
import { InterfaceInfo } from 'src/app/services/patch-db/data-model'
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
@@ -23,7 +24,7 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
*ngIf="info.type === 'ui'"
tuiIconButton
appearance="flat"
icon="tuiIconExternalLinkLarge"
iconLeft="tuiIconExternalLinkLarge"
[style.border-radius.%]="100"
(click.stop.prevent)="launchUI(info)"
[disabled]="disabled"

View File

@@ -7,8 +7,8 @@ import {
LoadingService,
} from '@start9labs/shared'
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiButtonModule,
TuiDialogContext,
TuiDialogService,
TuiLoaderModule,

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, SharedPipesModule } from '@start9labs/shared'
import { TuiForModule } from '@taiga-ui/cdk'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { BehaviorSubject } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'

View File

@@ -7,7 +7,8 @@ import {
Output,
} from '@angular/core'
import { TuiForModule } from '@taiga-ui/cdk'
import { TuiButtonModule, TuiSvgModule } from '@taiga-ui/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { UnknownDisk } from 'src/app/services/api/api.types'
import { IonicModule } from '@ionic/angular'
import { UnitConversionPipesModule } from '@start9labs/shared'

View File

@@ -8,8 +8,8 @@ import {
Output,
} from '@angular/core'
import { TuiForModule } from '@taiga-ui/cdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiButtonModule,
TuiDialogOptions,
TuiDialogService,
TuiSvgModule,
@@ -52,7 +52,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
tuiIconButton
size="xs"
appearance="icon"
icon="tuiIconEdit2"
iconLeft="tuiIconEdit2"
(click)="update.emit(target)"
>
Update
@@ -61,7 +61,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
tuiIconButton
size="xs"
appearance="icon"
icon="tuiIconTrash2"
iconLeft="tuiIconTrash2"
(click)="delete$.next(target.id)"
>
Delete

View File

@@ -3,12 +3,12 @@ import { Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiForModule } from '@taiga-ui/cdk'
import {
TuiButtonModule,
TuiDialogContext,
TuiDialogOptions,
TuiGroupModule,
TuiLoaderModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiCheckboxBlockModule } from '@taiga-ui/kit'
import {
POLYMORPHEUS_CONTEXT,

View File

@@ -36,6 +36,7 @@ import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
appearance="secondary"
type="button"
class="button"
size="l"
(click)="selectTarget()"
>
Target
@@ -48,6 +49,7 @@ import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
appearance="secondary"
type="button"
class="button"
size="l"
(click)="selectPackages()"
>
Packages
@@ -70,7 +72,6 @@ import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
</div>
<button
tuiButton
size="m"
class="submit"
[style.margin-left]="'auto'"
(click)="save()"

View File

@@ -8,13 +8,8 @@ import {
ALWAYS_TRUE_HANDLER,
TuiForModule,
} from '@taiga-ui/cdk'
import {
TuiButtonModule,
TuiDialogService,
TuiLinkModule,
TuiSvgModule,
} from '@taiga-ui/core'
import { TuiFadeModule } from '@taiga-ui/experimental'
import { TuiDialogService, TuiLinkModule, TuiSvgModule } from '@taiga-ui/core'
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
import { TuiCheckboxModule } from '@taiga-ui/kit'
import { BehaviorSubject } from 'rxjs'
import { BackupRun } from 'src/app/services/api/api.types'
@@ -31,7 +26,6 @@ import { REPORT } from './report.component'
Past Events
<button
tuiButton
size="m"
appearance="secondary-destructive"
[disabled]="disabled"
(click)="delete()"

View File

@@ -3,13 +3,12 @@ import { Component, inject, OnInit } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiForModule } from '@taiga-ui/cdk'
import {
TuiButtonModule,
TuiDialogOptions,
TuiDialogService,
TuiNotificationModule,
TuiSvgModule,
} from '@taiga-ui/core'
import { TuiFadeModule } from '@taiga-ui/experimental'
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { BehaviorSubject, filter } from 'rxjs'
@@ -36,7 +35,7 @@ import { EDIT } from './edit.component'
</tui-notification>
<h3 class="g-title">
Saved Jobs
<button tuiButton size="s" icon="tuiIconPlus" (click)="create()">
<button tuiButton size="s" iconLeft="tuiIconPlus" (click)="create()">
Create New Job
</button>
</h3>
@@ -65,14 +64,14 @@ import { EDIT } from './edit.component'
tuiIconButton
appearance="icon"
size="xs"
icon="tuiIconEdit2"
iconLeft="tuiIconEdit2"
(click)="update(job)"
></button>
<button
tuiIconButton
appearance="icon"
size="xs"
icon="tuiIconTrash2"
iconLeft="tuiIconTrash2"
(click)="delete(job.id)"
></button>
</td>

View File

@@ -2,11 +2,8 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { ErrorService, LoadingService } from '@start9labs/shared'
import {
TuiButtonModule,
TuiDialogContext,
TuiGroupModule,
} from '@taiga-ui/core'
import { TuiDialogContext, TuiGroupModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiCheckboxBlockModule } from '@taiga-ui/kit'
import {
POLYMORPHEUS_CONTEXT,

View File

@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService } from '@start9labs/shared'
import { TuiForModule } from '@taiga-ui/cdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiButtonModule,
TuiDialogContext,
TuiDialogOptions,
TuiDialogService,

View File

@@ -5,18 +5,10 @@ import {
unionSelectKey,
unionValueKey,
} from '@start9labs/start-sdk/lib/config/configTypes'
import { TuiButtonModule, TuiNotificationModule } from '@taiga-ui/core'
import { TuiNotificationModule } from '@taiga-ui/core'
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import {
BehaviorSubject,
catchError,
from,
Observable,
of,
share,
startWith,
switchMap,
} from 'rxjs'
import { BehaviorSubject } from 'rxjs'
import { FormPage } from 'src/app/apps/ui/modals/form/form.page'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
import {
@@ -37,7 +29,6 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { BackupConfig } from '../types/backup-config'
import { BackupsPhysicalComponent } from '../components/physical.component'
import { BackupsTargetsComponent } from '../components/targets.component'
import { TuiFadeModule } from '@taiga-ui/experimental'
@Component({
template: `

View File

@@ -0,0 +1,58 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { EmverPipesModule } from '@start9labs/shared'
import {
TuiAvatarModule,
TuiCellModule,
TuiTitleModule,
} from '@taiga-ui/experimental'
@Component({
selector: 'sideload-dependencies',
template: `
<h3 class="g-title" [style.text-indent.rem]="1">Dependencies</h3>
<div *ngFor="let dep of package.manifest.dependencies | keyvalue" tuiCell>
<tui-avatar [src]="getImage(dep.key)"></tui-avatar>
<div tuiTitle>
<div>
<strong>{{ getTitle(dep.key) }}&nbsp;</strong>
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</div>
<div tuiSubtitle [style.color]="'var(--tui-text-03)'">
{{ dep.value.version | displayEmver }}
</div>
<div tuiSubtitle>
{{ dep.value.description }}
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
TuiTitleModule,
EmverPipesModule,
TuiAvatarModule,
TuiCellModule,
],
})
export class SideloadDependenciesComponent {
@Input({ required: true })
package!: MarketplacePkg
getTitle(key: string): string {
return this.package['dependency-metadata'][key]?.title || key
}
getImage(key: string): string {
const icon = this.package['dependency-metadata'][key]?.icon
return icon ? `data:image/png;base64,${icon}` : key.substring(0, 2)
}
}

View File

@@ -0,0 +1,131 @@
import { CommonModule } from '@angular/common'
import { Component, inject, Input } from '@angular/core'
import { Router, RouterLink } from '@angular/router'
import {
AboutModule,
AdditionalModule,
MarketplacePkg,
PackageModule,
} from '@start9labs/marketplace'
import {
Emver,
ErrorService,
LoadingService,
SharedPipesModule,
} from '@start9labs/shared'
import { TuiLetModule } from '@taiga-ui/cdk'
import { TuiAlertService } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ClientStorageService } from 'src/app/services/client-storage.service'
import { toDesktopItem } from '../../../utils/to-desktop-item'
import { NavigationService } from '../../../services/navigation.service'
import { SideloadDependenciesComponent } from './dependencies.component'
@Component({
selector: 'sideload-package',
template: `
<ng-content></ng-content>
<marketplace-package *tuiLet="button$ | async as button" [pkg]="package">
<a
*ngIf="button !== null && button !== 'Install'"
tuiButton
appearance="secondary"
[routerLink]="'/portal/service/' + package.manifest.id"
>
View installed
</a>
<button *ngIf="button" tuiButton (click)="upload()">
{{ button }}
</button>
</marketplace-package>
<marketplace-about [pkg]="package"></marketplace-about>
<sideload-dependencies
*ngIf="!(package.manifest.dependencies | empty)"
[package]="package"
></sideload-dependencies>
<marketplace-additional [pkg]="package"></marketplace-additional>
`,
standalone: true,
imports: [
CommonModule,
RouterLink,
SharedPipesModule,
AboutModule,
AdditionalModule,
PackageModule,
TuiButtonModule,
TuiLetModule,
SideloadDependenciesComponent,
],
})
export class SideloadPackageComponent {
private readonly loader = inject(LoadingService)
private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService)
private readonly router = inject(Router)
private readonly navigation = inject(NavigationService)
private readonly alerts = inject(TuiAlertService)
private readonly emver = inject(Emver)
readonly button$ = combineLatest([
inject(ClientStorageService).showDevTools$,
inject(PatchDB<DataModel>)
.watch$('package-data')
.pipe(
map(local =>
local[this.package.manifest.id]
? this.emver.compare(
local[this.package.manifest.id].manifest.version,
this.package.manifest.version,
)
: null,
),
),
]).pipe(
map(([devtools, version]) => {
switch (version) {
case null:
return 'Install'
case 1:
return 'Update'
case -1:
return devtools ? 'Downgrade' : ''
default:
return ''
}
}),
)
@Input({ required: true })
package!: MarketplacePkg
@Input({ required: true })
file!: File
async upload() {
const loader = this.loader.open('Uploading package').subscribe()
const { manifest, icon } = this.package
const { size } = this.file
try {
const pkg = await this.api.sideloadPackage({ manifest, icon, size })
await this.api.uploadPackage(pkg, this.file)
await this.router.navigate(['/portal/service', manifest.id])
this.navigation.removeTab(toDesktopItem('/portal/system/sideload'))
this.alerts
.open('Package uploaded successfully', { status: 'success' })
.subscribe()
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -0,0 +1,119 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { MarketplacePkg } from '@start9labs/marketplace'
import { TuiLinkModule, TuiWrapperModule } from '@taiga-ui/core'
import { TuiAvatarModule, TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiInputFilesModule,
tuiInputFilesOptionsProvider,
} from '@taiga-ui/kit'
import { Subject } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
import { parseS9pk, validateS9pk } from './sideload.utils'
import { SideloadPackageComponent } from './package.component'
@Component({
template: `
<ng-container *ngIf="refresh$ | async"></ng-container>
<sideload-package
*ngIf="package && file; else upload"
[package]="package"
[file]="file"
>
<button
tuiIconButton
appearance="secondary"
iconLeft="tuiIconXLarge"
[style.border-radius.%]="100"
[style.float]="'right'"
(click)="clear()"
>
Close
</button>
</sideload-package>
<ng-template #upload>
<tui-input-files
[ngModel]="null"
(ngModelChange)="onFile($event)"
(click)="clear()"
>
<input tuiInputFiles accept=".s9pk" />
<ng-template>
<div *ngIf="invalid; else valid">
<tui-avatar
tuiWrapper
appearance="secondary"
src="tuiIconXCircleLarge"
></tui-avatar>
<p [style.color]="'var(--tui-negative)'">Invalid package file</p>
<button tuiButton>Try again</button>
</div>
<ng-template #valid>
<div>
<tui-avatar
tuiWrapper
appearance="secondary"
src="tuiIconUploadCloudLarge"
></tui-avatar>
<p>Upload .s9pk package file</p>
<p *ngIf="isTor" [style.color]="'var(--tui-positive)'">
Tip: switch to LAN for faster uploads
</p>
<button tuiButton>Upload</button>
</div>
</ng-template>
</ng-template>
</tui-input-files>
</ng-template>
`,
host: { class: 'g-page', '[style.padding-top.rem]': '2' },
styles: [
`
tui-input-files {
height: 100%;
max-width: 40rem;
margin: 0 auto;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [tuiInputFilesOptionsProvider({ maxFileSize: Infinity })],
standalone: true,
imports: [
CommonModule,
FormsModule,
TuiInputFilesModule,
TuiLinkModule,
TuiAvatarModule,
TuiWrapperModule,
TuiButtonModule,
SideloadPackageComponent,
],
})
export class SideloadComponent {
readonly refresh$ = new Subject<void>()
readonly isTor = inject(ConfigService).isTor()
invalid = false
file: File | null = null
package: MarketplacePkg | null = null
clear() {
this.invalid = false
this.file = null
this.package = null
}
async onFile(file: File | null) {
if (!file || !(await validateS9pk(file))) {
this.invalid = true
} else {
this.package = await parseS9pk(file)
this.file = file
}
this.refresh$.next()
}
}

View File

@@ -0,0 +1,162 @@
import { Manifest, MarketplacePkg } from '@start9labs/marketplace'
import cbor from 'cbor'
interface Positions {
[key: string]: [bigint, bigint] // [position, length]
}
const MAGIC = new Uint8Array([59, 59])
const VERSION = new Uint8Array([1])
export async function validateS9pk(file: File): Promise<boolean> {
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
return compare(magic, MAGIC) && compare(version, VERSION)
}
export async function parseS9pk(file: File): Promise<MarketplacePkg> {
const positions: Positions = {}
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
let start = 103
let end = start + 1 // 104
const tocLength = new DataView(
await blobToBuffer(file.slice(99, 103) ?? new Blob()),
).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any)
const manifest = await getAsset(positions, file, 'manifest')
const [icon] = await Promise.all([
await getIcon(positions, file, manifest),
// getAsset(positions, file, 'license'),
// getAsset(positions, file, 'instructions'),
])
return {
manifest,
icon,
license: '',
instructions: '',
categories: [],
versions: [],
'dependency-metadata': {},
'published-at': '',
}
}
async function getPositions(
initialStart: number,
initialEnd: number,
file: Blob,
positions: Positions,
tocLength: number,
) {
let start = initialStart
let end = initialEnd
const titleLength = new Uint8Array(
await blobToBuffer(file.slice(start, end)),
)[0]
const tocTitle = await file.slice(end, end + titleLength).text()
start = end + titleLength
end = start + 8
const chapterPosition = new DataView(
await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
start = end
end = start + 8
const chapterLength = new DataView(
await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
positions[tocTitle] = [chapterPosition, chapterLength]
start = end
end = start + 1
if (end <= tocLength + (initialStart - 1)) {
await getPositions(start, end, file, positions, tocLength)
}
}
async function readBlobAsDataURL(
f: Blob | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onloadend = () => {
resolve(reader.result)
}
reader.readAsDataURL(f)
reader.onerror = _ => reject(new Error('error reading blob'))
})
}
async function blobToDataURL(data: Blob | File): Promise<string> {
const res = await readBlobAsDataURL(data)
if (res instanceof ArrayBuffer) {
throw new Error('readBlobAsDataURL response should not be an array buffer')
}
if (res == null) {
throw new Error('readBlobAsDataURL response should not be null')
}
if (typeof res === 'string') return res
throw new Error('no possible blob to data url resolution found')
}
async function blobToBuffer(data: Blob | File): Promise<ArrayBuffer> {
const res = await readBlobToArrayBuffer(data)
if (res instanceof String) {
throw new Error('readBlobToArrayBuffer response should not be a string')
}
if (res == null) {
throw new Error('readBlobToArrayBuffer response should not be null')
}
if (res instanceof ArrayBuffer) return res
throw new Error('no possible blob to array buffer resolution found')
}
async function readBlobToArrayBuffer(
f: Blob | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onloadend = () => {
resolve(reader.result)
}
reader.readAsArrayBuffer(f)
reader.onerror = _ => reject(new Error('error reading blob'))
})
}
function compare(a: Uint8Array, b: Uint8Array) {
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
async function getAsset(
positions: Positions,
file: Blob,
asset: 'manifest' | 'license' | 'instructions',
): Promise<any> {
const data = await blobToBuffer(
file.slice(
Number(positions[asset][0]),
Number(positions[asset][0]) + Number(positions[asset][1]),
),
)
return cbor.decode(data, true)
}
async function getIcon(
positions: Positions,
file: Blob,
manifest: Manifest,
): Promise<string> {
const contentType = `image/${manifest.assets.icon.split('.').pop()}`
const data = file.slice(
Number(positions['icon'][0]),
Number(positions['icon'][0]) + Number(positions['icon'][1]),
contentType,
)
return blobToDataURL(data)
}

View File

@@ -11,6 +11,13 @@ const ROUTES: Routes = [
import('./backups/backups.component').then(m => m.BackupsComponent),
data: toDesktopItem('/portal/system/backups'),
},
{
title: systemTabResolver,
path: 'sideload',
loadComponent: () =>
import('./sideload/sideload.component').then(m => m.SideloadComponent),
data: toDesktopItem('/portal/system/sideload'),
},
{
title: systemTabResolver,
path: 'updates',

View File

@@ -15,13 +15,12 @@ import {
SharedPipesModule,
} from '@start9labs/shared'
import {
TuiButtonModule,
TuiDialogService,
TuiLinkModule,
TuiLoaderModule,
TuiSvgModule,
} from '@taiga-ui/core'
import { TuiAvatarModule } from '@taiga-ui/experimental'
import { TuiAvatarModule, TuiButtonModule } from '@taiga-ui/experimental'
import {
TUI_PROMPT,
TuiAccordionModule,

View File

@@ -12,8 +12,8 @@ export class NavigationService {
return this.tabs
}
removeTab(tab: NavigationItem) {
this.tabs.next(this.tabs.value.filter(t => t !== tab))
removeTab({ routerLink }: NavigationItem) {
this.tabs.next(this.tabs.value.filter(t => t.routerLink !== routerLink))
}
addTab(tab: NavigationItem) {

View File

@@ -3,7 +3,8 @@ import { CommonModule } from '@angular/common'
import { ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import { TuiValueChangesModule } from '@taiga-ui/cdk'
import { TuiButtonModule, TuiModeModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiModeModule } from '@taiga-ui/core'
import { FormModule } from 'src/app/common/form/form.module'
import { FormPage } from './form.page'

View File

@@ -34,7 +34,7 @@
title="Toggle masking"
size="xs"
class="button"
[icon]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
(click)="masked = !masked"
></button>
</ng-template>

View File

@@ -1,7 +1,8 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiButtonModule, TuiTextfieldControllerModule } from '@taiga-ui/core'
import { TuiTextfieldControllerModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiInputModule } from '@taiga-ui/kit'
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
import { PromptComponent } from './prompt.component'

View File

@@ -1,7 +1,8 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { TuiButtonModule, TuiGroupModule } from '@taiga-ui/core'
import { TuiGroupModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiCheckboxBlockModule } from '@taiga-ui/kit'
import { BackupSelectPage } from './backup-select.page'

View File

@@ -1,7 +1,8 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { TuiButtonModule, TuiGroupModule } from '@taiga-ui/core'
import { TuiGroupModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiCheckboxBlockModule } from '@taiga-ui/kit'
import { RecoverSelectPage } from './recover-select.page'
import { ToOptionsPipe } from './to-options.pipe'

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TargetSelectPage, TargetStatusComponent } from './target-select.page'
import { TargetPipesModule } from '../../pipes/target-pipes.module'
import { TextSpinnerComponentModule } from '@start9labs/shared'

View File

@@ -3,11 +3,8 @@ import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { RouterModule, Routes } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
TuiButtonModule,
TuiNotificationModule,
TuiWrapperModule,
} from '@taiga-ui/core'
import { TuiNotificationModule, TuiWrapperModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiInputModule, TuiToggleModule } from '@taiga-ui/kit'
import { BackupJobsPage } from './backup-jobs.page'
import { EditJobComponent } from './edit-job/edit-job.component'

View File

@@ -2,11 +2,11 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ReactiveFormsModule } from '@angular/forms'
import {
TuiButtonModule,
TuiLoaderModule,
TuiModeModule,
TuiNotificationModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { FormPageModule } from 'src/app/apps/ui/modals/form/form.module'
import { AppConfigPage } from './app-config.page'

View File

@@ -1,7 +1,8 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { TuiButtonModule, TuiNotificationModule } from '@taiga-ui/core'
import { TuiNotificationModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { EmailPage } from './email.page'
import { Routes, RouterModule } from '@angular/router'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

View File

@@ -4,13 +4,13 @@ import { FormsModule } from '@angular/forms'
import { RouterModule, Routes } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
TuiButtonModule,
TuiDataListModule,
TuiHostedDropdownModule,
TuiNotificationModule,
TuiSvgModule,
TuiWrapperModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiBadgeModule, TuiInputModule, TuiToggleModule } from '@taiga-ui/kit'
import { ProxiesPage } from './proxies.page'

View File

@@ -102,11 +102,8 @@
appearance="flat"
tuiHostedDropdownHost
size="s"
[icon]="icon"
iconLeft="tuiIconMoreHorizontal"
></button>
<ng-template #icon>
<tui-svg src="tuiIconMoreHorizontal"></tui-svg>
</ng-template>
</tui-hosted-dropdown>
<ng-template #dropdown let-close="close">
<tui-data-list>

View File

@@ -1,7 +1,8 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { MarkdownPipeModule, SafeLinksDirective } from '@start9labs/shared'
import { TuiButtonModule, TuiScrollbarModule } from '@taiga-ui/core'
import { TuiScrollbarModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { OSUpdatePage } from './os-update.page'

View File

@@ -48,7 +48,7 @@
tuiIconButton
type="button"
class="remove"
icon="tuiIconTrash"
iconLeft="tuiIconTrash"
appearance="icon"
size="m"
title="Remove"

View File

@@ -2,11 +2,11 @@
<button
tuiIconButton
size="s"
icon="tuiIconChevronDown"
iconLeft="tuiIconChevronDown"
type="button"
shape="rounded"
class="button"
[class.button_open]="open"
[style.border-radius.%]="100"
[appearance]="invalid ? 'secondary-destructive' : 'secondary'"
></button>
<ng-content></ng-content>

View File

@@ -27,7 +27,7 @@
title="Generate"
size="xs"
class="button"
icon="tuiIconRefreshCcw"
iconLeft="tuiIconRefreshCcw"
(click)="generate()"
></button>
<button
@@ -38,7 +38,7 @@
title="Toggle masking"
size="xs"
class="button"
[icon]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
(click)="masked = !masked"
></button>
</ng-template>

View File

@@ -4,7 +4,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MaskitoModule } from '@maskito/angular'
import { TuiMapperPipeModule, TuiValueChangesModule } from '@taiga-ui/cdk'
import {
TuiButtonModule,
TuiErrorModule,
TuiExpandModule,
TuiHintModule,
@@ -15,6 +14,7 @@ import {
TuiTooltipModule,
TuiWrapperModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiElasticContainerModule,
TuiFieldErrorPipeModule,

View File

@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { OSWelcomePage } from './os-welcome.page'
@NgModule({

View File

@@ -2,11 +2,8 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
import {
TuiAlertModule,
TuiButtonModule,
TuiDialogModule,
} from '@taiga-ui/core'
import { TuiAlertModule, TuiDialogModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { ToastContainerComponent } from './toast-container.component'
import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component'

View File

@@ -375,6 +375,7 @@ ul {
padding: 1px 2rem 3rem;
box-sizing: border-box;
overflow: auto;
isolation: isolate;
// TODO: Theme
background: #373a3f;