mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
Bugfix/040 UI (#2881)
* fix sideload and install flow * move updates chevron inside upddate button * update dictionaries to include langauge names * fix: address todos (#2880) * fix: address todos * fix enlgish translation --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * use existing translation, no need to duplicate * fix: update dialog and other fixes (#2882) --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -92,7 +92,24 @@ cp proxy.conf-sample.json proxy.conf.json
|
|||||||
npm run start:ui:proxy
|
npm run start:ui:proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
## Updating translations
|
## Translations
|
||||||
|
|
||||||
|
### Currently supported languages
|
||||||
|
|
||||||
|
- Spanish
|
||||||
|
- Polish
|
||||||
|
- German
|
||||||
|
<!-- - Korean
|
||||||
|
- Russian
|
||||||
|
- Japanese
|
||||||
|
- Hebrew
|
||||||
|
- Arabic
|
||||||
|
- Mandarin
|
||||||
|
- Hindi
|
||||||
|
- Portuguese
|
||||||
|
- French
|
||||||
|
- Italian
|
||||||
|
- Thai -->
|
||||||
|
|
||||||
### Adding a new translation
|
### Adding a new translation
|
||||||
|
|
||||||
@@ -119,22 +136,20 @@ Translate the English dictionary below into `<language>`. Format the result as a
|
|||||||
|
|
||||||
#### Sample AI prompt
|
#### Sample AI prompt
|
||||||
|
|
||||||
Translate `<original>` into the languages below. Return the translations as a JSON object with the languages as keys.
|
Translate the English dictionary below into the languages beneath the dictionary. Format the result as a javascript object with translated language as keys, mapping to a javascript object with the numeric values of the English dictionary as keys and the translations as values. These translations are for the web UI of StartOS, a graphical server operating system optimized for self-hosting. Comments may be included in the English dictionary to provide additional context.
|
||||||
|
|
||||||
|
English dictionary:
|
||||||
|
|
||||||
|
```
|
||||||
|
'Hello': 420,
|
||||||
|
'Goodby': 421
|
||||||
|
```
|
||||||
|
|
||||||
|
Languages:
|
||||||
|
|
||||||
- Spanish
|
- Spanish
|
||||||
- Polish
|
- Polish
|
||||||
- German
|
- German
|
||||||
<!-- - Korean
|
|
||||||
- Russian
|
|
||||||
- Japanese
|
|
||||||
- Hebrew
|
|
||||||
- Arabic
|
|
||||||
- Mandarin
|
|
||||||
- Hindi
|
|
||||||
- Portuguese
|
|
||||||
- French
|
|
||||||
- Italian
|
|
||||||
- Thai -->
|
|
||||||
|
|
||||||
#### Adding to StartOS
|
#### Adding to StartOS
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,7 @@
|
|||||||
[(query)]="query"
|
[(query)]="query"
|
||||||
(queryChange)="onQueryChange($event)"
|
(queryChange)="onQueryChange($event)"
|
||||||
/>
|
/>
|
||||||
<button
|
<button tuiButton type="button" appearance="" (click)="open.set(true)">
|
||||||
tuiButton
|
|
||||||
type="button"
|
|
||||||
appearance="link"
|
|
||||||
(click)="toggleMenu(true)"
|
|
||||||
(tuiActiveZoneChange)="toggleMenu($event)"
|
|
||||||
[style.--tui-padding]="'1.2rem'"
|
|
||||||
>
|
|
||||||
<store-icon
|
<store-icon
|
||||||
size="42px"
|
size="42px"
|
||||||
[style.height.px]="42"
|
[style.height.px]="42"
|
||||||
@@ -37,46 +30,47 @@
|
|||||||
[marketplace]="iconConfig"
|
[marketplace]="iconConfig"
|
||||||
[tuiSkeleton]="!registry"
|
[tuiSkeleton]="!registry"
|
||||||
/>
|
/>
|
||||||
<nav
|
<tui-drawer
|
||||||
*tuiSidebar="open; direction: 'right'; autoWidth: true"
|
*tuiPopup="open()"
|
||||||
class="nav-mobile-sidebar divide-bar"
|
[overlay]="true"
|
||||||
|
(click.self)="open.set(false)"
|
||||||
>
|
>
|
||||||
<div class="nav-mobile-sidebar-top">
|
<nav class="nav-mobile-sidebar divide-bar">
|
||||||
<h1 [tuiSkeleton]="!registry">
|
<div class="nav-mobile-sidebar-top">
|
||||||
{{ registry?.info?.name }}
|
<h1 [style.margin]="0" [tuiSkeleton]="!registry">
|
||||||
</h1>
|
{{ registry?.info?.name }}
|
||||||
<button
|
</h1>
|
||||||
[style.--tui-padding]="0"
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
type="button"
|
type="button"
|
||||||
appearance="icon"
|
appearance="icon"
|
||||||
iconStart="@tui.x"
|
iconStart="@tui.x"
|
||||||
(tuiActiveZoneChange)="toggleMenu($event)"
|
(click)="open.set(false)"
|
||||||
(click)="toggleMenu(false)"
|
></button>
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
<!-- change registry modal -->
|
|
||||||
<ng-content select="[slot=mobile]"></ng-content>
|
|
||||||
<div class="nav-mobile-sidebar-bottom divide-bar">
|
|
||||||
<marketplace-categories
|
|
||||||
[categories]="registry?.info?.categories"
|
|
||||||
[category]="query ? '' : category"
|
|
||||||
(categoryChange)="onCategoryChange($event); toggleMenu(false)"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<!-- link to store for brochure -->
|
|
||||||
<ng-content select="[slot=store-mobile]" />
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
href="https://docs.start9.com/0.3.5.x/developer-docs/"
|
|
||||||
>
|
|
||||||
<span>Package a service</span>
|
|
||||||
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- change registry modal -->
|
||||||
</nav>
|
<ng-content select="[slot=mobile]"></ng-content>
|
||||||
|
<div class="nav-mobile-sidebar-bottom divide-bar">
|
||||||
|
<marketplace-categories
|
||||||
|
[categories]="registry?.info?.categories"
|
||||||
|
[category]="query ? '' : category"
|
||||||
|
(categoryChange)="onCategoryChange($event); open.set(false)"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<!-- link to store for brochure -->
|
||||||
|
<ng-content select="[slot=store-mobile]" />
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://docs.start9.com/0.3.5.x/developer-docs/"
|
||||||
|
>
|
||||||
|
<span>Package a service</span>
|
||||||
|
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</tui-drawer>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
import { TuiLet } from '@taiga-ui/cdk'
|
||||||
|
import {
|
||||||
import { MenuComponent } from './menu.component'
|
TuiAppearance,
|
||||||
import { TuiLoader, TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core'
|
TuiButton,
|
||||||
import { TuiActiveZone, TuiLet } from '@taiga-ui/cdk'
|
TuiIcon,
|
||||||
import { TuiSidebar } from '@taiga-ui/addon-mobile'
|
TuiLoader,
|
||||||
import { SearchModule } from '../../pages/list/search/search.module'
|
TuiPopup,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import { CategoriesModule } from '../../pages/list/categories/categories.module'
|
import { CategoriesModule } from '../../pages/list/categories/categories.module'
|
||||||
|
import { SearchModule } from '../../pages/list/search/search.module'
|
||||||
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
|
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
|
||||||
|
import { MenuComponent } from './menu.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -17,8 +21,6 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod
|
|||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
CategoriesModule,
|
CategoriesModule,
|
||||||
TuiActiveZone,
|
|
||||||
...TuiSidebar,
|
|
||||||
TuiLoader,
|
TuiLoader,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
CategoriesModule,
|
CategoriesModule,
|
||||||
@@ -27,6 +29,8 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod
|
|||||||
TuiAppearance,
|
TuiAppearance,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiSkeleton,
|
TuiSkeleton,
|
||||||
|
TuiDrawer,
|
||||||
|
TuiPopup,
|
||||||
],
|
],
|
||||||
declarations: [MenuComponent],
|
declarations: [MenuComponent],
|
||||||
exports: [MenuComponent],
|
exports: [MenuComponent],
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tui-drawer {
|
||||||
|
top: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: rgb(var(--tw-color-zinc-700) / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
@include scrollbar-hidden();
|
@include scrollbar-hidden();
|
||||||
|
|
||||||
@@ -131,12 +137,10 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-sidebar {
|
&-sidebar {
|
||||||
background-color: rgb(var(--tw-color-zinc-700) / 0.9);
|
|
||||||
width: 70vw;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
margin: -1.25rem -1.5rem;
|
||||||
|
|
||||||
&-top {
|
&-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -166,7 +170,7 @@ header {
|
|||||||
|
|
||||||
marketplace-categories {
|
marketplace-categories {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 1.25rem 1.25rem 0px 1.25rem;
|
padding: 1.25rem 1.25rem 0 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep a {
|
::ng-deep a {
|
||||||
@@ -191,7 +195,7 @@ header {
|
|||||||
|
|
||||||
.divide-bar > * + * {
|
.divide-bar > * + * {
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
border-bottom-width: 0px;
|
border-bottom-width: 0;
|
||||||
border-color: rgb(113 113 122);
|
border-color: rgb(113 113 122);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { MarketplaceConfig } from '@start9labs/shared'
|
import { MarketplaceConfig } from '@start9labs/shared'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
@@ -27,7 +28,7 @@ export class MenuComponent implements OnDestroy {
|
|||||||
private readonly categoryService = inject(AbstractCategoryService)
|
private readonly categoryService = inject(AbstractCategoryService)
|
||||||
category = ''
|
category = ''
|
||||||
query = ''
|
query = ''
|
||||||
open = false
|
readonly open = signal(false)
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.categoryService
|
this.categoryService
|
||||||
@@ -57,10 +58,6 @@ export class MenuComponent implements OnDestroy {
|
|||||||
this.categoryService.setQuery(query)
|
this.categoryService.setQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMenu(open: boolean): void {
|
|
||||||
this.open = open
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next()
|
this.destroy$.next()
|
||||||
this.destroy$.complete()
|
this.destroy$.complete()
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiLineClamp } from '@taiga-ui/kit'
|
import { TuiFade } from '@taiga-ui/kit'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-additional-item',
|
selector: 'marketplace-additional-item',
|
||||||
template: `
|
template: `
|
||||||
<div class="item-container">
|
<label tuiTitle>
|
||||||
<label tuiTitle>
|
<span tuiSubtitle>{{ label }}</span>
|
||||||
<span tuiSubtitle>{{ label }}</span>
|
<span tuiFade>{{ data }}</span>
|
||||||
<tui-line-clamp [content]="data" [linesLimit]="1" />
|
</label>
|
||||||
</label>
|
<tui-icon [icon]="icon" />
|
||||||
<tui-icon [icon]="icon" />
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
.item-container {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
padding: 0.75rem 0.25rem;
|
padding: 0.75rem 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgb(113 113 122 / 0.1);
|
background-color: var(--tui-background-neutral-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[tuiSubtitle] {
|
[tuiSubtitle] {
|
||||||
@@ -34,16 +34,11 @@ import { TuiLineClamp } from '@taiga-ui/kit'
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .t-text {
|
|
||||||
font-family: 'Montserrat';
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiLineClamp, TuiIcon, TuiTitle],
|
imports: [CommonModule, TuiIcon, TuiTitle, TuiFade],
|
||||||
})
|
})
|
||||||
export class MarketplaceAdditionalItemComponent {
|
export class MarketplaceAdditionalItemComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
.detail-container {
|
.detail-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
|
grid-auto-columns: minmax(0, 1fr);
|
||||||
|
|
||||||
& > * + * {
|
& > * + * {
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ActivatedRoute, Data } from '@angular/router'
|
|||||||
import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core'
|
import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core'
|
||||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||||
import { catchError, ignoreElements, of } from 'rxjs'
|
import { catchError, ignoreElements, Observable, of } from 'rxjs'
|
||||||
import { SafeLinksDirective } from '../directives/safe-links.directive'
|
import { SafeLinksDirective } from '../directives/safe-links.directive'
|
||||||
import { MarkdownPipe } from '../pipes/markdown.pipe'
|
import { MarkdownPipe } from '../pipes/markdown.pipe'
|
||||||
import { getErrorMessage } from '../services/error.service'
|
import { getErrorMessage } from '../services/error.service'
|
||||||
@@ -36,8 +36,9 @@ import { getErrorMessage } from '../services/error.service'
|
|||||||
})
|
})
|
||||||
export class MarkdownComponent {
|
export class MarkdownComponent {
|
||||||
private readonly data =
|
private readonly data =
|
||||||
injectContext<TuiDialogContext<void, Data>>({ optional: true })?.data ||
|
injectContext<TuiDialogContext<void, { content: Observable<string> }>>({
|
||||||
inject(ActivatedRoute).snapshot.data
|
optional: true,
|
||||||
|
})?.data || inject(ActivatedRoute).snapshot.data
|
||||||
|
|
||||||
readonly content = toSignal<string>(this.data['content'])
|
readonly content = toSignal<string>(this.data['content'])
|
||||||
readonly error = toSignal(
|
readonly error = toSignal(
|
||||||
|
|||||||
@@ -1,28 +1,39 @@
|
|||||||
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core'
|
|
||||||
import { DOCUMENT } from '@angular/common'
|
import { DOCUMENT } from '@angular/common'
|
||||||
|
import { Directive, inject } from '@angular/core'
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
|
import {
|
||||||
|
MutationObserverService,
|
||||||
|
provideMutationObserverInit,
|
||||||
|
} from '@ng-web-apis/mutation-observer'
|
||||||
|
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||||
|
|
||||||
// @TODO Alex: Refactor to use `MutationObserver` so it works with dynamic content
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[safeLinks]',
|
selector: '[safeLinks]',
|
||||||
|
providers: [
|
||||||
|
MutationObserverService,
|
||||||
|
provideMutationObserverInit({
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class SafeLinksDirective implements AfterViewInit {
|
export class SafeLinksDirective {
|
||||||
constructor(
|
private readonly doc = inject(DOCUMENT)
|
||||||
@Inject(DOCUMENT) private readonly document: Document,
|
private readonly el = tuiInjectElement()
|
||||||
private readonly elementRef: ElementRef<HTMLElement>,
|
private readonly sub = inject(MutationObserverService)
|
||||||
) {}
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe(() => {
|
||||||
ngAfterViewInit() {
|
Array.from(this.doc.links)
|
||||||
Array.from(this.document.links)
|
.filter(
|
||||||
.filter(
|
link =>
|
||||||
link =>
|
link.hostname !== this.doc.location.hostname &&
|
||||||
link.hostname !== this.document.location.hostname &&
|
this.el.contains(link),
|
||||||
this.elementRef.nativeElement.contains(link),
|
)
|
||||||
)
|
.forEach(link => {
|
||||||
.forEach(link => {
|
link.target = '_blank'
|
||||||
link.target = '_blank'
|
link.setAttribute('rel', 'noreferrer')
|
||||||
link.setAttribute('rel', 'noreferrer')
|
link.classList.add('g-external-link')
|
||||||
link.classList.add('g-external-link')
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,4 +488,8 @@ export default {
|
|||||||
485: 'StartOS-Benutzeroberfläche',
|
485: 'StartOS-Benutzeroberfläche',
|
||||||
486: 'WiFi',
|
486: 'WiFi',
|
||||||
487: 'Anleitungen',
|
487: 'Anleitungen',
|
||||||
|
488: 'spanisch',
|
||||||
|
489: 'polnisch',
|
||||||
|
490: 'deutsch',
|
||||||
|
491: 'englisch',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
@@ -487,4 +487,8 @@ export const ENGLISH = {
|
|||||||
'StartOS UI': 485,
|
'StartOS UI': 485,
|
||||||
'WiFi': 486,
|
'WiFi': 486,
|
||||||
'Instructions': 487,
|
'Instructions': 487,
|
||||||
|
'spanish': 488,
|
||||||
|
'polish': 489,
|
||||||
|
'german': 490,
|
||||||
|
'english': 491,
|
||||||
} as const
|
} as const
|
||||||
@@ -488,4 +488,8 @@ export default {
|
|||||||
485: 'Interfaz de StartOS',
|
485: 'Interfaz de StartOS',
|
||||||
486: 'WiFi',
|
486: 'WiFi',
|
||||||
487: 'Instrucciones',
|
487: 'Instrucciones',
|
||||||
|
488: 'español',
|
||||||
|
489: 'polaco',
|
||||||
|
490: 'alemán',
|
||||||
|
491: 'inglés',
|
||||||
} as any satisfies i18n
|
} as any satisfies i18n
|
||||||
@@ -488,4 +488,8 @@ export default {
|
|||||||
485: 'Interfejs StartOS',
|
485: 'Interfejs StartOS',
|
||||||
486: 'WiFi',
|
486: 'WiFi',
|
||||||
487: 'instrukcje',
|
487: 'instrukcje',
|
||||||
|
488: 'hiszpański',
|
||||||
|
489: 'polski',
|
||||||
|
490: 'niemiecki',
|
||||||
|
491: 'angielski',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
|
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
|
||||||
import { ENGLISH } from './dictionaries/english'
|
import { ENGLISH } from './dictionaries/en'
|
||||||
import { I18N, i18nKey } from './i18n.providers'
|
import { I18N, i18nKey } from './i18n.providers'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
tuiLanguageSwitcher,
|
tuiLanguageSwitcher,
|
||||||
TuiLanguageSwitcherService,
|
TuiLanguageSwitcherService,
|
||||||
} from '@taiga-ui/i18n'
|
} from '@taiga-ui/i18n'
|
||||||
import { ENGLISH } from './dictionaries/english'
|
import { ENGLISH } from './dictionaries/en'
|
||||||
import { i18nService } from './i18n.service'
|
import { i18nService } from './i18n.service'
|
||||||
|
|
||||||
export type i18nKey = keyof typeof ENGLISH
|
export type i18nKey = keyof typeof ENGLISH
|
||||||
@@ -36,11 +36,11 @@ export const I18N_PROVIDERS = [
|
|||||||
useValue: async (language: TuiLanguageName): Promise<unknown> => {
|
useValue: async (language: TuiLanguageName): Promise<unknown> => {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case 'spanish':
|
case 'spanish':
|
||||||
return import('./dictionaries/spanish').then(v => v.default)
|
return import('./dictionaries/es').then(v => v.default)
|
||||||
case 'polish':
|
case 'polish':
|
||||||
return import('./dictionaries/polish').then(v => v.default)
|
return import('./dictionaries/pl').then(v => v.default)
|
||||||
case 'german':
|
case 'german':
|
||||||
return import('./dictionaries/german').then(v => v.default)
|
return import('./dictionaries/de').then(v => v.default)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { ErrorHandler, inject, Injectable } from '@angular/core'
|
|||||||
import { TuiAlertService } from '@taiga-ui/core'
|
import { TuiAlertService } from '@taiga-ui/core'
|
||||||
import { HttpError } from '../classes/http-error'
|
import { HttpError } from '../classes/http-error'
|
||||||
|
|
||||||
// @TODO Alex: Enable this as ErrorHandler
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -134,11 +134,6 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[tuiSidebar] > div.t-wrapper {
|
|
||||||
backdrop-filter: blur(1rem);
|
|
||||||
background: rgb(34 34 34 / 80%);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO Alex: Move to Taiga UI
|
// @TODO Alex: Move to Taiga UI
|
||||||
a[tuiIconButton]:not([href]) {
|
a[tuiIconButton]:not([href]) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<tui-root tuiTheme="dark" [class.offline]="offline$ | async">
|
|
||||||
<router-outlet />
|
|
||||||
<toast-container />
|
|
||||||
<sidebar-host ngProjectAs="tuiOverContent" />
|
|
||||||
</tui-root>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
tui-root {
|
|
||||||
@include transition(filter);
|
|
||||||
height: 100%;
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
|
|
||||||
&.offline {
|
|
||||||
filter: saturate(0.75) contrast(0.85);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,36 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
import { Title } from '@angular/platform-browser'
|
import { Title } from '@angular/platform-browser'
|
||||||
import { i18nService } from '@start9labs/shared'
|
import { i18nService } from '@start9labs/shared'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
import { merge } from 'rxjs'
|
||||||
import { ConnectionService } from './services/connection.service'
|
|
||||||
import { PatchDataService } from './services/patch-data.service'
|
import { PatchDataService } from './services/patch-data.service'
|
||||||
import { DataModel } from './services/patch-db/data-model'
|
import { DataModel } from './services/patch-db/data-model'
|
||||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: 'app.component.html',
|
template: `
|
||||||
styleUrls: ['app.component.scss'],
|
<tui-root tuiTheme="dark">
|
||||||
|
<router-outlet />
|
||||||
|
<toast-container />
|
||||||
|
</tui-root>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tui-root {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
}
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent {
|
||||||
private readonly title = inject(Title)
|
private readonly title = inject(Title)
|
||||||
private readonly i18n = inject(i18nService)
|
private readonly i18n = inject(i18nService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
|
||||||
|
|
||||||
readonly subscription = merge(
|
readonly subscription = merge(
|
||||||
inject(PatchDataService),
|
inject(PatchDataService),
|
||||||
@@ -26,23 +39,11 @@ export class AppComponent implements OnInit {
|
|||||||
.pipe(takeUntilDestroyed())
|
.pipe(takeUntilDestroyed())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|
||||||
readonly offline$ = combineLatest([
|
readonly ui = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
inject(ConnectionService),
|
.watch$('ui')
|
||||||
this.patch
|
.pipe(takeUntilDestroyed())
|
||||||
.watch$('serverInfo', 'statusInfo')
|
.subscribe(({ name, language }) => {
|
||||||
.pipe(startWith({ restarting: false, shuttingDown: false })),
|
|
||||||
]).pipe(
|
|
||||||
map(
|
|
||||||
([connected, { restarting, shuttingDown }]) =>
|
|
||||||
connected && (restarting || shuttingDown),
|
|
||||||
),
|
|
||||||
startWith(true),
|
|
||||||
)
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.patch.watch$('ui').subscribe(({ name, language }) => {
|
|
||||||
this.title.setTitle(name || 'StartOS')
|
this.title.setTitle(name || 'StartOS')
|
||||||
this.i18n.setLanguage(language || 'english')
|
this.i18n.setLanguage(language || 'english')
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'
|
|||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||||
import { TuiRoot } from '@taiga-ui/core'
|
import { TuiRoot } from '@taiga-ui/core'
|
||||||
import { SidebarHostComponent } from 'src/app/components/sidebar-host.component'
|
|
||||||
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
|
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
|
||||||
import { environment } from '../environments/environment'
|
import { environment } from '../environments/environment'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
@@ -24,7 +23,6 @@ import { RoutingModule } from './routing.module'
|
|||||||
// or after 30 seconds (whichever comes first).
|
// or after 30 seconds (whichever comes first).
|
||||||
registrationStrategy: 'registerWhenStable:30000',
|
registrationStrategy: 'registerWhenStable:30000',
|
||||||
}),
|
}),
|
||||||
SidebarHostComponent,
|
|
||||||
],
|
],
|
||||||
providers: APP_PROVIDERS,
|
providers: APP_PROVIDERS,
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
tuiDropdownOptionsProvider,
|
tuiDropdownOptionsProvider,
|
||||||
tuiNumberFormatProvider,
|
tuiNumberFormatProvider,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { NG_EVENT_PLUGINS } from '@taiga-ui/event-plugins'
|
import { provideEventPlugins } from '@taiga-ui/event-plugins'
|
||||||
import {
|
import {
|
||||||
TUI_DATE_TIME_VALUE_TRANSFORMER,
|
TUI_DATE_TIME_VALUE_TRANSFORMER,
|
||||||
TUI_DATE_VALUE_TRANSFORMER,
|
TUI_DATE_VALUE_TRANSFORMER,
|
||||||
@@ -48,7 +48,7 @@ const {
|
|||||||
} = require('../../../../config.json') as WorkspaceConfig
|
} = require('../../../../config.json') as WorkspaceConfig
|
||||||
|
|
||||||
export const APP_PROVIDERS: Provider[] = [
|
export const APP_PROVIDERS: Provider[] = [
|
||||||
NG_EVENT_PLUGINS,
|
provideEventPlugins(),
|
||||||
I18N_PROVIDERS,
|
I18N_PROVIDERS,
|
||||||
FilterPackagesPipe,
|
FilterPackagesPipe,
|
||||||
UntypedFormBuilder,
|
UntypedFormBuilder,
|
||||||
|
|||||||
@@ -1,73 +1,94 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { SwUpdate } from '@angular/service-worker'
|
import { SwUpdate } from '@angular/service-worker'
|
||||||
import { Exver, LoadingService } from '@start9labs/shared'
|
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||||
|
import { LoadingService } from '@start9labs/shared'
|
||||||
|
import { Version } from '@start9labs/start-sdk'
|
||||||
|
import { TuiResponsiveDialog } from '@taiga-ui/addon-mobile'
|
||||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||||
import { TuiButton, TuiDialog } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { debounceTime, endWith, map, merge, Subject } from 'rxjs'
|
import { distinctUntilChanged, map, merge, Subject } from 'rxjs'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
// @TODO Alex
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'refresh-alert',
|
selector: 'refresh-alert',
|
||||||
template: `
|
template: `
|
||||||
<!-- <ng-template-->
|
<ng-template
|
||||||
<!-- [tuiDialog]="show$ | async"-->
|
[tuiResponsiveDialog]="show()"
|
||||||
<!-- [tuiDialogOptions]="{ label: 'Refresh Needed', size: 's' }"-->
|
[tuiResponsiveDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
|
||||||
<!-- (tuiDialogChange)="onDismiss()"-->
|
(tuiResponsiveDialogChange)="dismiss$.next()"
|
||||||
<!-- >-->
|
>
|
||||||
<!-- Your user interface is cached and out of date. Hard refresh the page to-->
|
@if (isPwa) {
|
||||||
<!-- get the latest UI.-->
|
<p>
|
||||||
<!-- <ul>-->
|
Your user interface is cached and out of date. Attempt to reload the
|
||||||
<!-- <li>-->
|
PWA using the button below. If you continue to see this message,
|
||||||
<!-- <b>On Mac</b>-->
|
uninstall and reinstall the PWA.
|
||||||
<!-- : cmd + shift + R-->
|
</p>
|
||||||
<!-- </li>-->
|
<button
|
||||||
<!-- <li>-->
|
tuiButton
|
||||||
<!-- <b>On Linux/Windows</b>-->
|
tuiAutoFocus
|
||||||
<!-- : ctrl + shift + R-->
|
appearance="secondary"
|
||||||
<!-- </li>-->
|
style="float: right"
|
||||||
<!-- </ul>-->
|
[tuiAppearanceFocus]="false"
|
||||||
<!-- <button-->
|
(click)="pwaReload()"
|
||||||
<!-- tuiButton-->
|
>
|
||||||
<!-- tuiAutoFocus-->
|
Reload
|
||||||
<!-- appearance="secondary"-->
|
</button>
|
||||||
<!-- style="float: right"-->
|
} @else {
|
||||||
<!-- (click)="onDismiss()"-->
|
Your user interface is cached and out of date. Hard refresh the page to
|
||||||
<!-- >-->
|
get the latest UI.
|
||||||
<!-- Ok-->
|
<ul>
|
||||||
<!-- </button>-->
|
<li>
|
||||||
<!-- </ng-template>-->
|
<b>On Mac</b>
|
||||||
|
: cmd + shift + R
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>On Linux/Windows</b>
|
||||||
|
: ctrl + shift + R
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
tuiAutoFocus
|
||||||
|
appearance="secondary"
|
||||||
|
style="float: right"
|
||||||
|
[tuiAppearanceFocus]="false"
|
||||||
|
(click)="dismiss$.next()"
|
||||||
|
>
|
||||||
|
Ok
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiDialog, AsyncPipe, TuiButton, TuiAutoFocus],
|
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus],
|
||||||
})
|
})
|
||||||
export class RefreshAlertComponent {
|
export class RefreshAlertComponent {
|
||||||
|
private readonly win = inject(WA_WINDOW)
|
||||||
private readonly updates = inject(SwUpdate)
|
private readonly updates = inject(SwUpdate)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly exver = inject(Exver)
|
private readonly version = Version.parse(inject(ConfigService).version)
|
||||||
private readonly config = inject(ConfigService)
|
|
||||||
private readonly dismiss$ = new Subject<boolean>()
|
|
||||||
|
|
||||||
readonly show$ = merge(
|
readonly dismiss$ = new Subject<void>()
|
||||||
this.dismiss$,
|
readonly isPwa = this.win.matchMedia('(display-mode: standalone)').matches
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
|
||||||
.watch$('serverInfo', 'version')
|
|
||||||
.pipe(
|
|
||||||
map(version => !!this.exver.compareExver(this.config.version, version)),
|
|
||||||
endWith(false),
|
|
||||||
),
|
|
||||||
).pipe(debounceTime(0))
|
|
||||||
|
|
||||||
// @TODO use this like we did on 0344
|
readonly show = toSignal(
|
||||||
onPwa = false
|
merge(
|
||||||
|
this.dismiss$.pipe(map(() => false)),
|
||||||
ngOnInit() {
|
inject<PatchDB<DataModel>>(PatchDB)
|
||||||
this.onPwa = window.matchMedia('(display-mode: standalone)').matches
|
.watch$('serverInfo', 'version')
|
||||||
}
|
.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map(v => this.version.compare(Version.parse(v)) !== 'equal'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialValue: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async pwaReload() {
|
async pwaReload() {
|
||||||
const loader = this.loader.open('Reloading PWA').subscribe()
|
const loader = this.loader.open('Reloading PWA').subscribe()
|
||||||
@@ -80,11 +101,7 @@ export class RefreshAlertComponent {
|
|||||||
} finally {
|
} finally {
|
||||||
loader.unsubscribe()
|
loader.unsubscribe()
|
||||||
// always reload, as this resolves most out of sync cases
|
// always reload, as this resolves most out of sync cases
|
||||||
window.location.reload()
|
this.win.location.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDismiss() {
|
|
||||||
this.dismiss$.next(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { TuiDropdownService } from '@taiga-ui/core'
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Directive,
|
|
||||||
Injectable,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { TuiPortals, TuiPortalService } from '@taiga-ui/cdk'
|
|
||||||
|
|
||||||
@Injectable({ providedIn: `root` })
|
|
||||||
export class SidebarService extends TuiPortalService {}
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: '[tuiSidebar]',
|
|
||||||
standalone: true,
|
|
||||||
providers: [{ provide: TuiDropdownService, useExisting: SidebarService }],
|
|
||||||
})
|
|
||||||
export class SidebarDirective {}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'sidebar-host',
|
|
||||||
template: '<ng-container #viewContainer></ng-container>',
|
|
||||||
styles: [':host { position: fixed; top: 0; }'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
standalone: true,
|
|
||||||
providers: [{ provide: TuiPortalService, useExisting: SidebarService }],
|
|
||||||
})
|
|
||||||
export class SidebarHostComponent extends TuiPortals {}
|
|
||||||
@@ -9,8 +9,7 @@ const ROUTES: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
loadChildren: () =>
|
loadComponent: () => import('./logs.component'),
|
||||||
import('./logs/logs.module').then(m => m.LogsPageModule),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,43 @@
|
|||||||
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
||||||
import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer'
|
import { RouterLink } from '@angular/router'
|
||||||
|
import {
|
||||||
|
WA_INTERSECTION_ROOT,
|
||||||
|
WaIntersectionObserver,
|
||||||
|
} from '@ng-web-apis/intersection-observer'
|
||||||
|
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
||||||
import { convertAnsi, ErrorService } from '@start9labs/shared'
|
import { convertAnsi, ErrorService } from '@start9labs/shared'
|
||||||
import { TuiScrollbar } from '@taiga-ui/core'
|
import { tuiProvide } from '@taiga-ui/cdk'
|
||||||
|
import { TuiButton, TuiLoader, TuiScrollbar } from '@taiga-ui/core'
|
||||||
|
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'logs',
|
standalone: true,
|
||||||
templateUrl: './logs.page.html',
|
template: `
|
||||||
|
<a
|
||||||
|
routerLink="../"
|
||||||
|
tuiButton
|
||||||
|
iconStart="@tui.chevron-left"
|
||||||
|
appearance="icon"
|
||||||
|
[style.align-self]="'flex-start'"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
<tui-scrollbar childList subtree (waMutationObserver)="restoreScroll()">
|
||||||
|
<section
|
||||||
|
class="top"
|
||||||
|
waIntersectionObserver
|
||||||
|
(waIntersectionObservee)="onTop(!!$event[0]?.isIntersecting)"
|
||||||
|
>
|
||||||
|
@if (loading) {
|
||||||
|
<tui-loader textContent="Loading logs" />
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
@for (log of logs; track log) {
|
||||||
|
<pre [innerHTML]="log | dompurify"></pre>
|
||||||
|
}
|
||||||
|
</tui-scrollbar>
|
||||||
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
@@ -18,14 +49,18 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
background: var(--tui-background-base);
|
background: var(--tui-background-base);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
providers: [
|
imports: [
|
||||||
{
|
RouterLink,
|
||||||
provide: INTERSECTION_ROOT,
|
WaIntersectionObserver,
|
||||||
useExisting: ElementRef,
|
WaMutationObserver,
|
||||||
},
|
NgDompurifyModule,
|
||||||
|
TuiButton,
|
||||||
|
TuiLoader,
|
||||||
|
TuiScrollbar,
|
||||||
],
|
],
|
||||||
|
providers: [tuiProvide(WA_INTERSECTION_ROOT, ElementRef)],
|
||||||
})
|
})
|
||||||
export class LogsPage implements OnInit {
|
export default class LogsPage implements OnInit {
|
||||||
@ViewChild(TuiScrollbar, { read: ElementRef })
|
@ViewChild(TuiScrollbar, { read: ElementRef })
|
||||||
private readonly scrollbar?: ElementRef<HTMLElement>
|
private readonly scrollbar?: ElementRef<HTMLElement>
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
|
||||||
import { WaIntersectionObserver } from '@ng-web-apis/intersection-observer'
|
|
||||||
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
|
||||||
import { TuiButton, TuiLoader, TuiScrollbar } from '@taiga-ui/core'
|
|
||||||
import { TuiBadge } from '@taiga-ui/kit'
|
|
||||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
|
||||||
import { LogsPage } from './logs.page'
|
|
||||||
|
|
||||||
const ROUTES: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: LogsPage,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
RouterModule.forChild(ROUTES),
|
|
||||||
...WaIntersectionObserver,
|
|
||||||
WaMutationObserver,
|
|
||||||
NgDompurifyModule,
|
|
||||||
TuiBadge,
|
|
||||||
TuiButton,
|
|
||||||
TuiLoader,
|
|
||||||
TuiScrollbar,
|
|
||||||
],
|
|
||||||
declarations: [LogsPage],
|
|
||||||
})
|
|
||||||
export class LogsPageModule {}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<a
|
|
||||||
routerLink="../"
|
|
||||||
tuiButton
|
|
||||||
iconStart="@tui.chevron-left"
|
|
||||||
appearance="icon"
|
|
||||||
[style.align-self]="'flex-start'"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</a>
|
|
||||||
<tui-scrollbar childList subtree (waMutationObserver)="restoreScroll()">
|
|
||||||
<section
|
|
||||||
class="top"
|
|
||||||
waIntersectionObserver
|
|
||||||
(waIntersectionObservee)="onTop(!!$event[0]?.isIntersecting)"
|
|
||||||
>
|
|
||||||
@if (loading) {
|
|
||||||
<tui-loader textContent="Loading logs" />
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
@for (log of logs; track log) {
|
|
||||||
<pre [innerHTML]="log | dompurify"></pre>
|
|
||||||
}
|
|
||||||
</tui-scrollbar>
|
|
||||||
@@ -80,7 +80,7 @@ export interface FormContext<T> {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
margin: 1rem 0 -1rem;
|
margin: 1rem -1px -1rem;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
background: var(--tui-background-elevation-1);
|
background: var(--tui-background-elevation-1);
|
||||||
border-top: 1px solid var(--tui-background-base-alt);
|
border-top: 1px solid var(--tui-background-base-alt);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export abstract class Control<
|
|||||||
return this.control.spec
|
return this.control.spec
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO Alex: Properly handle already set immutable value
|
|
||||||
get readOnly(): boolean {
|
get readOnly(): boolean {
|
||||||
return (
|
return (
|
||||||
!!this.value && !!this.control.control?.pristine && this.control.immutable
|
!!this.value && !!this.control.control?.pristine && this.control.immutable
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Control } from '../control'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'form-toggle',
|
selector: 'form-toggle',
|
||||||
templateUrl: './form-toggle.component.html',
|
templateUrl: './form-toggle.component.html',
|
||||||
host: { style: 'display: flex' },
|
host: { class: 'g-toggle' },
|
||||||
})
|
})
|
||||||
export class FormToggleComponent extends Control<
|
export class FormToggleComponent extends Control<
|
||||||
IST.ValueSpecToggle,
|
IST.ValueSpecToggle,
|
||||||
|
|||||||
@@ -24,27 +24,28 @@ import { tuiPure } from '@taiga-ui/cdk'
|
|||||||
})
|
})
|
||||||
export class FormUnionComponent implements OnChanges {
|
export class FormUnionComponent implements OnChanges {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
spec!: IST.ValueSpecUnion
|
spec!: IST.ValueSpecUnion & { others?: Record<string, any> }
|
||||||
|
|
||||||
selectSpec!: IST.ValueSpecSelect
|
selectSpec!: IST.ValueSpecSelect
|
||||||
|
|
||||||
private readonly form = inject(FormGroupName)
|
private readonly form = inject(FormGroupName)
|
||||||
private readonly formService = inject(FormService)
|
private readonly formService = inject(FormService)
|
||||||
private readonly values: Record<string, any> = {}
|
|
||||||
|
|
||||||
get union(): string {
|
get union(): string {
|
||||||
return this.form.value.selection
|
return this.form.value.selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OTHER?
|
||||||
@tuiPure
|
@tuiPure
|
||||||
onUnion(union: string) {
|
onUnion(union: string) {
|
||||||
this.values[this.union] = this.form.control.controls['value']?.value
|
this.spec.others = this.spec.others || {}
|
||||||
|
this.spec.others[this.union] = this.form.control.controls['value']?.value
|
||||||
this.form.control.setControl(
|
this.form.control.setControl(
|
||||||
'value',
|
'value',
|
||||||
this.formService.getFormGroup(
|
this.formService.getFormGroup(
|
||||||
union ? this.spec.variants[union]?.spec || {} : {},
|
union ? this.spec.variants[union]?.spec || {} : {},
|
||||||
[],
|
[],
|
||||||
this.values[union],
|
this.spec.others[union],
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
emitEvent: false,
|
emitEvent: false,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ import { HeaderStatusComponent } from './status.component'
|
|||||||
border-radius: var(--bumper);
|
border-radius: var(--bumper);
|
||||||
margin: var(--bumper);
|
margin: var(--bumper);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
filter: grayscale(1) brightness(0.75);
|
||||||
|
|
||||||
|
@include transition(filter);
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -88,10 +91,12 @@ import { HeaderStatusComponent } from './status.component'
|
|||||||
|
|
||||||
&:has([data-status='neutral']) {
|
&:has([data-status='neutral']) {
|
||||||
--status: var(--tui-status-neutral);
|
--status: var(--tui-status-neutral);
|
||||||
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has([data-status='success']) {
|
&:has([data-status='success']) {
|
||||||
--status: transparent;
|
--status: transparent;
|
||||||
|
filter: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
|||||||
tuiHintDirection="bottom"
|
tuiHintDirection="bottom"
|
||||||
[tuiHintShowDelay]="1000"
|
[tuiHintShowDelay]="1000"
|
||||||
[routerLink]="item.routerLink"
|
[routerLink]="item.routerLink"
|
||||||
|
[class.link_system]="item.routerLink === '/portal/system'"
|
||||||
[tuiHint]="!rla.isActive ? item.name : ''"
|
[tuiHint]="!rla.isActive ? item.name : ''"
|
||||||
>
|
>
|
||||||
<tui-badged-content
|
<tui-badged-content
|
||||||
@@ -121,6 +122,10 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
|||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
margin: 0 calc(var(--bumper) + 0.5rem);
|
margin: 0 calc(var(--bumper) + 0.5rem);
|
||||||
|
|
||||||
|
&.link_system {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
+ .link::before {
|
+ .link::before {
|
||||||
left: -0.5rem;
|
left: -0.5rem;
|
||||||
border-top-left-radius: var(--bumper);
|
border-top-left-radius: var(--bumper);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
} @else {
|
} @else {
|
||||||
<tui-loader [textContent]="'Loading logs' | i18n" [style.margin-top.rem]="5" />
|
<tui-loader class="loader" [textContent]="'Loading logs' | i18n" />
|
||||||
}
|
}
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
iconStart="@tui.circle-arrow-down"
|
iconStart="@tui.circle-arrow-down"
|
||||||
(click)="setScroll(true); scrollToBottom()"
|
(click)="setScroll(true); scrollToBottom()"
|
||||||
>
|
>
|
||||||
{{'Scroll to bottom' | i18n}}
|
{{ 'Scroll to bottom' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
@@ -61,6 +61,6 @@
|
|||||||
iconStart="@tui.download"
|
iconStart="@tui.download"
|
||||||
[logsDownload]="fetchLogs"
|
[logsDownload]="fetchLogs"
|
||||||
>
|
>
|
||||||
{{'Download' | i18n}}
|
{{ 'Download' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
margin-bottom: -5rem;
|
margin-bottom: -5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -32,6 +34,14 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 var(--bumper) var(--bumper);
|
margin: 0 var(--bumper) var(--bumper);
|
||||||
|
filter: grayscale(1) brightness(0.75);
|
||||||
|
|
||||||
|
@include transition(filter);
|
||||||
|
|
||||||
|
header:has([data-status='success']) + &,
|
||||||
|
header:has([data-status='neutral']) + & {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
inject,
|
inject,
|
||||||
input,
|
|
||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
@@ -76,7 +75,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
type="button"
|
type="button"
|
||||||
appearance="outline-grayscale"
|
appearance="secondary-grayscale"
|
||||||
(click)="showService()"
|
(click)="showService()"
|
||||||
>
|
>
|
||||||
{{ 'View Installed' | i18n }}
|
{{ 'View Installed' | i18n }}
|
||||||
@@ -180,8 +179,9 @@ export class MarketplaceControlsComponent {
|
|||||||
private async installOrUpload(url: string | null) {
|
private async installOrUpload(url: string | null) {
|
||||||
if (this.file) {
|
if (this.file) {
|
||||||
await this.upload()
|
await this.upload()
|
||||||
|
this.router.navigate(['/portal', 'services'])
|
||||||
} else if (url) {
|
} else if (url) {
|
||||||
this.install(url)
|
await this.install(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
|
||||||
import { TuiPortals, TuiPortalService } from '@taiga-ui/cdk'
|
|
||||||
import { MarketplaceSidebarService } from '../services/sidebar.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
selector: 'marketplace-sidebars',
|
|
||||||
template: '<ng-container #viewContainer></ng-container>',
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
position: fixed;
|
|
||||||
inset: 3.5rem 0 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: TuiPortalService,
|
|
||||||
useExisting: MarketplaceSidebarService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class MarketplaceSidebarsComponent extends TuiPortals {}
|
|
||||||
@@ -10,9 +10,9 @@ import { toObservable, toSignal } from '@angular/core/rxjs-interop'
|
|||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
|
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
|
||||||
import { Exver } from '@start9labs/shared'
|
import { Exver } from '@start9labs/shared'
|
||||||
import { TuiSidebar } from '@taiga-ui/addon-mobile'
|
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||||
import { TuiAutoFocus, TuiClickOutside } from '@taiga-ui/cdk'
|
import { TuiButton, TuiDropdownService, TuiPopup } from '@taiga-ui/core'
|
||||||
import { TuiButton, TuiDropdownService } from '@taiga-ui/core'
|
import { TuiDrawer } from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@@ -35,40 +35,48 @@ import { MarketplaceControlsComponent } from './controls.component'
|
|||||||
selector: 'marketplace-tile',
|
selector: 'marketplace-tile',
|
||||||
template: `
|
template: `
|
||||||
<marketplace-item [pkg]="pkg()" (click)="toggle(true)">
|
<marketplace-item [pkg]="pkg()" (click)="toggle(true)">
|
||||||
<marketplace-preview
|
<tui-drawer
|
||||||
*tuiSidebar="!!open(); direction: 'right'; autoWidth: true"
|
*tuiPopup="open()"
|
||||||
[pkgId]="pkg().id"
|
[overlay]="true"
|
||||||
class="preview-wrapper"
|
(click.self)="toggle(false)"
|
||||||
(tuiClickOutside)="toggle(false)"
|
|
||||||
>
|
>
|
||||||
<button
|
<marketplace-preview [pkgId]="pkg().id" class="preview-wrapper">
|
||||||
tuiAutoFocus
|
<button
|
||||||
slot="close"
|
tuiAutoFocus
|
||||||
size="xs"
|
slot="close"
|
||||||
class="close-button"
|
size="xs"
|
||||||
tuiIconButton
|
class="close-button"
|
||||||
type="button"
|
tuiIconButton
|
||||||
appearance="icon"
|
type="button"
|
||||||
iconStart="@tui.x"
|
appearance="icon"
|
||||||
[tuiAppearanceFocus]="false"
|
iconStart="@tui.x"
|
||||||
(click)="toggle(false)"
|
[tuiAppearanceFocus]="false"
|
||||||
></button>
|
(click)="toggle(false)"
|
||||||
<marketplace-controls
|
></button>
|
||||||
slot="controls"
|
<marketplace-controls
|
||||||
class="controls-wrapper"
|
slot="controls"
|
||||||
[pkg]="pkg()"
|
class="controls-wrapper"
|
||||||
[localPkg]="local$ | async"
|
[pkg]="pkg()"
|
||||||
[localFlavor]="!!(flavor$ | async)"
|
[localPkg]="local$ | async"
|
||||||
/>
|
[localFlavor]="!!(flavor$ | async)"
|
||||||
</marketplace-preview>
|
/>
|
||||||
|
</marketplace-preview>
|
||||||
|
</tui-drawer>
|
||||||
</marketplace-item>
|
</marketplace-item>
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
|
cursor: pointer;
|
||||||
animation: animateIn 400ms calc(var(--animation-order) * 200ms) both;
|
animation: animateIn 400ms calc(var(--animation-order) * 200ms) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tui-drawer {
|
||||||
|
top: 0;
|
||||||
|
width: 28rem;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes animateIn {
|
@keyframes animateIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -119,9 +127,9 @@ import { MarketplaceControlsComponent } from './controls.component'
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
TuiAutoFocus,
|
TuiAutoFocus,
|
||||||
TuiClickOutside,
|
|
||||||
TuiSidebar,
|
|
||||||
TuiButton,
|
TuiButton,
|
||||||
|
TuiPopup,
|
||||||
|
TuiDrawer,
|
||||||
MarketplaceControlsComponent,
|
MarketplaceControlsComponent,
|
||||||
MarketplacePreviewComponent,
|
MarketplacePreviewComponent,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
FilterPackagesPipe,
|
FilterPackagesPipe,
|
||||||
FilterPackagesPipeModule,
|
FilterPackagesPipeModule,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
import { TuiScrollbar } from '@taiga-ui/core'
|
import { TuiScrollbar } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { tap, withLatestFrom } from 'rxjs'
|
import { tap, withLatestFrom } from 'rxjs'
|
||||||
@@ -15,9 +16,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { MarketplaceMenuComponent } from './components/menu.component'
|
import { MarketplaceMenuComponent } from './components/menu.component'
|
||||||
import { MarketplaceNotificationComponent } from './components/notification.component'
|
import { MarketplaceNotificationComponent } from './components/notification.component'
|
||||||
import { MarketplaceSidebarsComponent } from './components/sidebars.component'
|
|
||||||
import { MarketplaceTileComponent } from './components/tile.component'
|
import { MarketplaceTileComponent } from './components/tile.component'
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -56,7 +55,6 @@ import { i18nPipe } from '@start9labs/shared'
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</tui-scrollbar>
|
</tui-scrollbar>
|
||||||
<marketplace-sidebars />
|
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
styles: [
|
styles: [
|
||||||
@@ -153,7 +151,6 @@ import { i18nPipe } from '@start9labs/shared'
|
|||||||
MarketplaceTileComponent,
|
MarketplaceTileComponent,
|
||||||
MarketplaceMenuComponent,
|
MarketplaceMenuComponent,
|
||||||
MarketplaceNotificationComponent,
|
MarketplaceNotificationComponent,
|
||||||
MarketplaceSidebarsComponent,
|
|
||||||
TuiScrollbar,
|
TuiScrollbar,
|
||||||
FilterPackagesPipeModule,
|
FilterPackagesPipeModule,
|
||||||
TitleDirective,
|
TitleDirective,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
DialogService,
|
DialogService,
|
||||||
Exver,
|
Exver,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
|
MARKDOWN,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core'
|
import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core'
|
||||||
@@ -105,14 +106,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
|||||||
.outer-container {
|
.outer-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1.75rem;
|
|
||||||
min-width: 100%;
|
|
||||||
height: calc(100vh - var(--portal-header-height) - var(--bumper));
|
height: calc(100vh - var(--portal-header-height) - var(--bumper));
|
||||||
margin-top: 5rem;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-container {
|
.inner-container {
|
||||||
@@ -226,8 +220,21 @@ export class MarketplacePreviewComponent {
|
|||||||
this.router.navigate([], { queryParams: { id } })
|
this.router.navigate([], { queryParams: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatic(type: 'license' | 'instructions') {
|
onStatic(asset: 'license' | 'instructions') {
|
||||||
// @TODO Alex need to display License or Instructions. This requires an API request, check out next/minor
|
const label = asset === 'license' ? 'License' : 'Instructions'
|
||||||
|
const content = this.pkg$.pipe(
|
||||||
|
filter(Boolean),
|
||||||
|
switchMap(pkg =>
|
||||||
|
this.marketplaceService.fetchStatic$(
|
||||||
|
pkg,
|
||||||
|
asset === 'license' ? 'LICENSE.md' : 'instructions.md',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.dialog
|
||||||
|
.openComponent(MARKDOWN, { label, size: 'l', data: { content } })
|
||||||
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
selectVersion(
|
selectVersion(
|
||||||
|
|||||||
@@ -64,24 +64,25 @@ export class MarketplaceAlertsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
|
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
|
||||||
const content = alerts.install as i18nKey
|
const content = alerts.install
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!!content &&
|
!content ||
|
||||||
new Promise(resolve => {
|
(!!content &&
|
||||||
this.dialog
|
new Promise(resolve => {
|
||||||
.openConfirm<boolean>({
|
this.dialog
|
||||||
label: 'Alert',
|
.openConfirm<boolean>({
|
||||||
size: 's',
|
label: 'Alert',
|
||||||
data: {
|
size: 's',
|
||||||
content,
|
data: {
|
||||||
yes: 'Install',
|
content: content as i18nKey,
|
||||||
no: 'Cancel',
|
yes: 'Install',
|
||||||
},
|
no: 'Cancel',
|
||||||
})
|
},
|
||||||
.pipe(defaultIfEmpty(false))
|
})
|
||||||
.subscribe(response => resolve(response))
|
.pipe(defaultIfEmpty(false))
|
||||||
})
|
.subscribe(response => resolve(response))
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ export class ServiceActionRequestsComponent {
|
|||||||
|
|
||||||
readonly requests = computed(() =>
|
readonly requests = computed(() =>
|
||||||
Object.values(this.pkg().requestedActions)
|
Object.values(this.pkg().requestedActions)
|
||||||
// @TODO Alex uncomment filter line below to produce infinite loop on service details page when dependency not installed. This means the page is infinitely trying to re-render
|
|
||||||
// .filter(r => r.active)
|
|
||||||
.filter(
|
.filter(
|
||||||
r =>
|
r =>
|
||||||
this.services()[r.request.packageId]?.actions[r.request.actionId] &&
|
this.services()[r.request.packageId]?.actions[r.request.actionId] &&
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, inject, Input } from '@angular/core'
|
import { Component, inject, input } from '@angular/core'
|
||||||
|
import { toObservable } from '@angular/core/rxjs-interop'
|
||||||
import {
|
import {
|
||||||
AboutModule,
|
AboutModule,
|
||||||
AdditionalModule,
|
AdditionalModule,
|
||||||
@@ -12,11 +13,11 @@ import {
|
|||||||
MARKDOWN,
|
MARKDOWN,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
|
|
||||||
import { filter, first, map } from 'rxjs'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { filter, first, map, of, switchMap } from 'rxjs'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
|
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
|
||||||
import { MarketplacePkgSideload } from './sideload.utils'
|
import { MarketplacePkgSideload } from './sideload.utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -24,25 +25,25 @@ import { MarketplacePkgSideload } from './sideload.utils'
|
|||||||
template: `
|
template: `
|
||||||
<div class="outer-container">
|
<div class="outer-container">
|
||||||
<ng-content />
|
<ng-content />
|
||||||
<marketplace-package-hero [pkg]="pkg">
|
<marketplace-package-hero [pkg]="pkg()">
|
||||||
<marketplace-controls
|
<marketplace-controls
|
||||||
slot="controls"
|
slot="controls"
|
||||||
class="controls-wrapper"
|
class="controls-wrapper"
|
||||||
[pkg]="pkg"
|
[pkg]="pkg()"
|
||||||
[localPkg]="local$ | async"
|
[localPkg]="local$ | async"
|
||||||
[localFlavor]="!!(flavor$ | async)"
|
[localFlavor]="!!(flavor$ | async)"
|
||||||
[file]="file"
|
[file]="file()"
|
||||||
/>
|
/>
|
||||||
</marketplace-package-hero>
|
</marketplace-package-hero>
|
||||||
<div class="package-details">
|
<div class="package-details">
|
||||||
<div class="package-details-main">
|
<div class="package-details-main">
|
||||||
<marketplace-about [pkg]="pkg" />
|
<marketplace-about [pkg]="pkg()" />
|
||||||
@if (!(pkg.dependencyMetadata | empty)) {
|
@if (!(pkg().dependencyMetadata | empty)) {
|
||||||
<marketplace-dependencies [pkg]="pkg" />
|
<marketplace-dependencies [pkg]="pkg()" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="package-details-additional">
|
<div class="package-details-additional">
|
||||||
<marketplace-additional [pkg]="pkg" (static)="onStatic($event)" />
|
<marketplace-additional [pkg]="pkg()" (static)="onStatic($event)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,38 +109,35 @@ export class SideloadPackageComponent {
|
|||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly dialog = inject(DialogService)
|
private readonly dialog = inject(DialogService)
|
||||||
|
|
||||||
// @Input({ required: true })
|
readonly pkg = input.required<MarketplacePkgSideload>()
|
||||||
// pkg!: MarketplacePkgSideload
|
readonly file = input.required<File>()
|
||||||
|
|
||||||
// @TODO Alex why do I need to initialize pkg below? I would prefer to do the above, but it's not working
|
readonly local$ = toObservable(this.pkg).pipe(
|
||||||
@Input({ required: true })
|
|
||||||
pkg: MarketplacePkgSideload = {} as MarketplacePkgSideload
|
|
||||||
|
|
||||||
@Input({ required: true })
|
|
||||||
file!: File
|
|
||||||
|
|
||||||
readonly local$ = this.patch.watch$('packageData', this.pkg.id).pipe(
|
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
map(pkg =>
|
switchMap(({ id, flavor }) =>
|
||||||
this.exver.getFlavor(getManifest(pkg).version) === this.pkg.flavor
|
this.patch.watch$('packageData', id).pipe(
|
||||||
? pkg
|
filter(Boolean),
|
||||||
: null,
|
map(pkg =>
|
||||||
|
this.exver.getFlavor(getManifest(pkg).version) === flavor
|
||||||
|
? pkg
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
first(),
|
first(),
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly flavor$ = this.local$.pipe(map(pkg => !pkg))
|
readonly flavor$ = this.local$.pipe(map(pkg => !pkg))
|
||||||
|
|
||||||
// @TODO Alex, struggling to get this working. I don't understand how to use this markdown component, only one other example, and it's very different.
|
|
||||||
onStatic(type: 'license' | 'instructions') {
|
onStatic(type: 'license' | 'instructions') {
|
||||||
|
const label = type === 'license' ? 'License' : 'Instructions'
|
||||||
|
const key = type === 'license' ? 'fullLicense' : 'instructions'
|
||||||
|
|
||||||
this.dialog
|
this.dialog
|
||||||
.openComponent(MARKDOWN, {
|
.openComponent(MARKDOWN, {
|
||||||
label: type === 'license' ? 'License' : 'Instructions',
|
label,
|
||||||
size: 'l',
|
size: 'l',
|
||||||
data: {
|
data: { content: of(this.pkg()[key]) },
|
||||||
content:
|
|
||||||
this.pkg[type === 'license' ? 'fullLicense' : 'instructions'],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
|
|||||||
tuiIconButton
|
tuiIconButton
|
||||||
appearance="neutral"
|
appearance="neutral"
|
||||||
iconStart="@tui.x"
|
iconStart="@tui.x"
|
||||||
|
size="s"
|
||||||
[style.border-radius.%]="100"
|
[style.border-radius.%]="100"
|
||||||
[style.justify-self]="'end'"
|
[style.justify-self]="'end'"
|
||||||
(click)="clear()"
|
(click)="clear()"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
CifsBackupTarget,
|
CifsBackupTarget,
|
||||||
DiskBackupTarget,
|
DiskBackupTarget,
|
||||||
} from 'src/app/services/api/api.types'
|
} from 'src/app/services/api/api.types'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { BACKUP } from './backup.component'
|
import { BACKUP } from './backup.component'
|
||||||
@@ -103,7 +103,7 @@ import { BACKUP_RESTORE } from './restore.component'
|
|||||||
</tui-notification>
|
</tui-notification>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (type === 'create' && (eos.backingUp$ | async)) {
|
@if (type === 'create' && (os.backingUp$ | async)) {
|
||||||
<section backupProgress></section>
|
<section backupProgress></section>
|
||||||
} @else {
|
} @else {
|
||||||
@if (service.loading()) {
|
@if (service.loading()) {
|
||||||
@@ -163,7 +163,7 @@ export default class SystemBackupComponent implements OnInit {
|
|||||||
readonly dialog = inject(DialogService)
|
readonly dialog = inject(DialogService)
|
||||||
readonly type = inject(ActivatedRoute).snapshot.data['type']
|
readonly type = inject(ActivatedRoute).snapshot.data['type']
|
||||||
readonly service = inject(BackupService)
|
readonly service = inject(BackupService)
|
||||||
readonly eos = inject(EOSService)
|
readonly os = inject(OSService)
|
||||||
readonly server = toSignal(
|
readonly server = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
|
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ const ERROR =
|
|||||||
width: 13rem;
|
width: 13rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
[tuiButton] {
|
[tuiButton] {
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ import { toSignal } from '@angular/core/rxjs-interop'
|
|||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
|
DialogService,
|
||||||
ErrorService,
|
ErrorService,
|
||||||
|
i18nKey,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
i18nService,
|
i18nService,
|
||||||
LoadingService,
|
|
||||||
DialogService,
|
|
||||||
languages,
|
languages,
|
||||||
i18nKey,
|
Languages,
|
||||||
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||||
|
import { TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
|
||||||
import {
|
import {
|
||||||
TuiAppearance,
|
TuiAppearance,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
@@ -38,7 +40,7 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SnekDirective } from './snek.directive'
|
import { SnekDirective } from './snek.directive'
|
||||||
@@ -72,13 +74,13 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
tuiButton
|
tuiButton
|
||||||
appearance="accent"
|
appearance="accent"
|
||||||
iconStart="@tui.refresh-cw"
|
iconStart="@tui.refresh-cw"
|
||||||
[disabled]="eos.updatingOrBackingUp$ | async"
|
[disabled]="os.updatingOrBackingUp$ | async"
|
||||||
(click)="onUpdate()"
|
(click)="onUpdate()"
|
||||||
>
|
>
|
||||||
@if (server.statusInfo.updated) {
|
@if (server.statusInfo.updated) {
|
||||||
{{ 'Restart to apply' | i18n }}
|
{{ 'Restart to apply' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
@if (eos.showUpdate$ | async) {
|
@if (os.showUpdate$ | async) {
|
||||||
{{ 'Update' | i18n }}
|
{{ 'Update' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
{{ 'Check for updates' | i18n }}
|
{{ 'Check for updates' | i18n }}
|
||||||
@@ -98,7 +100,13 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
<tui-icon icon="@tui.languages" />
|
<tui-icon icon="@tui.languages" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>{{ 'Language' | i18n }}</strong>
|
<strong>{{ 'Language' | i18n }}</strong>
|
||||||
<span tuiSubtitle>{{ i18nService.language }}</span>
|
<span tuiSubtitle>
|
||||||
|
@if (language; as lang) {
|
||||||
|
{{ lang | i18n }}
|
||||||
|
} @else {
|
||||||
|
{{ i18nService.language }}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
tuiButtonSelect
|
tuiButtonSelect
|
||||||
@@ -112,6 +120,7 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
*tuiTextfieldDropdown
|
*tuiTextfieldDropdown
|
||||||
size="l"
|
size="l"
|
||||||
[items]="languages"
|
[items]="languages"
|
||||||
|
[itemContent]="translation"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,24 +228,32 @@ export default class SystemGeneralComponent {
|
|||||||
private readonly isTor = inject(ConfigService).isTor()
|
private readonly isTor = inject(ConfigService).isTor()
|
||||||
private readonly document = inject(DOCUMENT)
|
private readonly document = inject(DOCUMENT)
|
||||||
private readonly dialog = inject(DialogService)
|
private readonly dialog = inject(DialogService)
|
||||||
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
|
||||||
wipe = false
|
wipe = false
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
||||||
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
||||||
readonly eos = inject(EOSService)
|
readonly os = inject(OSService)
|
||||||
readonly i18nService = inject(i18nService)
|
readonly i18nService = inject(i18nService)
|
||||||
readonly languages = languages
|
readonly languages = languages
|
||||||
|
readonly translation: TuiStringHandler<TuiContext<Languages>> = ({
|
||||||
|
$implicit,
|
||||||
|
}) => this.i18n.transform($implicit)!
|
||||||
readonly score = toSignal(
|
readonly score = toSignal(
|
||||||
this.patch.watch$('ui', 'gaming', 'snake', 'highScore'),
|
this.patch.watch$('ui', 'gaming', 'snake', 'highScore'),
|
||||||
{ initialValue: 0 },
|
{ initialValue: 0 },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
get language(): Languages | undefined {
|
||||||
|
return this.languages.find(lang => lang === this.i18nService.language)
|
||||||
|
}
|
||||||
|
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
if (this.server()?.statusInfo.updated) {
|
if (this.server()?.statusInfo.updated) {
|
||||||
this.restart()
|
this.restart()
|
||||||
} else if (this.eos.updateAvailable$.value) {
|
} else if (this.os.updateAvailable$.value) {
|
||||||
this.update()
|
this.update()
|
||||||
} else {
|
} else {
|
||||||
this.check()
|
this.check()
|
||||||
@@ -334,9 +351,9 @@ export default class SystemGeneralComponent {
|
|||||||
const loader = this.loader.open('Checking for updates').subscribe()
|
const loader = this.loader.open('Checking for updates').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.eos.loadEos()
|
await this.os.loadOS()
|
||||||
|
|
||||||
if (this.eos.updateAvailable$.value) {
|
if (this.os.updateAvailable$.value) {
|
||||||
this.update()
|
this.update()
|
||||||
} else {
|
} else {
|
||||||
this.dialog
|
this.dialog
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { TuiDialogContext, TuiScrollbar, TuiButton } from '@taiga-ui/core'
|
import { TuiDialogContext, TuiScrollbar, TuiButton } from '@taiga-ui/core'
|
||||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -47,7 +47,7 @@ import { EOSService } from 'src/app/services/eos.service'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SystemUpdateModal {
|
export class SystemUpdateModal {
|
||||||
readonly versions = Object.entries(this.eosService.osUpdate?.releaseNotes!)
|
readonly versions = Object.entries(this.os.osUpdate?.releaseNotes!)
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.reverse()
|
.reverse()
|
||||||
.map(([version, notes]) => ({
|
.map(([version, notes]) => ({
|
||||||
@@ -60,7 +60,7 @@ export class SystemUpdateModal {
|
|||||||
private readonly loader: LoadingService,
|
private readonly loader: LoadingService,
|
||||||
private readonly errorService: ErrorService,
|
private readonly errorService: ErrorService,
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly eosService: EOSService,
|
private readonly os: OSService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
import { TuiButton, TuiIcon, TuiLink, TuiTitle } from '@taiga-ui/core'
|
import { TuiButton, TuiIcon, TuiLink, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiExpand } from '@taiga-ui/experimental'
|
import { TuiExpand } from '@taiga-ui/experimental'
|
||||||
import {
|
import {
|
||||||
TUI_CONFIRM,
|
|
||||||
TuiAvatar,
|
TuiAvatar,
|
||||||
TuiButtonLoading,
|
TuiButtonLoading,
|
||||||
TuiChevron,
|
TuiChevron,
|
||||||
@@ -73,6 +72,14 @@ import UpdatesComponent from './updates.component'
|
|||||||
<td class="desktop">{{ item().s9pk.publishedAt | date }}</td>
|
<td class="desktop">{{ item().s9pk.publishedAt | date }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
size="m"
|
||||||
|
appearance="icon"
|
||||||
|
[tuiChevron]="expanded()"
|
||||||
|
>
|
||||||
|
{{ 'Show more' | i18n }}
|
||||||
|
</button>
|
||||||
@if (local().stateInfo.state === 'updating') {
|
@if (local().stateInfo.state === 'updating') {
|
||||||
<tui-progress-circle
|
<tui-progress-circle
|
||||||
class="g-positive"
|
class="g-positive"
|
||||||
@@ -94,14 +101,6 @@ import UpdatesComponent from './updates.component'
|
|||||||
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
|
{{ error() ? ('Retry' | i18n) : ('Update' | i18n) }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
size="s"
|
|
||||||
appearance="icon"
|
|
||||||
[tuiChevron]="expanded()"
|
|
||||||
>
|
|
||||||
{{ 'Show more' | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1850,6 +1850,11 @@ export namespace Mock {
|
|||||||
listitems: ['192.168.1.1', '192.1681.23'],
|
listitems: ['192.168.1.1', '192.1681.23'],
|
||||||
name: 'Matt',
|
name: 'Matt',
|
||||||
},
|
},
|
||||||
|
other: {
|
||||||
|
external: {
|
||||||
|
'public-domain': 'test.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
port: 20,
|
port: 20,
|
||||||
rpcallowip: undefined,
|
rpcallowip: undefined,
|
||||||
|
|||||||
@@ -1058,11 +1058,11 @@ export class MockApiService extends ApiService {
|
|||||||
...Mock.LocalPkgs[params.id]!,
|
...Mock.LocalPkgs[params.id]!,
|
||||||
stateInfo: {
|
stateInfo: {
|
||||||
// if installing
|
// if installing
|
||||||
// state: 'installing',
|
state: 'installing',
|
||||||
|
|
||||||
// if updating
|
// if updating
|
||||||
state: 'updating',
|
// state: 'updating',
|
||||||
manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!,
|
// manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!,
|
||||||
|
|
||||||
// both
|
// both
|
||||||
installingInfo: {
|
installingInfo: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { NotificationService } from 'src/app/services/notification.service'
|
import { NotificationService } from 'src/app/services/notification.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -27,7 +27,7 @@ export class BadgeService {
|
|||||||
private readonly notifications = inject(NotificationService)
|
private readonly notifications = inject(NotificationService)
|
||||||
private readonly exver = inject(Exver)
|
private readonly exver = inject(Exver)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly system$ = inject(EOSService).updateAvailable$.pipe(
|
private readonly system$ = inject(OSService).updateAvailable$.pipe(
|
||||||
map(Number),
|
map(Number),
|
||||||
)
|
)
|
||||||
private readonly metrics$ = this.patch
|
private readonly metrics$ = this.patch
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class FormService {
|
|||||||
UntypedFormGroup | UntypedFormArray | UntypedFormControl
|
UntypedFormGroup | UntypedFormArray | UntypedFormControl
|
||||||
> = {}
|
> = {}
|
||||||
Object.entries(config).map(([key, spec]) => {
|
Object.entries(config).map(([key, spec]) => {
|
||||||
group[key] = this.getFormEntry(spec, current ? current[key] : undefined)
|
group[key] = this.getFormEntry(spec, current?.[key])
|
||||||
})
|
})
|
||||||
return this.formBuilder.group(group, { validators })
|
return this.formBuilder.group(group, { validators })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export class MarketplaceService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatic$(
|
fetchStatic$(
|
||||||
pkg: MarketplacePkg,
|
pkg: MarketplacePkg,
|
||||||
type: 'LICENSE.md' | 'instructions.md',
|
type: 'LICENSE.md' | 'instructions.md',
|
||||||
): Observable<string> {
|
): Observable<string> {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Version } from '@start9labs/start-sdk'
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class EOSService {
|
export class OSService {
|
||||||
osUpdate?: OSUpdate
|
osUpdate?: OSUpdate
|
||||||
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export class EOSService {
|
|||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async loadEos(): Promise<void> {
|
async loadOS(): Promise<void> {
|
||||||
const { version, id } = await getServerInfo(this.patch)
|
const { version, id } = await getServerInfo(this.patch)
|
||||||
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
|
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
|
||||||
const updateAvailable =
|
const updateAvailable =
|
||||||
@@ -1,41 +1,33 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { inject, Injectable } from '@angular/core'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { filter, map, share, switchMap } from 'rxjs/operators'
|
import { filter, map, share, switchMap } from 'rxjs/operators'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||||
|
|
||||||
// @TODO Alex this file has just become checking for StartOS updates. Maybe it can be removed/simplified. I'm not sure why getMarketplace$() line is commented out, I assume we are checking for service updates somewhere else?
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class PatchDataService extends Observable<void> {
|
export class PatchDataService extends Observable<void> {
|
||||||
private readonly stream$ = this.connection$.pipe(
|
private readonly patch: PatchDB<DataModel> = inject(PatchDB)
|
||||||
|
private readonly os = inject(OSService)
|
||||||
|
private readonly bootstrapper = inject(LocalStorageBootstrap)
|
||||||
|
private readonly stream$ = inject(ConnectionService).pipe(
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
switchMap(() => this.patch.watch$()),
|
switchMap(() => this.patch.watch$()),
|
||||||
map((cache, index) => {
|
map((cache, index) => {
|
||||||
this.bootstrapper.update(cache)
|
this.bootstrapper.update(cache)
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
this.checkForUpdates()
|
this.os.loadOS()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
share(),
|
share(),
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
private readonly patch: PatchDB<DataModel>,
|
|
||||||
private readonly eosService: EOSService,
|
|
||||||
private readonly connection$: ConnectionService,
|
|
||||||
private readonly bootstrapper: LocalStorageBootstrap,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkForUpdates(): void {
|
|
||||||
this.eosService.loadEos()
|
|
||||||
// this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const STATUS = new InjectionToken('', {
|
|||||||
return CONNECTED
|
return CONNECTED
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ initialValue: CONNECTED },
|
{ initialValue: CONNECTING },
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user