diff --git a/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts b/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts new file mode 100644 index 000000000..0daef9e94 --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts @@ -0,0 +1,121 @@ +import { Component } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' +import { TuiButton, TuiDialogContext } from '@taiga-ui/core' +import { injectContext } from '@taiga-ui/polymorpheus' + +@Component({ + standalone: true, + imports: [TuiButton, i18nPipe], + template: ` +
+
+
+
+
+
+
+
+
+

+ {{ + 'Remove USB stick or other installation media from your server' | i18n + }} +

+ + `, + styles: ` + :host { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + .animation-container { + position: relative; + width: 160px; + height: 69px; + } + + .port { + position: absolute; + left: 20px; + top: 50%; + transform: translateY(-50%); + width: 28px; + height: 18px; + background: var(--tui-background-neutral-1); + border: 2px solid var(--tui-border-normal); + border-radius: 2px; + } + + .port-inner { + position: absolute; + top: 3px; + left: 3px; + right: 3px; + bottom: 3px; + background: var(--tui-background-neutral-2); + border-radius: 1px; + } + + .usb-stick { + position: absolute; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + animation: slide-out 2s ease-in-out 0.5s infinite; + left: 32px; + } + + .usb-connector { + width: 20px; + height: 12px; + background: var(--tui-text-secondary); + border-radius: 1px; + } + + .usb-body { + width: 40px; + height: 20px; + background: var(--tui-status-info); + border-radius: 2px 4px 4px 2px; + } + + @keyframes slide-out { + 0% { + left: 32px; + opacity: 0; + } + 5% { + left: 32px; + opacity: 1; + } + 80% { + left: 130px; + opacity: 0; + } + 100% { + left: 130px; + opacity: 0; + } + } + + p { + margin: 0 0 2rem; + } + + footer { + display: flex; + justify-content: center; + } + `, +}) +export class RemoveMediaDialog { + protected readonly context = injectContext>() +} diff --git a/web/projects/setup-wizard/src/app/pages/drives.page.ts b/web/projects/setup-wizard/src/app/pages/drives.page.ts index f3c7ded42..2790fc306 100644 --- a/web/projects/setup-wizard/src/app/pages/drives.page.ts +++ b/web/projects/setup-wizard/src/app/pages/drives.page.ts @@ -1,4 +1,9 @@ -import { ChangeDetectorRef, Component, inject } from '@angular/core' +import { + ChangeDetectorRef, + Component, + HostListener, + inject, +} from '@angular/core' import { Router } from '@angular/router' import { FormsModule } from '@angular/forms' import { @@ -21,13 +26,14 @@ import { import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit' import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' -import { filter } from 'rxjs' +import { filter, Subscription } from 'rxjs' import { ApiService } from '../services/api.service' import { StateService } from '../services/state.service' import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog' @Component({ template: ` + @if (!shuttingDown) {

{{ 'Select Drives' | i18n }}

@@ -132,6 +138,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog }
+ } `, styles: ` .no-drives { @@ -176,6 +183,14 @@ export default class DrivesPage { protected readonly mobile = inject(TUI_IS_MOBILE) + @HostListener('document:keydown', ['$event']) + onKeydown(event: KeyboardEvent) { + if (event.ctrlKey && event.shiftKey && event.key === 'X') { + event.preventDefault() + this.shutdownServer() + } + } + readonly osDriveTooltip = this.i18n.transform( 'The drive where the StartOS operating system will be installed.', ) @@ -185,6 +200,8 @@ export default class DrivesPage { drives: DiskInfo[] = [] loading = true + shuttingDown = false + private dialogSub?: Subscription selectedOsDrive: DiskInfo | null = null selectedDataDrive: DiskInfo | null = null preserveData: boolean | null = null @@ -339,22 +356,18 @@ export default class DrivesPage { loader.unsubscribe() // Show success dialog - this.dialogs - .openConfirm({ + this.dialogSub = this.dialogs + .openAlert('StartOS has been installed successfully.', { label: 'Installation Complete!', size: 's', - data: { - content: 'StartOS has been installed successfully.', - yes: 'Continue to Setup', - no: 'Shutdown', - }, + dismissible: false, + closeable: true, + data: { button: this.i18n.transform('Continue to Setup') }, }) - .subscribe(continueSetup => { - if (continueSetup) { + .subscribe({ + complete: () => { this.navigateToNextStep(result.attach) - } else { - this.shutdownServer() - } + }, }) } catch (e: any) { loader.unsubscribe() @@ -372,10 +385,12 @@ export default class DrivesPage { } private async shutdownServer() { + this.dialogSub?.unsubscribe() const loader = this.loader.open('Beginning shutdown').subscribe() try { await this.api.shutdown() + this.shuttingDown = true } catch (e: any) { this.errorService.handleError(e) } finally { diff --git a/web/projects/setup-wizard/src/app/pages/success.page.ts b/web/projects/setup-wizard/src/app/pages/success.page.ts index 8f84f788b..03dae0203 100644 --- a/web/projects/setup-wizard/src/app/pages/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success.page.ts @@ -6,7 +6,12 @@ import { ViewChild, DOCUMENT, } from '@angular/core' -import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared' +import { + DialogService, + DownloadHTMLService, + ErrorService, + i18nPipe, +} from '@start9labs/shared' import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiAvatar } from '@taiga-ui/kit' import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout' @@ -14,7 +19,9 @@ import { ApiService } from '../services/api.service' import { StateService } from '../services/state.service' import { DocumentationComponent } from '../components/documentation.component' import { MatrixComponent } from '../components/matrix.component' +import { RemoveMediaDialog } from '../components/remove-media.dialog' import { SetupCompleteRes } from '../types' +import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' @Component({ template: ` @@ -29,12 +36,8 @@ import { SetupCompleteRes } from '../types' @if (!stateService.kiosk) { {{ - stateService.setupType === 'restore' - ? ('You can unplug your backup drive' | i18n) - : stateService.setupType === 'transfer' - ? ('You can unplug your transfer drive' | i18n) - : ('http://start.local was for setup only. It will no longer work.' - | i18n) + 'http://start.local was for setup only. It will no longer work.' + | i18n }} } @@ -69,14 +72,15 @@ import { SetupCompleteRes } from '../types' tuiCell="l" [class.disabled]="!stateService.kiosk && !downloaded" [disabled]="(!stateService.kiosk && !downloaded) || usbRemoved" - (click)="usbRemoved = true" + (click)="removeMedia()" >
- {{ 'USB Removed' | i18n }} + {{ 'Remove Installation Media' | i18n }}
{{ - 'Remove the USB installation media from your server' | i18n + 'Remove USB stick or other installation media from your server' + | i18n }}
@@ -184,6 +188,7 @@ export default class SuccessPage implements AfterViewInit { private readonly errorService = inject(ErrorService) private readonly api = inject(ApiService) private readonly downloadHtml = inject(DownloadHTMLService) + private readonly dialogs = inject(DialogService) private readonly i18n = inject(i18nPipe) readonly stateService = inject(StateService) @@ -225,6 +230,21 @@ export default class SuccessPage implements AfterViewInit { }) } + removeMedia() { + this.dialogs + .openComponent( + new PolymorpheusComponent(RemoveMediaDialog), + { + size: 's', + dismissible: false, + closeable: false, + }, + ) + .subscribe(() => { + this.usbRemoved = true + }) + } + exitKiosk() { this.api.exit() } diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index e229d9f24..16d7b9473 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -100,6 +100,7 @@ export default { 102: 'Verlassen', 103: 'Sind Sie sicher?', 104: 'Neues Netzwerk-Gateway', + 107: 'Onion-Domains', 108: 'Öffentlich', 109: 'privat', 111: 'Keine Onion-Domains', @@ -639,13 +640,11 @@ export default { 667: 'Einrichtung wird gestartet', 670: 'Warten Sie 1–2 Minuten und aktualisieren Sie die Seite', 672: 'Einrichtung abgeschlossen!', - 673: 'Sie können Ihr Backup-Laufwerk entfernen', - 674: 'Sie können Ihr Übertragungs-Laufwerk entfernen', 675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.', 676: 'Adressinformationen herunterladen', 677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA', - 678: 'USB entfernt', - 679: 'Entfernen Sie das USB-Installationsmedium aus Ihrem Server', + 678: 'Installationsmedium entfernen', + 679: 'Entfernen Sie den USB-Stick oder andere Installationsmedien von Ihrem Server', 680: 'Server neu starten', 681: 'Warten, bis der Server wieder online ist', 682: 'Server ist wieder online', diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index b7e05c7b6..d769ded05 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -99,6 +99,7 @@ export const ENGLISH: Record = { 'Leave': 102, 'Are you sure?': 103, 'New gateway': 104, // as in, a network gateway + 'Tor Domains': 107, 'public': 108, 'private': 109, 'No Tor domains': 111, @@ -639,13 +640,11 @@ export const ENGLISH: Record = { 'Starting setup': 667, 'Wait 1-2 minutes and refresh the page': 670, 'Setup Complete!': 672, - 'You can unplug your backup drive': 673, - 'You can unplug your transfer drive': 674, 'http://start.local was for setup only. It will no longer work.': 675, 'Download Address Info': 676, "Contains your server's permanent local address and Root CA": 677, - 'USB Removed': 678, - 'Remove the USB installation media from your server': 679, + 'Remove Installation Media': 678, + 'Remove USB stick or other installation media from your server': 679, 'Restart Server': 680, 'Waiting for server to come back online': 681, 'Server is back online': 682, diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index 3233d1496..1f82b395e 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -100,6 +100,7 @@ export default { 102: 'Salir', 103: '¿Estás seguro?', 104: 'Nueva puerta de enlace de red', + 107: 'dominios onion', 108: 'público', 109: 'privado', 111: 'Sin dominios onion', @@ -639,13 +640,11 @@ export default { 667: 'Iniciando configuración', 670: 'Espere 1–2 minutos y actualice la página', 672: '¡Configuración completa!', - 673: 'Puede desconectar su unidad de copia de seguridad', - 674: 'Puede desconectar su unidad de transferencia', 675: 'http://start.local era solo para la configuración. Ya no funcionará.', 676: 'Descargar información de direcciones', 677: 'Contiene la dirección local permanente de su servidor y la CA raíz', - 678: 'USB retirado', - 679: 'Retire el medio de instalación USB de su servidor', + 678: 'Retirar medio de instalación', + 679: 'Retire la memoria USB u otro medio de instalación de su servidor', 680: 'Reiniciar servidor', 681: 'Esperando a que el servidor vuelva a estar en línea', 682: 'El servidor ha vuelto a estar en línea', diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index fc86d901c..db13e7d97 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -100,6 +100,7 @@ export default { 102: 'Quitter', 103: 'Êtes-vous sûr ?', 104: 'Nouvelle passerelle réseau', + 107: 'domaine onion', 108: 'public', 109: 'privé', 111: 'Aucune domaine onion', @@ -639,13 +640,11 @@ export default { 667: 'Démarrage de la configuration', 670: 'Attendez 1 à 2 minutes puis actualisez la page', 672: 'Configuration terminée !', - 673: 'Vous pouvez débrancher votre disque de sauvegarde', - 674: 'Vous pouvez débrancher votre disque de transfert', 675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.', 676: 'Télécharger les informations d’adresse', - 677: 'Contient l’adresse locale permanente de votre serveur et la CA racine', - 678: 'USB retiré', - 679: 'Retirez le support d’installation USB de votre serveur', + 677: 'Contient l\u2019adresse locale permanente de votre serveur et la CA racine', + 678: 'Retirer le support d\u2019installation', + 679: 'Retirez la clé USB ou tout autre support d\u2019installation de votre serveur', 680: 'Redémarrer le serveur', 681: 'En attente du retour en ligne du serveur', 682: 'Le serveur est de nouveau en ligne', diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 3911af7bb..13a3c4671 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -100,6 +100,7 @@ export default { 102: 'Opuść', 103: 'Czy jesteś pewien?', 104: 'Nowa brama sieciowa', + 107: 'domeny onion', 108: 'publiczny', 109: 'prywatny', 111: 'Brak domeny onion', @@ -639,13 +640,11 @@ export default { 667: 'Rozpoczynanie konfiguracji', 670: 'Poczekaj 1–2 minuty i odśwież stronę', 672: 'Konfiguracja zakończona!', - 673: 'Możesz odłączyć dysk kopii zapasowej', - 674: 'Możesz odłączyć dysk transferowy', 675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.', 676: 'Pobierz informacje adresowe', 677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)', - 678: 'USB usunięty', - 679: 'Usuń instalacyjny nośnik USB z serwera', + 678: 'Usuń nośnik instalacyjny', + 679: 'Usuń pamięć USB lub inny nośnik instalacyjny z serwera', 680: 'Uruchom ponownie serwer', 681: 'Oczekiwanie na ponowne połączenie serwera', 682: 'Serwer jest ponownie online', diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts index 9fb233423..001bcd71a 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts @@ -35,7 +35,7 @@ type OnionForm = { selector: 'section[torDomains]', template: `
- Tor Domains + {{ 'Tor Domains' | i18n }} >(PatchDB) private readonly i18n = inject(i18nPipe) + private readonly i18nService = inject(i18nService) async start({ title, alerts, id }: T.Manifest, unmet: boolean) { const deps = @@ -31,7 +33,7 @@ export class ControlsService { if ( (unmet && !(await this.alert(deps))) || - (alerts.start && !(await this.alert(alerts.start as i18nKey))) + (alerts.start && !(await this.alert(alerts.start))) ) { return } @@ -49,7 +51,7 @@ export class ControlsService { async stop({ id, title, alerts }: T.Manifest) { const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` - let content = alerts.stop || '' + let content = alerts.stop ? this.i18nService.localize(alerts.stop) : '' if (hasCurrentDeps(id, await getAllPackages(this.patch))) { content = content ? `${content}.\n\n${depMessage}` : depMessage @@ -113,14 +115,14 @@ export class ControlsService { }) } - private alert(content: i18nKey): Promise { + private alert(content: T.LocaleString): Promise { return firstValueFrom( this.dialog .openConfirm({ label: 'Warning', size: 's', data: { - content, + content: this.i18nService.localize(content), yes: 'Continue', no: 'Cancel', }, diff --git a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts index daaacc705..b0902944c 100644 --- a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -31,7 +31,7 @@ export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus { (!statusInfo.started || Object.values(statusInfo.health) .filter(h => !!h) - .some(h => h.result === 'starting')) + .some(h => h.result === 'starting' || h.result === 'waiting')) ) { return 'starting' } diff --git a/web/projects/ui/src/app/services/standard-actions.service.ts b/web/projects/ui/src/app/services/standard-actions.service.ts index 6b3386877..5cd002139 100644 --- a/web/projects/ui/src/app/services/standard-actions.service.ts +++ b/web/projects/ui/src/app/services/standard-actions.service.ts @@ -5,6 +5,7 @@ import { ErrorService, i18nKey, i18nPipe, + i18nService, LoadingService, } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' @@ -27,6 +28,7 @@ export class StandardActionsService { private readonly loader = inject(LoadingService) private readonly router = inject(Router) private readonly i18n = inject(i18nPipe) + private readonly i18nService = inject(i18nService) async rebuild(id: string) { const loader = this.loader.open('Rebuilding container').subscribe() @@ -50,11 +52,12 @@ export class StandardActionsService { ): Promise { let content = soft ? '' - : alerts.uninstall || - `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}` + : alerts.uninstall + ? this.i18nService.localize(alerts.uninstall) + : `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}` if (hasCurrentDeps(id, await getAllPackages(this.patch))) { - content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` + content = `${content ? `${content} ` : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` } if (!content) {