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:
Aiden McClelland
2026-01-27 14:44:41 -08:00
committed by GitHub
parent 99871805bd
commit c65db31fd9
251 changed files with 12163 additions and 3966 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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