mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
refactor: finalize new portal (#2543)
This commit is contained in:
@@ -178,8 +178,9 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
||||
box-shadow:
|
||||
1rem 0 var(--tui-clear),
|
||||
-1rem 0 var(--tui-clear);
|
||||
padding-top: 0.375rem !important;
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiDataListModule } from '@taiga-ui/core'
|
||||
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||
|
||||
export interface Action {
|
||||
icon: string
|
||||
label: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
template: `
|
||||
<tui-data-list>
|
||||
<h3 class="title"><ng-content /></h3>
|
||||
<tui-opt-group
|
||||
*ngFor="let group of actions | keyvalue: asIsOrder"
|
||||
[label]="group.key.toUpperCase()"
|
||||
>
|
||||
<button
|
||||
*ngFor="let action of group.value"
|
||||
tuiOption
|
||||
class="item"
|
||||
(click)="action.action()"
|
||||
>
|
||||
<tui-icon class="icon" [icon]="action.icon" />
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item {
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiDataListModule, CommonModule, TuiIconModule],
|
||||
})
|
||||
export class ActionsComponent {
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<tui-data-list>
|
||||
<h3 class="title"><ng-content></ng-content></h3>
|
||||
<tui-opt-group
|
||||
*ngFor="let group of actions | keyvalue : asIsOrder"
|
||||
[label]="group.key.toUpperCase()"
|
||||
>
|
||||
<button
|
||||
*ngFor="let action of group.value"
|
||||
tuiOption
|
||||
class="item"
|
||||
(click)="action.action()"
|
||||
>
|
||||
<tui-svg class="icon" [src]="action.icon"></tui-svg>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
@@ -1,16 +0,0 @@
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item {
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiDataListModule, TuiSvgModule } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
export interface Action {
|
||||
icon: string
|
||||
label: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiDataListModule, TuiSvgModule, CommonModule],
|
||||
})
|
||||
export class ActionsComponent {
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
158
web/projects/ui/src/app/apps/portal/components/card.component.ts
Normal file
158
web/projects/ui/src/app/apps/portal/components/card.component.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
TuiBadgedContentModule,
|
||||
TuiBadgeNotificationModule,
|
||||
TuiButtonModule,
|
||||
TuiIconModule,
|
||||
} from '@taiga-ui/experimental'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TickerModule } from '@start9labs/shared'
|
||||
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
||||
import { NavigationService } from '../services/navigation.service'
|
||||
import { Action, ActionsComponent } from './actions.component'
|
||||
import { toRouterLink } from '../utils/to-router-link'
|
||||
|
||||
@Component({
|
||||
selector: '[appCard]',
|
||||
template: `
|
||||
<span class="link">
|
||||
<tui-badged-content [style.--tui-radius.rem]="1.5">
|
||||
@if (badge) {
|
||||
<tui-badge-notification size="m" tuiSlot="top">
|
||||
{{ badge }}
|
||||
</tui-badge-notification>
|
||||
}
|
||||
@if (icon?.startsWith('tuiIcon')) {
|
||||
<tui-icon class="icon" [icon]="icon" />
|
||||
} @else {
|
||||
<img alt="" class="icon" [src]="icon" />
|
||||
}
|
||||
</tui-badged-content>
|
||||
<label ticker class="title">{{ title }}</label>
|
||||
</span>
|
||||
@if (isService) {
|
||||
<span class="side">
|
||||
<tui-hosted-dropdown
|
||||
[content]="content"
|
||||
(click.stop.prevent)="(0)"
|
||||
(pointerdown.stop)="(0)"
|
||||
>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="outline"
|
||||
size="xs"
|
||||
iconLeft="tuiIconMoreHorizontal"
|
||||
[style.border-radius.%]="100"
|
||||
>
|
||||
Actions
|
||||
</button>
|
||||
<ng-template #content let-close="close">
|
||||
<app-actions [actions]="actions" (click)="close()">
|
||||
{{ title }}
|
||||
</app-actions>
|
||||
</ng-template>
|
||||
</tui-hosted-dropdown>
|
||||
</span>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: flex;
|
||||
height: 5.5rem;
|
||||
width: 12.5rem;
|
||||
border-radius: var(--tui-radius-l);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||
// TODO: Theme
|
||||
background: rgb(111 109 109);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
gap: 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
font: var(--tui-font-text-m);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 100%;
|
||||
color: var(--tui-text-01-night);
|
||||
}
|
||||
|
||||
.side {
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||
// TODO: Theme
|
||||
background: #4b4a4a;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
TuiButtonModule,
|
||||
TuiHostedDropdownModule,
|
||||
TuiDataListModule,
|
||||
TuiIconModule,
|
||||
TickerModule,
|
||||
TuiBadgedContentModule,
|
||||
TuiBadgeNotificationModule,
|
||||
ActionsComponent,
|
||||
],
|
||||
})
|
||||
export class CardComponent {
|
||||
private readonly navigation = inject(NavigationService)
|
||||
|
||||
@Input({ required: true })
|
||||
id!: string
|
||||
|
||||
@Input({ required: true })
|
||||
icon!: string
|
||||
|
||||
@Input({ required: true })
|
||||
title!: string
|
||||
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
@Input()
|
||||
badge: number | null = null
|
||||
|
||||
get isService(): boolean {
|
||||
return !this.id.includes('/')
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
const { id, icon, title } = this
|
||||
const routerLink = toRouterLink(id)
|
||||
|
||||
this.navigation.addTab({ icon, title, routerLink })
|
||||
}
|
||||
|
||||
// Prevents Firefox from starting a native drag
|
||||
@HostListener('pointerdown.prevent')
|
||||
onDown() {}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<span class="link">
|
||||
<tui-badged-content [style.--tui-radius.rem]="1.5">
|
||||
<tui-badge-notification *ngIf="badge" size="m" tuiSlot="top">
|
||||
{{ badge }}
|
||||
</tui-badge-notification>
|
||||
<tui-icon
|
||||
*ngIf="icon?.startsWith('tuiIcon'); else url"
|
||||
class="icon"
|
||||
[icon]="icon"
|
||||
/>
|
||||
<ng-template #url>
|
||||
<img alt="" class="icon" [src]="icon" />
|
||||
</ng-template>
|
||||
</tui-badged-content>
|
||||
<label ticker class="title">{{ title }}</label>
|
||||
</span>
|
||||
<span *ngIf="isService" class="side">
|
||||
<tui-hosted-dropdown
|
||||
#dropdown
|
||||
[content]="content"
|
||||
(click.stop.prevent)="(0)"
|
||||
(pointerdown.stop)="(0)"
|
||||
>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="outline"
|
||||
size="xs"
|
||||
iconLeft="tuiIconMoreHorizontal"
|
||||
[style.border-radius.%]="100"
|
||||
>
|
||||
Actions
|
||||
</button>
|
||||
<ng-template #content>
|
||||
<app-actions
|
||||
[actions]="actions"
|
||||
(click)="dropdown.openChange.next(false)"
|
||||
>
|
||||
{{ title }}
|
||||
</app-actions>
|
||||
</ng-template>
|
||||
</tui-hosted-dropdown>
|
||||
</span>
|
||||
@@ -1,49 +0,0 @@
|
||||
:host {
|
||||
display: flex;
|
||||
height: 5.5rem;
|
||||
width: 12.5rem;
|
||||
border-radius: var(--tui-radius-l);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||
// TODO: Theme
|
||||
background: rgb(111 109 109);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
gap: 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
font: var(--tui-font-text-m);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 100%;
|
||||
color: var(--tui-text-01-night);
|
||||
}
|
||||
|
||||
tui-svg.icon {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.side {
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||
// TODO: Theme
|
||||
background: #4b4a4a;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
TuiBadgedContentModule,
|
||||
TuiBadgeNotificationModule,
|
||||
TuiButtonModule,
|
||||
TuiIconModule,
|
||||
} from '@taiga-ui/experimental'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TickerModule } from '@start9labs/shared'
|
||||
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
||||
import { NavigationService } from '../../services/navigation.service'
|
||||
import { Action, ActionsComponent } from '../actions/actions.component'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
|
||||
@Component({
|
||||
selector: '[appCard]',
|
||||
templateUrl: 'card.component.html',
|
||||
styleUrls: ['card.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
TuiButtonModule,
|
||||
TuiHostedDropdownModule,
|
||||
TuiDataListModule,
|
||||
TuiIconModule,
|
||||
TickerModule,
|
||||
TuiBadgedContentModule,
|
||||
TuiBadgeNotificationModule,
|
||||
ActionsComponent,
|
||||
],
|
||||
})
|
||||
export class CardComponent {
|
||||
private readonly navigation = inject(NavigationService)
|
||||
|
||||
@Input({ required: true })
|
||||
id!: string
|
||||
|
||||
@Input({ required: true })
|
||||
icon!: string
|
||||
|
||||
@Input({ required: true })
|
||||
title!: string
|
||||
|
||||
@Input()
|
||||
actions: Record<string, readonly Action[]> = {}
|
||||
|
||||
@Input()
|
||||
badge: number | null = null
|
||||
|
||||
get isService(): boolean {
|
||||
return !this.id.includes('/')
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
const { id, icon, title } = this
|
||||
const routerLink = toRouterLink(id)
|
||||
|
||||
this.navigation.addTab({ icon, title, routerLink })
|
||||
}
|
||||
|
||||
@HostListener('pointerdown.prevent')
|
||||
onDown() {
|
||||
// Prevents Firefox from starting a native drag
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="content" (tuiActiveZoneChange)="open = $event">
|
||||
<button class="toggle" (click)="open = !open" (mousedown.prevent)="(0)">
|
||||
<tui-svg src="tuiIconArrowUpCircleLarge" class="icon"></tui-svg>
|
||||
<tui-icon icon="tuiIconArrowUpCircle" class="icon" />
|
||||
Toggle drawer
|
||||
</button>
|
||||
<tui-input
|
||||
@@ -16,37 +16,42 @@
|
||||
<tui-scrollbar class="scrollbar">
|
||||
<h2 class="title">System Utilities</h2>
|
||||
<div class="items">
|
||||
<a
|
||||
*ngFor="
|
||||
let item of system | keyvalue | tuiFilter : bySearch : search;
|
||||
empty: empty
|
||||
"
|
||||
appCard
|
||||
[badge]="item.key | toBadge | async"
|
||||
[drawerItem]="item.key"
|
||||
[id]="item.key"
|
||||
[title]="item.value.title"
|
||||
[icon]="item.value.icon"
|
||||
[routerLink]="item.key"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
@for (
|
||||
item of system | keyvalue | tuiFilter: bySearch : search;
|
||||
track $index
|
||||
) {
|
||||
<a
|
||||
appCard
|
||||
[badge]="item.key | toBadge | async"
|
||||
[drawerItem]="item.key"
|
||||
[id]="item.key"
|
||||
[title]="item.value.title"
|
||||
[icon]="item.value.icon"
|
||||
[routerLink]="item.key"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
} @empty {
|
||||
Nothing found
|
||||
}
|
||||
</div>
|
||||
<h2 class="title">Installed services</h2>
|
||||
<div class="items">
|
||||
<a
|
||||
*ngFor="
|
||||
let item of (services$ | async) || [] | tuiFilter : bySearch : search;
|
||||
empty: empty
|
||||
"
|
||||
appCard
|
||||
[drawerItem]="item.manifest.id"
|
||||
[id]="item.manifest.id"
|
||||
[icon]="item.icon"
|
||||
[title]="item.manifest.title"
|
||||
[routerLink]="getLink(item.manifest.id)"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
@for (
|
||||
item of (services$ | async) || [] | tuiFilter: bySearch : search;
|
||||
track $index
|
||||
) {
|
||||
<a
|
||||
appCard
|
||||
[drawerItem]="item.manifest.id"
|
||||
[id]="item.manifest.id"
|
||||
[icon]="item.icon"
|
||||
[title]="item.manifest.title"
|
||||
[routerLink]="getLink(item.manifest.id)"
|
||||
(click)="open = false"
|
||||
></a>
|
||||
} @empty {
|
||||
Nothing found
|
||||
}
|
||||
</div>
|
||||
<ng-template #empty>Nothing found</ng-template>
|
||||
</tui-scrollbar>
|
||||
</div>
|
||||
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
} from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiScrollbarModule,
|
||||
TuiSvgModule,
|
||||
TuiTextfieldControllerModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||
import { TuiInputModule } from '@taiga-ui/kit'
|
||||
import { CardComponent } from '../card/card.component'
|
||||
import { CardComponent } from '../card.component'
|
||||
import { ServicesService } from '../../services/services.service'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
import { DrawerItemDirective } from './drawer-item.directive'
|
||||
@@ -36,7 +36,6 @@ import { ToBadgePipe } from '../../pipes/to-badge'
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
TuiSvgModule,
|
||||
TuiScrollbarModule,
|
||||
TuiActiveZoneModule,
|
||||
TuiInputModule,
|
||||
@@ -46,6 +45,7 @@ import { ToBadgePipe } from '../../pipes/to-badge'
|
||||
CardComponent,
|
||||
DrawerItemDirective,
|
||||
ToBadgePipe,
|
||||
TuiIconModule,
|
||||
],
|
||||
})
|
||||
export class DrawerComponent {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, Observable, startWith } from 'rxjs'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'header-connection',
|
||||
template: `
|
||||
@if (connection$ | async; as connection) {
|
||||
<tui-icon
|
||||
[title]="connection.message"
|
||||
[icon]="connection.icon"
|
||||
[style.color]="connection.color"
|
||||
[style.margin.rem]="0.5"
|
||||
></tui-icon>
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiIconModule, AsyncPipe],
|
||||
})
|
||||
export class HeaderConnectionComponent {
|
||||
readonly connection$: Observable<{
|
||||
message: string
|
||||
color: string
|
||||
icon: string
|
||||
}> = combineLatest([
|
||||
inject(ConnectionService).networkConnected$,
|
||||
inject(ConnectionService).websocketConnected$.pipe(startWith(false)),
|
||||
inject(PatchDB<DataModel>)
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
]).pipe(
|
||||
map(([network, websocket, status]) => {
|
||||
if (!network)
|
||||
return {
|
||||
message: 'No Internet',
|
||||
color: 'var(--tui-error-fill)',
|
||||
icon: 'tuiIconCloudOff',
|
||||
}
|
||||
if (!websocket)
|
||||
return {
|
||||
message: 'Connecting',
|
||||
color: 'var(--tui-warning-fill)',
|
||||
icon: 'tuiIconCloudOff',
|
||||
}
|
||||
if (status['shutting-down'])
|
||||
return {
|
||||
message: 'Shutting Down',
|
||||
color: 'var(--tui-neutral-fill)',
|
||||
icon: 'tuiIconPower',
|
||||
}
|
||||
if (status.restarting)
|
||||
return {
|
||||
message: 'Restarting',
|
||||
color: 'var(--tui-neutral-fill)',
|
||||
icon: 'tuiIconPower',
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Connected',
|
||||
color: 'var(--tui-success-fill)',
|
||||
icon: 'tuiIconCloud',
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,26 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import {
|
||||
TuiDataListModule,
|
||||
TuiDialogOptions,
|
||||
TuiDialogService,
|
||||
TuiHostedDropdownModule,
|
||||
TuiSvgModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ABOUT } from './about.component'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'header-menu',
|
||||
template: `
|
||||
<tui-hosted-dropdown
|
||||
[content]="content"
|
||||
[tuiDropdownMaxHeight]="9999"
|
||||
(click.stop.prevent)="(0)"
|
||||
(pointerdown.stop)="(0)"
|
||||
>
|
||||
<tui-hosted-dropdown [content]="content" [tuiDropdownMaxHeight]="9999">
|
||||
<button tuiIconButton appearance="">
|
||||
<img style="max-width: 62%" src="assets/img/icon.png" alt="StartOS" />
|
||||
</button>
|
||||
@@ -26,43 +28,35 @@ import { ABOUT } from './about.component'
|
||||
<tui-data-list>
|
||||
<h3 class="title">StartOS</h3>
|
||||
<button tuiOption class="item" (click)="about()">
|
||||
<tui-svg src="tuiIconInfo"></tui-svg>
|
||||
<tui-icon icon="tuiIconInfo" />
|
||||
About this server
|
||||
</button>
|
||||
<tui-opt-group>
|
||||
<button tuiOption class="item" (click)="({})">
|
||||
<tui-svg src="tuiIconBookOpen"></tui-svg>
|
||||
User Manual
|
||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
||||
</button>
|
||||
<button tuiOption class="item" (click)="({})">
|
||||
<tui-svg src="tuiIconHeadphones"></tui-svg>
|
||||
Contact Support
|
||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
||||
</button>
|
||||
<button tuiOption class="item" (click)="({})">
|
||||
<tui-svg src="tuiIconDollarSign"></tui-svg>
|
||||
Donate to Start9
|
||||
<tui-svg class="external" src="tuiIconArrowUpRight"></tui-svg>
|
||||
</button>
|
||||
@for (link of links; track $index) {
|
||||
<a
|
||||
tuiOption
|
||||
class="item"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="link.href"
|
||||
>
|
||||
<tui-icon [icon]="link.icon" />
|
||||
{{ link.name }}
|
||||
<tui-icon class="external" icon="tuiIconArrowUpRight" />
|
||||
</a>
|
||||
}
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button tuiOption class="item" (click)="({})">
|
||||
<tui-svg src="tuiIconTool"></tui-svg>
|
||||
System Rebuild
|
||||
</button>
|
||||
<button tuiOption class="item" (click)="({})">
|
||||
<tui-svg src="tuiIconRefreshCw"></tui-svg>
|
||||
Restart
|
||||
</button>
|
||||
<button tuiOption class="item" (click)="({})">
|
||||
<tui-svg src="tuiIconPower"></tui-svg>
|
||||
Shutdown
|
||||
</button>
|
||||
@for (item of system; track $index) {
|
||||
<button tuiOption class="item" (click)="prompt(item.action)">
|
||||
<tui-icon [icon]="item.icon" />
|
||||
{{ item.action }}
|
||||
</button>
|
||||
}
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button tuiOption class="item" (click)="logout()">
|
||||
<tui-svg src="tuiIconLogOut"></tui-svg>
|
||||
<tui-icon icon="tuiIconLogOut" />
|
||||
Logout
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
@@ -72,6 +66,10 @@ import { ABOUT } from './about.component'
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
tui-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
@@ -80,7 +78,6 @@ import { ABOUT } from './about.component'
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -98,13 +95,50 @@ import { ABOUT } from './about.component'
|
||||
TuiDataListModule,
|
||||
TuiSvgModule,
|
||||
TuiButtonModule,
|
||||
TuiIconModule,
|
||||
],
|
||||
})
|
||||
export class HeaderMenuComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly auth = inject(AuthService)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
readonly links = [
|
||||
{
|
||||
name: 'User Manual',
|
||||
icon: 'tuiIconBookOpen',
|
||||
href: 'https://docs.start9.com/0.3.5.x/user-manual',
|
||||
},
|
||||
{
|
||||
name: 'Contact Support',
|
||||
icon: 'tuiIconHeadphones',
|
||||
href: 'https://start9.com/contact',
|
||||
},
|
||||
{
|
||||
name: 'Donate to Start9',
|
||||
icon: 'tuiIconDollarSign',
|
||||
href: 'https://donate.start9.com',
|
||||
},
|
||||
]
|
||||
|
||||
readonly system = [
|
||||
{
|
||||
icon: 'tuiIconTool',
|
||||
action: 'System Rebuild',
|
||||
},
|
||||
{
|
||||
icon: 'tuiIconRefreshCw',
|
||||
action: 'Restart',
|
||||
},
|
||||
{
|
||||
icon: 'tuiIconPower',
|
||||
action: 'Shutdown',
|
||||
},
|
||||
] as const
|
||||
|
||||
about() {
|
||||
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
||||
}
|
||||
@@ -113,4 +147,72 @@ export class HeaderMenuComponent {
|
||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||
this.auth.setUnverified()
|
||||
}
|
||||
|
||||
async prompt(action: keyof typeof METHODS) {
|
||||
const minutes =
|
||||
action === 'System Rebuild'
|
||||
? Object.keys(await getAllPackages(this.patch)).length * 2
|
||||
: ''
|
||||
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, getOptions(action, minutes))
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api[METHODS[action]]({})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const METHODS = {
|
||||
Restart: 'restartServer',
|
||||
Shutdown: 'shutdownServer',
|
||||
'System Rebuild': 'systemRebuild',
|
||||
} as const
|
||||
|
||||
function getOptions(
|
||||
key: keyof typeof METHODS,
|
||||
minutes: unknown,
|
||||
): Partial<TuiDialogOptions<TuiPromptData>> {
|
||||
switch (key) {
|
||||
case 'Restart':
|
||||
return {
|
||||
label: 'Restart',
|
||||
size: 's',
|
||||
data: {
|
||||
content:
|
||||
'Are you sure you want to restart your server? It can take several minutes to come back online.',
|
||||
yes: 'Restart',
|
||||
no: 'Cancel',
|
||||
},
|
||||
}
|
||||
case 'Shutdown':
|
||||
return {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content:
|
||||
'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in',
|
||||
yes: 'Shutdown',
|
||||
no: 'Cancel',
|
||||
},
|
||||
}
|
||||
default:
|
||||
return {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`,
|
||||
yes: 'Rebuild',
|
||||
no: 'Cancel',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,19 +25,13 @@ import { SidebarDirective } from '../../../../app/sidebar-host.component'
|
||||
import { HeaderMenuComponent } from './header-menu.component'
|
||||
import { HeaderNotificationsComponent } from './header-notifications.component'
|
||||
import { NotificationService } from '../../services/notification.service'
|
||||
import { HeaderConnectionComponent } from './header-connection.component'
|
||||
|
||||
@Component({
|
||||
selector: 'header[appHeader]',
|
||||
template: `
|
||||
<ng-content></ng-content>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconLeft="tuiIconCloudLarge"
|
||||
appearance="icon-success"
|
||||
[style.margin-left]="'auto'"
|
||||
>
|
||||
Connection
|
||||
</button>
|
||||
<header-connection [style.margin-left]="'auto'" />
|
||||
<tui-badged-content
|
||||
*tuiLet="notificationService.unreadCount$ | async as unread"
|
||||
[style.--tui-radius.%]="50"
|
||||
@@ -87,6 +81,7 @@ import { NotificationService } from '../../services/notification.service'
|
||||
SidebarDirective,
|
||||
HeaderMenuComponent,
|
||||
HeaderNotificationsComponent,
|
||||
HeaderConnectionComponent,
|
||||
TuiLetModule,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { Router, RouterModule } from '@angular/router'
|
||||
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||
import { NavigationService } from '../services/navigation.service'
|
||||
|
||||
@Component({
|
||||
selector: 'nav[appNavigation]',
|
||||
template: `
|
||||
<a
|
||||
class="tab"
|
||||
routerLink="desktop"
|
||||
routerLinkActive="tab_active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<tui-icon icon="tuiIconHome" class="icon" />
|
||||
</a>
|
||||
@for (tab of tabs$ | async; track tab) {
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
class="tab"
|
||||
routerLinkActive="tab_active"
|
||||
[routerLink]="tab.routerLink"
|
||||
>
|
||||
@if (tab.icon.startsWith('tuiIcon')) {
|
||||
<tui-icon class="icon" [icon]="tab.icon" />
|
||||
} @else {
|
||||
<img class="icon" [src]="tab.icon" [alt]="tab.title" />
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
size="xs"
|
||||
iconLeft="tuiIconClose"
|
||||
appearance="icon"
|
||||
class="close"
|
||||
(click.stop.prevent)="removeTab(tab.routerLink, rla.isActive)"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
@include scrollbar-hidden;
|
||||
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
// TODO: Theme
|
||||
background: rgb(97 95 95 / 84%);
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 7.5rem;
|
||||
|
||||
&_active {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
// TODO: Theme
|
||||
background: #373a3f;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 100%;
|
||||
color: var(--tui-base-08);
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule],
|
||||
})
|
||||
export class NavigationComponent {
|
||||
private readonly router = inject(Router)
|
||||
private readonly navigation = inject(NavigationService)
|
||||
|
||||
readonly tabs$ = this.navigation.getTabs()
|
||||
|
||||
removeTab(routerLink: string, active: boolean) {
|
||||
this.navigation.removeTab(routerLink)
|
||||
|
||||
if (active) this.router.navigate(['/portal/desktop'])
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<a
|
||||
class="tab"
|
||||
routerLink="desktop"
|
||||
routerLinkActive="tab_active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<tui-icon icon="tuiIconHome" class="icon" />
|
||||
</a>
|
||||
<a
|
||||
*ngFor="let tab of tabs$ | async"
|
||||
#rla="routerLinkActive"
|
||||
class="tab"
|
||||
routerLinkActive="tab_active"
|
||||
[routerLink]="tab.routerLink"
|
||||
>
|
||||
<tui-icon
|
||||
*ngIf="tab.icon.startsWith('tuiIcon'); else url"
|
||||
class="icon"
|
||||
[icon]="tab.icon"
|
||||
/>
|
||||
<ng-template #url>
|
||||
<img class="icon" [src]="tab.icon" [alt]="tab.title" />
|
||||
</ng-template>
|
||||
<button
|
||||
tuiIconButton
|
||||
size="xs"
|
||||
iconLeft="tuiIconClose"
|
||||
appearance="icon"
|
||||
class="close"
|
||||
(click.stop.prevent)="removeTab(tab.routerLink, rla.isActive)"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</a>
|
||||
@@ -1,42 +0,0 @@
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
@include scrollbar-hidden;
|
||||
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
// TODO: Theme
|
||||
background: rgb(97 95 95 / 84%);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 7.5rem;
|
||||
|
||||
&_active {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
// TODO: Theme
|
||||
background: #373a3f;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 100%;
|
||||
color: var(--tui-base-08);
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { Router, RouterModule } from '@angular/router'
|
||||
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
||||
import { NavigationService } from '../../services/navigation.service'
|
||||
|
||||
@Component({
|
||||
selector: 'nav[appNavigation]',
|
||||
templateUrl: 'navigation.component.html',
|
||||
styleUrls: ['navigation.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule],
|
||||
})
|
||||
export class NavigationComponent {
|
||||
private readonly router = inject(Router)
|
||||
private readonly navigation = inject(NavigationService)
|
||||
|
||||
readonly tabs$ = this.navigation.getTabs()
|
||||
|
||||
removeTab(routerLink: string, active: boolean) {
|
||||
this.navigation.removeTab(routerLink)
|
||||
|
||||
if (active) this.router.navigate(['/portal/desktop'])
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<header appHeader>My server</header>
|
||||
<nav appNavigation></nav>
|
||||
<main>
|
||||
<router-outlet />
|
||||
</main>
|
||||
<app-drawer />
|
||||
@@ -1,10 +0,0 @@
|
||||
:host {
|
||||
// TODO: Theme
|
||||
background: url(/assets/img/background_dark.jpeg);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,10 +1,43 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { HeaderComponent } from './components/header/header.component'
|
||||
import { NavigationComponent } from './components/navigation.component'
|
||||
import { DrawerComponent } from './components/drawer/drawer.component'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
templateUrl: 'portal.component.html',
|
||||
styleUrls: ['portal.component.scss'],
|
||||
standalone: true,
|
||||
template: `
|
||||
<header appHeader>{{ name$ | async }}</header>
|
||||
<nav appNavigation></nav>
|
||||
<main><router-outlet /></main>
|
||||
<app-drawer />
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
// TODO: Theme
|
||||
background: url(/assets/img/background_dark.jpeg);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterOutlet,
|
||||
HeaderComponent,
|
||||
NavigationComponent,
|
||||
DrawerComponent,
|
||||
],
|
||||
providers: [
|
||||
// TODO: Move to global
|
||||
tuiDropdownOptionsProvider({
|
||||
@@ -12,4 +45,6 @@ import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class PortalComponent {}
|
||||
export class PortalComponent {
|
||||
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { HeaderComponent } from './components/header/header.component'
|
||||
import { PortalComponent } from './portal.component'
|
||||
import { NavigationComponent } from './components/navigation/navigation.component'
|
||||
import { DrawerComponent } from './components/drawer/drawer.component'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PortalComponent,
|
||||
children: [
|
||||
{
|
||||
redirectTo: 'desktop',
|
||||
pathMatch: 'full',
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
path: 'desktop',
|
||||
loadChildren: () =>
|
||||
import('./routes/desktop/desktop.module').then(m => m.DesktopModule),
|
||||
},
|
||||
{
|
||||
path: 'service',
|
||||
loadChildren: () =>
|
||||
import('./routes/service/service.module').then(m => m.ServiceModule),
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
loadChildren: () =>
|
||||
import('./routes/system/system.module').then(m => m.SystemModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(ROUTES),
|
||||
HeaderComponent,
|
||||
NavigationComponent,
|
||||
DrawerComponent,
|
||||
],
|
||||
declarations: [PortalComponent],
|
||||
exports: [PortalComponent],
|
||||
})
|
||||
export class PortalModule {}
|
||||
35
web/projects/ui/src/app/apps/portal/portal.routes.ts
Normal file
35
web/projects/ui/src/app/apps/portal/portal.routes.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Routes } from '@angular/router'
|
||||
import { PortalComponent } from './portal.component'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PortalComponent,
|
||||
children: [
|
||||
{
|
||||
redirectTo: 'desktop',
|
||||
pathMatch: 'full',
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
path: 'desktop',
|
||||
loadComponent: () =>
|
||||
import('./routes/desktop/desktop.component').then(
|
||||
m => m.DesktopComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'service',
|
||||
loadChildren: () =>
|
||||
import('./routes/service/service.module').then(m => m.ServiceModule),
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
loadChildren: () =>
|
||||
import('./routes/system/system.module').then(m => m.SystemModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
HostBinding,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { TuiTilesComponent } from '@taiga-ui/kit'
|
||||
@@ -17,7 +16,7 @@ import { TuiTilesComponent } from '@taiga-ui/kit'
|
||||
selector: '[desktopItem]',
|
||||
standalone: true,
|
||||
})
|
||||
export class DesktopItemDirective implements OnInit, OnDestroy {
|
||||
export class DesktopItemDirective implements OnInit {
|
||||
private readonly element: Element = inject(ElementRef).nativeElement
|
||||
private readonly tiles = inject(TuiTilesComponent)
|
||||
|
||||
@@ -32,9 +31,4 @@ export class DesktopItemDirective implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
if (this.empty) this.tiles.element = this.element
|
||||
}
|
||||
|
||||
// TODO: Remove after Taiga UI updated to 3.40.0
|
||||
ngOnDestroy() {
|
||||
if (this.tiles.element === this.element) this.tiles.element = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -6,18 +7,48 @@ import {
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { DragScrollerDirective } from '@start9labs/shared'
|
||||
import { EMPTY_QUERY, TUI_PARENT_STOP } from '@taiga-ui/cdk'
|
||||
import { tuiFadeIn, tuiScaleIn } from '@taiga-ui/core'
|
||||
import { TuiTileComponent, TuiTilesComponent } from '@taiga-ui/kit'
|
||||
import {
|
||||
tuiFadeIn,
|
||||
TuiLoaderModule,
|
||||
tuiScaleIn,
|
||||
TuiSvgModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiFadeModule } from '@taiga-ui/experimental'
|
||||
import {
|
||||
TuiTileComponent,
|
||||
TuiTilesComponent,
|
||||
TuiTilesModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { DesktopService } from '../../services/desktop.service'
|
||||
import { DektopLoadingService } from './dektop-loading.service'
|
||||
import { CardComponent } from '../../components/card.component'
|
||||
import { DesktopItemDirective } from './desktop-item.directive'
|
||||
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
||||
import { ToBadgePipe } from '../../pipes/to-badge'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: 'desktop.component.html',
|
||||
styleUrls: ['desktop.component.scss'],
|
||||
animations: [TUI_PARENT_STOP, tuiScaleIn, tuiFadeIn],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
CardComponent,
|
||||
DesktopItemDirective,
|
||||
TuiSvgModule,
|
||||
TuiLoaderModule,
|
||||
TuiTilesModule,
|
||||
ToNavigationItemPipe,
|
||||
TuiFadeModule,
|
||||
DragScrollerDirective,
|
||||
ToBadgePipe,
|
||||
],
|
||||
})
|
||||
export class DesktopComponent {
|
||||
@ViewChildren(TuiTileComponent, { read: ElementRef })
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { DragScrollerDirective } from '@start9labs/shared'
|
||||
import { TuiLoaderModule, TuiSvgModule } from '@taiga-ui/core'
|
||||
import { TuiFadeModule } from '@taiga-ui/experimental'
|
||||
import { TuiTilesModule } from '@taiga-ui/kit'
|
||||
import { DesktopComponent } from './desktop.component'
|
||||
import { CardComponent } from '../../components/card/card.component'
|
||||
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
||||
import { ToBadgePipe } from '../../pipes/to-badge'
|
||||
import { DesktopItemDirective } from './desktop-item.directive'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DesktopComponent,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardComponent,
|
||||
DesktopItemDirective,
|
||||
TuiSvgModule,
|
||||
TuiLoaderModule,
|
||||
TuiTilesModule,
|
||||
ToNavigationItemPipe,
|
||||
RouterModule.forChild(ROUTES),
|
||||
TuiFadeModule,
|
||||
DragScrollerDirective,
|
||||
ToBadgePipe,
|
||||
],
|
||||
declarations: [DesktopComponent],
|
||||
exports: [DesktopComponent],
|
||||
})
|
||||
export class DesktopModule {}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { inject, Pipe, PipeTransform, Type } from '@angular/core'
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
import { Params } from '@angular/router'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
@@ -33,8 +33,6 @@ export class ToMenuPipe implements PipeTransform {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly proxyService = inject(ProxyService)
|
||||
|
||||
transform({ manifest, installed }: PackageDataEntry): ServiceMenu[] {
|
||||
|
||||
@@ -10,15 +10,6 @@ import { SettingBtn } from '../settings.types'
|
||||
<button *ngIf="button.action" class="g-action" (click)="button.action()">
|
||||
<ng-container *ngTemplateOutlet="template" />
|
||||
</button>
|
||||
<a
|
||||
*ngIf="button.href"
|
||||
class="g-action"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="button.href"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="template" />
|
||||
</a>
|
||||
<a
|
||||
*ngIf="button.routerLink"
|
||||
class="g-action"
|
||||
@@ -34,7 +25,6 @@ import { SettingBtn } from '../settings.types'
|
||||
<ng-content />
|
||||
</div>
|
||||
<tui-icon *ngIf="button.routerLink" icon="tuiIconChevronRight" />
|
||||
<tui-icon *ngIf="button.href" icon="tuiIconExternalLink" />
|
||||
</ng-template>
|
||||
`,
|
||||
styles: [
|
||||
|
||||
@@ -111,26 +111,6 @@ export class SettingsService {
|
||||
routerLink: 'sessions',
|
||||
},
|
||||
],
|
||||
Support: [
|
||||
{
|
||||
title: 'User Manual',
|
||||
description: 'Discover what StartOS can do',
|
||||
icon: 'tuiIconMap',
|
||||
href: 'https://docs.start9.com/0.3.5.x/user-manual',
|
||||
},
|
||||
{
|
||||
title: 'Contact Support',
|
||||
description: 'Get help from the Start9 team and community',
|
||||
icon: 'tuiIconMessageSquare',
|
||||
href: 'https://start9.com/contact',
|
||||
},
|
||||
{
|
||||
title: 'Donate to Start9',
|
||||
description: `Support StartOS development`,
|
||||
icon: 'tuiIconDollarSign',
|
||||
href: 'https://donate.start9.com',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
private async setBrowserTab(): Promise<void> {
|
||||
|
||||
@@ -6,7 +6,6 @@ export interface SettingBtn {
|
||||
description: string
|
||||
icon: string
|
||||
action?: Function
|
||||
href?: string
|
||||
routerLink?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ const routes: Routes = [
|
||||
path: 'portal',
|
||||
canActivate: [AuthGuard],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
import('./apps/portal/portal.module').then(m => m.PortalModule),
|
||||
loadChildren: () => import('./apps/portal/portal.routes').then(m => m),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
|
||||
Reference in New Issue
Block a user