diff --git a/web/README.md b/web/README.md index affc8510c..23dc8027e 100644 --- a/web/README.md +++ b/web/README.md @@ -92,7 +92,24 @@ cp proxy.conf-sample.json proxy.conf.json npm run start:ui:proxy ``` -## Updating translations +## Translations + +### Currently supported languages + +- Spanish +- Polish +- German + ### Adding a new translation @@ -119,22 +136,20 @@ Translate the English dictionary below into ``. Format the result as a #### Sample AI prompt -Translate `` 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 - Polish - German - #### Adding to StartOS diff --git a/web/projects/marketplace/src/components/menu/menu.component.html b/web/projects/marketplace/src/components/menu/menu.component.html index 2964329e3..69d7e8510 100644 --- a/web/projects/marketplace/src/components/menu/menu.component.html +++ b/web/projects/marketplace/src/components/menu/menu.component.html @@ -21,14 +21,7 @@ [(query)]="query" (queryChange)="onQueryChange($event)" /> - - - - - diff --git a/web/projects/marketplace/src/components/menu/menu.component.module.ts b/web/projects/marketplace/src/components/menu/menu.component.module.ts index 64a8d63ed..b8e54ecbd 100644 --- a/web/projects/marketplace/src/components/menu/menu.component.module.ts +++ b/web/projects/marketplace/src/components/menu/menu.component.module.ts @@ -1,15 +1,19 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { SharedPipesModule } from '@start9labs/shared' -import { TuiSkeleton } from '@taiga-ui/kit' - -import { MenuComponent } from './menu.component' -import { TuiLoader, TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core' -import { TuiActiveZone, TuiLet } from '@taiga-ui/cdk' -import { TuiSidebar } from '@taiga-ui/addon-mobile' -import { SearchModule } from '../../pages/list/search/search.module' +import { TuiLet } from '@taiga-ui/cdk' +import { + TuiAppearance, + TuiButton, + TuiIcon, + TuiLoader, + TuiPopup, +} from '@taiga-ui/core' +import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit' 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 { MenuComponent } from './menu.component' @NgModule({ imports: [ @@ -17,8 +21,6 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod SharedPipesModule, SearchModule, CategoriesModule, - TuiActiveZone, - ...TuiSidebar, TuiLoader, TuiButton, CategoriesModule, @@ -27,6 +29,8 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod TuiAppearance, TuiIcon, TuiSkeleton, + TuiDrawer, + TuiPopup, ], declarations: [MenuComponent], exports: [MenuComponent], diff --git a/web/projects/marketplace/src/components/menu/menu.component.scss b/web/projects/marketplace/src/components/menu/menu.component.scss index 760aef902..9970bd4ee 100644 --- a/web/projects/marketplace/src/components/menu/menu.component.scss +++ b/web/projects/marketplace/src/components/menu/menu.component.scss @@ -5,6 +5,12 @@ padding: 0; } +tui-drawer { + top: 0; + border-radius: 0; + background-color: rgb(var(--tw-color-zinc-700) / 0.9); +} + header { @include scrollbar-hidden(); @@ -131,12 +137,10 @@ header { } &-sidebar { - background-color: rgb(var(--tw-color-zinc-700) / 0.9); - width: 70vw; display: flex; flex-direction: column; height: 100%; - overflow: auto; + margin: -1.25rem -1.5rem; &-top { display: flex; @@ -166,7 +170,7 @@ header { marketplace-categories { flex-grow: 1; - padding: 1.25rem 1.25rem 0px 1.25rem; + padding: 1.25rem 1.25rem 0 1.25rem; } ::ng-deep a { @@ -191,7 +195,7 @@ header { .divide-bar > * + * { border-top-width: 1px; - border-bottom-width: 0px; + border-bottom-width: 0; border-color: rgb(113 113 122); } } diff --git a/web/projects/marketplace/src/components/menu/menu.component.ts b/web/projects/marketplace/src/components/menu/menu.component.ts index bcf242935..6601e54f4 100644 --- a/web/projects/marketplace/src/components/menu/menu.component.ts +++ b/web/projects/marketplace/src/components/menu/menu.component.ts @@ -4,6 +4,7 @@ import { inject, Input, OnDestroy, + signal, } from '@angular/core' import { MarketplaceConfig } from '@start9labs/shared' import { Subject, takeUntil } from 'rxjs' @@ -27,7 +28,7 @@ export class MenuComponent implements OnDestroy { private readonly categoryService = inject(AbstractCategoryService) category = '' query = '' - open = false + readonly open = signal(false) ngOnInit() { this.categoryService @@ -57,10 +58,6 @@ export class MenuComponent implements OnDestroy { this.categoryService.setQuery(query) } - toggleMenu(open: boolean): void { - this.open = open - } - ngOnDestroy(): void { this.destroy$.next() this.destroy$.complete() diff --git a/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts b/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts index b25950f52..ae0112a1f 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts @@ -1,29 +1,29 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { TuiIcon, TuiTitle } from '@taiga-ui/core' -import { TuiLineClamp } from '@taiga-ui/kit' +import { TuiFade } from '@taiga-ui/kit' @Component({ selector: 'marketplace-additional-item', template: ` -
- - -
+ + `, styles: [ ` - .item-container { + :host { display: flex; justify-content: space-between; align-items: center; + gap: 0.5rem; padding: 0.75rem 0.25rem; + white-space: nowrap; &:hover { - background-color: rgb(113 113 122 / 0.1); + background-color: var(--tui-background-neutral-1); } [tuiSubtitle] { @@ -34,16 +34,11 @@ import { TuiLineClamp } from '@taiga-ui/kit' opacity: 0.7; } } - - ::ng-deep .t-text { - font-family: 'Montserrat'; - font-weight: 600; - } `, ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, TuiLineClamp, TuiIcon, TuiTitle], + imports: [CommonModule, TuiIcon, TuiTitle, TuiFade], }) export class MarketplaceAdditionalItemComponent { @Input({ required: true }) diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.scss b/web/projects/marketplace/src/pages/show/additional/additional.component.scss index c8c41396e..37bce5a39 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.scss +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.scss @@ -7,6 +7,7 @@ .detail-container { display: grid; grid-auto-flow: row; + grid-auto-columns: minmax(0, 1fr); & > * + * { border-top-width: 1px; diff --git a/web/projects/shared/src/components/markdown.component.ts b/web/projects/shared/src/components/markdown.component.ts index 11962e5bd..48f954d1b 100644 --- a/web/projects/shared/src/components/markdown.component.ts +++ b/web/projects/shared/src/components/markdown.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute, Data } from '@angular/router' import { TuiDialogContext, TuiLoader, TuiNotification } from '@taiga-ui/core' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' 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 { MarkdownPipe } from '../pipes/markdown.pipe' import { getErrorMessage } from '../services/error.service' @@ -36,8 +36,9 @@ import { getErrorMessage } from '../services/error.service' }) export class MarkdownComponent { private readonly data = - injectContext>({ optional: true })?.data || - inject(ActivatedRoute).snapshot.data + injectContext }>>({ + optional: true, + })?.data || inject(ActivatedRoute).snapshot.data readonly content = toSignal(this.data['content']) readonly error = toSignal( diff --git a/web/projects/shared/src/directives/safe-links.directive.ts b/web/projects/shared/src/directives/safe-links.directive.ts index c946e101c..16be4553a 100644 --- a/web/projects/shared/src/directives/safe-links.directive.ts +++ b/web/projects/shared/src/directives/safe-links.directive.ts @@ -1,28 +1,39 @@ -import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core' 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({ selector: '[safeLinks]', + providers: [ + MutationObserverService, + provideMutationObserverInit({ + childList: true, + subtree: true, + }), + ], standalone: true, }) -export class SafeLinksDirective implements AfterViewInit { - constructor( - @Inject(DOCUMENT) private readonly document: Document, - private readonly elementRef: ElementRef, - ) {} - - ngAfterViewInit() { - Array.from(this.document.links) - .filter( - link => - link.hostname !== this.document.location.hostname && - this.elementRef.nativeElement.contains(link), - ) - .forEach(link => { - link.target = '_blank' - link.setAttribute('rel', 'noreferrer') - link.classList.add('g-external-link') - }) - } +export class SafeLinksDirective { + private readonly doc = inject(DOCUMENT) + private readonly el = tuiInjectElement() + private readonly sub = inject(MutationObserverService) + .pipe(takeUntilDestroyed()) + .subscribe(() => { + Array.from(this.doc.links) + .filter( + link => + link.hostname !== this.doc.location.hostname && + this.el.contains(link), + ) + .forEach(link => { + link.target = '_blank' + link.setAttribute('rel', 'noreferrer') + link.classList.add('g-external-link') + }) + }) } diff --git a/web/projects/shared/src/i18n/dictionaries/german.ts b/web/projects/shared/src/i18n/dictionaries/de.ts similarity index 99% rename from web/projects/shared/src/i18n/dictionaries/german.ts rename to web/projects/shared/src/i18n/dictionaries/de.ts index 2d8d1a32d..b5431dc09 100644 --- a/web/projects/shared/src/i18n/dictionaries/german.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -488,4 +488,8 @@ export default { 485: 'StartOS-Benutzeroberfläche', 486: 'WiFi', 487: 'Anleitungen', + 488: 'spanisch', + 489: 'polnisch', + 490: 'deutsch', + 491: 'englisch', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/english.ts b/web/projects/shared/src/i18n/dictionaries/en.ts similarity index 99% rename from web/projects/shared/src/i18n/dictionaries/english.ts rename to web/projects/shared/src/i18n/dictionaries/en.ts index 070b8d788..e0dc43267 100644 --- a/web/projects/shared/src/i18n/dictionaries/english.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -487,4 +487,8 @@ export const ENGLISH = { 'StartOS UI': 485, 'WiFi': 486, 'Instructions': 487, + 'spanish': 488, + 'polish': 489, + 'german': 490, + 'english': 491, } as const diff --git a/web/projects/shared/src/i18n/dictionaries/spanish.ts b/web/projects/shared/src/i18n/dictionaries/es.ts similarity index 99% rename from web/projects/shared/src/i18n/dictionaries/spanish.ts rename to web/projects/shared/src/i18n/dictionaries/es.ts index 128c3d593..577f1cc3e 100644 --- a/web/projects/shared/src/i18n/dictionaries/spanish.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -488,4 +488,8 @@ export default { 485: 'Interfaz de StartOS', 486: 'WiFi', 487: 'Instrucciones', + 488: 'español', + 489: 'polaco', + 490: 'alemán', + 491: 'inglés', } as any satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/polish.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts similarity index 99% rename from web/projects/shared/src/i18n/dictionaries/polish.ts rename to web/projects/shared/src/i18n/dictionaries/pl.ts index 2f2d0cdb0..c3d03db64 100644 --- a/web/projects/shared/src/i18n/dictionaries/polish.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -488,4 +488,8 @@ export default { 485: 'Interfejs StartOS', 486: 'WiFi', 487: 'instrukcje', + 488: 'hiszpański', + 489: 'polski', + 490: 'niemiecki', + 491: 'angielski', } satisfies i18n diff --git a/web/projects/shared/src/i18n/i18n.pipe.ts b/web/projects/shared/src/i18n/i18n.pipe.ts index 9313e038b..0c4bb3240 100644 --- a/web/projects/shared/src/i18n/i18n.pipe.ts +++ b/web/projects/shared/src/i18n/i18n.pipe.ts @@ -1,5 +1,5 @@ import { inject, Injectable, Pipe, PipeTransform } from '@angular/core' -import { ENGLISH } from './dictionaries/english' +import { ENGLISH } from './dictionaries/en' import { I18N, i18nKey } from './i18n.providers' @Pipe({ diff --git a/web/projects/shared/src/i18n/i18n.providers.ts b/web/projects/shared/src/i18n/i18n.providers.ts index 83ee2a741..03380833a 100644 --- a/web/projects/shared/src/i18n/i18n.providers.ts +++ b/web/projects/shared/src/i18n/i18n.providers.ts @@ -5,7 +5,7 @@ import { tuiLanguageSwitcher, TuiLanguageSwitcherService, } from '@taiga-ui/i18n' -import { ENGLISH } from './dictionaries/english' +import { ENGLISH } from './dictionaries/en' import { i18nService } from './i18n.service' export type i18nKey = keyof typeof ENGLISH @@ -36,11 +36,11 @@ export const I18N_PROVIDERS = [ useValue: async (language: TuiLanguageName): Promise => { switch (language) { case 'spanish': - return import('./dictionaries/spanish').then(v => v.default) + return import('./dictionaries/es').then(v => v.default) case 'polish': - return import('./dictionaries/polish').then(v => v.default) + return import('./dictionaries/pl').then(v => v.default) case 'german': - return import('./dictionaries/german').then(v => v.default) + return import('./dictionaries/de').then(v => v.default) default: return null } diff --git a/web/projects/shared/src/services/error.service.ts b/web/projects/shared/src/services/error.service.ts index fa5394b11..5246778df 100644 --- a/web/projects/shared/src/services/error.service.ts +++ b/web/projects/shared/src/services/error.service.ts @@ -2,7 +2,6 @@ import { ErrorHandler, inject, Injectable } from '@angular/core' import { TuiAlertService } from '@taiga-ui/core' import { HttpError } from '../classes/http-error' -// @TODO Alex: Enable this as ErrorHandler @Injectable({ providedIn: 'root', }) diff --git a/web/projects/shared/styles/taiga.scss b/web/projects/shared/styles/taiga.scss index 2a5893db5..fc3716a9f 100644 --- a/web/projects/shared/styles/taiga.scss +++ b/web/projects/shared/styles/taiga.scss @@ -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 a[tuiIconButton]:not([href]) { pointer-events: none; diff --git a/web/projects/ui/src/app/app.component.html b/web/projects/ui/src/app/app.component.html deleted file mode 100644 index fffece163..000000000 --- a/web/projects/ui/src/app/app.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/projects/ui/src/app/app.component.scss b/web/projects/ui/src/app/app.component.scss deleted file mode 100644 index 81d9d1279..000000000 --- a/web/projects/ui/src/app/app.component.scss +++ /dev/null @@ -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); - } -} diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 5daf01308..95c00182e 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -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 { Title } from '@angular/platform-browser' import { i18nService } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' -import { combineLatest, map, merge, startWith } from 'rxjs' -import { ConnectionService } from './services/connection.service' +import { merge } from 'rxjs' import { PatchDataService } from './services/patch-data.service' import { DataModel } from './services/patch-db/data-model' import { PatchMonitorService } from './services/patch-monitor.service' @Component({ selector: 'app-root', - templateUrl: 'app.component.html', - styleUrls: ['app.component.scss'], + template: ` + + + + + `, + 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 i18n = inject(i18nService) - private readonly patch = inject>(PatchDB) readonly subscription = merge( inject(PatchDataService), @@ -26,23 +39,11 @@ export class AppComponent implements OnInit { .pipe(takeUntilDestroyed()) .subscribe() - readonly offline$ = combineLatest([ - inject(ConnectionService), - this.patch - .watch$('serverInfo', 'statusInfo') - .pipe(startWith({ restarting: false, shuttingDown: false })), - ]).pipe( - map( - ([connected, { restarting, shuttingDown }]) => - connected && (restarting || shuttingDown), - ), - startWith(true), - ) - - ngOnInit() { - this.patch.watch$('ui').subscribe(({ name, language }) => { + readonly ui = inject>(PatchDB) + .watch$('ui') + .pipe(takeUntilDestroyed()) + .subscribe(({ name, language }) => { this.title.setTitle(name || 'StartOS') this.i18n.setLanguage(language || 'english') }) - } } diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index a5b3dbe86..ae27adb55 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -3,7 +3,6 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { ServiceWorkerModule } from '@angular/service-worker' 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 { environment } from '../environments/environment' import { AppComponent } from './app.component' @@ -24,7 +23,6 @@ import { RoutingModule } from './routing.module' // or after 30 seconds (whichever comes first). registrationStrategy: 'registerWhenStable:30000', }), - SidebarHostComponent, ], providers: APP_PROVIDERS, bootstrap: [AppComponent], diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index fa5baa8b5..79abcedfe 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -19,7 +19,7 @@ import { tuiDropdownOptionsProvider, tuiNumberFormatProvider, } from '@taiga-ui/core' -import { NG_EVENT_PLUGINS } from '@taiga-ui/event-plugins' +import { provideEventPlugins } from '@taiga-ui/event-plugins' import { TUI_DATE_TIME_VALUE_TRANSFORMER, TUI_DATE_VALUE_TRANSFORMER, @@ -48,7 +48,7 @@ const { } = require('../../../../config.json') as WorkspaceConfig export const APP_PROVIDERS: Provider[] = [ - NG_EVENT_PLUGINS, + provideEventPlugins(), I18N_PROVIDERS, FilterPackagesPipe, UntypedFormBuilder, diff --git a/web/projects/ui/src/app/components/refresh-alert.component.ts b/web/projects/ui/src/app/components/refresh-alert.component.ts index 1d97ba15e..bf2676141 100644 --- a/web/projects/ui/src/app/components/refresh-alert.component.ts +++ b/web/projects/ui/src/app/components/refresh-alert.component.ts @@ -1,73 +1,94 @@ -import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' 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 { TuiButton, TuiDialog } from '@taiga-ui/core' +import { TuiButton } from '@taiga-ui/core' 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 { DataModel } from 'src/app/services/patch-db/data-model' -// @TODO Alex @Component({ standalone: true, selector: 'refresh-alert', template: ` - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @if (isPwa) { +

+ Your user interface is cached and out of date. Attempt to reload the + PWA using the button below. If you continue to see this message, + uninstall and reinstall the PWA. +

+ + } @else { + Your user interface is cached and out of date. Hard refresh the page to + get the latest UI. +
    +
  • + On Mac + : cmd + shift + R +
  • +
  • + On Linux/Windows + : ctrl + shift + R +
  • +
+ + } +
`, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiDialog, AsyncPipe, TuiButton, TuiAutoFocus], + imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus], }) export class RefreshAlertComponent { + private readonly win = inject(WA_WINDOW) private readonly updates = inject(SwUpdate) private readonly loader = inject(LoadingService) - private readonly exver = inject(Exver) - private readonly config = inject(ConfigService) - private readonly dismiss$ = new Subject() + private readonly version = Version.parse(inject(ConfigService).version) - readonly show$ = merge( - this.dismiss$, - inject>(PatchDB) - .watch$('serverInfo', 'version') - .pipe( - map(version => !!this.exver.compareExver(this.config.version, version)), - endWith(false), - ), - ).pipe(debounceTime(0)) + readonly dismiss$ = new Subject() + readonly isPwa = this.win.matchMedia('(display-mode: standalone)').matches - // @TODO use this like we did on 0344 - onPwa = false - - ngOnInit() { - this.onPwa = window.matchMedia('(display-mode: standalone)').matches - } + readonly show = toSignal( + merge( + this.dismiss$.pipe(map(() => false)), + inject>(PatchDB) + .watch$('serverInfo', 'version') + .pipe( + distinctUntilChanged(), + map(v => this.version.compare(Version.parse(v)) !== 'equal'), + ), + ), + { + initialValue: false, + }, + ) async pwaReload() { const loader = this.loader.open('Reloading PWA').subscribe() @@ -80,11 +101,7 @@ export class RefreshAlertComponent { } finally { loader.unsubscribe() // always reload, as this resolves most out of sync cases - window.location.reload() + this.win.location.reload() } } - - onDismiss() { - this.dismiss$.next(false) - } } diff --git a/web/projects/ui/src/app/components/sidebar-host.component.ts b/web/projects/ui/src/app/components/sidebar-host.component.ts deleted file mode 100644 index bbea6f3d8..000000000 --- a/web/projects/ui/src/app/components/sidebar-host.component.ts +++ /dev/null @@ -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: '', - styles: [':host { position: fixed; top: 0; }'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - providers: [{ provide: TuiPortalService, useExisting: SidebarService }], -}) -export class SidebarHostComponent extends TuiPortals {} diff --git a/web/projects/ui/src/app/routes/diagnostic/diagnostic.module.ts b/web/projects/ui/src/app/routes/diagnostic/diagnostic.module.ts index 8acd99578..b1a03dcd8 100644 --- a/web/projects/ui/src/app/routes/diagnostic/diagnostic.module.ts +++ b/web/projects/ui/src/app/routes/diagnostic/diagnostic.module.ts @@ -9,8 +9,7 @@ const ROUTES: Routes = [ }, { path: 'logs', - loadChildren: () => - import('./logs/logs.module').then(m => m.LogsPageModule), + loadComponent: () => import('./logs.component'), }, ] diff --git a/web/projects/ui/src/app/routes/diagnostic/logs/logs.page.ts b/web/projects/ui/src/app/routes/diagnostic/logs.component.ts similarity index 56% rename from web/projects/ui/src/app/routes/diagnostic/logs/logs.page.ts rename to web/projects/ui/src/app/routes/diagnostic/logs.component.ts index 15c44adb7..6042fbdc8 100644 --- a/web/projects/ui/src/app/routes/diagnostic/logs/logs.page.ts +++ b/web/projects/ui/src/app/routes/diagnostic/logs.component.ts @@ -1,12 +1,43 @@ 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 { 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' @Component({ - selector: 'logs', - templateUrl: './logs.page.html', + standalone: true, + template: ` + + Back + + +
+ @if (loading) { + + } +
+ @for (log of logs; track log) { +

+      }
+    
+ `, styles: ` :host { max-height: 100vh; @@ -18,14 +49,18 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' background: var(--tui-background-base); } `, - providers: [ - { - provide: INTERSECTION_ROOT, - useExisting: ElementRef, - }, + imports: [ + RouterLink, + WaIntersectionObserver, + 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 }) private readonly scrollbar?: ElementRef private readonly api = inject(ApiService) diff --git a/web/projects/ui/src/app/routes/diagnostic/logs/logs.module.ts b/web/projects/ui/src/app/routes/diagnostic/logs/logs.module.ts deleted file mode 100644 index e3e498efb..000000000 --- a/web/projects/ui/src/app/routes/diagnostic/logs/logs.module.ts +++ /dev/null @@ -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 {} diff --git a/web/projects/ui/src/app/routes/diagnostic/logs/logs.page.html b/web/projects/ui/src/app/routes/diagnostic/logs/logs.page.html deleted file mode 100644 index df2def9fc..000000000 --- a/web/projects/ui/src/app/routes/diagnostic/logs/logs.page.html +++ /dev/null @@ -1,23 +0,0 @@ - - Back - - -
- @if (loading) { - - } -
- @for (log of logs; track log) { -

-  }
-
diff --git a/web/projects/ui/src/app/routes/portal/components/form.component.ts b/web/projects/ui/src/app/routes/portal/components/form.component.ts index 31cc464c5..c6554ed72 100644 --- a/web/projects/ui/src/app/routes/portal/components/form.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form.component.ts @@ -80,7 +80,7 @@ export interface FormContext { display: flex; justify-content: flex-end; padding: 1rem 0; - margin: 1rem 0 -1rem; + margin: 1rem -1px -1rem; gap: 1rem; background: var(--tui-background-elevation-1); border-top: 1px solid var(--tui-background-base-alt); diff --git a/web/projects/ui/src/app/routes/portal/components/form/control.ts b/web/projects/ui/src/app/routes/portal/components/form/control.ts index 91ad64489..f21f06736 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/control.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/control.ts @@ -17,7 +17,6 @@ export abstract class Control< return this.control.spec } - // @TODO Alex: Properly handle already set immutable value get readOnly(): boolean { return ( !!this.value && !!this.control.control?.pristine && this.control.immutable diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts index 2295bbc2f..fd4f18218 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts @@ -5,7 +5,7 @@ import { Control } from '../control' @Component({ selector: 'form-toggle', templateUrl: './form-toggle.component.html', - host: { style: 'display: flex' }, + host: { class: 'g-toggle' }, }) export class FormToggleComponent extends Control< IST.ValueSpecToggle, diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts index 9d889a9fa..1d702e86e 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts @@ -24,27 +24,28 @@ import { tuiPure } from '@taiga-ui/cdk' }) export class FormUnionComponent implements OnChanges { @Input({ required: true }) - spec!: IST.ValueSpecUnion + spec!: IST.ValueSpecUnion & { others?: Record } selectSpec!: IST.ValueSpecSelect private readonly form = inject(FormGroupName) private readonly formService = inject(FormService) - private readonly values: Record = {} get union(): string { return this.form.value.selection } + // OTHER? @tuiPure 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( 'value', this.formService.getFormGroup( union ? this.spec.variants[union]?.spec || {} : {}, [], - this.values[union], + this.spec.others[union], ), { emitEvent: false, diff --git a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts index 0b50916d1..c6a2ffa4f 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts @@ -31,6 +31,9 @@ import { HeaderStatusComponent } from './status.component' border-radius: var(--bumper); margin: var(--bumper); overflow: hidden; + filter: grayscale(1) brightness(0.75); + + @include transition(filter); .mobile { display: none; @@ -88,10 +91,12 @@ import { HeaderStatusComponent } from './status.component' &:has([data-status='neutral']) { --status: var(--tui-status-neutral); + filter: none; } &:has([data-status='success']) { --status: transparent; + filter: none; } } diff --git a/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts b/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts index e50543cb7..1753a1aad 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts @@ -25,6 +25,7 @@ import { getMenu } from 'src/app/utils/system-utilities' tuiHintDirection="bottom" [tuiHintShowDelay]="1000" [routerLink]="item.routerLink" + [class.link_system]="item.routerLink === '/portal/system'" [tuiHint]="!rla.isActive ? item.name : ''" > } @else { - + }
- {{'Scroll to bottom' | i18n}} + {{ 'Scroll to bottom' | i18n }} diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss index 928250f8b..4bc5db042 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss @@ -21,6 +21,11 @@ margin-bottom: -5rem; } +.loader { + position: absolute; + inset: 0; +} + .bottom { height: 3rem; } diff --git a/web/projects/ui/src/app/routes/portal/portal.component.ts b/web/projects/ui/src/app/routes/portal/portal.component.ts index c7275e75e..b5c300b3c 100644 --- a/web/projects/ui/src/app/routes/portal/portal.component.ts +++ b/web/projects/ui/src/app/routes/portal/portal.component.ts @@ -20,6 +20,8 @@ import { HeaderComponent } from './components/header/header.component' `, styles: [ ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + :host { height: 100%; display: flex; @@ -32,6 +34,14 @@ import { HeaderComponent } from './components/header/header.component' flex: 1; overflow: hidden; 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; + } } `, ], diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts index 8239d0b4d..0abef3b10 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts @@ -4,7 +4,6 @@ import { ChangeDetectionStrategy, Component, inject, - input, Input, } from '@angular/core' import { Router } from '@angular/router' @@ -76,7 +75,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' - - + + + + + `, styles: [ ` :host { + cursor: pointer; animation: animateIn 400ms calc(var(--animation-order) * 200ms) both; } + tui-drawer { + top: 0; + width: 28rem; + border-radius: 0; + } + @keyframes animateIn { from { opacity: 0; @@ -119,9 +127,9 @@ import { MarketplaceControlsComponent } from './controls.component' CommonModule, ItemModule, TuiAutoFocus, - TuiClickOutside, - TuiSidebar, TuiButton, + TuiPopup, + TuiDrawer, MarketplaceControlsComponent, MarketplacePreviewComponent, ], diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts index 46516c634..aff8a07fd 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts @@ -7,6 +7,7 @@ import { FilterPackagesPipe, FilterPackagesPipeModule, } from '@start9labs/marketplace' +import { i18nPipe } from '@start9labs/shared' import { TuiScrollbar } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' 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 { MarketplaceMenuComponent } from './components/menu.component' import { MarketplaceNotificationComponent } from './components/notification.component' -import { MarketplaceSidebarsComponent } from './components/sidebars.component' import { MarketplaceTileComponent } from './components/tile.component' -import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, @@ -56,7 +55,6 @@ import { i18nPipe } from '@start9labs/shared' - `, host: { class: 'g-page' }, styles: [ @@ -153,7 +151,6 @@ import { i18nPipe } from '@start9labs/shared' MarketplaceTileComponent, MarketplaceMenuComponent, MarketplaceNotificationComponent, - MarketplaceSidebarsComponent, TuiScrollbar, FilterPackagesPipeModule, TitleDirective, diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts index 9139a6874..48d84c439 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts @@ -21,6 +21,7 @@ import { DialogService, Exver, i18nPipe, + MARKDOWN, SharedPipesModule, } from '@start9labs/shared' import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core' @@ -105,14 +106,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service' .outer-container { display: flex; flex-direction: column; - padding: 1.75rem; - min-width: 100%; height: calc(100vh - var(--portal-header-height) - var(--bumper)); - margin-top: 5rem; - - @media (min-width: 768px) { - margin-top: 0; - } } .inner-container { @@ -226,8 +220,21 @@ export class MarketplacePreviewComponent { this.router.navigate([], { queryParams: { id } }) } - onStatic(type: 'license' | 'instructions') { - // @TODO Alex need to display License or Instructions. This requires an API request, check out next/minor + onStatic(asset: 'license' | 'instructions') { + 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( diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts index d40c589ff..a108442c2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts @@ -64,24 +64,25 @@ export class MarketplaceAlertsService { } async alertInstall({ alerts }: MarketplacePkgBase): Promise { - const content = alerts.install as i18nKey + const content = alerts.install return ( - !!content && - new Promise(resolve => { - this.dialog - .openConfirm({ - label: 'Alert', - size: 's', - data: { - content, - yes: 'Install', - no: 'Cancel', - }, - }) - .pipe(defaultIfEmpty(false)) - .subscribe(response => resolve(response)) - }) + !content || + (!!content && + new Promise(resolve => { + this.dialog + .openConfirm({ + label: 'Alert', + size: 's', + data: { + content: content as i18nKey, + yes: 'Install', + no: 'Cancel', + }, + }) + .pipe(defaultIfEmpty(false)) + .subscribe(response => resolve(response)) + })) ) } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/action-requests.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/action-requests.component.ts index e48cd52a0..185720b49 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/action-requests.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/action-requests.component.ts @@ -57,8 +57,6 @@ export class ServiceActionRequestsComponent { readonly requests = computed(() => 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( r => this.services()[r.request.packageId]?.actions[r.request.actionId] && diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts index 4cbbd00f4..3ec61d0ae 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts @@ -1,5 +1,6 @@ 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 { AboutModule, AdditionalModule, @@ -12,11 +13,11 @@ import { MARKDOWN, SharedPipesModule, } from '@start9labs/shared' -import { MarketplaceControlsComponent } from '../marketplace/components/controls.component' -import { filter, first, map } from 'rxjs' 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 { getManifest } from 'src/app/utils/get-package-data' +import { MarketplaceControlsComponent } from '../marketplace/components/controls.component' import { MarketplacePkgSideload } from './sideload.utils' @Component({ @@ -24,25 +25,25 @@ import { MarketplacePkgSideload } from './sideload.utils' template: `
- +
- - @if (!(pkg.dependencyMetadata | empty)) { - + + @if (!(pkg().dependencyMetadata | empty)) { + }
- +
@@ -108,38 +109,35 @@ export class SideloadPackageComponent { private readonly patch = inject>(PatchDB) private readonly dialog = inject(DialogService) - // @Input({ required: true }) - // pkg!: MarketplacePkgSideload + readonly pkg = input.required() + readonly file = input.required() - // @TODO Alex why do I need to initialize pkg below? I would prefer to do the above, but it's not working - @Input({ required: true }) - pkg: MarketplacePkgSideload = {} as MarketplacePkgSideload - - @Input({ required: true }) - file!: File - - readonly local$ = this.patch.watch$('packageData', this.pkg.id).pipe( + readonly local$ = toObservable(this.pkg).pipe( filter(Boolean), - map(pkg => - this.exver.getFlavor(getManifest(pkg).version) === this.pkg.flavor - ? pkg - : null, + switchMap(({ id, flavor }) => + this.patch.watch$('packageData', id).pipe( + filter(Boolean), + map(pkg => + this.exver.getFlavor(getManifest(pkg).version) === flavor + ? pkg + : null, + ), + ), ), first(), ) 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') { + const label = type === 'license' ? 'License' : 'Instructions' + const key = type === 'license' ? 'fullLicense' : 'instructions' + this.dialog .openComponent(MARKDOWN, { - label: type === 'license' ? 'License' : 'Instructions', + label, size: 'l', - data: { - content: - this.pkg[type === 'license' ? 'fullLicense' : 'instructions'], - }, + data: { content: of(this.pkg()[key]) }, }) .subscribe() } diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts index 54cec21ed..b76efc4f6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts @@ -27,6 +27,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared' tuiIconButton appearance="neutral" iconStart="@tui.x" + size="s" [style.border-radius.%]="100" [style.justify-self]="'end'" (click)="clear()" diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts index e12091604..742ca5f8a 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts @@ -26,7 +26,7 @@ import { CifsBackupTarget, DiskBackupTarget, } 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 { TitleDirective } from 'src/app/services/title.service' import { BACKUP } from './backup.component' @@ -103,7 +103,7 @@ import { BACKUP_RESTORE } from './restore.component' } - @if (type === 'create' && (eos.backingUp$ | async)) { + @if (type === 'create' && (os.backingUp$ | async)) {
} @else { @if (service.loading()) { @@ -163,7 +163,7 @@ export default class SystemBackupComponent implements OnInit { readonly dialog = inject(DialogService) readonly type = inject(ActivatedRoute).snapshot.data['type'] readonly service = inject(BackupService) - readonly eos = inject(EOSService) + readonly os = inject(OSService) readonly server = toSignal( inject>(PatchDB).watch$('serverInfo'), ) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts index 010fdf4bf..0ddc88479 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts @@ -115,6 +115,11 @@ const ERROR = width: 13rem; } + td:last-child { + white-space: nowrap; + text-align: right; + } + [tuiButton] { margin-inline-start: auto; } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts index 86a539f19..ac0d54483 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts @@ -9,15 +9,17 @@ import { toSignal } from '@angular/core/rxjs-interop' import { FormsModule } from '@angular/forms' import { RouterLink } from '@angular/router' import { + DialogService, ErrorService, + i18nKey, i18nPipe, i18nService, - LoadingService, - DialogService, languages, - i18nKey, + Languages, + LoadingService, } from '@start9labs/shared' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' +import { TuiContext, TuiStringHandler } from '@taiga-ui/cdk' import { TuiAppearance, TuiButton, @@ -38,7 +40,7 @@ import { PatchDB } from 'patch-db-client' import { filter } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.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 { TitleDirective } from 'src/app/services/title.service' import { SnekDirective } from './snek.directive' @@ -72,13 +74,13 @@ import { SystemWipeComponent } from './wipe.component' tuiButton appearance="accent" iconStart="@tui.refresh-cw" - [disabled]="eos.updatingOrBackingUp$ | async" + [disabled]="os.updatingOrBackingUp$ | async" (click)="onUpdate()" > @if (server.statusInfo.updated) { {{ 'Restart to apply' | i18n }} } @else { - @if (eos.showUpdate$ | async) { + @if (os.showUpdate$ | async) { {{ 'Update' | i18n }} } @else { {{ 'Check for updates' | i18n }} @@ -98,7 +100,13 @@ import { SystemWipeComponent } from './wipe.component' {{ 'Language' | i18n }} - {{ i18nService.language }} + + @if (language; as lang) { + {{ lang | i18n }} + } @else { + {{ i18nService.language }} + } + @@ -219,24 +228,32 @@ export default class SystemGeneralComponent { private readonly isTor = inject(ConfigService).isTor() private readonly document = inject(DOCUMENT) private readonly dialog = inject(DialogService) + private readonly i18n = inject(i18nPipe) wipe = false count = 0 readonly server = toSignal(this.patch.watch$('serverInfo')) readonly name = toSignal(this.patch.watch$('ui', 'name')) - readonly eos = inject(EOSService) + readonly os = inject(OSService) readonly i18nService = inject(i18nService) readonly languages = languages + readonly translation: TuiStringHandler> = ({ + $implicit, + }) => this.i18n.transform($implicit)! readonly score = toSignal( this.patch.watch$('ui', 'gaming', 'snake', 'highScore'), { initialValue: 0 }, ) + get language(): Languages | undefined { + return this.languages.find(lang => lang === this.i18nService.language) + } + onUpdate() { if (this.server()?.statusInfo.updated) { this.restart() - } else if (this.eos.updateAvailable$.value) { + } else if (this.os.updateAvailable$.value) { this.update() } else { this.check() @@ -334,9 +351,9 @@ export default class SystemGeneralComponent { const loader = this.loader.open('Checking for updates').subscribe() try { - await this.eos.loadEos() + await this.os.loadOS() - if (this.eos.updateAvailable$.value) { + if (this.os.updateAvailable$.value) { this.update() } else { this.dialog diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/update.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/update.component.ts index 8de31b638..98185636b 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/update.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/update.component.ts @@ -15,7 +15,7 @@ import { import { TuiDialogContext, TuiScrollbar, TuiButton } from '@taiga-ui/core' import { NgDompurifyModule } from '@tinkoff/ng-dompurify' 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({ template: ` @@ -47,7 +47,7 @@ import { EOSService } from 'src/app/services/eos.service' ], }) 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)) .reverse() .map(([version, notes]) => ({ @@ -60,7 +60,7 @@ export class SystemUpdateModal { private readonly loader: LoadingService, private readonly errorService: ErrorService, private readonly embassyApi: ApiService, - private readonly eosService: EOSService, + private readonly os: OSService, ) {} async update() { diff --git a/web/projects/ui/src/app/routes/portal/routes/updates/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/updates/item.component.ts index a42107efa..6f994d69e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/updates/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/updates/item.component.ts @@ -18,7 +18,6 @@ import { import { TuiButton, TuiIcon, TuiLink, TuiTitle } from '@taiga-ui/core' import { TuiExpand } from '@taiga-ui/experimental' import { - TUI_CONFIRM, TuiAvatar, TuiButtonLoading, TuiChevron, @@ -73,6 +72,14 @@ import UpdatesComponent from './updates.component' {{ item().s9pk.publishedAt | date }}
+ @if (local().stateInfo.state === 'updating') { } -
diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index a9cb0b70e..b7f1bbdaa 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1850,6 +1850,11 @@ export namespace Mock { listitems: ['192.168.1.1', '192.1681.23'], name: 'Matt', }, + other: { + external: { + 'public-domain': 'test.com', + }, + }, }, port: 20, rpcallowip: undefined, diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 51ee24ca6..395aa126a 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1058,11 +1058,11 @@ export class MockApiService extends ApiService { ...Mock.LocalPkgs[params.id]!, stateInfo: { // if installing - // state: 'installing', + state: 'installing', // if updating - state: 'updating', - manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!, + // state: 'updating', + // manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!, // both installingInfo: { diff --git a/web/projects/ui/src/app/services/badge.service.ts b/web/projects/ui/src/app/services/badge.service.ts index 0efcb94ce..d68d4f5c3 100644 --- a/web/projects/ui/src/app/services/badge.service.ts +++ b/web/projects/ui/src/app/services/badge.service.ts @@ -14,7 +14,7 @@ import { switchMap, } from 'rxjs' 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 { NotificationService } from 'src/app/services/notification.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -27,7 +27,7 @@ export class BadgeService { private readonly notifications = inject(NotificationService) private readonly exver = inject(Exver) private readonly patch = inject>(PatchDB) - private readonly system$ = inject(EOSService).updateAvailable$.pipe( + private readonly system$ = inject(OSService).updateAvailable$.pipe( map(Number), ) private readonly metrics$ = this.patch diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index b015d6a91..e03b3f5b0 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -73,7 +73,7 @@ export class FormService { UntypedFormGroup | UntypedFormArray | UntypedFormControl > = {} 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 }) } diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index 7f27a467e..b879c1ae8 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -194,7 +194,7 @@ export class MarketplaceService { ) } - getStatic$( + fetchStatic$( pkg: MarketplacePkg, type: 'LICENSE.md' | 'instructions.md', ): Observable { diff --git a/web/projects/ui/src/app/services/eos.service.ts b/web/projects/ui/src/app/services/os.service.ts similarity index 96% rename from web/projects/ui/src/app/services/eos.service.ts rename to web/projects/ui/src/app/services/os.service.ts index fed507575..95d859b90 100644 --- a/web/projects/ui/src/app/services/eos.service.ts +++ b/web/projects/ui/src/app/services/os.service.ts @@ -10,7 +10,7 @@ import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', }) -export class EOSService { +export class OSService { osUpdate?: OSUpdate updateAvailable$ = new BehaviorSubject(false) @@ -45,7 +45,7 @@ export class EOSService { private readonly patch: PatchDB, ) {} - async loadEos(): Promise { + async loadOS(): Promise { const { version, id } = await getServerInfo(this.patch) this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) const updateAvailable = diff --git a/web/projects/ui/src/app/services/patch-data.service.ts b/web/projects/ui/src/app/services/patch-data.service.ts index 3c2b9eda6..24e75e3ba 100644 --- a/web/projects/ui/src/app/services/patch-data.service.ts +++ b/web/projects/ui/src/app/services/patch-data.service.ts @@ -1,41 +1,33 @@ -import { Injectable } from '@angular/core' +import { inject, Injectable } from '@angular/core' import { Observable } from 'rxjs' import { filter, map, share, switchMap } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' 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 { 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({ providedIn: 'root', }) export class PatchDataService extends Observable { - private readonly stream$ = this.connection$.pipe( + private readonly patch: PatchDB = inject(PatchDB) + private readonly os = inject(OSService) + private readonly bootstrapper = inject(LocalStorageBootstrap) + private readonly stream$ = inject(ConnectionService).pipe( filter(Boolean), switchMap(() => this.patch.watch$()), map((cache, index) => { this.bootstrapper.update(cache) if (index === 0) { - this.checkForUpdates() + this.os.loadOS() } }), share(), ) - constructor( - private readonly patch: PatchDB, - private readonly eosService: EOSService, - private readonly connection$: ConnectionService, - private readonly bootstrapper: LocalStorageBootstrap, - ) { + constructor() { super(subscriber => this.stream$.subscribe(subscriber)) } - - private checkForUpdates(): void { - this.eosService.loadEos() - // this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe() - } } diff --git a/web/projects/ui/src/app/services/status.service.ts b/web/projects/ui/src/app/services/status.service.ts index 7b2676314..92bbedfc1 100644 --- a/web/projects/ui/src/app/services/status.service.ts +++ b/web/projects/ui/src/app/services/status.service.ts @@ -26,7 +26,7 @@ export const STATUS = new InjectionToken('', { return CONNECTED }), ), - { initialValue: CONNECTED }, + { initialValue: CONNECTING }, ), })