mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 20:43:41 +00:00
finish setup wizard and ui language-keyboard feature
This commit is contained in:
@@ -39,7 +39,7 @@ 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')
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
I18N_PROVIDERS,
|
||||
I18N_STORAGE,
|
||||
i18nService,
|
||||
Languages,
|
||||
RELATIVE_URL,
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
@@ -128,7 +129,7 @@ export const APP_PROVIDERS = [
|
||||
useFactory: () => {
|
||||
const api = inject(ApiService)
|
||||
|
||||
return (language: string) => api.setDbValue(['language'], language)
|
||||
return (language: Languages) => api.setLanguage({ language })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,11 +12,16 @@ import { RouterLink } from '@angular/router'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
getAllKeyboardsSorted,
|
||||
getKeyboardName,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
i18nService,
|
||||
Keyboard,
|
||||
KeyboardCode,
|
||||
languages,
|
||||
Languages,
|
||||
LANGUAGE_TO_CODE,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
@@ -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: `
|
||||
@@ -134,20 +140,38 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
<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 +238,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,
|
||||
@@ -271,6 +310,51 @@ export default class SystemGeneralComponent {
|
||||
return this.languages.find(lang => lang === this.i18nService.language)
|
||||
}
|
||||
|
||||
// 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 currentKeyboard = (server.keyboard?.layout as KeyboardCode) || null
|
||||
|
||||
this.dialog
|
||||
.openComponent<KeyboardCode | null>(
|
||||
new PolymorpheusComponent(KeyboardSelectComponent, this.injector),
|
||||
{
|
||||
label: 'Select Keyboard Layout',
|
||||
size: 's',
|
||||
data: { keyboards, currentKeyboard },
|
||||
},
|
||||
)
|
||||
.pipe(filter((code): code is KeyboardCode => code !== null))
|
||||
.subscribe(keyboardCode => {
|
||||
this.saveKeyboard(keyboardCode)
|
||||
})
|
||||
}
|
||||
|
||||
private async saveKeyboard(keyboardCode: KeyboardCode) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setKeyboard({
|
||||
layout: keyboardCode,
|
||||
model: null,
|
||||
variant: null,
|
||||
options: [],
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
if (this.server()?.statusInfo.updated) {
|
||||
this.restart()
|
||||
@@ -352,19 +436,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<KeyboardCode | null>(
|
||||
new PolymorpheusComponent(KeyboardSelectComponent, this.injector),
|
||||
{
|
||||
label: 'Select Keyboard Layout',
|
||||
size: 's',
|
||||
data: { keyboards, currentKeyboard: null },
|
||||
},
|
||||
)
|
||||
.pipe(filter((code): code is KeyboardCode => code !== null))
|
||||
.subscribe(keyboardCode => {
|
||||
this.enableKioskWithKeyboard(keyboardCode)
|
||||
})
|
||||
}
|
||||
|
||||
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 +488,52 @@ export default class SystemGeneralComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async enableKioskWithKeyboard(keyboardCode: KeyboardCode) {
|
||||
const loader = this.loader.open('Enabling').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setKeyboard({
|
||||
layout: keyboardCode,
|
||||
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, KeyboardCode } 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.code === initialCode"
|
||||
(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<
|
||||
KeyboardCode | null,
|
||||
{ keyboards: Keyboard[]; currentKeyboard: KeyboardCode | null }
|
||||
>
|
||||
>()
|
||||
|
||||
protected readonly mobile = inject(TUI_IS_MOBILE)
|
||||
readonly keyboards = this.context.data.keyboards
|
||||
readonly initialCode = this.context.data.currentKeyboard
|
||||
selected =
|
||||
this.keyboards.find(kb => kb.code === this.initialCode) ||
|
||||
this.keyboards[0]!
|
||||
|
||||
readonly stringify = (kb: Keyboard) => kb.name
|
||||
|
||||
cancel() {
|
||||
this.context.completeWith(null)
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.context.completeWith(this.selected.code)
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -453,6 +453,41 @@ 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: {
|
||||
layout: params.layout,
|
||||
model: params.model,
|
||||
variant: params.variant,
|
||||
options: params.options,
|
||||
},
|
||||
},
|
||||
]
|
||||
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,14 @@ export const mockPatchData: DataModel = {
|
||||
ram: 8 * 1024 * 1024 * 1024,
|
||||
devices: [],
|
||||
kiosk: true,
|
||||
language: 'english',
|
||||
keyboard: {
|
||||
layout: '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