mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
rename frontend to web
This commit is contained in:
16
web/.browserslistrc
Normal file
16
web/.browserslistrc
Normal file
@@ -0,0 +1,16 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
16
web/.editorconfig
Normal file
16
web/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
42
web/.gitignore
vendored
Normal file
42
web/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Specifies intentionally untracked files to ignore when using Git
|
||||
# http://git-scm.com/docs/gitignore
|
||||
|
||||
*~
|
||||
*.sw[mnpcod]
|
||||
.tmp
|
||||
*.tmp
|
||||
*.tmp.*
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
UserInterfaceState.xcuserstate
|
||||
$RECYCLE.BIN/
|
||||
|
||||
start9-ambassador
|
||||
*.tar.gz
|
||||
config.json
|
||||
|
||||
ambassador.tar.gz
|
||||
*.log
|
||||
log.txt
|
||||
npm-debug.log*
|
||||
|
||||
postprocess.js
|
||||
|
||||
/.angular
|
||||
/.idea
|
||||
/.ionic
|
||||
/.sass-cache
|
||||
/.sourcemaps
|
||||
/.versions
|
||||
/.vscode
|
||||
/.gradle
|
||||
/dist
|
||||
/out-tsc
|
||||
/node_modules
|
||||
/platforms
|
||||
/plugins
|
||||
|
||||
config.json
|
||||
proxy.conf.json
|
||||
7
web/.prettierrc
Normal file
7
web/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "all",
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
75
web/README.md
Normal file
75
web/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# StartOS Frontend
|
||||
|
||||
StartOS has three user interfaces and a shared library, all written in Ionic/Angular/Typescript using an Angular workspace environment:
|
||||
|
||||
1. **ui**: the main user interface
|
||||
1. **install-wizard**: used to install StartOS
|
||||
1. **setup-wizard**: used to facilitate initial setup
|
||||
1. **marketplace**: abstracted ui elements to search for, list and display details for packages and their dependencies
|
||||
1. **shared**: contains components, types, and functions shared amongst all of the UIs.
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
- Requirements:
|
||||
- [Install nodejs](https://nodejs.org/en/)
|
||||
- [Install npm](https://www.npmjs.com/get-npm)
|
||||
|
||||
Check your versions
|
||||
|
||||
```sh
|
||||
node --version
|
||||
v18.15.0
|
||||
|
||||
npm --version
|
||||
v8.0.0
|
||||
```
|
||||
|
||||
## Running locally with mocks
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Start9Labs/start-os.git
|
||||
cd start-os
|
||||
git submodule update --init --recursive
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build:deps
|
||||
```
|
||||
|
||||
2. Copy `config-sample.json` and its contents to a new file `config.json`.
|
||||
|
||||
```sh
|
||||
cp config-sample.json config.json
|
||||
```
|
||||
|
||||
By default, "useMocks" is set to `true`.
|
||||
Valid values for "maskAs" are `tor` and `lan`.
|
||||
|
||||
3. Start the development server(s)
|
||||
|
||||
```sh
|
||||
npm run start:ui
|
||||
npm run start:install
|
||||
npm run start:setup
|
||||
```
|
||||
|
||||
## Running locally with proxied backend
|
||||
|
||||
This section enables you to run a local frontend with a remote backend (eg. hosted on a live Start9 server). It assumes you have completed Step 1 and Step 2 in the [section above](#running-locally-with-mocks)
|
||||
|
||||
1. Set `useMocks: false` in `config.json`
|
||||
|
||||
2. Create a proxy configuration file from the sample:
|
||||
|
||||
```sh
|
||||
cp proxy.conf-sample.json proxy.conf.json
|
||||
```
|
||||
|
||||
3. Change the target address to desired IP address in `proxy.conf.json`
|
||||
|
||||
4. Start the development server
|
||||
|
||||
```sh
|
||||
npm run start:ui:proxy
|
||||
```
|
||||
470
web/angular.json
Normal file
470
web/angular.json
Normal file
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"ui": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "projects/ui",
|
||||
"sourceRoot": "projects/ui/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"preserveSymlinks": true,
|
||||
"outputPath": "dist/raw/ui",
|
||||
"index": "projects/ui/src/index.html",
|
||||
"main": "projects/ui/src/main.ts",
|
||||
"polyfills": "projects/ui/src/polyfills.ts",
|
||||
"tsConfig": "projects/ui/tsconfig.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/shared/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||
"output": "./svg"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/monaco-editor",
|
||||
"output": "assets/monaco-editor/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@taiga-ui/icons/src",
|
||||
"output": "assets/taiga-ui/icons"
|
||||
},
|
||||
"projects/ui/src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "ngsw.json",
|
||||
"input": "dist/raw/ui",
|
||||
"output": "projects/ui/src"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@taiga-ui/icons/src",
|
||||
"output": "assets/taiga-ui/icons"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
|
||||
"node_modules/@taiga-ui/styles/taiga-ui-global.less",
|
||||
"projects/shared/styles/taiga.scss",
|
||||
"projects/shared/styles/variables.scss",
|
||||
"projects/shared/styles/global.scss",
|
||||
"projects/shared/styles/shared.scss",
|
||||
"projects/ui/src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"ngswConfigPath": "projects/ui/ngsw-config.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"serviceWorker": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "projects/ui/src/environments/environment.ts",
|
||||
"with": "projects/ui/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"ci": {
|
||||
"progress": false
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "ui:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "ui:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "ui:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"projects/ui/src/**/*.ts",
|
||||
"projects/ui/src/**/*.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ionic-cordova-build": {
|
||||
"builder": "@ionic/angular-toolkit:cordova-build",
|
||||
"options": {
|
||||
"browserTarget": "ui:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "ui:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ionic-cordova-serve": {
|
||||
"builder": "@ionic/angular-toolkit:cordova-serve",
|
||||
"options": {
|
||||
"cordovaBuildTarget": "ui:ionic-cordova-build",
|
||||
"devServerTarget": "ui:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"cordovaBuildTarget": "ui:ionic-cordova-build:production",
|
||||
"devServerTarget": "ui:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"install-wizard": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "projects/install-wizard",
|
||||
"sourceRoot": "projects/install-wizard/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/raw/install-wizard",
|
||||
"index": "projects/install-wizard/src/index.html",
|
||||
"main": "projects/install-wizard/src/main.ts",
|
||||
"polyfills": "projects/install-wizard/src/polyfills.ts",
|
||||
"tsConfig": "projects/install-wizard/tsconfig.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/shared/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||
"output": "./svg"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@taiga-ui/icons/src",
|
||||
"output": "assets/taiga-ui/icons"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
|
||||
"projects/shared/styles/taiga.scss",
|
||||
"projects/shared/styles/variables.scss",
|
||||
"projects/shared/styles/global.scss",
|
||||
"projects/shared/styles/shared.scss",
|
||||
"projects/install-wizard/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "projects/install-wizard/src/environments/environment.ts",
|
||||
"with": "projects/install-wizard/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ci": {
|
||||
"progress": false
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "install-wizard:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "install-wizard:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "install-wizard:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "install-wizard:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"projects/install-wizard/src/**/*.ts",
|
||||
"projects/install-wizard/src/**/*.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ionic-cordova-build": {
|
||||
"builder": "@ionic/angular-toolkit:cordova-build",
|
||||
"options": {
|
||||
"browserTarget": "install-wizard:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "install-wizard:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ionic-cordova-serve": {
|
||||
"builder": "@ionic/angular-toolkit:cordova-serve",
|
||||
"options": {
|
||||
"cordovaBuildTarget": "install-wizard:ionic-cordova-build",
|
||||
"devServerTarget": "install-wizard:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"cordovaBuildTarget": "install-wizard:ionic-cordova-build:production",
|
||||
"devServerTarget": "install-wizard:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setup-wizard": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "projects/setup-wizard",
|
||||
"sourceRoot": "projects/setup-wizard/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/raw/setup-wizard",
|
||||
"index": "projects/setup-wizard/src/index.html",
|
||||
"main": "projects/setup-wizard/src/main.ts",
|
||||
"polyfills": "projects/setup-wizard/src/polyfills.ts",
|
||||
"tsConfig": "projects/setup-wizard/tsconfig.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/shared/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||
"output": "./svg"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@taiga-ui/icons/src",
|
||||
"output": "assets/taiga-ui/icons"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
|
||||
"projects/shared/styles/taiga.scss",
|
||||
"projects/shared/styles/variables.scss",
|
||||
"projects/shared/styles/global.scss",
|
||||
"projects/shared/styles/shared.scss",
|
||||
"projects/setup-wizard/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "projects/setup-wizard/src/environments/environment.ts",
|
||||
"with": "projects/setup-wizard/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"ci": {
|
||||
"progress": false
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "setup-wizard:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "setup-wizard:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "setup-wizard:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"projects/setup-wizard/src/**/*.ts",
|
||||
"projects/setup-wizard/src/**/*.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ionic-cordova-build": {
|
||||
"builder": "@ionic/angular-toolkit:cordova-build",
|
||||
"options": {
|
||||
"browserTarget": "setup-wizard:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "setup-wizard:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ionic-cordova-serve": {
|
||||
"builder": "@ionic/angular-toolkit:cordova-serve",
|
||||
"options": {
|
||||
"cordovaBuildTarget": "setup-wizard:ionic-cordova-build",
|
||||
"devServerTarget": "setup-wizard:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"cordovaBuildTarget": "setup-wizard:ionic-cordova-build:production",
|
||||
"devServerTarget": "setup-wizard:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
"projectType": "library",
|
||||
"root": "projects/marketplace",
|
||||
"sourceRoot": "projects/marketplace/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/marketplace/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"projectType": "library",
|
||||
"root": "projects/shared",
|
||||
"sourceRoot": "projects/shared/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/shared/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"schematicCollections": ["@ionic/angular-toolkit"],
|
||||
"analytics": false
|
||||
},
|
||||
"schematics": {
|
||||
"@ionic/angular-toolkit:component": {
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@ionic/angular-toolkit:page": {
|
||||
"styleext": "scss"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
web/build-config.js
Executable file
12
web/build-config.js
Executable file
@@ -0,0 +1,12 @@
|
||||
// @ts-check
|
||||
const fs = require('fs')
|
||||
const childProcess = require('child_process')
|
||||
|
||||
const gitHash = String(
|
||||
childProcess.execSync('git describe --always --abbrev=40 --dirty=-modified'),
|
||||
).trim()
|
||||
|
||||
const origConfig = require('./config.json')
|
||||
|
||||
origConfig['gitHash'] = gitHash
|
||||
fs.writeFileSync('./config.json', JSON.stringify(origConfig, null, 2))
|
||||
20
web/config-sample.json
Normal file
20
web/config-sample.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"useMocks": true,
|
||||
"enableWidgets": false,
|
||||
"ui": {
|
||||
"api": {
|
||||
"url": "rpc",
|
||||
"version": "v1"
|
||||
},
|
||||
"marketplace": {
|
||||
"start9": "https://registry.start9.com/",
|
||||
"community": "https://community-registry.start9.com/"
|
||||
},
|
||||
"mocks": {
|
||||
"maskAs": "tor",
|
||||
"maskAsHttps": true,
|
||||
"skipStartupAlerts": true
|
||||
}
|
||||
},
|
||||
"gitHash": ""
|
||||
}
|
||||
23
web/ionic.config.json
Normal file
23
web/ionic.config.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"projects": {
|
||||
"ui": {
|
||||
"name": "ui",
|
||||
"integrations": {},
|
||||
"type": "angular",
|
||||
"root": "projects/ui"
|
||||
},
|
||||
"install-wizard": {
|
||||
"name": "install-wizard",
|
||||
"integrations": {},
|
||||
"type": "angular",
|
||||
"root": "projects/install-wizard"
|
||||
},
|
||||
"setup-wizard": {
|
||||
"name": "setup-wizard",
|
||||
"integrations": {},
|
||||
"type": "angular",
|
||||
"root": "projects/setup-wizard"
|
||||
}
|
||||
},
|
||||
"defaultProject": "ui"
|
||||
}
|
||||
9
web/lint-staged.config.js
Normal file
9
web/lint-staged.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
'**/*.{js,ts,html,md,json}': 'prettier --write',
|
||||
'*.ts': 'tslint --fix',
|
||||
'projects/ui/**/*.ts': () => 'npm run check:ui',
|
||||
'projects/shared/**/*.ts': () => 'npm run check:shared',
|
||||
'projects/marketplace/**/*.ts': () => 'npm run check:marketplace',
|
||||
'projects/install-wizard/**/*.ts': () => 'npm run check:install',
|
||||
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
|
||||
}
|
||||
17449
web/package-lock.json
generated
Normal file
17449
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
113
web/package.json
Normal file
113
web/package.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.5",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install && npm run check:setup",
|
||||
"check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
||||
"build:install": "ng run install-wizard:build",
|
||||
"build:setup": "ng run setup-wizard:build",
|
||||
"build:ui": "ng run ui:build",
|
||||
"build:ui:dev": "ng run ui:build:development",
|
||||
"build:ui:stats": "ng run ui:build --stats-json",
|
||||
"build:all": "npm run build:deps && npm run build:setup && npm run build:ui && npm run build:install",
|
||||
"build:shared": "ng build shared",
|
||||
"build:marketplace": "npm run build:shared && ng build marketplace",
|
||||
"analyze:ui": "webpack-bundle-analyzer dist/raw/ui/stats.json",
|
||||
"publish:shared": "npm run build:shared && npm publish ./dist/shared --access public",
|
||||
"publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public",
|
||||
"start:install": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0",
|
||||
"start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0",
|
||||
"start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0",
|
||||
"start:ui:proxy": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0 -- --proxy-config proxy.conf.json",
|
||||
"build-config": "node build-config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.1.4",
|
||||
"@angular/common": "^16.1.4",
|
||||
"@angular/compiler": "^16.1.4",
|
||||
"@angular/core": "^16.1.4",
|
||||
"@angular/forms": "^16.1.4",
|
||||
"@angular/platform-browser": "^16.1.4",
|
||||
"@angular/platform-browser-dynamic": "^16.1.4",
|
||||
"@angular/pwa": "^16.1.4",
|
||||
"@angular/router": "^16.1.4",
|
||||
"@angular/service-worker": "^16.1.4",
|
||||
"@ionic/angular": "^6.1.15",
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||
"@taiga-ui/addon-charts": "3.53.0",
|
||||
"@taiga-ui/cdk": "3.53.0",
|
||||
"@taiga-ui/core": "3.53.0",
|
||||
"@taiga-ui/experimental": "3.53.0",
|
||||
"@taiga-ui/icons": "3.53.0",
|
||||
"@taiga-ui/kit": "3.53.0",
|
||||
"@taiga-ui/styles": "3.53.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
"cbor-web": "^8.1.0",
|
||||
"core-js": "^3.21.1",
|
||||
"cron": "^2.2.0",
|
||||
"cronstrue": "^2.21.0",
|
||||
"dompurify": "^2.3.6",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"fuse.js": "^6.4.6",
|
||||
"jose": "^4.9.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"marked": "^4.0.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^16.0.0",
|
||||
"node-jose": "^2.1.1",
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zone.js": "^0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.1.4",
|
||||
"@angular/cli": "^16.1.4",
|
||||
"@angular/compiler-cli": "^16.1.4",
|
||||
"@angular/language-service": "^16.1.4",
|
||||
"@ionic/cli": "^6.19.0",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/estree": "^0.0.51",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/marked": "^4.0.3",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^18.15.0",
|
||||
"@types/node-jose": "^1.1.10",
|
||||
"@types/pbkdf2": "^3.1.0",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^13.2.0",
|
||||
"ng-packagr": "^16.1.0",
|
||||
"node-html-parser": "^5.3.3",
|
||||
"prettier": "^2.6.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^5.1.6",
|
||||
"webpack-bundle-analyzer": "^4.8.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged --concurrent false"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
web/patchdb-ui-seed.json
Normal file
20
web/patchdb-ui-seed.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": null,
|
||||
"ack-welcome": "0.3.4.4",
|
||||
"marketplace": {
|
||||
"selected-url": "https://registry.start9.com/",
|
||||
"known-hosts": {
|
||||
"https://registry.start9.com/": {},
|
||||
"https://community-registry.start9.com/": {}
|
||||
}
|
||||
},
|
||||
"dev": {},
|
||||
"gaming": {
|
||||
"snake": {
|
||||
"high-score": 0
|
||||
}
|
||||
},
|
||||
"ack-instructions": {},
|
||||
"theme": "Dark",
|
||||
"widgets": []
|
||||
}
|
||||
22
web/projects/install-wizard/src/app/app-routing.module.ts
Normal file
22
web/projects/install-wizard/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
useHash: true,
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
6
web/projects/install-wizard/src/app/app.component.html
Normal file
6
web/projects/install-wizard/src/app/app.component.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<tui-root tuiMode="onDark">
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
8
web/projects/install-wizard/src/app/app.component.scss
Normal file
8
web/projects/install-wizard/src/app/app.component.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
10
web/projects/install-wizard/src/app/app.component.ts
Normal file
10
web/projects/install-wizard/src/app/app.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() {}
|
||||
}
|
||||
56
web/projects/install-wizard/src/app/app.module.ts
Normal file
56
web/projects/install-wizard/src/app/app.module.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||
import {
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiRootModule,
|
||||
TuiThemeNightModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { MockApiService } from './services/api/mock-api.service'
|
||||
import { LiveApiService } from './services/api/live-api.service'
|
||||
import {
|
||||
LoadingModule,
|
||||
RELATIVE_URL,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
IonicModule.forRoot({
|
||||
mode: 'md',
|
||||
}),
|
||||
AppRoutingModule,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
LoadingModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
import {
|
||||
UnitConversionPipesModule,
|
||||
GuidPipePipesModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SwiperModule,
|
||||
UnitConversionPipesModule,
|
||||
GuidPipePipesModule,
|
||||
],
|
||||
declarations: [HomePage],
|
||||
})
|
||||
export class HomePageModule {}
|
||||
107
web/projects/install-wizard/src/app/pages/home/home.page.html
Normal file
107
web/projects/install-wizard/src/app/pages/home/home.page.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<div style="padding: 64px 0 32px 0">
|
||||
<img src="assets/img/icon.png" style="max-width: 100px" />
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-header>
|
||||
<ion-button
|
||||
*ngIf="swiper?.activeIndex === 1"
|
||||
class="back-button"
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="previous()"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-card-title>
|
||||
{{ !swiper || swiper.activeIndex === 0 ? 'Select Disk' : 'Install
|
||||
Type' }}
|
||||
</ion-card-title>
|
||||
<ion-card-subtitle *ngIf="error">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
<ion-card-content class="ion-margin-bottom">
|
||||
<swiper (swiper)="setSwiperInstance($event)">
|
||||
<!-- SLIDE 1 -->
|
||||
<ng-template swiperSlide>
|
||||
<ion-item
|
||||
*ngFor="let disk of disks"
|
||||
button
|
||||
(click)="next(disk)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="start"
|
||||
name="save-outline"
|
||||
size="large"
|
||||
color="dark"
|
||||
></ion-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h1>
|
||||
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model ||
|
||||
'Unknown Model' }}
|
||||
</h1>
|
||||
<h2>
|
||||
{{ disk.logicalname }} - {{ disk.capacity | convertBytes
|
||||
}}
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
|
||||
<!-- SLIDE 2 -->
|
||||
<ng-template swiperSlide>
|
||||
<ng-container *ngIf="selectedDisk">
|
||||
<!-- re-install -->
|
||||
<ion-item
|
||||
*ngIf="selectedDisk | guid"
|
||||
button
|
||||
(click)="tryInstall(false)"
|
||||
>
|
||||
<ion-icon
|
||||
color="dark"
|
||||
slot="start"
|
||||
size="large"
|
||||
name="medkit-outline"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h1>
|
||||
<ion-text color="success">Re-Install StartOS</ion-text>
|
||||
</h1>
|
||||
<h2>Will preserve existing StartOS data</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- fresh install -->
|
||||
<ion-item button lines="none" (click)="tryInstall(true)">
|
||||
<ion-icon
|
||||
color="dark"
|
||||
slot="start"
|
||||
size="large"
|
||||
name="download-outline"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h1>
|
||||
<ion-text
|
||||
[color]="(selectedDisk | guid) ? 'danger' : 'success'"
|
||||
>
|
||||
{{ (selectedDisk | guid) ? 'Factory Reset' : 'Install
|
||||
StartOS' }}
|
||||
</ion-text>
|
||||
</h1>
|
||||
<h2>Will delete existing data on disk</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</swiper>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,28 @@
|
||||
/** Ionic CSS Variables overrides **/
|
||||
:root {
|
||||
--ion-font-family: 'Benton Sans', sans-serif;
|
||||
}
|
||||
|
||||
ion-content {
|
||||
--background: var(--ion-color-medium);
|
||||
}
|
||||
|
||||
ion-grid {
|
||||
padding-top: 32px;
|
||||
height: 100%;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 24px;
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
ion-card-title {
|
||||
margin: 16px 0;
|
||||
font-family: 'Montserrat';
|
||||
font-size: x-large;
|
||||
--color: var(--ion-color-light);
|
||||
}
|
||||
137
web/projects/install-wizard/src/app/pages/home/home.page.ts
Normal file
137
web/projects/install-wizard/src/app/pages/home/home.page.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { IonicSlides } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { DiskInfo, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage {
|
||||
swiper?: Swiper
|
||||
disks: DiskInfo[] = []
|
||||
selectedDisk?: DiskInfo
|
||||
error = ''
|
||||
|
||||
constructor(
|
||||
private readonly loader: LoadingService,
|
||||
private readonly api: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.disks = await this.api.getDisks()
|
||||
}
|
||||
|
||||
async ionViewDidEnter() {
|
||||
if (this.swiper) {
|
||||
this.swiper.allowTouchMove = false
|
||||
}
|
||||
}
|
||||
|
||||
setSwiperInstance(swiper: any) {
|
||||
this.swiper = swiper
|
||||
}
|
||||
|
||||
next(disk: DiskInfo) {
|
||||
this.selectedDisk = disk
|
||||
this.swiper?.slideNext(500)
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.swiper?.slidePrev(500)
|
||||
}
|
||||
|
||||
async tryInstall(overwrite: boolean) {
|
||||
if (overwrite) {
|
||||
return this.presentAlertDanger()
|
||||
}
|
||||
|
||||
this.install(false)
|
||||
}
|
||||
|
||||
private async install(overwrite: boolean) {
|
||||
const loader = this.loader.open('Installing StartOS...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.install({
|
||||
logicalname: this.selectedDisk!.logicalname,
|
||||
overwrite,
|
||||
})
|
||||
this.presentAlertReboot()
|
||||
} catch (e: any) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private presentAlertDanger() {
|
||||
const { vendor, model } = this.selectedDisk!
|
||||
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `This action will COMPLETELY erase the disk ${
|
||||
vendor || 'Unknown Vendor'
|
||||
} - ${model || 'Unknown Model'} and install StartOS in its place`,
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
this.install(true)
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertReboot() {
|
||||
this.dialogs
|
||||
.open(
|
||||
'Remove the USB stick and reboot your device to begin using your new Start9 server',
|
||||
{
|
||||
label: 'Install Success',
|
||||
closeable: false,
|
||||
dismissible: false,
|
||||
size: 's',
|
||||
data: { button: 'Reboot' },
|
||||
},
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
this.reboot()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async reboot() {
|
||||
const loader = this.loader.open('').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.reboot()
|
||||
this.presentAlertComplete()
|
||||
} catch (e: any) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private presentAlertComplete() {
|
||||
this.dialogs
|
||||
.open('Please wait for StartOS to restart, then refresh this page', {
|
||||
label: 'Rebooting',
|
||||
size: 's',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DiskInfo } from '@start9labs/shared'
|
||||
|
||||
export abstract class ApiService {
|
||||
abstract getDisks(): Promise<GetDisksRes> // install.disk.list
|
||||
abstract install(params: InstallReq): Promise<void> // install.execute
|
||||
abstract reboot(): Promise<void> // install.reboot
|
||||
}
|
||||
|
||||
export type GetDisksRes = DiskInfo[]
|
||||
|
||||
export type InstallReq = {
|
||||
logicalname: string
|
||||
overwrite: boolean
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService, GetDisksRes, InstallReq } from './api.service'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(private readonly http: HttpService) {}
|
||||
|
||||
async getDisks(): Promise<GetDisksRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'install.disk.list',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async install(params: InstallReq): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'install.execute',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async reboot(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'install.reboot',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts)
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
throw new RpcError(rpcRes.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { ApiService, GetDisksRes, InstallReq } from './api.service'
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService implements ApiService {
|
||||
async getDisks(): Promise<GetDisksRes> {
|
||||
await pauseFor(500)
|
||||
return [
|
||||
{
|
||||
logicalname: 'abcd',
|
||||
vendor: 'Samsung',
|
||||
model: 'T5',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pabcd',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.2.17',
|
||||
full: true,
|
||||
'password-hash':
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': null,
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 123456789123,
|
||||
guid: 'uuid-uuid-uuid-uuid',
|
||||
},
|
||||
{
|
||||
logicalname: 'dcba',
|
||||
vendor: 'Crucial',
|
||||
model: 'MX500',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pbcba',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.3.3',
|
||||
full: true,
|
||||
'password-hash':
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': null,
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 124456789123,
|
||||
guid: null,
|
||||
},
|
||||
{
|
||||
logicalname: 'wxyz',
|
||||
vendor: 'SanDisk',
|
||||
model: 'Specialness',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pbcba',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.3.2',
|
||||
full: true,
|
||||
'password-hash':
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': null,
|
||||
},
|
||||
guid: 'guid-guid-guid-guid',
|
||||
},
|
||||
],
|
||||
capacity: 123459789123,
|
||||
guid: null,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async install(params: InstallReq): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async reboot(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
}
|
||||
16
web/projects/install-wizard/src/environments/environment.ts
Normal file
16
web/projects/install-wizard/src/environments/environment.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
}
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
23
web/projects/install-wizard/src/index.html
Normal file
23
web/projects/install-wizard/src/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>StartOS Install Wizard</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/icon/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
12
web/projects/install-wizard/src/main.ts
Normal file
12
web/projects/install-wizard/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { AppModule } from './app/app.module'
|
||||
import { environment } from './environments/environment'
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err))
|
||||
64
web/projects/install-wizard/src/polyfills.ts
Normal file
64
web/projects/install-wizard/src/polyfills.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
import './zone-flags'
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
59
web/projects/install-wizard/src/styles.scss
Normal file
59
web/projects/install-wizard/src/styles.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf');
|
||||
}
|
||||
|
||||
/** Ionic CSS Variables overrides **/
|
||||
:root {
|
||||
--ion-font-family: 'Montserrat', sans-serif;
|
||||
|
||||
--ion-color-primary: #0075e1;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
|
||||
--ion-item-background: #2b2b2b;
|
||||
--ion-toolbar-background: #2b2b2b;
|
||||
--ion-card-background: #2b2b2b;
|
||||
|
||||
--ion-background-color: #282828;
|
||||
--ion-background-color-rgb: 30,30,30;
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
--ion-text-color-rgb: var(--ion-color-dark-rgb);
|
||||
}
|
||||
|
||||
.loader {
|
||||
--spinner-color: var(--ion-color-warning) !important;
|
||||
z-index: 40000 !important;
|
||||
}
|
||||
|
||||
.alert-danger-message {
|
||||
.alert-title {
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-success-message {
|
||||
.alert-title {
|
||||
color: var(--ion-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
ion-alert {
|
||||
.alert-button {
|
||||
color: var(--ion-color-dark) !important;
|
||||
}
|
||||
}
|
||||
6
web/projects/install-wizard/src/zone-flags.ts
Normal file
6
web/projects/install-wizard/src/zone-flags.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Prevents Angular change detection from
|
||||
* running with certain Web Component callbacks
|
||||
*/
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(window as any).__Zone_disable_customElements = true
|
||||
9
web/projects/install-wizard/tsconfig.json
Normal file
9
web/projects/install-wizard/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
7
web/projects/marketplace/ng-package.json
Normal file
7
web/projects/marketplace/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/marketplace",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
16
web/projects/marketplace/package.json
Normal file
16
web/projects/marketplace/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@start9labs/marketplace",
|
||||
"version": "0.3.12",
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=13.2.0",
|
||||
"@angular/core": ">=13.2.0",
|
||||
"@ionic/angular": ">=6.0.0",
|
||||
"@start9labs/shared": ">=0.3.0",
|
||||
"@taiga-ui/cdk": ">=3.0.0",
|
||||
"@tinkoff/ng-dompurify": ">=4.0.0",
|
||||
"fuse.js": "^6.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<img
|
||||
*ngIf="icon; else noIcon"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt="Service Icon"
|
||||
/>
|
||||
<ng-template #noIcon>
|
||||
<ion-icon name="storefront-outline" [style.font-size]="size"></ion-icon>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { StoreIconComponent } from './store-icon.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [StoreIconComponent],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [StoreIconComponent],
|
||||
})
|
||||
export class StoreIconComponentModule {}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'store-icon',
|
||||
templateUrl: './store-icon.component.html',
|
||||
styleUrls: ['./store-icon.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StoreIconComponent {
|
||||
@Input()
|
||||
url = ''
|
||||
@Input()
|
||||
size?: string
|
||||
@Input({ required: true })
|
||||
marketplace!: MarketplaceConfig
|
||||
|
||||
get icon() {
|
||||
const { start9, community } = this.marketplace
|
||||
|
||||
if (sameUrl(this.url, start9)) {
|
||||
return 'assets/img/icon_transparent.png'
|
||||
} else if (sameUrl(this.url, community)) {
|
||||
return 'assets/img/community-store.png'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<ion-button
|
||||
*ngFor="let cat of categories"
|
||||
fill="clear"
|
||||
class="category"
|
||||
[class.category_selected]="cat === category"
|
||||
(click)="switchCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
</ion-button>
|
||||
@@ -0,0 +1,14 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.category {
|
||||
font-weight: 300;
|
||||
color: var(--ion-color-dark-shade);
|
||||
|
||||
&_selected {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-categories',
|
||||
templateUrl: 'categories.component.html',
|
||||
styleUrls: ['categories.component.scss'],
|
||||
host: {
|
||||
class: 'hidden-scrollbar ion-text-center',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CategoriesComponent {
|
||||
@Input()
|
||||
categories: readonly string[] = []
|
||||
|
||||
@Input()
|
||||
category = ''
|
||||
|
||||
@Output()
|
||||
readonly categoryChange = new EventEmitter<string>()
|
||||
|
||||
switchCategory(category: string): void {
|
||||
this.category = category
|
||||
this.categoryChange.emit(category)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { CategoriesComponent } from './categories.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule],
|
||||
declarations: [CategoriesComponent],
|
||||
exports: [CategoriesComponent],
|
||||
})
|
||||
export class CategoriesModule {}
|
||||
@@ -0,0 +1,12 @@
|
||||
<ion-item class="service-card" [routerLink]="['/marketplace', pkg.manifest.id]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="pkg | mimeType | trustUrl" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="montserrat">
|
||||
<strong>{{ pkg.manifest.title }}</strong>
|
||||
</h2>
|
||||
<h3>{{ pkg.manifest.description.short }}</h3>
|
||||
<ng-content></ng-content>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-item',
|
||||
templateUrl: 'item.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ItemComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
}
|
||||
20
web/projects/marketplace/src/pages/list/item/item.module.ts
Normal file
20
web/projects/marketplace/src/pages/list/item/item.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { ItemComponent } from './item.component'
|
||||
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ItemComponent],
|
||||
exports: [ItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule,
|
||||
SharedPipesModule,
|
||||
MimeTypePipeModule,
|
||||
],
|
||||
})
|
||||
export class ItemModule {}
|
||||
@@ -0,0 +1,14 @@
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col responsiveCol class="column" sizeSm="8" sizeLg="6">
|
||||
<ion-toolbar color="transparent" class="ion-text-left">
|
||||
<ion-searchbar
|
||||
[color]="(theme$ | async) === 'Light' ? 'light' : 'dark'"
|
||||
debounce="250"
|
||||
[ngModel]="query"
|
||||
(ngModelChange)="onModelChange($event)"
|
||||
></ion-searchbar>
|
||||
</ion-toolbar>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.column {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { THEME } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-search',
|
||||
templateUrl: 'search.component.html',
|
||||
styleUrls: ['search.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchComponent {
|
||||
@Input()
|
||||
query = ''
|
||||
|
||||
@Output()
|
||||
readonly queryChange = new EventEmitter<string>()
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
onModelChange(query: string) {
|
||||
this.query = query
|
||||
this.queryChange.emit(query)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ResponsiveColDirective } from '@start9labs/shared'
|
||||
|
||||
import { SearchComponent } from './search.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [IonicModule, FormsModule, CommonModule, ResponsiveColDirective],
|
||||
declarations: [SearchComponent],
|
||||
exports: [SearchComponent],
|
||||
})
|
||||
export class SearchModule {}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="hidden-scrollbar ion-text-center">
|
||||
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 80px; border-radius: 0"
|
||||
></ion-skeleton-text>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="divider" style="margin: 24px 0"></div>
|
||||
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col
|
||||
*ngFor="let pkg of ['', '', '', '']"
|
||||
responsiveCol
|
||||
sizeXs="12"
|
||||
sizeSm="12"
|
||||
sizeMd="6"
|
||||
>
|
||||
<ion-item>
|
||||
<ion-thumbnail slot="start">
|
||||
<ion-skeleton-text
|
||||
style="border-radius: 100%"
|
||||
animated
|
||||
></ion-skeleton-text>
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<ion-skeleton-text
|
||||
animated
|
||||
style="width: 150px; height: 18px; margin-bottom: 8px"
|
||||
></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 400px"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 100px"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-skeleton',
|
||||
templateUrl: 'skeleton.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SkeletonComponent {}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ResponsiveColDirective } from '@start9labs/shared'
|
||||
|
||||
import { SkeletonComponent } from './skeleton.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, ResponsiveColDirective],
|
||||
declarations: [SkeletonComponent],
|
||||
exports: [SkeletonComponent],
|
||||
})
|
||||
export class SkeletonModule {}
|
||||
@@ -0,0 +1,33 @@
|
||||
<ion-content class="with-widgets">
|
||||
<ng-container *ngIf="notes$ | async as notes; else loading">
|
||||
<div *ngFor="let note of notes | keyvalue : asIsOrder">
|
||||
<ion-button
|
||||
expand="full"
|
||||
color="light"
|
||||
class="version-button"
|
||||
[class.ion-activated]="isSelected(note.key)"
|
||||
(click)="setSelected(note.key)"
|
||||
>
|
||||
<p class="version">{{ note.key | displayEmver }}</p>
|
||||
</ion-button>
|
||||
<ion-card
|
||||
tuiElement
|
||||
#element="elementRef"
|
||||
class="panel"
|
||||
color="light"
|
||||
[id]="note.key"
|
||||
[style.maxHeight.px]="getDocSize(note.key, element)"
|
||||
>
|
||||
<ion-text
|
||||
id="release-notes"
|
||||
safeLinks
|
||||
[innerHTML]="note.value | markdown | dompurify"
|
||||
></ion-text>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Loading Release Notes"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,23 @@
|
||||
:host {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 0;
|
||||
padding: 0 24px;
|
||||
transition: max-height 0.2s ease-out;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 5px solid #4d4d4d;
|
||||
}
|
||||
|
||||
.version-button {
|
||||
height: 50px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '../../services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'release-notes',
|
||||
templateUrl: './release-notes.component.html',
|
||||
styleUrls: ['./release-notes.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesComponent {
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
) {}
|
||||
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId)
|
||||
|
||||
isSelected(key: string): boolean {
|
||||
return this.selected === key
|
||||
}
|
||||
|
||||
setSelected(selected: string) {
|
||||
this.selected = this.isSelected(selected) ? null : selected
|
||||
}
|
||||
|
||||
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
|
||||
return this.isSelected(key) ? nativeElement.scrollHeight : 0
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksDirective,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiElementModule } from '@taiga-ui/cdk'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
|
||||
import { ReleaseNotesComponent } from './release-notes.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TextSpinnerComponentModule,
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
TuiElementModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksDirective,
|
||||
],
|
||||
declarations: [ReleaseNotesComponent],
|
||||
exports: [ReleaseNotesComponent],
|
||||
})
|
||||
export class ReleaseNotesModule {}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!-- release notes -->
|
||||
<ion-item-divider>
|
||||
New in {{ pkg.manifest.version | displayEmver }}
|
||||
</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div
|
||||
safeLinks
|
||||
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
|
||||
></div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-button routerLink="notes" fill="clear" strong>
|
||||
Past Release Notes
|
||||
<ion-icon slot="end" name="arrow-forward"></ion-icon>
|
||||
</ion-button>
|
||||
<!-- description -->
|
||||
<ion-item-divider>Description</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<h2>{{ pkg.manifest.description.long }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div
|
||||
*ngIf="pkg.manifest['marketing-site'] as url"
|
||||
style="padding: 4px 0 10px 14px"
|
||||
>
|
||||
<ion-button [href]="url" target="_blank" rel="noreferrer" color="tertiary">
|
||||
View website
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-about',
|
||||
templateUrl: 'about.component.html',
|
||||
styleUrls: ['about.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AboutComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksDirective,
|
||||
} from '@start9labs/shared'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
|
||||
import { AboutComponent } from './about.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
IonicModule,
|
||||
MarkdownPipeModule,
|
||||
EmverPipesModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksDirective,
|
||||
],
|
||||
declarations: [AboutComponent],
|
||||
exports: [AboutComponent],
|
||||
})
|
||||
export class AboutModule {}
|
||||
@@ -0,0 +1,131 @@
|
||||
<ng-container *ngIf="pkg.manifest.replaces as replaces">
|
||||
<div *ngIf="replaces.length" class="ion-padding-bottom">
|
||||
<ion-item-divider>Intended to replace</ion-item-divider>
|
||||
<ul>
|
||||
<li *ngFor="let app of replaces">
|
||||
{{ app }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider>Additional Info</ion-item-divider>
|
||||
<ion-grid *ngIf="pkg.manifest as manifest">
|
||||
<ion-row>
|
||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
*ngIf="manifest['git-hash'] as gitHash; else noHash"
|
||||
button
|
||||
detail="false"
|
||||
(click)="copyService.copy(gitHash)"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Git Hash</h2>
|
||||
<p>{{ gitHash }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="copy-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ng-template #noHash>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>Git Hash</h2>
|
||||
<p>Unknown</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
<ion-item button detail="false" (click)="presentAlertVersions(version)">
|
||||
<ion-label>
|
||||
<h2>Other Versions</h2>
|
||||
<p>Click to view other versions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
<ng-template #version let-data="data" let-completeWith="completeWith">
|
||||
<tui-radio-list
|
||||
class="radio"
|
||||
size="l"
|
||||
[items]="data.items"
|
||||
[itemContent]="displayEmver | tuiStringifyContent"
|
||||
[(ngModel)]="data.value"
|
||||
></tui-radio-list>
|
||||
<footer class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(null)"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(data.value)"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</footer>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
<ion-item button detail="false" (click)="presentModalMd('License')">
|
||||
<ion-label>
|
||||
<h2>License</h2>
|
||||
<p>{{ manifest.license }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
button
|
||||
detail="false"
|
||||
(click)="presentModalMd('Instructions')"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Instructions</h2>
|
||||
<p>Click to view instructions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
[href]="manifest['upstream-repo']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Source Repository</h2>
|
||||
<p>{{ manifest['upstream-repo'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="manifest['wrapper-repo']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Wrapper Repository</h2>
|
||||
<p>{{ manifest['wrapper-repo'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="manifest['support-site']"
|
||||
[disabled]="!manifest['support-site']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Support Site</h2>
|
||||
<p>{{ manifest['support-site'] || 'Not provided' }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
@@ -0,0 +1,10 @@
|
||||
.radio {
|
||||
display: block;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef,
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import {
|
||||
TuiAlertService,
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
CopyService,
|
||||
copyToClipboard,
|
||||
displayEmver,
|
||||
Emver,
|
||||
MarkdownComponent,
|
||||
} from '@start9labs/shared'
|
||||
import { filter } from 'rxjs'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional',
|
||||
templateUrl: 'additional.component.html',
|
||||
styleUrls: ['additional.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AdditionalComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
@Output()
|
||||
version = new EventEmitter<string>()
|
||||
|
||||
readonly displayEmver = displayEmver
|
||||
|
||||
constructor(
|
||||
readonly copyService: CopyService,
|
||||
private readonly alerts: TuiAlertService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly emver: Emver,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
|
||||
|
||||
presentAlertVersions(version: TemplateRef<TuiDialogContext>) {
|
||||
this.dialogs
|
||||
.open<string>(version, {
|
||||
label: 'Versions',
|
||||
size: 's',
|
||||
data: {
|
||||
value: this.pkg.manifest.version,
|
||||
items: this.pkg.versions.sort(
|
||||
(a, b) => -1 * (this.emver.compare(a, b) || 0),
|
||||
),
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(version => this.version.emit(version))
|
||||
}
|
||||
|
||||
presentModalMd(label: string) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(MarkdownComponent), {
|
||||
label,
|
||||
size: 'l',
|
||||
data: {
|
||||
content: this.marketplaceService.fetchStatic$(
|
||||
this.pkg.manifest.id,
|
||||
label.toLowerCase(),
|
||||
this.url,
|
||||
),
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarkdownModule, ResponsiveColDirective } from '@start9labs/shared'
|
||||
|
||||
import { AdditionalComponent } from './additional.component'
|
||||
import {
|
||||
TuiRadioListModule,
|
||||
TuiStringifyContentPipeModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiButtonModule } from '@taiga-ui/core'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
MarkdownModule,
|
||||
ResponsiveColDirective,
|
||||
TuiRadioListModule,
|
||||
FormsModule,
|
||||
TuiStringifyContentPipeModule,
|
||||
TuiButtonModule,
|
||||
],
|
||||
declarations: [AdditionalComponent],
|
||||
exports: [AdditionalComponent],
|
||||
})
|
||||
export class AdditionalModule {}
|
||||
@@ -0,0 +1,35 @@
|
||||
<ion-item-divider>Dependencies</ion-item-divider>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col
|
||||
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
|
||||
responsiveCol
|
||||
sizeSm="12"
|
||||
sizeMd="6"
|
||||
>
|
||||
<ion-item [routerLink]="['/marketplace', dep.key]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img
|
||||
alt=""
|
||||
style="border-radius: 100%"
|
||||
[src]="getImg(dep.key) | trustUrl"
|
||||
/>
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{ pkg['dependency-metadata'][dep.key].title }}
|
||||
<ng-container [ngSwitch]="dep.value.requirement.type">
|
||||
<span *ngSwitchCase="'required'">(required)</span>
|
||||
<span *ngSwitchCase="'opt-out'">(required by default)</span>
|
||||
<span *ngSwitchCase="'opt-in'">(optional)</span>
|
||||
</ng-container>
|
||||
</h2>
|
||||
<p>
|
||||
<small>{{ dep.value.version | displayEmver }}</small>
|
||||
</p>
|
||||
<p>{{ dep.value.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-dependencies',
|
||||
templateUrl: 'dependencies.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DependenciesComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
getImg(key: string): string {
|
||||
// @TODO fix when registry api is updated to include mimetype in icon url
|
||||
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
ResponsiveColDirective,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { DependenciesComponent } from './dependencies.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
IonicModule,
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
ResponsiveColDirective,
|
||||
],
|
||||
declarations: [DependenciesComponent],
|
||||
exports: [DependenciesComponent],
|
||||
})
|
||||
export class DependenciesModule {}
|
||||
@@ -0,0 +1,9 @@
|
||||
<img class="logo" alt="" [src]="pkg | mimeType | trustUrl" />
|
||||
<div class="text">
|
||||
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
|
||||
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
|
||||
<p *ngIf="pkg['published-at'] as published" class="published">
|
||||
Released: {{ published | date : 'medium' }}
|
||||
</p>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 80px;
|
||||
margin-right: 16px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 0 -2px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.published {
|
||||
margin: 0;
|
||||
padding: 4px 0 12px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.logo {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-package',
|
||||
templateUrl: 'package.component.html',
|
||||
styleUrls: ['package.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PackageComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
SharedPipesModule,
|
||||
TickerModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { PackageComponent } from './package.component'
|
||||
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [PackageComponent],
|
||||
exports: [PackageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
TickerModule,
|
||||
MimeTypePipeModule,
|
||||
],
|
||||
})
|
||||
export class PackageModule {}
|
||||
85
web/projects/marketplace/src/pipes/filter-packages.pipe.ts
Normal file
85
web/projects/marketplace/src/pipes/filter-packages.pipe.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
||||
import { MarketplacePkg } from '../types'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterPackages',
|
||||
})
|
||||
export class FilterPackagesPipe implements PipeTransform {
|
||||
transform(
|
||||
packages: MarketplacePkg[],
|
||||
query: string,
|
||||
category: string,
|
||||
): MarketplacePkg[] {
|
||||
// query
|
||||
if (query) {
|
||||
let options: Fuse.IFuseOptions<MarketplacePkg> = {
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
}
|
||||
|
||||
if (query.length < 4) {
|
||||
options = {
|
||||
...options,
|
||||
threshold: 0.2,
|
||||
location: 0,
|
||||
distance: 16,
|
||||
keys: [
|
||||
{
|
||||
name: 'manifest.title',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
name: 'manifest.id',
|
||||
weight: 0.5,
|
||||
},
|
||||
],
|
||||
}
|
||||
} else {
|
||||
options = {
|
||||
...options,
|
||||
ignoreLocation: true,
|
||||
useExtendedSearch: true,
|
||||
keys: [
|
||||
{
|
||||
name: 'manifest.title',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
name: 'manifest.id',
|
||||
weight: 0.5,
|
||||
},
|
||||
{
|
||||
name: 'manifest.description.short',
|
||||
weight: 0.4,
|
||||
},
|
||||
{
|
||||
name: 'manifest.description.long',
|
||||
weight: 0.1,
|
||||
},
|
||||
],
|
||||
}
|
||||
query = `'${query}`
|
||||
}
|
||||
|
||||
const fuse = new Fuse(packages, options)
|
||||
return fuse.search(query).map(p => p.item)
|
||||
}
|
||||
|
||||
// category
|
||||
return packages
|
||||
.filter(p => category === 'all' || p.categories.includes(category))
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
new Date(b['published-at']).valueOf() -
|
||||
new Date(a['published-at']).valueOf()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FilterPackagesPipe],
|
||||
exports: [FilterPackagesPipe],
|
||||
})
|
||||
export class FilterPackagesPipeModule {}
|
||||
34
web/projects/marketplace/src/pipes/mime-type.pipe.ts
Normal file
34
web/projects/marketplace/src/pipes/mime-type.pipe.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
||||
import { MarketplacePkg } from '../types'
|
||||
|
||||
@Pipe({
|
||||
name: 'mimeType',
|
||||
})
|
||||
export class MimeTypePipe implements PipeTransform {
|
||||
transform(pkg: MarketplacePkg): string {
|
||||
if (pkg.icon.startsWith('data:')) return pkg.icon
|
||||
|
||||
if (pkg.manifest.assets.icon) {
|
||||
switch (pkg.manifest.assets.icon.split('.').pop()) {
|
||||
case 'png':
|
||||
return `data:image/png;base64,${pkg.icon}`
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
return `data:image/jpeg;base64,${pkg.icon}`
|
||||
case 'gif':
|
||||
return `data:image/gif;base64,${pkg.icon}`
|
||||
case 'svg':
|
||||
return `data:image/svg+xml;base64,${pkg.icon}`
|
||||
default:
|
||||
return `data:image/png;base64,${pkg.icon}`
|
||||
}
|
||||
}
|
||||
return `data:image/png;base64,${pkg.icon}`
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MimeTypePipe],
|
||||
exports: [MimeTypePipe],
|
||||
})
|
||||
export class MimeTypePipeModule {}
|
||||
33
web/projects/marketplace/src/public-api.ts
Normal file
33
web/projects/marketplace/src/public-api.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Public API Surface of @start9labs/marketplace
|
||||
*/
|
||||
|
||||
export * from './pages/list/categories/categories.component'
|
||||
export * from './pages/list/categories/categories.module'
|
||||
export * from './pages/list/item/item.component'
|
||||
export * from './pages/list/item/item.module'
|
||||
export * from './pages/list/search/search.component'
|
||||
export * from './pages/list/search/search.module'
|
||||
export * from './pages/list/skeleton/skeleton.component'
|
||||
export * from './pages/list/skeleton/skeleton.module'
|
||||
export * from './pages/release-notes/release-notes.component'
|
||||
export * from './pages/release-notes/release-notes.module'
|
||||
export * from './pages/show/about/about.component'
|
||||
export * from './pages/show/about/about.module'
|
||||
export * from './pages/show/additional/additional.component'
|
||||
export * from './pages/show/additional/additional.module'
|
||||
export * from './pages/show/dependencies/dependencies.component'
|
||||
export * from './pages/show/dependencies/dependencies.module'
|
||||
export * from './pages/show/package/package.component'
|
||||
export * from './pages/show/package/package.module'
|
||||
|
||||
export * from './pipes/filter-packages.pipe'
|
||||
export * from './pipes/mime-type.pipe'
|
||||
|
||||
export * from './components/store-icon/store-icon.component'
|
||||
export * from './components/store-icon/store-icon.component.module'
|
||||
export * from './components/store-icon/store-icon.component'
|
||||
|
||||
export * from './services/marketplace.service'
|
||||
|
||||
export * from './types'
|
||||
29
web/projects/marketplace/src/services/marketplace.service.ts
Normal file
29
web/projects/marketplace/src/services/marketplace.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { MarketplacePkg, Marketplace, StoreData, StoreIdentity } from '../types'
|
||||
|
||||
export abstract class AbstractMarketplaceService {
|
||||
abstract getKnownHosts$(): Observable<StoreIdentity[]>
|
||||
|
||||
abstract getSelectedHost$(): Observable<StoreIdentity>
|
||||
|
||||
abstract getMarketplace$(): Observable<Marketplace>
|
||||
|
||||
abstract getSelectedStore$(): Observable<StoreData>
|
||||
|
||||
abstract getPackage$(
|
||||
id: string,
|
||||
version: string,
|
||||
url?: string,
|
||||
): Observable<MarketplacePkg> // could be {} so need to check in show page
|
||||
|
||||
abstract fetchReleaseNotes$(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>>
|
||||
|
||||
abstract fetchStatic$(
|
||||
id: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
): Observable<string>
|
||||
}
|
||||
87
web/projects/marketplace/src/types.ts
Normal file
87
web/projects/marketplace/src/types.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
|
||||
export type StoreURL = string
|
||||
export type StoreName = string
|
||||
|
||||
export interface StoreIdentity {
|
||||
url: StoreURL
|
||||
name?: StoreName
|
||||
}
|
||||
export type Marketplace = Record<StoreURL, StoreData | null>
|
||||
|
||||
export interface StoreData {
|
||||
info: StoreInfo
|
||||
packages: MarketplacePkg[]
|
||||
}
|
||||
|
||||
export interface StoreInfo {
|
||||
name: StoreName
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export interface MarketplacePkg {
|
||||
icon: Url
|
||||
license: Url
|
||||
instructions: Url
|
||||
manifest: Manifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: DependencyMetadata
|
||||
}
|
||||
'published-at': string
|
||||
}
|
||||
|
||||
export interface DependencyMetadata {
|
||||
title: string
|
||||
icon: Url
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export interface Manifest {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
'git-hash'?: string
|
||||
description: {
|
||||
short: string
|
||||
long: string
|
||||
}
|
||||
assets: {
|
||||
icon: Url // filename
|
||||
}
|
||||
replaces?: string[]
|
||||
'release-notes': string
|
||||
license: string // name of license
|
||||
'wrapper-repo': Url
|
||||
'upstream-repo': Url
|
||||
'support-site': Url
|
||||
'marketing-site': Url
|
||||
'donation-url': Url | null
|
||||
alerts: {
|
||||
install: string | null
|
||||
uninstall: string | null
|
||||
restore: string | null
|
||||
start: string | null
|
||||
stop: string | null
|
||||
}
|
||||
dependencies: Record<string, Dependency>
|
||||
'os-version': string
|
||||
}
|
||||
|
||||
export interface Dependency {
|
||||
version: string
|
||||
requirement:
|
||||
| {
|
||||
type: 'opt-in'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'opt-out'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'required'
|
||||
}
|
||||
description: string | null
|
||||
}
|
||||
12
web/projects/marketplace/tsconfig.json
Normal file
12
web/projects/marketplace/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true
|
||||
},
|
||||
"exclude": ["src/test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
56
web/projects/setup-wizard/src/app/app-routing.module.ts
Normal file
56
web/projects/setup-wizard/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'attach',
|
||||
loadChildren: () =>
|
||||
import('./pages/attach/attach.module').then(m => m.AttachPageModule),
|
||||
},
|
||||
{
|
||||
path: 'recover',
|
||||
loadChildren: () =>
|
||||
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
|
||||
},
|
||||
{
|
||||
path: 'transfer',
|
||||
loadChildren: () =>
|
||||
import('./pages/transfer/transfer.module').then(
|
||||
m => m.TransferPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'storage',
|
||||
loadChildren: () =>
|
||||
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
|
||||
},
|
||||
{
|
||||
path: 'loading',
|
||||
loadChildren: () =>
|
||||
import('./pages/loading/loading.module').then(m => m.LoadingPageModule),
|
||||
},
|
||||
{
|
||||
path: 'success',
|
||||
loadChildren: () =>
|
||||
import('./pages/success/success.module').then(m => m.SuccessPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
useHash: true,
|
||||
initialNavigation: 'disabled',
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
6
web/projects/setup-wizard/src/app/app.component.html
Normal file
6
web/projects/setup-wizard/src/app/app.component.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<tui-root tuiMode="onDark">
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
8
web/projects/setup-wizard/src/app/app.component.scss
Normal file
8
web/projects/setup-wizard/src/app/app.component.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
32
web/projects/setup-wizard/src/app/app.component.ts
Normal file
32
web/projects/setup-wizard/src/app/app.component.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly navCtrl: NavController,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const inProgress = await this.apiService.getSetupStatus()
|
||||
|
||||
let route = '/home'
|
||||
if (inProgress) {
|
||||
route = inProgress.complete ? '/success' : '/loading'
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(route)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
79
web/projects/setup-wizard/src/app/app.module.ts
Normal file
79
web/projects/setup-wizard/src/app/app.module.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import {
|
||||
TuiAlertModule,
|
||||
tuiButtonOptionsProvider,
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiRootModule,
|
||||
TuiThemeNightModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { MockApiService } from './services/api/mock-api.service'
|
||||
import { LiveApiService } from './services/api/live-api.service'
|
||||
import {
|
||||
IonicModule,
|
||||
IonicRouteStrategy,
|
||||
iosTransitionAnimation,
|
||||
} from '@ionic/angular'
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { SuccessPageModule } from './pages/success/success.module'
|
||||
import { HomePageModule } from './pages/home/home.module'
|
||||
import { LoadingPageModule } from './pages/loading/loading.module'
|
||||
import { RecoverPageModule } from './pages/recover/recover.module'
|
||||
import { TransferPageModule } from './pages/transfer/transfer.module'
|
||||
import {
|
||||
LoadingModule,
|
||||
provideSetupLogsService,
|
||||
provideSetupService,
|
||||
RELATIVE_URL,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
IonicModule.forRoot({
|
||||
mode: 'md',
|
||||
navAnimation: iosTransitionAnimation,
|
||||
}),
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
SuccessPageModule,
|
||||
HomePageModule,
|
||||
LoadingPageModule,
|
||||
RecoverPageModule,
|
||||
TransferPageModule,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
TuiAlertModule,
|
||||
LoadingModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
],
|
||||
providers: [
|
||||
provideSetupService(ApiService),
|
||||
provideSetupLogsService(ApiService),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiFieldErrorPipeModule,
|
||||
TuiInputModule,
|
||||
TuiInputPasswordModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { CifsModal } from './cifs-modal.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [CifsModal],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiButtonModule,
|
||||
TuiInputModule,
|
||||
TuiErrorModule,
|
||||
ReactiveFormsModule,
|
||||
TuiFieldErrorPipeModule,
|
||||
TuiInputPasswordModule,
|
||||
],
|
||||
exports: [CifsModal],
|
||||
})
|
||||
export class CifsModalModule {}
|
||||
@@ -0,0 +1,39 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<tui-input formControlName="hostname">
|
||||
Hostname
|
||||
<input tuiTextfield placeholder="'My Computer' OR 'my-computer.local'" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="hostname"
|
||||
[error]="['required'] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
|
||||
<tui-input formControlName="path" class="input">
|
||||
Path
|
||||
<input tuiTextfield placeholder="/Desktop/my-folder'" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="path"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
|
||||
<tui-input formControlName="username" class="input">
|
||||
Username
|
||||
<input tuiTextfield placeholder="Enter username" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
formControlName="username"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
></tui-error>
|
||||
|
||||
<tui-input-password formControlName="password" class="input">
|
||||
Password
|
||||
</tui-input-password>
|
||||
|
||||
<footer class="modal-buttons">
|
||||
<button tuiButton appearance="secondary" type="button" (click)="cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
<button tuiButton [disabled]="form.invalid">Verify</button>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -0,0 +1,3 @@
|
||||
.input {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms'
|
||||
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
|
||||
import { LoadingService, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
ApiService,
|
||||
CifsBackupTarget,
|
||||
CifsRecoverySource,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { PASSWORD } from '../password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'cifs-modal',
|
||||
templateUrl: 'cifs-modal.page.html',
|
||||
styleUrls: ['cifs-modal.page.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
useValue: {
|
||||
required: 'This field is required',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CifsModal {
|
||||
readonly form = new FormGroup({
|
||||
hostname: new FormControl('', {
|
||||
validators: [
|
||||
Validators.required,
|
||||
Validators.pattern(/^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$/),
|
||||
],
|
||||
nonNullable: true,
|
||||
}),
|
||||
path: new FormControl('', {
|
||||
validators: [Validators.required],
|
||||
nonNullable: true,
|
||||
}),
|
||||
username: new FormControl('', {
|
||||
validators: [Validators.required],
|
||||
nonNullable: true,
|
||||
}),
|
||||
password: new FormControl(),
|
||||
})
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<{
|
||||
cifs: CifsRecoverySource
|
||||
recoveryPassword: string
|
||||
}>,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loader: LoadingService,
|
||||
) {}
|
||||
|
||||
cancel() {
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
const loader = this.loader
|
||||
.open('Connecting to shared folder...')
|
||||
.subscribe()
|
||||
|
||||
try {
|
||||
const diskInfo = await this.api.verifyCifs({
|
||||
...this.form.getRawValue(),
|
||||
type: 'cifs',
|
||||
password: this.form.value.password
|
||||
? await this.api.encrypt(String(this.form.value.password))
|
||||
: null,
|
||||
})
|
||||
|
||||
loader.unsubscribe()
|
||||
|
||||
this.presentModalPassword(diskInfo)
|
||||
} catch (e) {
|
||||
loader.unsubscribe()
|
||||
this.presentAlertFailed()
|
||||
}
|
||||
}
|
||||
|
||||
private presentModalPassword(diskInfo: StartOSDiskInfo) {
|
||||
const target: CifsBackupTarget = {
|
||||
...this.form.getRawValue(),
|
||||
mountable: true,
|
||||
'embassy-os': diskInfo,
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Unlock Drive',
|
||||
size: 's',
|
||||
data: { target },
|
||||
})
|
||||
.subscribe(recoveryPassword => {
|
||||
this.context.completeWith({
|
||||
cifs: { ...this.form.getRawValue(), type: 'cifs' },
|
||||
recoveryPassword,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private presentAlertFailed() {
|
||||
this.dialogs
|
||||
.open(
|
||||
'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.',
|
||||
{
|
||||
label: 'Connection Failed',
|
||||
size: 's',
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core'
|
||||
import { TuiInputPasswordModule } from '@taiga-ui/kit'
|
||||
import { PasswordPage } from './password.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [PasswordPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiButtonModule,
|
||||
TuiInputPasswordModule,
|
||||
TuiErrorModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
exports: [PasswordPage],
|
||||
})
|
||||
export class PasswordPageModule {}
|
||||
@@ -0,0 +1,35 @@
|
||||
<p *ngIf="!storageDrive else choose">
|
||||
Enter the password that was used to encrypt this drive.
|
||||
</p>
|
||||
<ng-template #choose>
|
||||
<p>
|
||||
Choose a password for your server.
|
||||
<i>Make it good. Write it down.</i>
|
||||
</p>
|
||||
</ng-template>
|
||||
|
||||
<form (ngSubmit)="storageDrive ? submitPw() : verifyPw()">
|
||||
<tui-input-password [formControl]="password">
|
||||
Enter Password
|
||||
<input tuiTextfield maxlength="64" />
|
||||
</tui-input-password>
|
||||
<tui-error [error]="passwordError"></tui-error>
|
||||
<ng-container *ngIf="storageDrive">
|
||||
<tui-input-password style="margin-top: 16px" [formControl]="confirm">
|
||||
Retype Password
|
||||
<input tuiTextfield maxlength="64" />
|
||||
</tui-input-password>
|
||||
<tui-error [error]="confirmError"></tui-error>
|
||||
</ng-container>
|
||||
<footer class="modal-buttons">
|
||||
<button tuiButton appearance="secondary" type="button" (click)="cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!password.value || !!confirmError || !!passwordError"
|
||||
>
|
||||
{{ storageDrive ? 'Finish' : 'Unlock' }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import {
|
||||
PolymorpheusComponent,
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.service'
|
||||
|
||||
interface DialogData {
|
||||
target?: CifsBackupTarget | DiskBackupTarget
|
||||
storageDrive?: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-password',
|
||||
templateUrl: 'password.page.html',
|
||||
})
|
||||
export class PasswordPage {
|
||||
readonly target = this.context.data.target
|
||||
readonly storageDrive = this.context.data.storageDrive
|
||||
readonly password = new FormControl('', { nonNullable: true })
|
||||
readonly confirm = new FormControl('', { nonNullable: true })
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<string, DialogData>,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
|
||||
get passwordError(): string | null {
|
||||
if (!this.password.touched || this.target) return null
|
||||
|
||||
if (!this.storageDrive && !this.target?.['embassy-os'])
|
||||
return 'No recovery target' // unreachable
|
||||
|
||||
if (this.password.value.length < 12)
|
||||
return 'Must be 12 characters or greater'
|
||||
|
||||
if (this.password.value.length > 64)
|
||||
return 'Must be less than 65 characters'
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
get confirmError(): string | null {
|
||||
return this.confirm.touched && this.password.value !== this.confirm.value
|
||||
? 'Passwords do not match'
|
||||
: null
|
||||
}
|
||||
|
||||
verifyPw() {
|
||||
try {
|
||||
const passwordHash = this.target!['embassy-os']?.['password-hash'] || ''
|
||||
|
||||
argon2.verify(passwordHash, this.password.value)
|
||||
this.context.completeWith(this.password.value)
|
||||
} catch (e) {
|
||||
this.errorService.handleError('Incorrect password provided')
|
||||
}
|
||||
}
|
||||
|
||||
submitPw() {
|
||||
this.context.completeWith(this.password.value)
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
}
|
||||
|
||||
export const PASSWORD = new PolymorpheusComponent(PasswordPage)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { AttachPage } from './attach.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AttachPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AttachPageRoutingModule {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
GuidPipePipesModule,
|
||||
UnitConversionPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { AttachPage } from './attach.page'
|
||||
import { AttachPageRoutingModule } from './attach-routing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AttachPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
AttachPageRoutingModule,
|
||||
UnitConversionPipesModule,
|
||||
GuidPipePipesModule,
|
||||
],
|
||||
})
|
||||
export class AttachPageModule {}
|
||||
@@ -0,0 +1,67 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col>
|
||||
<ion-card color="dark">
|
||||
<ion-card-header class="ion-text-center">
|
||||
<ion-card-title>Use existing drive</ion-card-title>
|
||||
<div class="center-wrapper">
|
||||
<ion-card-subtitle>
|
||||
Select the physical drive containing your StartOS data
|
||||
</ion-card-subtitle>
|
||||
</div>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<ion-spinner
|
||||
*ngIf="loading"
|
||||
class="center-spinner"
|
||||
name="lines"
|
||||
></ion-spinner>
|
||||
|
||||
<!-- loaded -->
|
||||
<ion-item-group *ngIf="!loading" class="ion-text-center">
|
||||
<!-- drives -->
|
||||
<p *ngIf="!drives.length">
|
||||
No valid StartOS data drives found. Please make sure the drive
|
||||
is a valid StartOS data drive (not a backup) and is firmly
|
||||
connected, then refresh the page.
|
||||
</p>
|
||||
|
||||
<ng-container *ngFor="let drive of drives">
|
||||
<ion-item
|
||||
*ngIf="drive | guid as guid"
|
||||
button
|
||||
(click)="select(guid)"
|
||||
lines="none"
|
||||
>
|
||||
<ion-icon
|
||||
slot="start"
|
||||
name="save-outline"
|
||||
size="large"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ drive.logicalname }}</h1>
|
||||
<p>
|
||||
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model ||
|
||||
'Unknown Model' }}
|
||||
</p>
|
||||
<p>Capacity: {{ drive.capacity | convertBytes }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-button
|
||||
class="ion-margin-top"
|
||||
color="primary"
|
||||
(click)="refresh()"
|
||||
>
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ion-item-group>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { DiskInfo, ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PASSWORD, PasswordPage } from 'src/app/modals/password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-attach',
|
||||
templateUrl: 'attach.page.html',
|
||||
styleUrls: ['attach.page.scss'],
|
||||
})
|
||||
export class AttachPage {
|
||||
loading = true
|
||||
drives: DiskInfo[] = []
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly stateService: StateService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.stateService.setupType = 'attach'
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
try {
|
||||
this.drives = await this.apiService.getDrives()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
select(guid: string) {
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Set Password',
|
||||
size: 's',
|
||||
data: { storageDrive: true },
|
||||
})
|
||||
.subscribe(password => {
|
||||
this.attachDrive(guid, password)
|
||||
})
|
||||
}
|
||||
|
||||
private async attachDrive(guid: string, password: string) {
|
||||
const loader = this.loader.open('Connecting to drive...').subscribe()
|
||||
|
||||
try {
|
||||
await this.stateService.importDrive(guid, password)
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { EmbassyPage } from './embassy.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: EmbassyPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class EmbassyPageRoutingModule { }
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import {
|
||||
GuidPipePipesModule,
|
||||
UnitConversionPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { EmbassyPage } from './embassy.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { EmbassyPageRoutingModule } from './embassy-routing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
EmbassyPageRoutingModule,
|
||||
PasswordPageModule,
|
||||
UnitConversionPipesModule,
|
||||
GuidPipePipesModule,
|
||||
],
|
||||
declarations: [EmbassyPage],
|
||||
})
|
||||
export class EmbassyPageModule {}
|
||||
@@ -0,0 +1,87 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-card color="dark">
|
||||
<ion-card-header
|
||||
class="ion-text-center"
|
||||
style="padding-bottom: 8px"
|
||||
*ngIf="loading || storageDrives.length; else empty"
|
||||
>
|
||||
<ion-card-title>Select storage drive</ion-card-title>
|
||||
<div class="center-wrapper">
|
||||
<ion-card-subtitle>
|
||||
This is the drive where your StartOS data will be stored.
|
||||
</ion-card-subtitle>
|
||||
</div>
|
||||
</ion-card-header>
|
||||
<ng-template #empty>
|
||||
<ion-card-header
|
||||
class="ion-text-center"
|
||||
style="padding-bottom: 8px"
|
||||
>
|
||||
<ion-card-title>No drives found</ion-card-title>
|
||||
<div class="center-wrapper">
|
||||
<ion-card-subtitle>
|
||||
Please connect a storage drive to your server. Then click
|
||||
"Refresh".
|
||||
</ion-card-subtitle>
|
||||
</div>
|
||||
</ion-card-header>
|
||||
</ng-template>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<!-- loading -->
|
||||
<ion-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
class="center-spinner"
|
||||
name="lines-sharp"
|
||||
></ion-spinner>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-template #loaded>
|
||||
<ion-item-group
|
||||
*ngIf="storageDrives.length"
|
||||
class="ion-padding-bottom"
|
||||
>
|
||||
<ion-item
|
||||
(click)="chooseDrive(drive)"
|
||||
class="ion-margin-bottom"
|
||||
[disabled]="tooSmall(drive)"
|
||||
button
|
||||
lines="none"
|
||||
*ngFor="let drive of storageDrives"
|
||||
>
|
||||
<ion-icon
|
||||
slot="start"
|
||||
name="save-outline"
|
||||
size="large"
|
||||
></ion-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h1>
|
||||
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model ||
|
||||
'Unknown Model' }}
|
||||
</h1>
|
||||
<h2>
|
||||
{{ drive.logicalname }} - {{ drive.capacity | convertBytes
|
||||
}}
|
||||
</h2>
|
||||
<p *ngIf="tooSmall(drive)">
|
||||
<ion-text color="danger">
|
||||
Drive capacity too small.
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-button fill="solid" color="primary" (click)="getDrives()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
148
web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts
Normal file
148
web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import {
|
||||
DiskInfo,
|
||||
ErrorService,
|
||||
GuidPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import {
|
||||
ApiService,
|
||||
BackupRecoverySource,
|
||||
DiskRecoverySource,
|
||||
DiskMigrateSource,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PASSWORD, PasswordPage } from '../../modals/password/password.page'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { filter, of, switchMap } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-embassy',
|
||||
templateUrl: 'embassy.page.html',
|
||||
styleUrls: ['embassy.page.scss'],
|
||||
providers: [GuidPipe],
|
||||
})
|
||||
export class EmbassyPage {
|
||||
storageDrives: DiskInfo[] = []
|
||||
loading = true
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly stateService: StateService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly guidPipe: GuidPipe,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
tooSmall(drive: DiskInfo) {
|
||||
return drive.capacity < 34359738368
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
this.loading = true
|
||||
try {
|
||||
const disks = await this.apiService.getDrives()
|
||||
if (this.stateService.setupType === 'fresh') {
|
||||
this.storageDrives = disks
|
||||
} else if (this.stateService.setupType === 'restore') {
|
||||
this.storageDrives = disks.filter(
|
||||
d =>
|
||||
!d.partitions
|
||||
.map(p => p.logicalname)
|
||||
.includes(
|
||||
(
|
||||
(this.stateService.recoverySource as BackupRecoverySource)
|
||||
?.target as DiskRecoverySource
|
||||
)?.logicalname,
|
||||
),
|
||||
)
|
||||
} else if (this.stateService.setupType === 'transfer') {
|
||||
const guid = (this.stateService.recoverySource as DiskMigrateSource)
|
||||
.guid
|
||||
this.storageDrives = disks.filter(d => {
|
||||
return (
|
||||
d.guid !== guid && !d.partitions.map(p => p.guid).includes(guid)
|
||||
)
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
chooseDrive(drive: DiskInfo) {
|
||||
of(!this.guidPipe.transform(drive) && !drive.partitions.some(p => p.used))
|
||||
.pipe(
|
||||
switchMap(unused =>
|
||||
unused
|
||||
? of(true)
|
||||
: this.dialogs.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content:
|
||||
'<strong>Drive contains data!</strong><p>All data stored on this drive will be permanently deleted.</p>',
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
// for backup recoveries
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(
|
||||
drive.logicalname,
|
||||
this.stateService.recoveryPassword,
|
||||
)
|
||||
} else {
|
||||
// for migrations and fresh setups
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private presentModalPassword(logicalname: string) {
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Set Password',
|
||||
size: 's',
|
||||
data: { storageDrive: true },
|
||||
})
|
||||
.subscribe(password => {
|
||||
this.setupEmbassy(logicalname, password)
|
||||
})
|
||||
}
|
||||
|
||||
private async setupEmbassy(
|
||||
logicalname: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const loader = this.loader.open('Connecting to drive...').subscribe()
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy(logicalname, password)
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user