chore: update Taiga to 5 (#3136)

* chore: update Taiga to 5

* chore: fix
This commit is contained in:
Alex Inkin
2026-03-15 19:51:50 +04:00
committed by GitHub
parent be921b7865
commit a90b96cddd
184 changed files with 1508 additions and 1958 deletions

View File

@@ -8,7 +8,7 @@ import { StateService } from './services/state.service'
@Component({
selector: 'app-root',
template: '<tui-root tuiTheme="dark"><router-outlet /></tui-root>',
template: '<tui-root><router-outlet /></tui-root>',
imports: [TuiRoot, RouterOutlet],
})
export class AppComponent implements OnInit {

View File

@@ -10,7 +10,6 @@ import {
provideZoneChangeDetection,
signal,
} from '@angular/core'
import { provideAnimations } from '@angular/platform-browser/animations'
import {
PreloadAllModules,
provideRouter,
@@ -29,8 +28,10 @@ import {
import {
tuiButtonOptionsProvider,
tuiTextfieldOptionsProvider,
provideTaiga,
tuiHintOptionsProvider,
tuiDialogOptionsProvider,
} from '@taiga-ui/core'
import { provideEventPlugins } from '@taiga-ui/event-plugins'
import { ROUTES } from './app.routes'
import { ApiService } from './services/api.service'
@@ -47,8 +48,9 @@ const version = require('../../../../package.json').version
export const APP_CONFIG: ApplicationConfig = {
providers: [
provideZoneChangeDetection(),
provideAnimations(),
provideEventPlugins(),
provideTaiga({ mode: 'dark' }),
tuiHintOptionsProvider({ appearance: 'primary-grayscale' }),
tuiDialogOptionsProvider({ size: 's' }),
provideRouter(
ROUTES,
withDisabledInitialNavigation(),

View File

@@ -13,14 +13,10 @@ import {
TuiDialogContext,
TuiError,
TuiIcon,
TuiTextfield,
TuiInput,
tuiValidationErrorsProvider,
} from '@taiga-ui/core'
import {
TUI_VALIDATION_ERRORS,
TuiButtonLoading,
TuiFieldErrorPipe,
TuiPassword,
} from '@taiga-ui/kit'
import { TuiButtonLoading, TuiPassword } from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { ApiService } from '../services/api.service'
import { StartOSDiskInfoWithId } from '../types'
@@ -36,42 +32,36 @@ export interface CifsResult {
<tui-textfield>
<label tuiLabel>{{ 'Hostname' | i18n }}*</label>
<input
tuiTextfield
tuiInput
formControlName="hostname"
placeholder="e.g. 'My Computer' OR 'my-computer.local'"
/>
</tui-textfield>
<tui-error
formControlName="hostname"
[error]="['required'] | tuiFieldError | async"
/>
<tui-error formControlName="hostname" [order]="['required']" />
<tui-textfield class="input">
<label tuiLabel>{{ 'Path' | i18n }}*</label>
<input
tuiTextfield
tuiInput
formControlName="path"
placeholder="/Desktop/my-folder"
/>
</tui-textfield>
<tui-error formControlName="path" [error]="[] | tuiFieldError | async" />
<tui-error formControlName="path" />
<tui-textfield class="input">
<label tuiLabel>{{ 'Username' | i18n }}*</label>
<input
tuiTextfield
tuiInput
formControlName="username"
placeholder="Enter username"
/>
</tui-textfield>
<tui-error
formControlName="username"
[error]="[] | tuiFieldError | async"
/>
<tui-error formControlName="username" />
<tui-textfield class="input">
<label tuiLabel>{{ 'Password' | i18n }}</label>
<input tuiTextfield type="password" formControlName="password" />
<input tuiInput type="password" formControlName="password" />
<tui-icon tuiPassword />
</tui-textfield>
@@ -107,20 +97,16 @@ export interface CifsResult {
ReactiveFormsModule,
TuiButton,
TuiButtonLoading,
TuiTextfield,
TuiInput,
TuiPassword,
TuiError,
TuiFieldErrorPipe,
TuiIcon,
i18nPipe,
],
providers: [
{
provide: TUI_VALIDATION_ERRORS,
useValue: {
required: 'This field is required',
},
},
tuiValidationErrorsProvider({
required: 'This field is required',
}),
],
})
export class CifsComponent {
@@ -183,10 +169,7 @@ export class CifsComponent {
this.dialogs
.openAlert(
'Unable to connect to network folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.',
{
label: 'Connection Failed',
size: 's',
},
{ label: 'Connection Failed' },
)
.subscribe()
}

View File

@@ -1,13 +1,19 @@
import { Component, inject } from '@angular/core'
import { Component } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core'
import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiDialogContext } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
import { TuiHeader } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@Component({
imports: [TuiButton, i18nPipe],
imports: [TuiButton, TuiHeader, TuiTitle, i18nPipe],
template: `
<p>{{ 'This drive contains existing StartOS data.' | i18n }}</p>
<header tuiHeader>
<hgroup tuiTitle>
<h2 [id]="context.id">{{ 'StartOS Data Detected' | i18n }}</h2>
<p>{{ 'This drive contains existing StartOS data.' | i18n }}</p>
</hgroup>
</header>
<ul>
<li>
<strong class="g-positive">{{ 'Preserve' | i18n }}</strong>
@@ -28,30 +34,19 @@ import { injectContext } from '@taiga-ui/polymorpheus'
</button>
<button
tuiButton
class="preserve-btn"
appearance=""
[style.background]="'var(--tui-status-positive)'"
(click)="context.completeWith(true)"
>
{{ 'Preserve' | i18n }}
</button>
</footer>
`,
styles: `
p {
margin: 0 0 0.75rem;
}
footer {
display: flex;
margin-top: 2rem;
gap: 0.5rem;
flex-direction: column-reverse;
}
.preserve-btn {
background: var(--tui-status-positive) !important;
}
`,
})
export class PreserveOverwriteDialog {
protected readonly context = injectContext<TuiDialogContext<boolean>>()
}
export const PRESERVE_OVERWRITE = new PolymorpheusComponent(
PreserveOverwriteDialog,
)

View File

@@ -1,8 +1,9 @@
import { Component, inject } from '@angular/core'
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { i18nPipe } from '@start9labs/shared'
import { TuiDialogContext, TuiTextfield } from '@taiga-ui/core'
import { TuiDialogContext, TuiTitle } from '@taiga-ui/core'
import { TuiDataListWrapper, TuiSelect } from '@taiga-ui/kit'
import { TuiHeader } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { StartOSDiskInfoWithId } from '../types'
@@ -11,36 +12,48 @@ interface Data {
}
@Component({
imports: [FormsModule, TuiTextfield, TuiSelect, TuiDataListWrapper, i18nPipe],
imports: [
FormsModule,
TuiSelect,
TuiDataListWrapper,
i18nPipe,
TuiHeader,
TuiTitle,
],
template: `
<p>{{ 'Multiple backups found. Select which one to restore.' | i18n }}</p>
<header tuiHeader>
<hgroup tuiTitle>
<h2 [id]="context.id">{{ 'Select Network Backup' | i18n }}</h2>
<p>
{{ 'Multiple backups found. Select which one to restore.' | i18n }}
</p>
</hgroup>
</header>
<tui-textfield [stringify]="stringify">
<label tuiLabel>{{ 'Backups' | i18n }}</label>
<input tuiSelect [(ngModel)]="selectedServer" />
<tui-data-list-wrapper
new
*tuiTextfieldDropdown
*tuiDropdown
[items]="context.data.servers"
[itemContent]="serverContent"
/>
</tui-textfield>
<ng-template #serverContent let-server>
<div class="server-item">
<span>{{ server.id }}</span>
<span tuiTitle>
{{ server.id }}
<!-- @TODO eos-version? -->
<small>{{ server['eos-version'] }}</small>
</div>
@if (server['eos-version']) {
<span tuiSubtitle>
{{ server['eos-version'] }}
</span>
}
</span>
</ng-template>
`,
styles: `
.server-item {
display: flex;
flex-direction: column;
small {
opacity: 0.7;
}
div {
margin-block-end: 1rem;
}
`,
})

View File

@@ -1,32 +1,27 @@
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { i18nPipe } from '@start9labs/shared'
import {
TuiButton,
TuiDialogContext,
TuiIcon,
TuiTextfield,
} from '@taiga-ui/core'
import { TuiButton, TuiDialogContext, TuiIcon, TuiInput } from '@taiga-ui/core'
import { TuiPassword } from '@taiga-ui/kit'
import { injectContext } from '@taiga-ui/polymorpheus'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@Component({
imports: [
FormsModule,
TuiButton,
TuiTextfield,
TuiPassword,
TuiIcon,
i18nPipe,
],
imports: [FormsModule, TuiButton, TuiInput, TuiPassword, TuiIcon, i18nPipe],
template: `
<p>
{{ 'Enter the password that was used to encrypt this backup.' | i18n }}
</p>
<header tuiHeader>
<hgroup tuiTitle>
<h2 [id]="context.id">{{ 'Unlock Backup' | i18n }}</h2>
<p>
{{
'Enter the password that was used to encrypt this backup.' | i18n
}}
</p>
</hgroup>
</header>
<tui-textfield>
<label tuiLabel>{{ 'Password' | i18n }}</label>
<input
tuiTextfield
tuiInput
type="password"
[(ngModel)]="password"
(keyup.enter)="unlock()"
@@ -62,3 +57,5 @@ export class UnlockPasswordDialog {
}
}
}
export const UNLOCK_PASSWORD = new PolymorpheusComponent(UnlockPasswordDialog)

View File

@@ -12,24 +12,29 @@ import {
ErrorService,
i18nKey,
i18nPipe,
LoadingService,
toGuid,
} from '@start9labs/shared'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
import { WA_IS_MOBILE } from '@ng-web-apis/platform'
import {
TuiButton,
TuiIcon,
TuiLoader,
TuiTextfield,
TuiInput,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit'
import {
TuiDataListWrapper,
TuiNotificationMiddleService,
TuiSelect,
TuiTooltip,
} from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter, Subscription } from 'rxjs'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog'
import { PRESERVE_OVERWRITE } from '../components/preserve-overwrite.dialog'
@Component({
template: `
@@ -42,7 +47,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
@if (loading) {
<tui-loader />
} @else if (drives.length === 0) {
<p class="no-drives">
<p tuiNotification size="m" appearance="warning">
{{
'No drives found. Please connect a drive and click Refresh.'
| i18n
@@ -70,8 +75,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
}
@if (!mobile) {
<tui-data-list-wrapper
new
*tuiTextfieldDropdown
*tuiDropdown
[items]="drives"
[itemContent]="driveContent"
/>
@@ -100,36 +104,27 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
}
@if (!mobile) {
<tui-data-list-wrapper
new
*tuiTextfieldDropdown
*tuiDropdown
[items]="drives"
[itemContent]="driveContent"
/>
}
@if (preserveData === true) {
<tui-icon
icon="@tui.database"
style="color: var(--tui-status-positive); pointer-events: none"
/>
<tui-icon icon="@tui.database" class="g-positive" />
}
@if (preserveData === false) {
<tui-icon
icon="@tui.database-zap"
style="color: var(--tui-status-negative); pointer-events: none"
/>
<tui-icon icon="@tui.database-zap" class="g-negative" />
}
<tui-icon [tuiTooltip]="dataDriveTooltip" />
</tui-textfield>
<ng-template #driveContent let-drive>
<div class="drive-item">
<span class="drive-name">
{{ driveName(drive) }}
</span>
<small>
<span tuiTitle>
{{ driveName(drive) }}
<span tuiSubtitle>
{{ formatCapacity(drive.capacity) }} · {{ drive.logicalname }}
</small>
</div>
</span>
</span>
</ng-template>
}
@@ -152,19 +147,8 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
}
`,
styles: `
.no-drives {
text-align: center;
color: var(--tui-text-secondary);
padding: 2rem;
}
.drive-item {
display: flex;
flex-direction: column;
small {
opacity: 0.7;
}
tui-icon:not([tuiTooltip]) {
pointer-events: none;
}
`,
imports: [
@@ -173,7 +157,8 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
TuiButton,
TuiIcon,
TuiLoader,
TuiTextfield,
TuiInput,
TuiNotification,
TuiSelect,
TuiDataListWrapper,
TuiTooltip,
@@ -186,13 +171,13 @@ export default class DrivesPage {
private readonly api = inject(ApiService)
private readonly router = inject(Router)
private readonly dialogs = inject(DialogService)
private readonly loader = inject(LoadingService)
private readonly loader = inject(TuiNotificationMiddleService)
private readonly errorService = inject(ErrorService)
private readonly stateService = inject(StateService)
private readonly cdr = inject(ChangeDetectorRef)
private readonly i18n = inject(i18nPipe)
protected readonly mobile = inject(TUI_IS_MOBILE)
protected readonly mobile = inject(WA_IS_MOBILE)
@HostListener('document:keydown', ['$event'])
onKeydown(event: KeyboardEvent) {
@@ -308,38 +293,27 @@ export default class DrivesPage {
private showPreserveOverwriteDialog() {
let selectionMade = false
this.dialogs
.openComponent<boolean>(
new PolymorpheusComponent(PreserveOverwriteDialog),
{
label: 'StartOS Data Detected',
size: 's',
dismissible: true,
closeable: true,
},
)
.subscribe({
next: preserve => {
selectionMade = true
this.preserveData = preserve
this.dialogs.openComponent<boolean>(PRESERVE_OVERWRITE).subscribe({
next: preserve => {
selectionMade = true
this.preserveData = preserve
this.cdr.markForCheck()
},
complete: () => {
if (!selectionMade) {
// Dialog was dismissed without selection - clear the data drive
this.selectedDataDrive = null
this.preserveData = null
this.cdr.markForCheck()
},
complete: () => {
if (!selectionMade) {
// Dialog was dismissed without selection - clear the data drive
this.selectedDataDrive = null
this.preserveData = null
this.cdr.markForCheck()
}
},
})
}
},
})
}
private showOsDriveWarning() {
this.dialogs
.openConfirm({
label: 'Warning',
size: 's',
data: {
content: `<ul>
<li class="g-negative">${this.i18n.transform('Data on the OS drive may be overwritten.')}</li>
@@ -363,7 +337,6 @@ export default class DrivesPage {
this.dialogs
.openConfirm({
label: 'Warning',
size: 's',
data: {
content: message as i18nKey,
yes: 'Continue',
@@ -397,10 +370,9 @@ export default class DrivesPage {
this.dialogSub = this.dialogs
.openAlert('StartOS has been installed successfully.', {
label: 'Installation Complete!',
size: 's',
dismissible: false,
closeable: true,
data: { button: this.i18n.transform('Continue to Setup') },
closable: true,
data: this.i18n.transform('Continue to Setup'),
})
.subscribe({
complete: () => {

View File

@@ -1,9 +1,9 @@
import { Component, inject } from '@angular/core'
import { Router } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
import { TuiAppearance, TuiTitle } from '@taiga-ui/core'
import { TuiTitle, TuiCell } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { StateService } from '../services/state.service'
@Component({
@@ -14,7 +14,7 @@ import { StateService } from '../services/state.service'
</header>
<button tuiCell="l" (click)="startFresh()">
<tui-avatar appearance="positive" src="@tui.plus" />
<span tuiAvatar="@tui.plus" appearance="positive"></span>
<div tuiTitle>
{{ 'Start Fresh' | i18n }}
<div tuiSubtitle>{{ 'Set up a brand new server' | i18n }}</div>
@@ -22,7 +22,7 @@ import { StateService } from '../services/state.service'
</button>
<button tuiCell="l" (click)="restore()">
<tui-avatar appearance="warning" src="@tui.archive-restore" />
<span tuiAvatar="@tui.archive-restore" appearance="warning"></span>
<div tuiTitle>
{{ 'Restore from Backup' | i18n }}
<div tuiSubtitle>
@@ -32,7 +32,7 @@ import { StateService } from '../services/state.service'
</button>
<button tuiCell="l" (click)="transfer()">
<tui-avatar appearance="info" src="@tui.hard-drive-download" />
<span tuiAvatar="@tui.hard-drive-download" appearance="info"></span>
<div tuiTitle>
{{ 'Transfer' | i18n }}
<div tuiSubtitle>
@@ -42,15 +42,7 @@ import { StateService } from '../services/state.service'
</button>
</div>
`,
imports: [
TuiAppearance,
TuiCardLarge,
TuiHeader,
TuiCell,
TuiTitle,
TuiAvatar,
i18nPipe,
],
imports: [TuiCardLarge, TuiHeader, TuiCell, TuiTitle, TuiAvatar, i18nPipe],
})
export default class HomePage {
private readonly router = inject(Router)

View File

@@ -6,8 +6,8 @@ import {
Keyboard,
LanguageCode,
} from '@start9labs/shared'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core'
import { WA_IS_MOBILE } from '@ng-web-apis/platform'
import { TuiButton, TuiTitle } from '@taiga-ui/core'
import {
TuiButtonLoading,
TuiChevron,
@@ -36,11 +36,7 @@ import { StateService } from '../services/state.service'
<input tuiSelect [(ngModel)]="selected" />
}
@if (!mobile) {
<tui-data-list-wrapper
new
*tuiTextfieldDropdown
[items]="keyboards"
/>
<tui-data-list-wrapper *tuiDropdown [items]="keyboards" />
}
</tui-textfield>
@@ -61,7 +57,6 @@ import { StateService } from '../services/state.service'
TuiCardLarge,
TuiButton,
TuiButtonLoading,
TuiTextfield,
TuiChevron,
TuiSelect,
TuiDataListWrapper,
@@ -74,7 +69,7 @@ export default class KeyboardPage {
private readonly api = inject(ApiService)
private readonly stateService = inject(StateService)
protected readonly mobile = inject(TUI_IS_MOBILE)
protected readonly mobile = inject(WA_IS_MOBILE)
// All keyboards, with language-specific keyboards at the top
readonly keyboards = getAllKeyboardsSorted(
this.stateService.language as LanguageCode,

View File

@@ -2,9 +2,10 @@ import { Component, computed, inject, signal } from '@angular/core'
import { Router } from '@angular/router'
import { FormsModule } from '@angular/forms'
import { i18nPipe, i18nService, Language, LANGUAGES } from '@start9labs/shared'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core'
import { WA_IS_MOBILE } from '@ng-web-apis/platform'
import { TuiButton, TuiCell, TuiTitle } from '@taiga-ui/core'
import {
TuiAvatar,
TuiButtonLoading,
TuiChevron,
TuiDataListWrapper,
@@ -18,13 +19,15 @@ import { StateService } from '../services/state.service'
template: `
<section tuiCardLarge="compact">
<header tuiHeader>
<h2 tuiTitle>
<span class="inline-title">
<img src="assets/img/icon.png" alt="Start9" />
<hgroup tuiTitle>
<h2 tuiCell="m">
<span tuiAvatar>
<img src="assets/img/icon.png" alt="Start9" />
</span>
{{ 'Welcome to' | i18n }} StartOS
</span>
<span tuiSubtitle>{{ 'Select your language' | i18n }}</span>
</h2>
</h2>
<p tuiSubtitle>{{ 'Select your language' | i18n }}</p>
</hgroup>
</header>
<tui-textfield
tuiChevron
@@ -48,8 +51,7 @@ import { StateService } from '../services/state.service'
}
@if (!mobile) {
<tui-data-list-wrapper
*tuiTextfieldDropdown
new
*tuiDropdown
[items]="languages"
[itemContent]="itemContent"
/>
@@ -57,11 +59,10 @@ import { StateService } from '../services/state.service'
</tui-textfield>
<ng-template #itemContent let-item>
@let lang = asLanguage(item);
<div class="language-item">
<span>{{ lang.nativeName }}</span>
<small>{{ lang.name | i18n }}</small>
</div>
<span tuiTitle>
{{ asLanguage(item).nativeName }}
<span tuiSubtitle>{{ asLanguage(item).name | i18n }}</span>
</span>
</ng-template>
<footer>
@@ -76,22 +77,13 @@ import { StateService } from '../services/state.service'
</footer>
</section>
`,
styles: `
.language-item {
display: flex;
flex-direction: column;
small {
opacity: 0.7;
}
}
`,
imports: [
FormsModule,
TuiCardLarge,
TuiButton,
TuiButtonLoading,
TuiTextfield,
TuiAvatar,
TuiCell,
TuiChevron,
TuiSelect,
TuiDataListWrapper,
@@ -106,7 +98,7 @@ export default class LanguagePage {
private readonly stateService = inject(StateService)
private readonly i18nService = inject(i18nService)
protected readonly mobile = inject(TUI_IS_MOBILE)
protected readonly mobile = inject(WA_IS_MOBILE)
readonly languages = LANGUAGES
selected =

View File

@@ -12,10 +12,10 @@ import {
getErrorMessage,
i18nPipe,
InitializingComponent,
LoadingService,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { TuiButton } from '@taiga-ui/core'
import { TuiNotificationMiddleService } from '@taiga-ui/kit'
import {
catchError,
filter,
@@ -64,7 +64,7 @@ import { StateService } from '../services/state.service'
})
export default class LoadingPage {
private readonly api = inject(ApiService)
private readonly loader = inject(LoadingService)
private readonly loader = inject(TuiNotificationMiddleService)
private readonly dialog = inject(DialogService)
private readonly router = inject(Router)

View File

@@ -1,4 +1,3 @@
import { AsyncPipe } from '@angular/common'
import { Component, inject } from '@angular/core'
import { Router } from '@angular/router'
import {
@@ -8,31 +7,28 @@ import {
ReactiveFormsModule,
Validators,
} from '@angular/forms'
import {
ErrorService,
i18nPipe,
LoadingService,
normalizeHostname,
} from '@start9labs/shared'
import { ErrorService, i18nPipe, normalizeHostname } from '@start9labs/shared'
import { TuiAutoFocus, TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
import {
TuiButton,
TuiError,
TuiIcon,
TuiTextfield,
TuiTitle,
} from '@taiga-ui/core'
import {
TuiFieldErrorPipe,
TuiPassword,
TuiInput,
tuiValidationErrorsProvider,
} from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
} from '@taiga-ui/core'
import { TuiNotificationMiddleService, TuiPassword } from '@taiga-ui/kit'
import { TuiCardLarge, TuiForm, TuiHeader } from '@taiga-ui/layout'
import { StateService } from '../services/state.service'
@Component({
template: `
<section tuiCardLarge="compact">
<form
tuiCardLarge="compact"
tuiForm
[formGroup]="form"
(ngSubmit)="submit()"
>
<header tuiHeader>
<h2 tuiTitle>
{{
@@ -43,104 +39,80 @@ import { StateService } from '../services/state.service'
</h2>
</header>
<form [formGroup]="form" (ngSubmit)="submit()">
@if (isFresh) {
<tui-textfield>
<label tuiLabel>{{ 'Server Name' | i18n }}</label>
<input tuiTextfield tuiAutoFocus formControlName="name" />
</tui-textfield>
<tui-error
formControlName="name"
[error]="[] | tuiFieldError | async"
/>
@if (form.controls.name.value?.trim()) {
<p class="hostname-preview">{{ derivedHostname }}.local</p>
}
@if (isFresh) {
<tui-textfield>
<label tuiLabel>{{ 'Server Name' | i18n }}</label>
<input tuiInput tuiAutoFocus formControlName="name" />
</tui-textfield>
<tui-error formControlName="name" />
@if (form.controls.name.value?.trim()) {
<tui-error class="g-secondary" error="{{ derivedHostname }}.local" />
}
}
<tui-textfield [style.margin-top.rem]="isFresh ? 1 : 0">
<label tuiLabel>
{{ isFresh ? ('Password' | i18n) : ('New Password' | i18n) }}
</label>
<input
tuiTextfield
type="password"
[tuiAutoFocus]="!isFresh"
maxlength="64"
formControlName="password"
/>
<tui-icon tuiPassword />
</tui-textfield>
<tui-error
<tui-textfield>
<label tuiLabel>
{{ isFresh ? ('Password' | i18n) : ('New Password' | i18n) }}
</label>
<input
tuiInput
type="password"
[tuiAutoFocus]="!isFresh"
maxlength="64"
formControlName="password"
[error]="[] | tuiFieldError | async"
/>
<tui-icon tuiPassword />
</tui-textfield>
<tui-error formControlName="password" />
<tui-textfield [style.margin-top.rem]="1">
<label tuiLabel>{{ 'Confirm Password' | i18n }}</label>
<input
tuiTextfield
type="password"
formControlName="confirm"
[tuiValidator]="
form.controls.password.value || '' | tuiMapper: validator
"
/>
<tui-icon tuiPassword />
</tui-textfield>
<tui-error
<tui-textfield>
<label tuiLabel>{{ 'Confirm Password' | i18n }}</label>
<input
tuiInput
type="password"
formControlName="confirm"
[error]="[] | tuiFieldError | async"
[tuiValidator]="
form.controls.password.value || '' | tuiMapper: validator
"
/>
<tui-icon tuiPassword />
</tui-textfield>
<tui-error formControlName="confirm" />
<footer>
<footer>
<button
tuiButton
size="m"
[disabled]="
isFresh
? form.invalid
: form.controls.password.value && form.invalid
"
>
{{ 'Finish' | i18n }}
</button>
@if (!isFresh) {
<button
tuiButton
[disabled]="
isFresh
? form.invalid
: form.controls.password.value && form.invalid
"
size="m"
appearance="secondary"
type="button"
(click)="skip()"
>
{{ 'Finish' | i18n }}
{{ 'Skip' | i18n }}
</button>
@if (!isFresh) {
<button
tuiButton
appearance="secondary"
type="button"
(click)="skip()"
>
{{ 'Skip' | i18n }}
</button>
}
</footer>
</form>
</section>
`,
styles: `
.hostname-preview {
color: var(--tui-text-secondary);
font: var(--tui-font-text-s);
margin-top: 0.25rem;
}
footer {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1.5rem;
}
}
</footer>
</form>
`,
imports: [
AsyncPipe,
ReactiveFormsModule,
TuiCardLarge,
TuiButton,
TuiError,
TuiAutoFocus,
TuiFieldErrorPipe,
TuiTextfield,
TuiInput,
TuiForm,
TuiPassword,
TuiValidator,
TuiIcon,
@@ -160,7 +132,7 @@ import { StateService } from '../services/state.service'
})
export default class PasswordPage {
private readonly router = inject(Router)
private readonly loader = inject(LoadingService)
private readonly loader = inject(TuiNotificationMiddleService)
private readonly errorService = inject(ErrorService)
private readonly stateService = inject(StateService)
private readonly i18n = inject(i18nPipe)

View File

@@ -6,11 +6,11 @@ import {
TuiButton,
TuiDataList,
TuiDropdown,
TuiIcon,
TuiLink,
TuiLoader,
TuiOptGroup,
TuiTitle,
} from '@taiga-ui/core'
import { TuiChevron } from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { ApiService } from '../services/api.service'
@@ -18,22 +18,25 @@ import { StateService } from '../services/state.service'
import { StartOSDiskInfoFull, StartOSDiskInfoWithId } from '../types'
import { CIFS, CifsResult } from '../components/cifs.component'
import { SELECT_NETWORK_BACKUP } from '../components/select-network-backup.dialog'
import { UnlockPasswordDialog } from '../components/unlock-password.dialog'
import { UNLOCK_PASSWORD } from '../components/unlock-password.dialog'
@Component({
template: `
<section tuiCardLarge="compact">
<header tuiHeader>
<h2 tuiTitle>
{{ 'Select Backup' | i18n }}
<span tuiSubtitle>
<hgroup tuiTitle>
<h2>{{ 'Select Backup' | i18n }}</h2>
<p tuiSubtitle>
{{ 'Select the StartOS backup you want to restore' | i18n }}
<a class="refresh" (click)="refresh()">
<tui-icon icon="@tui.rotate-cw" />
{{ 'Refresh' | i18n }}
</a>
</span>
</h2>
<button
tuiLink
appearance="action"
iconEnd="@tui.rotate-cw"
[textContent]="'Refresh' | i18n"
(click)="refresh()"
></button>
</p>
</hgroup>
</header>
@if (loading) {
@@ -41,82 +44,50 @@ import { UnlockPasswordDialog } from '../components/unlock-password.dialog'
} @else {
<button
tuiButton
iconEnd="@tui.chevron-down"
[tuiDropdown]="dropdown"
[tuiDropdownLimitWidth]="'fixed'"
tuiChevron
tuiDropdown
tuiDropdownLimitWidth="fixed"
[(tuiDropdownOpen)]="open"
style="width: 100%"
>
{{ 'Select Backup' | i18n }}
</button>
<ng-template #dropdown>
<tui-data-list>
<tui-opt-group>
<button tuiOption new (click)="openCifs()">
<tui-icon icon="@tui.folder-plus" />
{{ 'Open Network Backup' | i18n }}
</button>
</tui-opt-group>
<tui-data-list *tuiDropdown>
<button tuiOption iconStart="@tui.folder-plus" (click)="openCifs()">
{{ 'Open Network Backup' | i18n }}
</button>
<hr />
<tui-opt-group [label]="'Physical Backups' | i18n">
@for (server of physicalServers; track server.id) {
<button tuiOption new (click)="selectPhysicalBackup(server)">
<div class="server-item">
<span>{{ server.id }}</span>
<small>
<button tuiOption (click)="selectPhysicalBackup(server)">
<span tuiTitle>
{{ server.id }}
<span tuiSubtitle>
{{ server.drive.vendor }} {{ server.drive.model }} ·
{{ server.partition.logicalname }}
</small>
</div>
</span>
</span>
</button>
} @empty {
<div class="no-items">{{ 'No physical backups' | i18n }}</div>
<button tuiOption [disabled]="true">
{{ 'No physical backups' | i18n }}
</button>
}
</tui-opt-group>
</tui-data-list>
</ng-template>
</button>
}
</section>
`,
styles: `
.refresh {
display: inline-flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
color: var(--tui-text-action);
tui-icon {
font-size: 0.875rem;
}
}
.server-item {
display: flex;
flex-direction: column;
small {
opacity: 0.7;
}
}
.no-items {
padding: 0.5rem 0.75rem;
color: var(--tui-text-secondary);
font-style: italic;
}
`,
imports: [
TuiButton,
TuiCardLarge,
TuiDataList,
TuiDropdown,
TuiLoader,
TuiIcon,
TuiOptGroup,
TuiTitle,
TuiHeader,
i18nPipe,
TuiLink,
TuiChevron,
],
})
export default class RestorePage {
@@ -142,10 +113,7 @@ export default class RestorePage {
openCifs() {
this.open = false
this.dialogs
.openComponent<CifsResult>(CIFS, {
label: 'Connect Network Folder',
size: 's',
})
.openComponent<CifsResult>(CIFS, { label: 'Connect Network Folder' })
.subscribe(result => {
if (result) {
this.handleCifsResult(result)
@@ -167,7 +135,7 @@ export default class RestorePage {
type: 'cifs',
...result.cifs,
})
} else if (result.servers.length > 1) {
} else {
this.showSelectNetworkBackupDialog(result.cifs, result.servers)
}
}
@@ -178,8 +146,6 @@ export default class RestorePage {
) {
this.dialogs
.openComponent<StartOSDiskInfoWithId | null>(SELECT_NETWORK_BACKUP, {
label: 'Select Network Backup',
size: 's',
data: { servers },
})
.subscribe(server => {
@@ -194,13 +160,7 @@ export default class RestorePage {
target: { type: 'disk'; logicalname: string } | ({ type: 'cifs' } & T.Cifs),
) {
this.dialogs
.openComponent<string | null>(
new PolymorpheusComponent(UnlockPasswordDialog),
{
label: 'Unlock Backup',
size: 's',
},
)
.openComponent<string | null>(UNLOCK_PASSWORD)
.subscribe(password => {
if (password) {
this.stateService.recoverySource = {

View File

@@ -12,9 +12,9 @@ import {
ErrorService,
i18nPipe,
} from '@start9labs/shared'
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiIcon, TuiLoader, TuiTitle, TuiCell } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { DocumentationComponent } from '../components/documentation.component'
@@ -50,7 +50,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
<!-- Step: Download Address Info (non-kiosk only) -->
@if (!stateService.kiosk) {
<button tuiCell="l" [disabled]="downloaded" (click)="download()">
<tui-avatar appearance="secondary" src="@tui.download" />
<span tuiAvatar="@tui.download" appearance="secondary"></span>
<div tuiTitle>
{{ 'Download Address Info' | i18n }}
<div tuiSubtitle>
@@ -74,7 +74,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
(click)="removeMedia()"
>
<tui-avatar appearance="secondary" src="@tui.usb" />
<span tuiAvatar="@tui.usb" appearance="secondary"></span>
<div tuiTitle>
{{ 'Remove Installation Media' | i18n }}
<div tuiSubtitle>
@@ -96,7 +96,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
[disabled]="!usbRemoved || rebooted || rebooting"
(click)="reboot()"
>
<tui-avatar appearance="secondary" src="@tui.rotate-cw" />
<span tuiAvatar="@tui.rotate-cw" appearance="secondary"></span>
<div tuiTitle>
{{ 'Restart Server' | i18n }}
<div tuiSubtitle>
@@ -125,7 +125,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
[disabled]="!canOpenAddress"
(click)="openLocalAddress()"
>
<tui-avatar appearance="secondary" src="@tui.external-link" />
<span tuiAvatar="@tui.external-link" appearance="secondary"></span>
<div tuiTitle>
{{ 'Open Local Address' | i18n }}
<div tuiSubtitle>{{ lanAddress }}</div>
@@ -143,7 +143,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
[disabled]="result.needsRestart && !rebooted"
(click)="exitKiosk()"
>
<tui-avatar appearance="secondary" src="@tui.log-in" />
<span tuiAvatar="@tui.log-in" appearance="secondary"></span>
<div tuiTitle>
{{ 'Continue to Login' | i18n }}
<div tuiSubtitle>
@@ -233,9 +233,8 @@ export default class SuccessPage implements AfterViewInit {
removeMedia() {
this.dialogs
.openComponent<boolean>(new PolymorpheusComponent(RemoveMediaDialog), {
size: 's',
dismissible: false,
closeable: false,
closable: false,
})
.subscribe(() => {
this.usbRemoved = true

View File

@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'
import { Component, inject, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import {
DialogService,
@@ -11,10 +11,11 @@ import {
TuiButton,
TuiDataList,
TuiDropdown,
TuiIcon,
TuiLink,
TuiLoader,
TuiTitle,
} from '@taiga-ui/core'
import { TuiChevron } from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { filter } from 'rxjs'
import { ApiService } from '../services/api.service'
@@ -24,18 +25,21 @@ import { StateService } from '../services/state.service'
template: `
<section tuiCardLarge="compact">
<header tuiHeader>
<h2 tuiTitle>
{{ 'Transfer Data' | i18n }}
<span tuiSubtitle>
<hgroup tuiTitle>
<h2>{{ 'Transfer Data' | i18n }}</h2>
<p tuiSubtitle>
{{
'Select the drive containing your existing StartOS data' | i18n
}}
<a class="refresh" (click)="refresh()">
<tui-icon icon="@tui.rotate-cw" />
{{ 'Refresh' | i18n }}
</a>
</span>
</h2>
<button
tuiLink
appearance="action"
iconEnd="@tui.rotate-cw"
[textContent]="'Refresh' | i18n"
(click)="refresh()"
></button>
</p>
</hgroup>
</header>
@if (loading) {
@@ -43,75 +47,43 @@ import { StateService } from '../services/state.service'
} @else {
<button
tuiButton
iconEnd="@tui.chevron-down"
[tuiDropdown]="dropdown"
[tuiDropdownLimitWidth]="'fixed'"
tuiChevron
tuiDropdown
tuiDropdownLimitWidth="fixed"
[(tuiDropdownOpen)]="open"
style="width: 100%"
>
{{ 'Select Drive' | i18n }}
</button>
<ng-template #dropdown>
<tui-data-list>
<tui-data-list
*tuiDropdown
[emptyContent]="'No StartOS data drives found' | i18n"
>
@for (drive of drives; track drive.logicalname) {
<button tuiOption new (click)="select(drive)">
<div class="drive-item">
<span>{{ drive.vendor }} {{ drive.model }}</span>
<small>{{ drive.logicalname }}</small>
</div>
<button tuiOption (click)="select(drive)">
<span tuiTitle>
{{ drive.vendor }} {{ drive.model }}
<span tuiSubtitle>{{ drive.logicalname }}</span>
</span>
</button>
} @empty {
<div class="no-items">
{{ 'No StartOS data drives found' | i18n }}
</div>
}
</tui-data-list>
</ng-template>
</button>
}
</section>
`,
styles: `
.refresh {
display: inline-flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
color: var(--tui-text-action);
tui-icon {
font-size: 0.875rem;
}
}
.drive-item {
display: flex;
flex-direction: column;
small {
opacity: 0.7;
}
}
.no-items {
padding: 0.5rem 0.75rem;
color: var(--tui-text-secondary);
font-style: italic;
}
`,
imports: [
TuiButton,
TuiCardLarge,
TuiDataList,
TuiDropdown,
TuiIcon,
TuiLink,
TuiChevron,
TuiLoader,
TuiTitle,
TuiHeader,
i18nPipe,
],
})
export default class TransferPage {
export default class TransferPage implements OnInit {
private readonly api = inject(ApiService)
private readonly router = inject(Router)
private readonly dialogs = inject(DialogService)
@@ -137,7 +109,6 @@ export default class TransferPage {
this.dialogs
.openConfirm({
label: 'Warning',
size: 's',
data: {
content:
'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.',

View File

@@ -12,6 +12,10 @@ tui-root {
height: 100%;
}
ul {
padding-inline-start: 1rem;
}
router-outlet + * {
height: 100%;
max-width: min(35rem, 100vw);
@@ -30,41 +34,11 @@ router-outlet + * {
}
}
.inline-title {
display: inline-flex;
align-items: center;
gap: 0.5rem;
:first-child {
width: 2rem;
height: 2rem;
}
}
button:disabled {
opacity: var(--tui-disabled-opacity);
pointer-events: none;
}
header {
position: relative;
display: flex;
flex-direction: column;
text-align: center;
font: var(--tui-font-heading-4);
p {
font: var(--tui-font-text-m);
color: var(--tui-text-secondary);
}
}
h2 {
margin: 0;
font: var(--tui-font-heading-6);
}
.g-positive {
color: var(--tui-status-positive);
}
@@ -77,14 +51,27 @@ h2 {
color: var(--tui-status-negative);
}
.g-secondary {
color: var(--tui-text-secondary) !important;
}
.g-info {
color: var(--tui-status-info);
}
[tuiCardLarge] footer button {
width: 100%;
[tuiCardLarge] footer {
display: flex;
button {
flex: 1;
}
}
[tuiCell]:not(:last-of-type) {
[tuiCell]:not([tuiOption]):not(:last-of-type) {
box-shadow: 0 calc(0.5rem + 1px) 0 -0.5rem var(--tui-border-normal);
}
}
// TODO: Remove in Taiga v5.0
[tuiButton] {
min-block-size: var(--t-size);
}