diff --git a/web/package-lock.json b/web/package-lock.json index a23c11ed2..d61efeaf1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -386,7 +386,6 @@ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.13.tgz", "integrity": "sha512-/D84T1Caxll3I2sRihPDR9UaWBhF50M+tAX15PdP6uSh/TxwAlLl9p7Rm1bD0mPjPercqaEKA+h9a9qLP16hug==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", @@ -414,7 +413,6 @@ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.13.tgz", "integrity": "sha512-hdMKY4rUTko8xqeWYGnwwDYDomkeOoLsYsP6SdaHWK7hpGvzWsT6Q/aIv8J8NrCYkLu+M+5nLiKOooweUZu3GQ==", "license": "MIT", - "peer": true, "dependencies": { "@angular-devkit/core": "20.3.13", "jsonc-parser": "3.3.1", @@ -451,7 +449,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.15.tgz", "integrity": "sha512-ikyKfhkxoqQA6JcBN0B9RaN6369sM1XYX81Id0lI58dmWCe7gYfrTp8ejqxxKftl514psQO3pkW8Gn1nJ131Gw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -566,7 +563,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz", "integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -583,7 +579,6 @@ "integrity": "sha512-G78I/HDJULloS2LSqfUfbmBlhDCbcWujIRWfuMnGsRf82TyGA2OEPe3IA/F8MrJfeOzPQim2fMyn24MqHL40Vg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-devkit/architect": "0.2003.13", "@angular-devkit/core": "20.3.13", @@ -618,7 +613,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.15.tgz", "integrity": "sha512-k4mCXWRFiOHK3bUKfWkRQQ8KBPxW8TAJuKLYCsSHPCpMz6u0eA1F0VlrnOkZVKWPI792fOaEAWH2Y4PTaXlUHw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -635,7 +629,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.15.tgz", "integrity": "sha512-lMicIAFAKZXa+BCZWs3soTjNQPZZXrF/WMVDinm8dQcggNarnDj4UmXgKSyXkkyqK5SLfnLsXVzrX6ndVT6z7A==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -649,7 +642,6 @@ "integrity": "sha512-8sJoxodxsfyZ8eJ5r6Bx7BCbazXYgsZ1+dE8t5u5rTQ6jNggwNtYEzkyReoD5xvP+MMtRkos3xpwq4rtFnpI6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -682,7 +674,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.15.tgz", "integrity": "sha512-NMbX71SlTZIY9+rh/SPhRYFJU0pMJYW7z/TBD4lqiO+b0DTOIg1k7Pg9ydJGqSjFO1Z4dQaA6TteNuF99TJCNw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -708,7 +699,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.15.tgz", "integrity": "sha512-gS5hQkinq52pm/7mxz4yHPCzEcmRWjtUkOVddPH0V1BW/HMni/p4Y6k2KqKBeGb9p8S5EAp6PDxDVLOPukp3mg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -737,7 +727,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.15.tgz", "integrity": "sha512-TxRM/wTW/oGXv/3/Iohn58yWoiYXOaeEnxSasiGNS1qhbkcKtR70xzxW6NjChBUYAixz2ERkLURkpx3pI8Q6Dw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -802,7 +791,6 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.15.tgz", "integrity": "sha512-6+qgk8swGSoAu7ISSY//GatAyCP36hEvvUgvjbZgkXLLH9yUQxdo77ij05aJ5s0OyB25q/JkqS8VTY0z1yE9NQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -821,7 +809,6 @@ "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-20.3.15.tgz", "integrity": "sha512-HCptODPVWg30XJwSueOz2zqsJjQ1chSscTs7FyIQcfuCTTthO35Lvz2Gtct8/GNHel9QNvvVwA5jrLjsU4dt1A==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -867,7 +854,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1832,7 +1818,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3451,7 +3436,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -3971,7 +3955,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.66.0.tgz", "integrity": "sha512-tRWyuqK5j5nEjlk0x5HaeLArgVpAIJZNeMiPy//95v4/8tlHdQLM4gh3qcvwS70GN5fnlFXINWhnblvxSDv2dw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": ">=2.8.1" }, @@ -4037,7 +4020,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.66.0.tgz", "integrity": "sha512-5DFbwHo7JHKBjgizbGTaIRJsai20+ZknhOQ1SRYwRTc9+6C1HbY/gGC+cjJTLmEQvk14rOoz8qbeWzJx88BU2Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "2.8.1" }, @@ -4069,7 +4051,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.66.0.tgz", "integrity": "sha512-AjjH+xhgonjf9Xnx3SHNrP5VbsS9jdtGB3BCTQbicYd6QuujQBKldK0fnYMjCY3L0+lboI2OPCVg9PTliOdJ8A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": ">=2.8.1" }, @@ -4109,7 +4090,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.7.0.tgz", "integrity": "sha512-j3HPRPR7XxKxgMeytb+r/CNUoLBMVrfdfL8KJr1XiFO9jyEvoC4chFXDXWlkGyUHJIC6wy5VIXlIlI/kpqOiGg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -4168,7 +4148,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.66.0.tgz", "integrity": "sha512-uqY3wslMs7KiBceaHPwCyWVrP8IPqb3OgAy1zd5DHosoUj/ciUl4JWVdx+QdsDypV/Cs4EZrqcIUtMDKQ/Zk0g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": ">=2.8.1" }, @@ -4197,7 +4176,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.66.0.tgz", "integrity": "sha512-D6REwySoaPGZlkdqTfrWahMqziXOY7GGTm1pXWVYDi5kEcSP9+F8ojo6saHDlwhN+V4/2jlMrkseSPlfXbmngQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": ">=2.8.1" }, @@ -4216,7 +4194,6 @@ "resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.9.0.tgz", "integrity": "sha512-TbIIwslbEnxunKuL9OyPZdmefrvJEK6HYiADEKQHUMUs4Pk2UbhMckUieURo83yPDamk/Mww+Nu/g60J/4uh2w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.1" }, @@ -4343,7 +4320,6 @@ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "license": "MIT", - "peer": true, "dependencies": { "@types/trusted-types": "*" } @@ -4393,9 +4369,8 @@ "version": "22.19.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4864,7 +4839,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5142,7 +5116,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -5788,8 +5761,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/domutils": { "version": "3.2.2", @@ -5861,7 +5833,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5872,7 +5843,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6155,7 +6125,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7699,7 +7668,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -8057,7 +8025,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -8897,7 +8864,6 @@ "integrity": "sha512-yW5ME0hqTz38r/th/7zVwX5oSIw1FviSA2PUlGZdVjghDme/KX6iiwmOBmlt9E9whNmwijEC6Gn3KKbrsBx8ig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.3.0", "@rollup/plugin-json": "^6.1.0", @@ -10704,7 +10670,6 @@ "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10850,7 +10815,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -10874,7 +10838,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -11791,8 +11754,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tslint": { "version": "6.1.3", @@ -12084,7 +12046,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12097,7 +12058,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/union": { @@ -12245,7 +12206,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -12713,7 +12673,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "devOptional": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -12732,8 +12691,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/web/projects/setup-wizard/src/app/app.component.ts b/web/projects/setup-wizard/src/app/app.component.ts index 847a6d584..b6bf0b3e5 100644 --- a/web/projects/setup-wizard/src/app/app.component.ts +++ b/web/projects/setup-wizard/src/app/app.component.ts @@ -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 diff --git a/web/projects/setup-wizard/src/app/pages/keyboard.page.ts b/web/projects/setup-wizard/src/app/pages/keyboard.page.ts index 7ed0fda2c..ce343d7a6 100644 --- a/web/projects/setup-wizard/src/app/pages/keyboard.page.ts +++ b/web/projects/setup-wizard/src/app/pages/keyboard.page.ts @@ -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' `, - 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) } } diff --git a/web/projects/setup-wizard/src/app/pages/language.page.ts b/web/projects/setup-wizard/src/app/pages/language.page.ts index d7a5af24a..b14cc3d0b 100644 --- a/web/projects/setup-wizard/src/app/pages/language.page.ts +++ b/web/projects/setup-wizard/src/app/pages/language.page.ts @@ -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);
{{ lang.nativeName }} - {{ lang.tuiName | i18n }} + {{ lang.name | i18n }}
`, 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']) } } } diff --git a/web/projects/setup-wizard/src/app/services/api.service.ts b/web/projects/setup-wizard/src/app/services/api.service.ts index d92ff67ed..82ff96857 100644 --- a/web/projects/setup-wizard/src/app/services/api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api.service.ts @@ -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 // setup.status abstract getPubKey(): Promise // setup.get-pubkey + abstract setKeyboard(params: FullKeyboard): Promise // setup.set-keyboard + abstract setLanguage(params: SetLanguageParams): Promise // setup.set-language // Install abstract getDisks(): Promise // setup.disk.list diff --git a/web/projects/setup-wizard/src/app/services/live-api.service.ts b/web/projects/setup-wizard/src/app/services/live-api.service.ts index 149d16dde..1308433ec 100644 --- a/web/projects/setup-wizard/src/app/services/live-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/live-api.service.ts @@ -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 { + return this.rpcRequest({ + method: 'setup.set-keyboard', + params, + }) + } + + async setLanguage(params: SetLanguageParams): Promise { + return this.rpcRequest({ + method: 'setup.set-language', + params, + }) + } + async getDisks() { return this.rpcRequest({ method: 'setup.disk.list', diff --git a/web/projects/setup-wizard/src/app/services/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/mock-api.service.ts index 3cc9b9216..47720229d 100644 --- a/web/projects/setup-wizard/src/app/services/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/mock-api.service.ts @@ -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 { + await pauseFor(300) + return null + } + + async setLanguage(params: SetLanguageParams): Promise { + await pauseFor(300) + return null + } + async getDisks(): Promise { await pauseFor(500) return MOCK_DISKS diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index 39e821ada..f0fe03a77 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -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, }) } diff --git a/web/projects/setup-wizard/src/app/types.ts b/web/projects/setup-wizard/src/app/types.ts index ea1d0facc..be5dcb3f5 100644 --- a/web/projects/setup-wizard/src/app/types.ts +++ b/web/projects/setup-wizard/src/app/types.ts @@ -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' diff --git a/web/projects/setup-wizard/src/app/utils/languages.ts b/web/projects/setup-wizard/src/app/utils/languages.ts deleted file mode 100644 index 034b0fb9b..000000000 --- a/web/projects/setup-wizard/src/app/utils/languages.ts +++ /dev/null @@ -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 = { - 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]! -} diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 593e1e2d1..8d148b0d1 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -673,4 +673,6 @@ export default { 709: 'Laufwerk', 710: 'Übertragen', 711: 'Die Liste ist leer', + 712: 'Jetzt neu starten', + 713: 'Später', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index bed021c85..1aef6d5e3 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -673,4 +673,6 @@ export const ENGLISH = { 'Drive': 709, // as in, a storage device 'Transfer': 710, // the verb 'The list is empty': 711, + 'Restart now': 712, + 'Later': 713, // as in, (do it) later } as const diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index dbabf0512..825100b30 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -673,4 +673,6 @@ export default { 709: 'Unidad', 710: 'Transferir', 711: 'La lista está vacía', + 712: 'Reiniciar ahora', + 713: 'Más tarde', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index dc1b99b10..83fe4f543 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -673,4 +673,6 @@ export default { 709: 'Disque', 710: 'Transférer', 711: 'La liste est vide', + 712: 'Redémarrer maintenant', + 713: 'Plus tard', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index dd02ac4c2..53577081d 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -673,4 +673,6 @@ export default { 709: 'Dysk', 710: 'Przenieś', 711: 'Lista jest pusta', + 712: 'Uruchom ponownie teraz', + 713: 'Później', } satisfies i18n diff --git a/web/projects/shared/src/public-api.ts b/web/projects/shared/src/public-api.ts index 3cd6a4dc3..3269be9aa 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -58,3 +58,5 @@ export * from './util/rpc.util' export * from './util/to-guid' export * from './util/to-local-iso-string' export * from './util/unused' +export * from './util/keyboards' +export * from './util/languages' diff --git a/web/projects/shared/src/util/keyboards.ts b/web/projects/shared/src/util/keyboards.ts new file mode 100644 index 000000000..f281267c7 --- /dev/null +++ b/web/projects/shared/src/util/keyboards.ts @@ -0,0 +1,90 @@ +import { LanguageCode } from './languages' + +/** + * Keyboard layout codes + */ +export type KeyboardCode = 'us' | 'gb' | 'es' | 'latam' | 'de' | 'fr' | 'pl' + +/** + * Keyboard layout display names + */ +export type KeyboardName = + | 'US English' + | 'UK English' + | 'Spanish' + | 'Latin American' + | 'German' + | 'French' + | 'Polish' + +/** + * Keyboard layout definition + */ +export interface Keyboard { + code: KeyboardCode + name: KeyboardName +} + +/** + * Full keyboard configuration for backend API + */ +export interface FullKeyboard { + layout: string + model: string | null + variant: string | null + options: string[] +} + +/** + * Keyboard layouts grouped by language code + */ +export const KEYBOARDS_BY_LANGUAGE: Record = { + 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' }], +} + +/** + * All available keyboard layouts + */ +export const ALL_KEYBOARDS: Keyboard[] = [ + { code: 'us', name: 'US English' }, + { code: 'gb', name: 'UK English' }, + { code: 'es', name: 'Spanish' }, + { code: 'latam', name: 'Latin American' }, + { code: 'de', name: 'German' }, + { code: 'fr', name: 'French' }, + { code: 'pl', name: 'Polish' }, +] + +/** + * Get all keyboards sorted with language-specific keyboards first, + * then remaining keyboards alphabetically by name. + */ +export function getAllKeyboardsSorted(languageCode: LanguageCode): Keyboard[] { + const languageKeyboards = KEYBOARDS_BY_LANGUAGE[languageCode] + const languageKeyboardCodes = new Set(languageKeyboards.map(kb => kb.code)) + const otherKeyboards = ALL_KEYBOARDS.filter( + kb => !languageKeyboardCodes.has(kb.code), + ).sort((a, b) => a.name.localeCompare(b.name)) + return [...languageKeyboards, ...otherKeyboards] +} + +/** + * Get the display name for a keyboard code. + */ +export function getKeyboardName( + code: KeyboardCode | string, +): KeyboardName | string { + const keyboard = ALL_KEYBOARDS.find(kb => kb.code === code) + if (keyboard) return keyboard.name + return code // fallback to the code itself if not found +} diff --git a/web/projects/shared/src/util/languages.ts b/web/projects/shared/src/util/languages.ts new file mode 100644 index 000000000..b2b24c78a --- /dev/null +++ b/web/projects/shared/src/util/languages.ts @@ -0,0 +1,44 @@ +import { Languages } from '../i18n/i18n.service' + +/** + * ISO language codes + */ +export type LanguageCode = 'en' | 'es' | 'de' | 'fr' | 'pl' + +/** + * Language definition with metadata + */ +export interface Language { + code: LanguageCode + name: Languages + nativeName: string +} + +/** + * Available languages with their metadata + */ +export const LANGUAGES: Language[] = [ + { code: 'en', name: 'english', nativeName: 'English' }, + { code: 'es', name: 'spanish', nativeName: 'Español' }, + { code: 'de', name: 'german', nativeName: 'Deutsch' }, + { code: 'fr', name: 'french', nativeName: 'Français' }, + { code: 'pl', name: 'polish', nativeName: 'Polski' }, +] + +/** + * Maps i18n language names to ISO language codes + */ +export const LANGUAGE_TO_CODE: Record = { + english: 'en', + spanish: 'es', + german: 'de', + french: 'fr', + polish: 'pl', +} + +/** + * Params for setting language via API + */ +export interface SetLanguageParams { + language: Languages +} diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 0e9c25734..0c359da07 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -39,7 +39,7 @@ export class AppComponent { .subscribe() readonly ui = inject>(PatchDB) - .watch$('ui', 'language') + .watch$('serverInfo', 'language') .pipe(takeUntilDestroyed()) .subscribe(language => { this.i18n.setLanguage(language || 'english') diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index e2db99464..edc8d0f4f 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -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 }) }, }, { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts index 14cc377fd..d77640356 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts @@ -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' {{ 'Kiosk Mode' | i18n }} - + {{ server.kiosk ? ('Enabled' | i18n) : ('Disabled' | i18n) }} - - {{ - 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) - }} + + @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) + }} + } + @if (server.kiosk !== null && server.keyboard?.layout; as layout) { + + + {{ getKeyboardName(layout) }} + + + } @if (server.kiosk !== null) { + + + `, + 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) + } +} diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index f0798b49a..565bca8e4 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -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 diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 25b1ebfb5..41d918ef0 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -117,6 +117,10 @@ export abstract class ApiService { abstract toggleKiosk(enable: boolean): Promise + abstract setKeyboard(params: RR.SetKeyboardReq): Promise + + abstract setLanguage(params: RR.SetLanguageReq): Promise + abstract setDns(params: RR.SetDnsReq): Promise abstract queryDns(params: RR.QueryDnsReq): Promise diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index bf4ba6097..47890f047 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -256,6 +256,14 @@ export class LiveApiService extends ApiService { }) } + async setKeyboard(params: RR.SetKeyboardReq): Promise { + return this.rpcRequest({ method: 'server.set-keyboard', params }) + } + + async setLanguage(params: RR.SetLanguageReq): Promise { + return this.rpcRequest({ method: 'server.set-language', params }) + } + async setDns(params: RR.SetDnsReq): Promise { return this.rpcRequest({ method: 'net.dns.set-static', diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 1ec9d6411..a1c8f105f 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -453,6 +453,41 @@ export class MockApiService extends ApiService { return null } + async setKeyboard(params: RR.SetKeyboardReq): Promise { + 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 { + 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 { await pauseFor(2000) diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 6eb52aa63..86e124d8a 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -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: { diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index 7e08174b2..30df37bec 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -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 snakeHighScore: number startosRegistry: string - language: Languages } export type PackageDataEntry =