refactor: finalize new portal (#2543)

This commit is contained in:
Alex Inkin
2023-12-23 01:22:16 +04:00
committed by GitHub
parent e47f126bd5
commit 90f5864f1e
31 changed files with 687 additions and 555 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
<header appHeader>My server</header>
<nav appNavigation></nav>
<main>
<router-outlet />
</main>
<app-drawer />

View File

@@ -1,10 +0,0 @@
:host {
// TODO: Theme
background: url(/assets/img/background_dark.jpeg);
background-size: cover;
}
main {
flex: 1;
overflow: hidden;
}

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ export interface SettingBtn {
description: string
icon: string
action?: Function
href?: string
routerLink?: string
}

View File

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