mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
finish setup wizard and ui language-keyboard feature
This commit is contained in:
@@ -31,6 +31,10 @@ export class AppComponent {
|
||||
|
||||
switch (status.status) {
|
||||
case 'needs-install':
|
||||
// Restore keyboard from status if it was previously set
|
||||
if (status.keyboard) {
|
||||
this.stateService.keyboard = status.keyboard.layout
|
||||
}
|
||||
// Start the install flow
|
||||
await this.router.navigate(['/language'])
|
||||
break
|
||||
@@ -39,6 +43,10 @@ export class AppComponent {
|
||||
// Store the data drive info from status
|
||||
this.stateService.dataDriveGuid = status.guid
|
||||
this.stateService.attach = status.attach
|
||||
// Restore keyboard from status if it was previously set
|
||||
if (status.keyboard) {
|
||||
this.stateService.keyboard = status.keyboard.layout
|
||||
}
|
||||
|
||||
await this.router.navigate(['/language'])
|
||||
break
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component, inject, signal } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
getAllKeyboardsSorted,
|
||||
i18nPipe,
|
||||
Keyboard,
|
||||
LanguageCode,
|
||||
} from '@start9labs/shared'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiChevron, TuiDataListWrapper, TuiSelect } from '@taiga-ui/kit'
|
||||
import {
|
||||
TuiButtonLoading,
|
||||
TuiChevron,
|
||||
TuiDataListWrapper,
|
||||
TuiSelect,
|
||||
} from '@taiga-ui/kit'
|
||||
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||
import { ApiService } from '../services/api.service'
|
||||
import { StateService } from '../services/state.service'
|
||||
import { Keyboard, getKeyboardsForLanguage } from '../utils/languages'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -36,30 +46,22 @@ import { Keyboard, getKeyboardsForLanguage } from '../utils/languages'
|
||||
</tui-textfield>
|
||||
|
||||
<footer>
|
||||
<button tuiButton [disabled]="!selected" (click)="continue()">
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!selected"
|
||||
[loading]="saving()"
|
||||
(click)="continue()"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TuiCardLarge,
|
||||
TuiButton,
|
||||
TuiButtonLoading,
|
||||
TuiTextfield,
|
||||
TuiChevron,
|
||||
TuiSelect,
|
||||
@@ -71,24 +73,38 @@ import { Keyboard, getKeyboardsForLanguage } from '../utils/languages'
|
||||
})
|
||||
export default class KeyboardPage {
|
||||
private readonly router = inject(Router)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly stateService = inject(StateService)
|
||||
|
||||
protected readonly mobile = inject(TUI_IS_MOBILE)
|
||||
readonly keyboards = getKeyboardsForLanguage(this.stateService.language)
|
||||
// All keyboards, with language-specific keyboards at the top
|
||||
readonly keyboards = getAllKeyboardsSorted(
|
||||
this.stateService.language as LanguageCode,
|
||||
)
|
||||
selected =
|
||||
this.keyboards.find(k => k.code === this.stateService.keyboard) ||
|
||||
this.keyboards[0]
|
||||
this.keyboards[0]!
|
||||
|
||||
readonly saving = signal(false)
|
||||
|
||||
readonly stringify = (kb: Keyboard) => kb.name
|
||||
|
||||
async back() {
|
||||
await this.router.navigate(['/language'])
|
||||
}
|
||||
|
||||
async continue() {
|
||||
if (this.selected) {
|
||||
this.saving.set(true)
|
||||
|
||||
try {
|
||||
// Send keyboard to backend
|
||||
await this.api.setKeyboard({
|
||||
layout: this.selected.code,
|
||||
model: null,
|
||||
variant: null,
|
||||
options: [],
|
||||
})
|
||||
|
||||
this.stateService.keyboard = this.selected.code
|
||||
await this.navigateToNextStep()
|
||||
} finally {
|
||||
this.saving.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component, computed, inject, signal } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe, i18nService } from '@start9labs/shared'
|
||||
import { i18nPipe, i18nService, Language, LANGUAGES } from '@start9labs/shared'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiChevron, TuiDataListWrapper, TuiSelect } from '@taiga-ui/kit'
|
||||
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||
import { StateService } from '../services/state.service'
|
||||
import {
|
||||
LANGUAGES,
|
||||
Language,
|
||||
getDefaultKeyboard,
|
||||
needsKeyboardSelection,
|
||||
} from '../utils/languages'
|
||||
TuiButtonLoading,
|
||||
TuiChevron,
|
||||
TuiDataListWrapper,
|
||||
TuiSelect,
|
||||
} from '@taiga-ui/kit'
|
||||
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||
import { ApiService } from '../services/api.service'
|
||||
import { StateService } from '../services/state.service'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -60,25 +60,23 @@ import {
|
||||
@let lang = asLanguage(item);
|
||||
<div class="language-item">
|
||||
<span>{{ lang.nativeName }}</span>
|
||||
<small>{{ lang.tuiName | i18n }}</small>
|
||||
<small>{{ lang.name | i18n }}</small>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<footer>
|
||||
<button tuiButton [disabled]="!selected" (click)="continue()">
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!selected"
|
||||
[loading]="loading()"
|
||||
(click)="continue()"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.language-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -92,6 +90,7 @@ import {
|
||||
FormsModule,
|
||||
TuiCardLarge,
|
||||
TuiButton,
|
||||
TuiButtonLoading,
|
||||
TuiTextfield,
|
||||
TuiChevron,
|
||||
TuiSelect,
|
||||
@@ -103,6 +102,7 @@ import {
|
||||
})
|
||||
export default class LanguagePage {
|
||||
private readonly router = inject(Router)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly stateService = inject(StateService)
|
||||
private readonly i18nService = inject(i18nService)
|
||||
|
||||
@@ -112,18 +112,23 @@ export default class LanguagePage {
|
||||
selected =
|
||||
LANGUAGES.find(l => l.code === this.stateService.language) || LANGUAGES[0]
|
||||
|
||||
private readonly saving = signal(false)
|
||||
|
||||
// Show loading when either language is loading or saving is in progress
|
||||
readonly loading = computed(() => this.i18nService.loading() || this.saving())
|
||||
|
||||
readonly stringify = (lang: Language) => lang.nativeName
|
||||
readonly asLanguage = (item: unknown): Language => item as Language
|
||||
|
||||
constructor() {
|
||||
if (this.selected) {
|
||||
this.i18nService.setLanguage(this.selected.tuiName)
|
||||
this.i18nService.setLanguage(this.selected.name)
|
||||
}
|
||||
}
|
||||
|
||||
onLanguageChange(language: Language) {
|
||||
if (language) {
|
||||
this.i18nService.setLanguage(language.tuiName)
|
||||
this.i18nService.setLanguage(language.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,31 +136,16 @@ export default class LanguagePage {
|
||||
if (this.selected) {
|
||||
this.stateService.language = this.selected.code
|
||||
|
||||
if (this.stateService.kiosk) {
|
||||
if (needsKeyboardSelection(this.selected.code)) {
|
||||
await this.router.navigate(['/keyboard'])
|
||||
} else {
|
||||
this.stateService.keyboard = getDefaultKeyboard(
|
||||
this.selected.code,
|
||||
).code
|
||||
await this.navigateToNextStep()
|
||||
}
|
||||
} else {
|
||||
await this.navigateToNextStep()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Save language to backend
|
||||
this.saving.set(true)
|
||||
|
||||
private async navigateToNextStep() {
|
||||
if (this.stateService.dataDriveGuid) {
|
||||
if (this.stateService.attach) {
|
||||
this.stateService.setupType = 'attach'
|
||||
await this.router.navigate(['/password'])
|
||||
} else {
|
||||
await this.router.navigate(['/home'])
|
||||
try {
|
||||
await this.api.setLanguage({ language: this.selected.name })
|
||||
// Always go to keyboard selection
|
||||
await this.router.navigate(['/keyboard'])
|
||||
} finally {
|
||||
this.saving.set(false)
|
||||
}
|
||||
} else {
|
||||
await this.router.navigate(['/drives'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as jose from 'node-jose'
|
||||
import { DiskInfo, FollowLogsRes, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import {
|
||||
DiskInfo,
|
||||
FollowLogsRes,
|
||||
FullKeyboard,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { Observable } from 'rxjs'
|
||||
import {
|
||||
@@ -21,6 +27,8 @@ export abstract class ApiService {
|
||||
// Status & Setup
|
||||
abstract getStatus(): Promise<SetupStatusRes> // setup.status
|
||||
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||
abstract setKeyboard(params: FullKeyboard): Promise<null> // setup.set-keyboard
|
||||
abstract setLanguage(params: SetLanguageParams): Promise<null> // setup.set-language
|
||||
|
||||
// Install
|
||||
abstract getDisks(): Promise<DiskInfo[]> // setup.disk.list
|
||||
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
DiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsRes,
|
||||
FullKeyboard,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -64,6 +66,20 @@ export class LiveApiService extends ApiService {
|
||||
this.pubkey = response
|
||||
}
|
||||
|
||||
async setKeyboard(params: FullKeyboard): Promise<null> {
|
||||
return this.rpcRequest({
|
||||
method: 'setup.set-keyboard',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async setLanguage(params: SetLanguageParams): Promise<null> {
|
||||
return this.rpcRequest({
|
||||
method: 'setup.set-language',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async getDisks() {
|
||||
return this.rpcRequest<DiskInfo[]>({
|
||||
method: 'setup.disk.list',
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
DiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsRes,
|
||||
FullKeyboard,
|
||||
pauseFor,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -68,8 +70,13 @@ export class MockApiService extends ApiService {
|
||||
this.statusIndex++
|
||||
|
||||
if (this.statusIndex === 1) {
|
||||
// return { status: 'needs-install' }
|
||||
return { status: 'incomplete', attach: false, guid: 'mock-data-guid' }
|
||||
// return { status: 'needs-install', keyboard: null }
|
||||
return {
|
||||
status: 'incomplete',
|
||||
attach: false,
|
||||
guid: 'mock-data-guid',
|
||||
keyboard: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (this.statusIndex > 3) {
|
||||
@@ -93,6 +100,16 @@ export class MockApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async setKeyboard(_params: FullKeyboard): Promise<null> {
|
||||
await pauseFor(300)
|
||||
return null
|
||||
}
|
||||
|
||||
async setLanguage(params: SetLanguageParams): Promise<null> {
|
||||
await pauseFor(300)
|
||||
return null
|
||||
}
|
||||
|
||||
async getDisks(): Promise<DiskInfo[]> {
|
||||
await pauseFor(500)
|
||||
return MOCK_DISKS
|
||||
|
||||
@@ -35,7 +35,7 @@ export class StateService {
|
||||
|
||||
// Set during install flow, or loaded from status response
|
||||
language = ''
|
||||
keyboard = '' // only used if kiosk
|
||||
keyboard = ''
|
||||
|
||||
// From install response or status response (incomplete)
|
||||
dataDriveGuid = ''
|
||||
@@ -52,8 +52,6 @@ export class StateService {
|
||||
await this.api.attach({
|
||||
guid: this.dataDriveGuid,
|
||||
startOsPassword: password ? await this.api.encrypt(password) : null,
|
||||
language: this.language,
|
||||
kiosk: this.kiosk ? { keyboard: this.keyboard } : null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,8 +79,6 @@ export class StateService {
|
||||
await this.api.execute({
|
||||
startOsLogicalname: this.dataDriveGuid,
|
||||
startOsPassword: password ? await this.api.encrypt(password) : null,
|
||||
language: this.language,
|
||||
kiosk: this.kiosk ? { keyboard: this.keyboard } : null,
|
||||
recoverySource,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { DiskInfo, PartitionInfo, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import {
|
||||
DiskInfo,
|
||||
FullKeyboard,
|
||||
PartitionInfo,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
// === Echo ===
|
||||
@@ -10,8 +15,13 @@ export type EchoReq = {
|
||||
// === Setup Status ===
|
||||
|
||||
export type SetupStatusRes =
|
||||
| { status: 'needs-install' }
|
||||
| { status: 'incomplete'; guid: string; attach: boolean }
|
||||
| { status: 'needs-install'; keyboard: FullKeyboard | null }
|
||||
| {
|
||||
status: 'incomplete'
|
||||
guid: string
|
||||
attach: boolean
|
||||
keyboard: FullKeyboard | null
|
||||
}
|
||||
| { status: 'running'; progress: T.FullProgress; guid: string }
|
||||
| { status: 'complete' }
|
||||
|
||||
@@ -35,8 +45,6 @@ export interface InstallOsRes {
|
||||
export interface AttachParams {
|
||||
startOsPassword: T.EncryptedWire | null
|
||||
guid: string // data drive
|
||||
language: string
|
||||
kiosk: { keyboard: string } | null
|
||||
}
|
||||
|
||||
// === Execute ===
|
||||
@@ -44,8 +52,6 @@ export interface AttachParams {
|
||||
export interface SetupExecuteParams {
|
||||
startOsLogicalname: string
|
||||
startOsPassword: T.EncryptedWire | null // null = keep existing password (for restore/transfer)
|
||||
language: string
|
||||
kiosk: { keyboard: string } | null
|
||||
recoverySource:
|
||||
| {
|
||||
type: 'migrate'
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
|
||||
export interface Keyboard {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
code: string
|
||||
tuiName: i18nKey
|
||||
nativeName: string
|
||||
}
|
||||
|
||||
export const LANGUAGES: Language[] = [
|
||||
{ code: 'en', tuiName: 'english', nativeName: 'English' },
|
||||
{ code: 'es', tuiName: 'spanish', nativeName: 'Español' },
|
||||
{ code: 'de', tuiName: 'german', nativeName: 'Deutsch' },
|
||||
{ code: 'fr', tuiName: 'french', nativeName: 'Français' },
|
||||
{ code: 'pl', tuiName: 'polish', nativeName: 'Polski' },
|
||||
]
|
||||
|
||||
export const KEYBOARDS_BY_LANGUAGE: Record<string, Keyboard[]> = {
|
||||
en: [
|
||||
{ code: 'us', name: 'US English' },
|
||||
{ code: 'gb', name: 'UK English' },
|
||||
],
|
||||
es: [
|
||||
{ code: 'es', name: 'Spanish' },
|
||||
{ code: 'latam', name: 'Latin American' },
|
||||
],
|
||||
de: [{ code: 'de', name: 'German' }],
|
||||
fr: [{ code: 'fr', name: 'French' }],
|
||||
pl: [{ code: 'pl', name: 'Polish' }],
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available keyboards for a language.
|
||||
* Returns array of keyboards (may be 1 or more).
|
||||
*/
|
||||
export function getKeyboardsForLanguage(languageCode: string): Keyboard[] {
|
||||
return (
|
||||
KEYBOARDS_BY_LANGUAGE[languageCode] || [{ code: 'us', name: 'US English' }]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if keyboard selection is needed for a language.
|
||||
* Returns true if there are multiple keyboard options.
|
||||
*/
|
||||
export function needsKeyboardSelection(languageCode: string): boolean {
|
||||
const keyboards = getKeyboardsForLanguage(languageCode)
|
||||
return keyboards.length > 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default keyboard for a language.
|
||||
* Returns the first keyboard option.
|
||||
*/
|
||||
export function getDefaultKeyboard(languageCode: string): Keyboard {
|
||||
return getKeyboardsForLanguage(languageCode)[0]!
|
||||
}
|
||||
Reference in New Issue
Block a user