mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
Feature/consolidate setup (#3092)
* start consolidating * add start-cli flash-os * combine install and setup and refactor all * use http * undo mock * fix translation * translations * use dialogservice wrapper * better ST messaging on setup * only warn on update if breakages (#3097) * finish setup wizard and ui language-keyboard feature * fix typo * wip: localization * remove start-tunnel readme * switch to posix strings for language internal * revert mock * translate backend strings * fix missing about text * help text for args * feat: add "Add new gateway" option (#3098) * feat: add "Add new gateway" option * Update web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add translation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix dns selection * keyboard keymap also * ability to shutdown after install * revert mock * working setup flow + manifest localization * (mostly) redundant localization on frontend * version bump * omit live medium from disk list and better space management * ignore missing package archive on 035 migration * fix device migration * add i18n helper to sdk * fix install over 0.3.5.1 * fix grub config --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -39,9 +39,9 @@ export class AppComponent {
|
||||
.subscribe()
|
||||
|
||||
readonly ui = inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('ui', 'language')
|
||||
.watch$('serverInfo', 'language')
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(language => {
|
||||
this.i18n.setLanguage(language || 'english')
|
||||
this.i18n.setLang(language || 'en_US')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, provideAppInitializer } from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { provideAnimations } from '@angular/platform-browser/animations'
|
||||
import { Router } from '@angular/router'
|
||||
import { ActivationStart, Router } from '@angular/router'
|
||||
import { WA_LOCATION } from '@ng-web-apis/common'
|
||||
import initArgon from '@start9labs/argon2'
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
I18N_PROVIDERS,
|
||||
I18N_STORAGE,
|
||||
i18nService,
|
||||
Languages,
|
||||
RELATIVE_URL,
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
TUI_DATE_VALUE_TRANSFORMER,
|
||||
} from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, identity, of, pairwise } from 'rxjs'
|
||||
import { filter, identity, merge, of, pairwise } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import {
|
||||
PATCH_CACHE,
|
||||
@@ -115,11 +116,15 @@ export const APP_PROVIDERS = [
|
||||
{
|
||||
provide: TUI_DIALOGS_CLOSE,
|
||||
useFactory: () =>
|
||||
inject(StateService).pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, curr]) =>
|
||||
prev === 'running' && (curr === 'error' || curr === 'initializing'),
|
||||
merge(
|
||||
inject(Router).events.pipe(filter(e => e instanceof ActivationStart)),
|
||||
inject(StateService).pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, curr]) =>
|
||||
prev === 'running' &&
|
||||
(curr === 'error' || curr === 'initializing'),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
@@ -128,7 +133,7 @@ export const APP_PROVIDERS = [
|
||||
useFactory: () => {
|
||||
const api = inject(ApiService)
|
||||
|
||||
return (language: string) => api.setDbValue(['language'], language)
|
||||
return (language: Languages) => api.setLanguage({ language })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Router, RouterLink } from '@angular/router'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
@@ -36,6 +37,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
[placeholder]="spec.name"
|
||||
[items]="items"
|
||||
[(ngModel)]="selected"
|
||||
(ngModelChange)="onChange($event)"
|
||||
></select>
|
||||
} @else {
|
||||
<input
|
||||
@@ -50,15 +52,27 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
@if (!mobile) {
|
||||
<tui-data-list *tuiTextfieldDropdown>
|
||||
@for (item of items; track item) {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
tuiFluidTypography
|
||||
[style.white-space]="'nowrap'"
|
||||
[value]="item"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
@if (inverted[item]?.startsWith('~')) {
|
||||
<a
|
||||
tuiOption
|
||||
new
|
||||
iconEnd="@tui.arrow-right"
|
||||
tuiFluidTypography
|
||||
[routerLink]="inverted[item]?.slice(1)"
|
||||
>
|
||||
{{ item }}
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
tuiFluidTypography
|
||||
[style.white-space]="'nowrap'"
|
||||
[value]="item"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</tui-data-list>
|
||||
}
|
||||
@@ -70,6 +84,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
providers: [tuiFluidTypographyOptionsProvider({ max: 1 })],
|
||||
imports: [
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
TuiTextfield,
|
||||
TuiSelect,
|
||||
TuiDataList,
|
||||
@@ -81,8 +96,8 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
],
|
||||
})
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
protected readonly router = inject(Router)
|
||||
protected readonly inverted = invert(this.spec.values)
|
||||
protected readonly mobile = inject(TUI_IS_MOBILE)
|
||||
protected readonly items = Object.values(this.spec.values)
|
||||
protected readonly disabledItemHandler = (item: string) =>
|
||||
@@ -101,4 +116,12 @@ export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
set selected(value: string | null) {
|
||||
this.value = (value && this.inverted[value]) || null
|
||||
}
|
||||
|
||||
protected onChange(value: string) {
|
||||
const mapped = this.inverted[value]
|
||||
|
||||
if (typeof mapped === 'string' && mapped.startsWith('~')) {
|
||||
this.router.navigate([mapped.slice(1)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +247,10 @@ export class PublicDomainService {
|
||||
),
|
||||
values: gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
[gateway.id]: gateway.name || gateway.ipInfo.name,
|
||||
...obj,
|
||||
}),
|
||||
{},
|
||||
{ '~/system/gateways': this.i18n.transform('New gateway') },
|
||||
),
|
||||
default: '',
|
||||
disabled: gateways
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Exver,
|
||||
ExverPipesModule,
|
||||
i18nPipe,
|
||||
isEmptyObject,
|
||||
i18nService,
|
||||
LoadingService,
|
||||
sameUrl,
|
||||
} from '@start9labs/shared'
|
||||
@@ -123,6 +123,7 @@ export class MarketplaceControlsComponent {
|
||||
private readonly router = inject(Router)
|
||||
private readonly marketplace = inject(MarketplaceService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly i18n = inject(i18nService)
|
||||
|
||||
readonly pkg = input.required<Pick<MarketplacePkg, KEYS>>()
|
||||
|
||||
@@ -149,7 +150,7 @@ export class MarketplaceControlsComponent {
|
||||
const originalUrl = localPkg?.registry || null
|
||||
|
||||
if (!localPkg) {
|
||||
if (await this.alerts.alertInstall(this.pkg().alerts.install || '')) {
|
||||
if (await this.alerts.alertInstall(this.i18n.localize(this.pkg().alerts.install || ''))) {
|
||||
this.installOrUpload(currentUrl)
|
||||
}
|
||||
return
|
||||
@@ -184,10 +185,7 @@ export class MarketplaceControlsComponent {
|
||||
const packages = await getAllPackages(this.patch)
|
||||
const breakages = dryUpdate({ id, version }, packages, this.exver)
|
||||
|
||||
if (
|
||||
isEmptyObject(breakages) ||
|
||||
(await this.alerts.alertBreakages(breakages))
|
||||
) {
|
||||
if (!breakages.length || (await this.alerts.alertBreakages(breakages))) {
|
||||
this.installOrUpload(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
@@ -21,6 +21,14 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
// IPv4
|
||||
const ipv4 =
|
||||
/(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/
|
||||
|
||||
// IPv6 (your existing pattern)
|
||||
const ipv6 =
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>
|
||||
@@ -111,17 +119,11 @@ export default class SystemDnsComponent {
|
||||
strategy: ISB.Value.union({
|
||||
name: 'strategy',
|
||||
default: 'dhcp',
|
||||
description: `<ul><li><b>DHCP</b>: ${this.i18n.transform('Use the DNS servers provided by your router')}</li><li><b>${this.i18n.transform('Static')}</b>: ${this.i18n.transform('Use DNS servers you specify manually')}</li></ul>`,
|
||||
variants: ISB.Variants.of({
|
||||
dhcp: {
|
||||
name: 'DHCP',
|
||||
spec: ISB.InputSpec.of({
|
||||
servers: ISB.Value.dynamicText(() => ({
|
||||
name: this.i18n.transform('DHCP Servers'),
|
||||
default: null,
|
||||
required: true,
|
||||
disabled: this.i18n.transform('Cannot edit DHCP servers'),
|
||||
})),
|
||||
}),
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
static: {
|
||||
name: this.i18n.transform('Static'),
|
||||
@@ -129,11 +131,21 @@ export default class SystemDnsComponent {
|
||||
servers: ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: this.i18n.transform('Static Servers'),
|
||||
name: this.i18n.transform('Servers'),
|
||||
minLength: 1,
|
||||
maxLength: 3,
|
||||
},
|
||||
{ placeholder: '1.1.1.1' },
|
||||
{
|
||||
placeholder: '1.1.1.1',
|
||||
patterns: [
|
||||
{
|
||||
regex: `^(${ipv4.source}(:\\d{1,5})?|${ipv6.source}|\\[${ipv6.source}\\](:\\d{1,5})?)$`,
|
||||
description: this.i18n.transform(
|
||||
'Must be a valid IPv4 or Ipv6 address with optional port',
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -12,15 +12,20 @@ import { RouterLink } from '@angular/router'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
getAllKeyboardsSorted,
|
||||
getKeyboardName,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
i18nService,
|
||||
languages,
|
||||
Languages,
|
||||
Keyboard,
|
||||
KeyboardLayout,
|
||||
Language,
|
||||
LANGUAGES,
|
||||
LANGUAGE_TO_CODE,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import { TuiAnimated, TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
|
||||
import { TuiAnimated } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
@@ -49,6 +54,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SnekDirective } from './snek.directive'
|
||||
import { UPDATE } from './update.component'
|
||||
import { SystemWipeComponent } from './wipe.component'
|
||||
import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -104,20 +110,16 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
<tui-icon icon="@tui.languages" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Language' | i18n }}</strong>
|
||||
<span tuiSubtitle [style.text-transform]="'capitalize'">
|
||||
@if (language; as lang) {
|
||||
{{ lang | i18n }}
|
||||
} @else {
|
||||
{{ i18nService.language }}
|
||||
}
|
||||
<span tuiSubtitle>
|
||||
{{ currentLanguage?.nativeName }}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
tuiButtonSelect
|
||||
tuiButton
|
||||
[loading]="i18nService.loading()"
|
||||
[ngModel]="i18nService.language"
|
||||
(ngModelChange)="i18nService.setLanguage($event)"
|
||||
[ngModel]="currentLanguage"
|
||||
(ngModelChange)="onLanguageChange($event)"
|
||||
>
|
||||
{{ 'Change' | i18n }}
|
||||
<tui-data-list-wrapper
|
||||
@@ -125,29 +127,50 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
new
|
||||
size="l"
|
||||
[items]="languages"
|
||||
[itemContent]="translation"
|
||||
[itemContent]="languageContent"
|
||||
/>
|
||||
</button>
|
||||
<ng-template #languageContent let-item>
|
||||
{{ item.nativeName }}
|
||||
</ng-template>
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.monitor" />
|
||||
<span tuiTitle>
|
||||
<strong>
|
||||
{{ 'Kiosk Mode' | i18n }}
|
||||
<tui-badge size="m" appearance="primary-grayscale">
|
||||
<tui-badge
|
||||
size="m"
|
||||
[appearance]="server.kiosk ? 'warning' : 'primary-grayscale'"
|
||||
>
|
||||
{{ server.kiosk ? ('Enabled' | i18n) : ('Disabled' | i18n) }}
|
||||
</tui-badge>
|
||||
</strong>
|
||||
<span tuiSubtitle>
|
||||
{{
|
||||
server.kiosk === true
|
||||
? ('Disable Kiosk Mode unless you need to attach a monitor'
|
||||
| i18n)
|
||||
: server.kiosk === false
|
||||
? ('Enable Kiosk Mode if you need to attach a monitor' | i18n)
|
||||
: ('Kiosk Mode is unavailable on this device' | i18n)
|
||||
}}
|
||||
<span tuiSubtitle [class.warning-text]="server.kiosk">
|
||||
@if (server.kiosk === null) {
|
||||
{{ 'Kiosk Mode is unavailable on this device' | i18n }}
|
||||
} @else {
|
||||
{{
|
||||
server.kiosk
|
||||
? ('Disable Kiosk Mode unless you need to attach a monitor'
|
||||
| i18n)
|
||||
: ('Enable Kiosk Mode if you need to attach a monitor' | i18n)
|
||||
}}
|
||||
}
|
||||
</span>
|
||||
@if (server.kiosk !== null && server.keyboard?.layout; as layout) {
|
||||
<span tuiSubtitle class="keyboard-info">
|
||||
<tui-icon icon="@tui.keyboard" />
|
||||
{{ getKeyboardName(layout) }}
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
iconStart="@tui.pencil"
|
||||
size="xs"
|
||||
(click)="onChangeKeyboard()"
|
||||
></button>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
@if (server.kiosk !== null) {
|
||||
<button tuiButton appearance="primary" (click)="toggleKiosk()">
|
||||
@@ -214,6 +237,21 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
[tuiAnimated].tui-leave {
|
||||
animation-name: tuiFade, tuiScale;
|
||||
}
|
||||
|
||||
.keyboard-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
tui-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-text,
|
||||
[tuiSubtitle].warning-text {
|
||||
color: var(--tui-status-warning) !important;
|
||||
}
|
||||
`,
|
||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -258,17 +296,60 @@ export default class SystemGeneralComponent {
|
||||
readonly score = toSignal(this.patch.watch$('ui', 'snakeHighScore'))
|
||||
readonly os = inject(OSService)
|
||||
readonly i18nService = inject(i18nService)
|
||||
readonly languages = languages
|
||||
readonly translation: TuiStringHandler<TuiContext<Languages>> = ({
|
||||
$implicit,
|
||||
}) => {
|
||||
const [head = '', ...result] = this.i18n.transform($implicit) || ''
|
||||
readonly languages = LANGUAGES
|
||||
|
||||
return [head.toUpperCase(), ...result].join('')
|
||||
get currentLanguage(): Language | undefined {
|
||||
return LANGUAGES.find(lang => lang.name === this.i18nService.lang)
|
||||
}
|
||||
|
||||
get language(): Languages | undefined {
|
||||
return this.languages.find(lang => lang === this.i18nService.language)
|
||||
onLanguageChange(language: Language) {
|
||||
this.i18nService.setLang(language.name)
|
||||
}
|
||||
|
||||
// Expose shared utilities for template use
|
||||
readonly getKeyboardName = getKeyboardName
|
||||
|
||||
/**
|
||||
* Open keyboard selection dialog to change keyboard layout
|
||||
*/
|
||||
onChangeKeyboard() {
|
||||
const server = this.server()
|
||||
if (!server) return
|
||||
|
||||
const keyboards = getAllKeyboardsSorted(LANGUAGE_TO_CODE[server.language])
|
||||
const currentLayout = (server.keyboard?.layout as KeyboardLayout) || null
|
||||
|
||||
this.dialog
|
||||
.openComponent<Keyboard | null>(
|
||||
new PolymorpheusComponent(KeyboardSelectComponent, this.injector),
|
||||
{
|
||||
label: 'Select Keyboard Layout',
|
||||
size: 's',
|
||||
data: { keyboards, currentLayout },
|
||||
},
|
||||
)
|
||||
.pipe(filter((keyboard): keyboard is Keyboard => keyboard !== null))
|
||||
.subscribe(keyboard => {
|
||||
this.saveKeyboard(keyboard)
|
||||
})
|
||||
}
|
||||
|
||||
private async saveKeyboard(keyboard: Keyboard) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setKeyboard({
|
||||
layout: keyboard.layout,
|
||||
keymap: keyboard.keymap,
|
||||
model: null,
|
||||
variant: null,
|
||||
options: [],
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
@@ -352,19 +433,51 @@ export default class SystemGeneralComponent {
|
||||
}
|
||||
|
||||
async toggleKiosk() {
|
||||
const kiosk = this.server()?.kiosk
|
||||
const server = this.server()
|
||||
if (!server) return
|
||||
|
||||
const loader = this.loader
|
||||
.open(kiosk ? 'Disabling' : 'Enabling')
|
||||
.subscribe()
|
||||
const kiosk = server.kiosk
|
||||
|
||||
// If disabling kiosk, just disable it
|
||||
if (kiosk) {
|
||||
await this.disableKiosk()
|
||||
return
|
||||
}
|
||||
|
||||
// Enabling kiosk - check if keyboard is already set
|
||||
if (server.keyboard) {
|
||||
// Keyboard already set, just enable kiosk
|
||||
await this.enableKiosk()
|
||||
return
|
||||
}
|
||||
|
||||
// No keyboard set - prompt user to select from all keyboards
|
||||
const keyboards = getAllKeyboardsSorted(LANGUAGE_TO_CODE[server.language])
|
||||
this.promptKeyboardSelection(keyboards)
|
||||
}
|
||||
|
||||
private promptKeyboardSelection(keyboards: Keyboard[]) {
|
||||
this.dialog
|
||||
.openComponent<Keyboard | null>(
|
||||
new PolymorpheusComponent(KeyboardSelectComponent, this.injector),
|
||||
{
|
||||
label: 'Select Keyboard Layout',
|
||||
size: 's',
|
||||
data: { keyboards, currentLayout: null },
|
||||
},
|
||||
)
|
||||
.pipe(filter((keyboard): keyboard is Keyboard => keyboard !== null))
|
||||
.subscribe(keyboard => {
|
||||
this.enableKioskWithKeyboard(keyboard)
|
||||
})
|
||||
}
|
||||
|
||||
private async enableKiosk() {
|
||||
const loader = this.loader.open('Enabling').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.toggleKiosk(!kiosk)
|
||||
this.dialog
|
||||
.openAlert('This change will take effect after the next boot', {
|
||||
label: 'Restart to apply',
|
||||
})
|
||||
.subscribe()
|
||||
await this.api.toggleKiosk(true)
|
||||
this.promptRestart()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -372,6 +485,53 @@ export default class SystemGeneralComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async enableKioskWithKeyboard(keyboard: Keyboard) {
|
||||
const loader = this.loader.open('Enabling').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setKeyboard({
|
||||
layout: keyboard.layout,
|
||||
keymap: keyboard.keymap,
|
||||
model: null,
|
||||
variant: null,
|
||||
options: [],
|
||||
})
|
||||
await this.api.toggleKiosk(true)
|
||||
this.promptRestart()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async disableKiosk() {
|
||||
const loader = this.loader.open('Disabling').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.toggleKiosk(false)
|
||||
this.promptRestart()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private promptRestart() {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Restart to apply',
|
||||
data: {
|
||||
content: 'This change will take effect after the next boot',
|
||||
yes: 'Restart now',
|
||||
no: 'Later',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.restart())
|
||||
}
|
||||
|
||||
private async resetTor(wipeState: boolean) {
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe, Keyboard, KeyboardLayout } from '@start9labs/shared'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiDialogContext, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiChevron, TuiDataListWrapper, TuiSelect } from '@taiga-ui/kit'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<tui-textfield
|
||||
tuiChevron
|
||||
[stringify]="stringify"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
>
|
||||
<label tuiLabel>{{ 'Keyboard' | i18n }}</label>
|
||||
@if (mobile) {
|
||||
<select tuiSelect [(ngModel)]="selected" [items]="keyboards"></select>
|
||||
} @else {
|
||||
<input tuiSelect [(ngModel)]="selected" />
|
||||
}
|
||||
@if (!mobile) {
|
||||
<tui-data-list-wrapper new *tuiTextfieldDropdown [items]="keyboards" />
|
||||
}
|
||||
</tui-textfield>
|
||||
<footer>
|
||||
<button tuiButton appearance="secondary" (click)="cancel()">
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!selected || selected.layout === initialLayout"
|
||||
(click)="confirm()"
|
||||
>
|
||||
{{ 'Confirm' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
styles: `
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiButton,
|
||||
TuiTextfield,
|
||||
TuiChevron,
|
||||
TuiSelect,
|
||||
TuiDataListWrapper,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class KeyboardSelectComponent {
|
||||
private readonly context =
|
||||
injectContext<
|
||||
TuiDialogContext<
|
||||
Keyboard | null,
|
||||
{ keyboards: Keyboard[]; currentLayout: KeyboardLayout | null }
|
||||
>
|
||||
>()
|
||||
|
||||
protected readonly mobile = inject(TUI_IS_MOBILE)
|
||||
readonly keyboards = this.context.data.keyboards
|
||||
readonly initialLayout = this.context.data.currentLayout
|
||||
selected =
|
||||
this.keyboards.find(kb => kb.layout === this.initialLayout) ||
|
||||
this.keyboards[0]!
|
||||
|
||||
readonly stringify = (kb: Keyboard) => kb.name
|
||||
|
||||
cancel() {
|
||||
this.context.completeWith(null)
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.context.completeWith(this.selected)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
LocalizePipe,
|
||||
MarkdownPipe,
|
||||
SafeLinksDirective,
|
||||
} from '@start9labs/shared'
|
||||
@@ -138,7 +139,7 @@ import UpdatesComponent from './updates.component'
|
||||
</p>
|
||||
<p
|
||||
safeLinks
|
||||
[innerHTML]="item().releaseNotes | markdown | dompurify"
|
||||
[innerHTML]="item().releaseNotes | localize | markdown | dompurify"
|
||||
></p>
|
||||
</tui-expand>
|
||||
</td>
|
||||
@@ -237,6 +238,7 @@ import UpdatesComponent from './updates.component'
|
||||
TuiProgressCircle,
|
||||
TuiTitle,
|
||||
TuiFade,
|
||||
LocalizePipe,
|
||||
MarkdownPipe,
|
||||
NgDompurifyPipe,
|
||||
SafeLinksDirective,
|
||||
|
||||
@@ -410,7 +410,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -452,7 +452,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -504,7 +504,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -546,7 +546,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -600,7 +600,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release to 0.17.5',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -655,7 +655,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release to 0.17.4',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -714,7 +714,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -756,7 +756,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -808,7 +808,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -863,7 +863,7 @@ export namespace Mock {
|
||||
marketingSite: '',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.47',
|
||||
sdkVersion: '0.4.0-beta.48',
|
||||
gitHash: 'fakehash',
|
||||
icon: PROXY_ICON,
|
||||
sourceVersion: null,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||
import {
|
||||
FetchLogsReq,
|
||||
FetchLogsRes,
|
||||
FullKeyboard,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { IST, T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import {
|
||||
@@ -120,6 +126,12 @@ export namespace RR {
|
||||
} // net.tor.reset
|
||||
export type ResetTorRes = null
|
||||
|
||||
export type SetKeyboardReq = FullKeyboard // server.set-keyboard
|
||||
export type SetKeyboardRes = null
|
||||
|
||||
export type SetLanguageReq = SetLanguageParams // server.set-language
|
||||
export type SetLanguageRes = null
|
||||
|
||||
// smtp
|
||||
|
||||
export type SetSMTPReq = T.SmtpValue // server.set-smtp
|
||||
|
||||
@@ -117,6 +117,10 @@ export abstract class ApiService {
|
||||
|
||||
abstract toggleKiosk(enable: boolean): Promise<null>
|
||||
|
||||
abstract setKeyboard(params: RR.SetKeyboardReq): Promise<RR.SetKeyboardRes>
|
||||
|
||||
abstract setLanguage(params: RR.SetLanguageReq): Promise<RR.SetLanguageRes>
|
||||
|
||||
abstract setDns(params: RR.SetDnsReq): Promise<RR.SetDnsRes>
|
||||
|
||||
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
||||
|
||||
@@ -256,6 +256,14 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async setKeyboard(params: RR.SetKeyboardReq): Promise<RR.SetKeyboardRes> {
|
||||
return this.rpcRequest({ method: 'server.set-keyboard', params })
|
||||
}
|
||||
|
||||
async setLanguage(params: RR.SetLanguageReq): Promise<RR.SetLanguageRes> {
|
||||
return this.rpcRequest({ method: 'server.set-language', params })
|
||||
}
|
||||
|
||||
async setDns(params: RR.SetDnsReq): Promise<RR.SetDnsRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.dns.set-static',
|
||||
|
||||
@@ -22,7 +22,6 @@ import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { toAuthorityUrl } from 'src/app/utils/acme'
|
||||
|
||||
@@ -454,6 +453,36 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async setKeyboard(params: RR.SetKeyboardReq): Promise<RR.SetKeyboardRes> {
|
||||
await pauseFor(1000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/keyboard',
|
||||
value: params,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setLanguage(params: RR.SetLanguageReq): Promise<RR.SetLanguageRes> {
|
||||
await pauseFor(1000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/language',
|
||||
value: params.language,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setDns(params: RR.SetDnsReq): Promise<RR.SetDnsRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
startosRegistry: 'https://registry.start9.com/',
|
||||
snakeHighScore: 0,
|
||||
language: 'english',
|
||||
},
|
||||
serverInfo: {
|
||||
arch: 'x86_64',
|
||||
@@ -220,6 +219,15 @@ export const mockPatchData: DataModel = {
|
||||
ram: 8 * 1024 * 1024 * 1024,
|
||||
devices: [],
|
||||
kiosk: true,
|
||||
language: 'en_US',
|
||||
keyboard: {
|
||||
layout: 'us',
|
||||
keymap: 'us',
|
||||
model: null,
|
||||
variant: null,
|
||||
options: [],
|
||||
},
|
||||
// keyboard: null,
|
||||
},
|
||||
packageData: {
|
||||
lnd: {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Languages } from '@start9labs/shared'
|
||||
import { FullKeyboard, Languages } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type DataModel = T.Public & {
|
||||
export type DataModel = {
|
||||
ui: UIData
|
||||
serverInfo: T.ServerInfo & {
|
||||
language: Languages
|
||||
keyboard: FullKeyboard | null
|
||||
}
|
||||
packageData: AllPackageData
|
||||
}
|
||||
|
||||
@@ -11,7 +15,6 @@ export type UIData = {
|
||||
registries: Record<string, string | null>
|
||||
snakeHighScore: number
|
||||
startosRegistry: string
|
||||
language: Languages
|
||||
}
|
||||
|
||||
export type PackageDataEntry<T extends StateInfo = StateInfo> =
|
||||
|
||||
Reference in New Issue
Block a user