mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
feat(marketplace): add separate package and move some entities in it (#1283)
* feat(marketplace): add separate package and move some entities in it * feat(marketplace): refactor release notes and list * feat(marketplace): refactor showing a package * chore: fix install progress * chore: fix angular.json * chore: properly share stream
This commit is contained in:
@@ -38,8 +38,9 @@
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"styles/variables.scss",
|
||||
"styles/global.scss",
|
||||
"projects/shared/styles/variables.scss",
|
||||
"projects/shared/styles/global.scss",
|
||||
"projects/shared/styles/shared.scss",
|
||||
"projects/ui/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
@@ -157,8 +158,8 @@
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"styles/variables.scss",
|
||||
"styles/global.scss",
|
||||
"projects/shared/styles/variables.scss",
|
||||
"projects/shared/styles/global.scss",
|
||||
"projects/setup-wizard/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
@@ -276,8 +277,8 @@
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"styles/variables.scss",
|
||||
"styles/global.scss",
|
||||
"projects/shared/styles/variables.scss",
|
||||
"projects/shared/styles/global.scss",
|
||||
"projects/diagnostic-ui/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
@@ -376,6 +377,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "projects/marketplace/tsconfig.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/marketplace/tsconfig.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"projectType": "library",
|
||||
"root": "projects/shared",
|
||||
|
||||
7
frontend/projects/marketplace/ng-package.json
Normal file
7
frontend/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"
|
||||
}
|
||||
}
|
||||
13
frontend/projects/marketplace/package.json
Normal file
13
frontend/projects/marketplace/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@start9labs/marketplace",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^13.2.0",
|
||||
"@angular/core": "^13.2.0",
|
||||
"@start9labs/shared": "^0.0.1",
|
||||
"fuse.js": "^6.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import Fuse from 'fuse.js/dist/fuse.min.js'
|
||||
|
||||
import { LocalPkg } from '../types/local-pkg'
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
|
||||
const defaultOps = {
|
||||
isCaseSensitive: false,
|
||||
includeScore: true,
|
||||
shouldSort: true,
|
||||
includeMatches: false,
|
||||
findAllMatches: false,
|
||||
minMatchCharLength: 1,
|
||||
location: 0,
|
||||
threshold: 0.6,
|
||||
distance: 100,
|
||||
useExtendedSearch: false,
|
||||
ignoreLocation: false,
|
||||
ignoreFieldNorm: false,
|
||||
keys: [
|
||||
'manifest.id',
|
||||
'manifest.title',
|
||||
'manifest.description.short',
|
||||
'manifest.description.long',
|
||||
],
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'filterPackages',
|
||||
})
|
||||
export class FilterPackagesPipe implements PipeTransform {
|
||||
transform(
|
||||
packages: MarketplacePkg[] | null,
|
||||
local: Record<string, LocalPkg>,
|
||||
query: string,
|
||||
category: string,
|
||||
): MarketplacePkg[] | null {
|
||||
if (!packages) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const fuse = new Fuse(packages, defaultOps)
|
||||
|
||||
return fuse.search(query).map(p => p.item)
|
||||
}
|
||||
|
||||
if (category === 'updates') {
|
||||
return packages.filter(
|
||||
({ manifest }) =>
|
||||
local[manifest.id] &&
|
||||
manifest.version !== local[manifest.id].manifest.version,
|
||||
)
|
||||
}
|
||||
|
||||
const pkgsToSort = packages.filter(
|
||||
p => category === 'all' || p.categories.includes(category),
|
||||
)
|
||||
const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 })
|
||||
|
||||
return fuse
|
||||
.search(category !== 'all' ? category || '' : 'bit')
|
||||
.map(p => p.item)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { InstallProgressPipe } from './install-progress.pipe'
|
||||
import { TrustPipe } from './trust.pipe'
|
||||
import { FilterPackagesPipe } from './filter-packages.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [InstallProgressPipe, TrustPipe],
|
||||
exports: [InstallProgressPipe, TrustPipe],
|
||||
declarations: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
|
||||
exports: [InstallProgressPipe, TrustPipe, FilterPackagesPipe],
|
||||
})
|
||||
export class MarketplacePipesModule {}
|
||||
17
frontend/projects/marketplace/src/public-api.ts
Normal file
17
frontend/projects/marketplace/src/public-api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Public API Surface of @start9labs/marketplace
|
||||
*/
|
||||
|
||||
export * from './pipes/install-progress.pipe'
|
||||
export * from './pipes/trust.pipe'
|
||||
export * from './pipes/marketplace-pipes.module'
|
||||
|
||||
export * from './services/marketplace.service'
|
||||
|
||||
export * from './types/local-pkg'
|
||||
export * from './types/marketplace'
|
||||
export * from './types/marketplace-data'
|
||||
export * from './types/marketplace-manifest'
|
||||
export * from './types/marketplace-pkg'
|
||||
|
||||
export * from './utils/spread-progress'
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
import { Marketplace } from '../types/marketplace'
|
||||
|
||||
export abstract class AbstractMarketplaceService {
|
||||
abstract install(id: string, version?: string): Observable<unknown>
|
||||
|
||||
abstract getMarketplace(): Observable<Marketplace>
|
||||
|
||||
abstract getReleaseNotes(id: string): Observable<Record<string, string>>
|
||||
|
||||
abstract getCategories(): Observable<string[]>
|
||||
|
||||
abstract getPackages(): Observable<MarketplacePkg[]>
|
||||
|
||||
abstract getPackage(id: string, version: string): Observable<MarketplacePkg>
|
||||
}
|
||||
8
frontend/projects/marketplace/src/types/local-pkg.ts
Normal file
8
frontend/projects/marketplace/src/types/local-pkg.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { PackageState } from '@start9labs/shared'
|
||||
|
||||
import { MarketplaceManifest } from './marketplace-manifest'
|
||||
|
||||
export interface LocalPkg {
|
||||
state: PackageState
|
||||
manifest: MarketplaceManifest
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface MarketplaceData {
|
||||
categories: string[]
|
||||
name: string
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
|
||||
export interface MarketplaceManifest {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
description: {
|
||||
short: string
|
||||
long: string
|
||||
}
|
||||
'release-notes': string
|
||||
license: string // name
|
||||
'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
|
||||
}
|
||||
}
|
||||
17
frontend/projects/marketplace/src/types/marketplace-pkg.ts
Normal file
17
frontend/projects/marketplace/src/types/marketplace-pkg.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
import { MarketplaceManifest } from './marketplace-manifest'
|
||||
|
||||
export interface MarketplacePkg {
|
||||
icon: Url
|
||||
license: Url
|
||||
instructions: Url
|
||||
manifest: MarketplaceManifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: {
|
||||
title: string
|
||||
icon: Url
|
||||
}
|
||||
}
|
||||
}
|
||||
4
frontend/projects/marketplace/src/types/marketplace.ts
Normal file
4
frontend/projects/marketplace/src/types/marketplace.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Marketplace {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { LocalPkg } from '../types/local-pkg'
|
||||
|
||||
export function spreadProgress(pkg: LocalPkg) {
|
||||
pkg['install-progress'] = { ...pkg['install-progress'] }
|
||||
}
|
||||
13
frontend/projects/marketplace/tsconfig.json
Normal file
13
frontend/projects/marketplace/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
/* 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,
|
||||
"types": []
|
||||
},
|
||||
"exclude": ["src/test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
10
frontend/projects/marketplace/tsconfig.prod.json
Normal file
10
frontend/projects/marketplace/tsconfig.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { ErrorToastService } from './services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from './services/state.service'
|
||||
|
||||
@Component({
|
||||
@@ -30,7 +30,7 @@ export class AppComponent {
|
||||
await this.navCtrl.navigateForward(`/recover`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorToastService.present(e.message)
|
||||
this.errorToastService.present(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DiskInfo,
|
||||
DiskRecoverySource,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
|
||||
@@ -74,7 +74,7 @@ export class EmbassyPage {
|
||||
await alert.present()
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorToastService.present(e.message)
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -142,9 +142,9 @@ export class EmbassyPage {
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorToastService.present(
|
||||
`${e.message}\n\nRestart Embassy to try again.`,
|
||||
)
|
||||
this.errorToastService.present({
|
||||
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||
})
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@ionic/angular'
|
||||
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
|
||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||
@@ -120,7 +120,7 @@ export class RecoverPage {
|
||||
this.hasShownGuidAlert = true
|
||||
}
|
||||
} catch (e) {
|
||||
this.errorToastService.present(e.message)
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export class RecoverPage {
|
||||
await this.stateService.importDrive(guid)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
} catch (e) {
|
||||
this.errorToastService.present(e.message)
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorToastService {
|
||||
private toast: HTMLIonToastElement
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
) { }
|
||||
|
||||
async present (message: string): Promise<void> {
|
||||
if (this.toast) return
|
||||
|
||||
this.toast = await this.toastCtrl.create({
|
||||
header: 'Error',
|
||||
message,
|
||||
duration: 0,
|
||||
position: 'top',
|
||||
cssClass: 'error-toast',
|
||||
animated: true,
|
||||
buttons: [
|
||||
{
|
||||
side: 'end',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
this.dismiss()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await this.toast.present()
|
||||
}
|
||||
|
||||
async dismiss (): Promise<void> {
|
||||
if (this.toast) {
|
||||
await this.toast.dismiss()
|
||||
this.toast = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
CifsRecoverySource,
|
||||
DiskRecoverySource,
|
||||
} from './api/api.service'
|
||||
import { ErrorToastService } from './error-toast.service'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -51,9 +50,9 @@ export class StateService {
|
||||
try {
|
||||
progress = await this.apiService.getRecoveryStatus()
|
||||
} catch (e) {
|
||||
this.errorToastService.present(
|
||||
`${e.message}\n\nRestart Embassy to try again.`,
|
||||
)
|
||||
this.errorToastService.present({
|
||||
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||
})
|
||||
}
|
||||
if (progress) {
|
||||
this.dataTransferProgress = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/shared",
|
||||
"assets": ["styles"],
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^13.2.0",
|
||||
"@angular/core": "^13.2.0",
|
||||
"@ionic/angular": "^6.0.3",
|
||||
"@start9labs/emver": "^0.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Directive, ElementRef, Inject } from '@angular/core'
|
||||
|
||||
@Directive({
|
||||
selector: '[elementRef]',
|
||||
exportAs: 'elementRef',
|
||||
})
|
||||
export class ElementDirective<T extends Element> extends ElementRef<T> {
|
||||
constructor(@Inject(ElementRef) { nativeElement }: ElementRef<T>) {
|
||||
super(nativeElement)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { ElementDirective } from './element.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ElementDirective],
|
||||
exports: [ElementDirective],
|
||||
})
|
||||
export class ElementModule {}
|
||||
@@ -5,6 +5,9 @@
|
||||
export * from './components/text-spinner/text-spinner.component.module'
|
||||
export * from './components/text-spinner/text-spinner.component'
|
||||
|
||||
export * from './directives/element/element.directive'
|
||||
export * from './directives/element/element.module'
|
||||
|
||||
export * from './pipes/emver/emver.module'
|
||||
export * from './pipes/emver/emver.pipe'
|
||||
export * from './pipes/markdown/markdown.module'
|
||||
@@ -17,11 +20,13 @@ export * from './pipes/unit-conversion/unit-conversion.pipe'
|
||||
|
||||
export * from './services/destroy.service'
|
||||
export * from './services/emver.service'
|
||||
export * from './services/error-toast.service'
|
||||
|
||||
export * from './types/dependent-info'
|
||||
export * from './types/install-progress'
|
||||
export * from './types/package-state'
|
||||
export * from './types/progress-data'
|
||||
export * from './types/url'
|
||||
export * from './types/workspace-config'
|
||||
|
||||
export * from './util/misc.util'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { IonicSafeString, ToastController } from '@ionic/angular'
|
||||
import { RequestError } from './http.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -10,7 +9,7 @@ export class ErrorToastService {
|
||||
|
||||
constructor(private readonly toastCtrl: ToastController) {}
|
||||
|
||||
async present(e: RequestError, link?: string): Promise<void> {
|
||||
async present(e: { message: string }, link?: string): Promise<void> {
|
||||
console.error(e)
|
||||
|
||||
if (this.toast) return
|
||||
@@ -43,18 +42,16 @@ export class ErrorToastService {
|
||||
}
|
||||
|
||||
export function getErrorMessage(
|
||||
e: RequestError,
|
||||
{ message }: { message: string },
|
||||
link?: string,
|
||||
): string | IonicSafeString {
|
||||
let message: string | IonicSafeString = e.message
|
||||
|
||||
if (!message) {
|
||||
message = 'Unknown Error.'
|
||||
link = 'https://start9.com/latest/support/FAQ'
|
||||
}
|
||||
|
||||
if (link) {
|
||||
message = new IonicSafeString(
|
||||
return new IonicSafeString(
|
||||
`${message}<br /><br /><a href=${link} target="_blank" rel="noreferrer" style="color: white;">Get Help</a>`,
|
||||
)
|
||||
}
|
||||
1
frontend/projects/shared/src/types/url.ts
Normal file
1
frontend/projects/shared/src/types/url.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Url = string
|
||||
@@ -56,8 +56,7 @@ export function isObject(val: any): boolean {
|
||||
}
|
||||
|
||||
export function isEmptyObject(obj: object): boolean {
|
||||
if (obj === undefined) return true
|
||||
return !Object.keys(obj).length
|
||||
return obj === undefined || !Object.keys(obj).length
|
||||
}
|
||||
|
||||
export function pauseFor(ms: number): Promise<void> {
|
||||
|
||||
@@ -24,53 +24,3 @@
|
||||
@import "~@ionic/angular/css/text-alignment.css";
|
||||
@import "~@ionic/angular/css/text-transformation.css";
|
||||
@import "~@ionic/angular/css/flex-utils.css";
|
||||
|
||||
ion-input {
|
||||
caret-color: gray;
|
||||
}
|
||||
|
||||
.item-interactive {
|
||||
--highlight-background: var(--ion-color-light) !important;
|
||||
}
|
||||
|
||||
ion-modal::part(content) {
|
||||
position: absolute;
|
||||
height: 90% !important;
|
||||
top: 5%;
|
||||
width: 90% !important;
|
||||
left: 5%;
|
||||
display: block;
|
||||
|
||||
border-radius: 6px;
|
||||
border: 2px solid rgba(255,255,255,.03);
|
||||
box-shadow: 0 0 70px 70px black;
|
||||
}
|
||||
|
||||
@media (min-width:1000px) {
|
||||
ion-modal::part(content) {
|
||||
position: absolute;
|
||||
height: 80% !important;
|
||||
top: 10%;
|
||||
width: 50% !important;
|
||||
left: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.alertlike-modal {
|
||||
&::part(content) {
|
||||
max-height: 380px !important;
|
||||
top: 25% !important;
|
||||
width: 90% !important;
|
||||
left: 5% !important;
|
||||
--box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1000px) {
|
||||
.alertlike-modal {
|
||||
&::part(content) {
|
||||
width: 60% !important;
|
||||
left: 20% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
85
frontend/projects/shared/styles/shared.scss
Normal file
85
frontend/projects/shared/styles/shared.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
ion-input {
|
||||
caret-color: gray;
|
||||
}
|
||||
|
||||
ion-modal::part(content) {
|
||||
position: absolute;
|
||||
height: 90% !important;
|
||||
top: 5%;
|
||||
width: 90% !important;
|
||||
left: 5%;
|
||||
display: block;
|
||||
|
||||
border-radius: 6px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.03);
|
||||
box-shadow: 0 0 70px 70px black;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
ion-modal::part(content) {
|
||||
position: absolute;
|
||||
height: 80% !important;
|
||||
top: 10%;
|
||||
width: 50% !important;
|
||||
left: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.alertlike-modal {
|
||||
&::part(content) {
|
||||
max-height: 380px !important;
|
||||
top: 25% !important;
|
||||
width: 90% !important;
|
||||
left: 5% !important;
|
||||
--box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.alertlike-modal {
|
||||
&::part(content) {
|
||||
width: 60% !important;
|
||||
left: 20% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-interactive {
|
||||
--highlight-background: var(--ion-color-light) !important;
|
||||
}
|
||||
|
||||
.hidden-scrollbar {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
height: 60px;
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.divider {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--ion-color-light) 0,
|
||||
var(--ion-color-dark) 50%,
|
||||
var(--ion-color-light) 100%
|
||||
);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.loading-dots:after {
|
||||
content: '...';
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
animation: ellipsis-dot 1s infinite 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
width: 1em;
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
import { Component, HostListener, NgZone } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { AuthService, AuthState } from './services/auth.service'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { Component, HostListener, Inject, NgZone } from '@angular/core'
|
||||
import { Router, RoutesRecognized } from '@angular/router'
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
take,
|
||||
} from 'rxjs/operators'
|
||||
import {
|
||||
AlertController,
|
||||
IonicSafeString,
|
||||
@@ -16,21 +7,33 @@ import {
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { ToastButton } from '@ionic/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import {
|
||||
debounce,
|
||||
isEmptyObject,
|
||||
Emver,
|
||||
ErrorToastService,
|
||||
} from '@start9labs/shared'
|
||||
import { Subscription } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
take,
|
||||
} from 'rxjs/operators'
|
||||
import { AuthService, AuthState } from './services/auth.service'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { PatchDbService } from './services/patch-db/patch-db.service'
|
||||
import {
|
||||
ConnectionFailure,
|
||||
ConnectionService,
|
||||
} from './services/connection.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { debounce, isEmptyObject, Emver } from '@start9labs/shared'
|
||||
import { ServerStatus, UIData } from 'src/app/services/patch-db/data-model'
|
||||
import { ErrorToastService } from './services/error-toast.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { LocalStorageService } from './services/local-storage.service'
|
||||
import { EOSService } from './services/eos.service'
|
||||
import { MarketplaceService } from './pages/marketplace-routes/marketplace.service'
|
||||
import { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
|
||||
import { SnakePage } from './modals/snake/snake.page'
|
||||
|
||||
@@ -128,7 +131,6 @@ export class AppComponent {
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly config: ConfigService,
|
||||
@@ -190,8 +192,6 @@ export class AppComponent {
|
||||
this.watchVersion(),
|
||||
// watch unread notification count to display toast
|
||||
this.watchNotifications(),
|
||||
// watch marketplace URL for changes
|
||||
this.marketplaceService.init(),
|
||||
])
|
||||
})
|
||||
// UNVERIFIED
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import { SharedPipesModule, WorkspaceConfig } from '@start9labs/shared'
|
||||
import { MarketplaceModule } from './marketplace.module'
|
||||
|
||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@@ -48,6 +49,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
GenericInputComponentModule,
|
||||
MonacoEditorModule,
|
||||
SharedPipesModule,
|
||||
MarketplaceModule,
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
@@ -79,4 +81,4 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
bootstrap: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { IonicSafeString } from '@ionic/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import {
|
||||
BackupTarget,
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { getErrorMessage, Emver } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { ApiService } from '../../services/api/embassy-api.service'
|
||||
import {
|
||||
InstallWizardComponent,
|
||||
@@ -9,13 +10,14 @@ import {
|
||||
TopbarParams,
|
||||
} from './install-wizard.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { MarketplaceService } from 'src/app/pages/marketplace-routes/marketplace.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WizardBaker {
|
||||
constructor(
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly config: ConfigService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
@@ -77,10 +79,12 @@ export class WizardBaker {
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
executeAction: () =>
|
||||
this.marketplaceService.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
}),
|
||||
this.marketplaceService
|
||||
.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
.toPromise(),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
@@ -202,10 +206,12 @@ export class WizardBaker {
|
||||
verb: 'beginning downgrade for',
|
||||
title,
|
||||
executeAction: () =>
|
||||
this.marketplaceService.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
}),
|
||||
this.marketplaceService
|
||||
.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
.toPromise(),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
var Convert = require('ansi-to-html')
|
||||
var convert = new Convert({
|
||||
@@ -14,7 +14,11 @@ var convert = new Convert({
|
||||
})
|
||||
export class LogsPage {
|
||||
@ViewChild(IonContent) private content: IonContent
|
||||
@Input() fetchLogs: (params: { before_flag?: boolean, limit?: number, cursor?: string }) => Promise<RR.LogsRes>
|
||||
@Input() fetchLogs: (params: {
|
||||
before_flag?: boolean
|
||||
limit?: number
|
||||
cursor?: string
|
||||
}) => Promise<RR.LogsRes>
|
||||
loading = true
|
||||
loadingMore = false
|
||||
logs: string
|
||||
@@ -25,15 +29,13 @@ export class LogsPage {
|
||||
scrollToBottomButton = false
|
||||
isOnBottom = true
|
||||
|
||||
constructor (
|
||||
private readonly errToast: ErrorToastService,
|
||||
) { }
|
||||
constructor(private readonly errToast: ErrorToastService) {}
|
||||
|
||||
ngOnInit () {
|
||||
ngOnInit() {
|
||||
this.getLogs()
|
||||
}
|
||||
|
||||
async fetch (isBefore: boolean = true) {
|
||||
async fetch(isBefore: boolean = true) {
|
||||
try {
|
||||
const cursor = isBefore ? this.startCursor : this.endCursor
|
||||
const logsRes = await this.fetchLogs({
|
||||
@@ -57,7 +59,7 @@ export class LogsPage {
|
||||
}
|
||||
}
|
||||
|
||||
async getLogs () {
|
||||
async getLogs() {
|
||||
try {
|
||||
// get logs
|
||||
const logs = await this.fetch()
|
||||
@@ -65,47 +67,60 @@ export class LogsPage {
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const beforeContainerHeight = container.scrollHeight
|
||||
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
|
||||
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
|
||||
const newLogs = document
|
||||
.getElementById('template')
|
||||
.cloneNode(true) as HTMLElement
|
||||
newLogs.innerHTML =
|
||||
logs
|
||||
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
|
||||
.join('\n') + (logs.length ? '\n' : '')
|
||||
container.prepend(newLogs)
|
||||
const afterContainerHeight = container.scrollHeight
|
||||
|
||||
// scroll down
|
||||
scrollBy(0, afterContainerHeight - beforeContainerHeight)
|
||||
this.content.scrollToPoint(0, afterContainerHeight - beforeContainerHeight)
|
||||
this.content.scrollToPoint(
|
||||
0,
|
||||
afterContainerHeight - beforeContainerHeight,
|
||||
)
|
||||
|
||||
if (logs.length < this.limit) {
|
||||
this.needInfinite = false
|
||||
}
|
||||
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async loadMore () {
|
||||
async loadMore() {
|
||||
try {
|
||||
this.loadingMore = true
|
||||
const logs = await this.fetch(false)
|
||||
if (!logs.length) return this.loadingMore = false
|
||||
if (!logs.length) return (this.loadingMore = false)
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const newLogs = document.getElementById('template').cloneNode(true) as HTMLElement
|
||||
newLogs.innerHTML = logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') + (logs.length ? '\n' : '')
|
||||
const newLogs = document
|
||||
.getElementById('template')
|
||||
.cloneNode(true) as HTMLElement
|
||||
newLogs.innerHTML =
|
||||
logs
|
||||
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
|
||||
.join('\n') + (logs.length ? '\n' : '')
|
||||
container.append(newLogs)
|
||||
this.loadingMore = false
|
||||
this.scrollEvent()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
scrollEvent () {
|
||||
scrollEvent() {
|
||||
const buttonDiv = document.getElementById('button-div')
|
||||
this.isOnBottom = buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
|
||||
this.isOnBottom =
|
||||
buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
scrollToBottom() {
|
||||
this.content.scrollToBottom(500)
|
||||
}
|
||||
|
||||
async loadData (e: any): Promise<void> {
|
||||
async loadData(e: any): Promise<void> {
|
||||
await this.getLogs()
|
||||
e.target.complete()
|
||||
}
|
||||
|
||||
13
frontend/projects/ui/src/app/marketplace.module.ts
Normal file
13
frontend/projects/ui/src/app/marketplace.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AbstractMarketplaceService,
|
||||
useClass: MarketplaceService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class MarketplaceModule {}
|
||||
@@ -7,16 +7,18 @@ import {
|
||||
IonicSafeString,
|
||||
} from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DependentInfo, isEmptyObject, isObject } from '@start9labs/shared'
|
||||
import {
|
||||
ErrorToastService,
|
||||
getErrorMessage,
|
||||
DependentInfo,
|
||||
isEmptyObject,
|
||||
isObject,
|
||||
} from '@start9labs/shared'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import {
|
||||
ErrorToastService,
|
||||
getErrorMessage,
|
||||
} from 'src/app/services/error-toast.service'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import {
|
||||
convertValuesRecursive,
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { getErrorMessage, Emver } from '@start9labs/shared'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { getErrorMessage } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'generic-input',
|
||||
@@ -14,11 +14,9 @@ export class GenericInputComponent {
|
||||
unmasked = false
|
||||
error: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
|
||||
ngOnInit () {
|
||||
ngOnInit() {
|
||||
const defaultOptions: Partial<GenericInputOptions> = {
|
||||
buttonText: 'Submit',
|
||||
placeholder: 'Enter value',
|
||||
@@ -34,19 +32,19 @@ export class GenericInputComponent {
|
||||
this.value = this.options.initialValue
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
|
||||
toggleMask () {
|
||||
toggleMask() {
|
||||
this.unmasked = !this.unmasked
|
||||
}
|
||||
|
||||
cancel () {
|
||||
cancel() {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
async submit () {
|
||||
async submit() {
|
||||
const value = this.value.trim()
|
||||
|
||||
if (!value && !this.options.nullable) return
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, IonicSafeString } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { pauseFor } from '../../../../../shared/src/util/misc.util'
|
||||
import { getErrorMessage, pauseFor } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'markdown',
|
||||
|
||||
@@ -18,8 +18,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -2,16 +2,18 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
|
||||
import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators'
|
||||
import { RecoveredInfo } from 'src/app/util/parse-data-model'
|
||||
import { MarketplaceService } from 'src/app/pages/marketplace-routes/marketplace.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-rec',
|
||||
@@ -33,17 +35,14 @@ export class AppListRecComponent {
|
||||
readonly installing$ = this.install$.pipe(
|
||||
switchMap(({ id, version }) =>
|
||||
// Mapping each installation to API request
|
||||
from(
|
||||
this.marketplaceService.installPackage({
|
||||
id,
|
||||
'version-spec': `>=${version}`,
|
||||
'version-priority': 'min',
|
||||
}),
|
||||
).pipe(
|
||||
// Mapping operation to true/false loading indication
|
||||
loading(this.errToast),
|
||||
),
|
||||
this.marketplaceService.installPackage({
|
||||
id,
|
||||
'version-spec': `>=${version}`,
|
||||
'version-priority': 'min',
|
||||
}),
|
||||
),
|
||||
// Mapping operation to true/false loading indication
|
||||
loading(this.errToast),
|
||||
)
|
||||
|
||||
// Deleting package
|
||||
@@ -66,6 +65,7 @@ export class AppListRecComponent {
|
||||
private readonly api: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import { IonContent } from '@ionic/angular'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Metric } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { MainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-metrics',
|
||||
|
||||
@@ -15,7 +15,7 @@ import { PackageProperties } from 'src/app/util/properties.util'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { getValueByPointer } from 'fast-json-patch'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -10,14 +10,17 @@ import {
|
||||
PackageDataEntry,
|
||||
Status,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { isEmptyObject, PackageState } from '@start9labs/shared'
|
||||
import {
|
||||
isEmptyObject,
|
||||
ErrorToastService,
|
||||
PackageState,
|
||||
} from '@start9labs/shared'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { debounce } from '../../../../../../shared/src/util/misc.util'
|
||||
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
|
||||
import { ErrorToastService } from '../../../services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'dev-config',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { debounce } from '../../../../../../shared/src/util/misc.util'
|
||||
import { MarkdownPage } from '../../../modals/markdown/markdown.page'
|
||||
|
||||
@@ -17,9 +17,8 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { v4 } from 'uuid'
|
||||
import { DevData } from 'src/app/services/patch-db/data-model'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { DestroyService } from '@start9labs/shared'
|
||||
import { DestroyService, ErrorToastService } from '@start9labs/shared'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { BasicInfo, getBasicInfoSpec } from './form-info'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { DevProjectData } from 'src/app/services/patch-db/data-model'
|
||||
import { DestroyService } from '../../../../../../shared/src/services/destroy.service'
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/marketplace/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Release Notes</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<text-spinner *ngIf="loading; else loaded" text="Loading Release Notes"></text-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<div style="margin: 0px;" *ngFor="let note of marketplaceService.releaseNotes[pkgId] | keyvalue : asIsOrder">
|
||||
<ion-button
|
||||
(click)="setSelected(note.key)"
|
||||
expand="full" color="light"
|
||||
style="height: 50px; margin: 1px;"
|
||||
[class]="selected === note.key ? 'ion-activated' : ''"
|
||||
>
|
||||
<p style="position: absolute; left: 10px;">{{ note.key | displayEmver }}</p>
|
||||
</ion-button>
|
||||
<ion-card
|
||||
[id]="note.key"
|
||||
[ngStyle]="{
|
||||
'max-height': selected === note.key ? getDocSize(note.key) : '0px',
|
||||
'transition': 'max-height 0.2s ease-out'
|
||||
}"
|
||||
class="panel"
|
||||
color="light" >
|
||||
<ion-text id='release-notes' [innerHTML]="note.value | markdown"></ion-text>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -1,8 +0,0 @@
|
||||
.panel {
|
||||
margin: 0px;
|
||||
padding: 0px 24px;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 5px solid #4d4d4d;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { MarketplaceService } from '../marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-release-notes',
|
||||
templateUrl: './app-release-notes.page.html',
|
||||
styleUrls: ['./app-release-notes.page.scss'],
|
||||
})
|
||||
export class AppReleaseNotes {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
selected: string
|
||||
pkgId: string
|
||||
loading = true
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
public marketplaceService: MarketplaceService,
|
||||
public errToast: ErrorToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
try {
|
||||
const promises = []
|
||||
if (!this.marketplaceService.releaseNotes[this.pkgId]) {
|
||||
promises.push(this.marketplaceService.cacheReleaseNotes(this.pkgId))
|
||||
}
|
||||
if (!this.marketplaceService.pkgs.length) {
|
||||
promises.push(this.marketplaceService.load())
|
||||
}
|
||||
await Promise.all(promises)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
setSelected(selected: string) {
|
||||
if (this.selected === selected) {
|
||||
this.selected = null
|
||||
} else {
|
||||
this.selected = selected
|
||||
}
|
||||
}
|
||||
|
||||
getDocSize(selected: string) {
|
||||
const element = document.getElementById(selected)
|
||||
return `${element.scrollHeight}px`
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<h1 class="heading ion-text-center">{{ name }}</h1>
|
||||
|
||||
<ion-grid class="grid">
|
||||
<ion-row>
|
||||
<ion-col sizeSm="8" offset-sm="2">
|
||||
<ion-toolbar color="transparent">
|
||||
<ion-searchbar
|
||||
enterkeyhint="search"
|
||||
color="dark"
|
||||
debounce="250"
|
||||
[(ngModel)]="query"
|
||||
></ion-searchbar>
|
||||
</ion-toolbar>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<ng-container *ngIf="pkgs && categories; else loading">
|
||||
<div class="hidden-scrollbar ion-text-center">
|
||||
<ion-button
|
||||
*ngFor="let cat of categories"
|
||||
fill="clear"
|
||||
class="category"
|
||||
[class.category_selected]="isSelected(cat)"
|
||||
(click)="switchCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<ion-grid *ngIf="pkgs | filterPackages: localPkgs:query:category as filtered">
|
||||
<div *ngIf="!filtered.length && category === 'updates'" class="ion-padding">
|
||||
<h1>All services are up to date!</h1>
|
||||
</div>
|
||||
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of filtered" sizeXs="12" sizeSm="12" sizeMd="6">
|
||||
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="'data:image/png;base64,' + pkg.icon | trust" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="pkg-title">
|
||||
{{ pkg.manifest.title }}
|
||||
</h2>
|
||||
<h3>{{ pkg.manifest.description.short }}</h3>
|
||||
<marketplace-status
|
||||
class="status"
|
||||
[pkg]="localPkgs[pkg.manifest.id]"
|
||||
></marketplace-status>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<marketplace-list-skeleton></marketplace-list-skeleton>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,46 @@
|
||||
.heading {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 42px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
.ion-padding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.pkg-title {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.eos-item {
|
||||
--border-style: none;
|
||||
--background: linear-gradient(
|
||||
45deg,
|
||||
var(--ion-color-dark) -380%,
|
||||
var(--ion-color-medium) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.category {
|
||||
font-weight: 300;
|
||||
color: var(--ion-color-dark-shade);
|
||||
|
||||
&_selected {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { LocalPkg, MarketplacePkg } from '@start9labs/marketplace'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-list-content',
|
||||
templateUrl: 'marketplace-list-content.component.html',
|
||||
styleUrls: ['./marketplace-list-content.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListContentComponent {
|
||||
@Input()
|
||||
pkgs: MarketplacePkg[] | null = null
|
||||
|
||||
@Input()
|
||||
localPkgs: Record<string, LocalPkg> = {}
|
||||
|
||||
@Input()
|
||||
categories: Set<string> | null = null
|
||||
|
||||
@Input()
|
||||
name = ''
|
||||
|
||||
category = 'featured'
|
||||
query = ''
|
||||
|
||||
isSelected(category: string) {
|
||||
return category === this.category && !this.query
|
||||
}
|
||||
|
||||
switchCategory(category: string): void {
|
||||
this.category = category
|
||||
this.query = ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-list-header',
|
||||
templateUrl: 'marketplace-list-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListHeaderComponent {}
|
||||
@@ -0,0 +1,38 @@
|
||||
<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 ['', '', '', '']"
|
||||
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-list-skeleton',
|
||||
templateUrl: 'marketplace-list-skeleton.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListSkeletonComponent {}
|
||||
@@ -8,9 +8,14 @@ import {
|
||||
EmverPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
|
||||
|
||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||
import { MarketplaceListPage } from './marketplace-list.page'
|
||||
import { MarketplaceListHeaderComponent } from './marketplace-list-header/marketplace-list-header.component'
|
||||
import { MarketplaceListSkeletonComponent } from './marketplace-list-skeleton/marketplace-list-skeleton.component'
|
||||
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -29,8 +34,20 @@ const routes: Routes = [
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
MarketplacePipesModule,
|
||||
MarketplaceStatusModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [MarketplaceListPage],
|
||||
declarations: [
|
||||
MarketplaceListPage,
|
||||
MarketplaceListHeaderComponent,
|
||||
MarketplaceListContentComponent,
|
||||
MarketplaceListSkeletonComponent,
|
||||
],
|
||||
exports: [
|
||||
MarketplaceListPage,
|
||||
MarketplaceListHeaderComponent,
|
||||
MarketplaceListContentComponent,
|
||||
MarketplaceListSkeletonComponent,
|
||||
],
|
||||
})
|
||||
export class MarketplaceListPageModule {}
|
||||
|
||||
@@ -1,175 +1,15 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<marketplace-list-header></marketplace-list-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<!-- loading -->
|
||||
<text-spinner
|
||||
*ngIf="!patch.loaded else data"
|
||||
text="Connecting to Embassy"
|
||||
></text-spinner>
|
||||
<marketplace-list-content
|
||||
*ngIf="loaded else loading"
|
||||
[localPkgs]="localPkgs$ | async"
|
||||
[pkgs]="pkgs$ | async"
|
||||
[categories]="categories$ | async"
|
||||
[name]="name$ | async"
|
||||
></marketplace-list-content>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-template #data>
|
||||
<h1
|
||||
style="font-family: 'Montserrat'; font-size: 42px; margin: 32px 0"
|
||||
class="ion-text-center"
|
||||
>
|
||||
{{ marketplaceService.marketplace.name }}
|
||||
</h1>
|
||||
|
||||
<ion-grid style="padding-bottom: 32px">
|
||||
<ion-row>
|
||||
<ion-col sizeSm="8" offset-sm="2">
|
||||
<ion-toolbar color="transparent">
|
||||
<ion-searchbar
|
||||
enterkeyhint="search"
|
||||
color="dark"
|
||||
debounce="250"
|
||||
[(ngModel)]="query"
|
||||
(ionChange)="search()"
|
||||
></ion-searchbar>
|
||||
</ion-toolbar>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="loading; else pageLoaded">
|
||||
<div class="scrollable 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>
|
||||
</ng-container>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-template #pageLoaded>
|
||||
<div class="scrollable ion-text-center">
|
||||
<ion-button
|
||||
*ngFor="let cat of categories"
|
||||
fill="clear"
|
||||
[class]="cat === category ? 'selected' : 'dim'"
|
||||
(click)="switchCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="divider" style="margin: 24px"></div>
|
||||
</ng-template>
|
||||
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="loading; else pkgsLoaded">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col
|
||||
*ngFor="let pkg of ['', '', '', '']"
|
||||
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>
|
||||
</ng-container>
|
||||
|
||||
<!-- packages loaded -->
|
||||
<ng-template #pkgsLoaded>
|
||||
<div
|
||||
class="ion-padding"
|
||||
*ngIf="!pkgs.length && category ==='updates'"
|
||||
style="text-align: center"
|
||||
>
|
||||
<h1>All services are up to date!</h1>
|
||||
</div>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeSm="12" sizeMd="6">
|
||||
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="('data:image/png;base64,' + pkg.icon) | trust" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 style="font-family: 'Montserrat'; font-weight: bold">
|
||||
{{ pkg.manifest.title }}
|
||||
</h2>
|
||||
<h3>{{ pkg.manifest.description.short }}</h3>
|
||||
<ng-container
|
||||
*ngIf="localPkgs[pkg.manifest.id] as localPkg; else none"
|
||||
>
|
||||
<p *ngIf="localPkg.state === PackageState.Installed">
|
||||
<ion-text
|
||||
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0"
|
||||
color="success"
|
||||
>Installed</ion-text
|
||||
>
|
||||
<ion-text
|
||||
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1"
|
||||
color="warning"
|
||||
>Update Available</ion-text
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"
|
||||
>
|
||||
<ion-text
|
||||
color="primary"
|
||||
*ngIf="(localPkg['install-progress'] | installProgress) as progress"
|
||||
>
|
||||
Installing
|
||||
<span class="loading-dots"></span>{{ progress }}
|
||||
</ion-text>
|
||||
</p>
|
||||
<p *ngIf="localPkg.state === PackageState.Removing">
|
||||
<ion-text color="danger">
|
||||
Removing
|
||||
<span class="loading-dots"></span>
|
||||
</ion-text>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-template #none>
|
||||
<p>Not Installed</p>
|
||||
</ng-template>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
.scrollable {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
// background-color: var(--ion-color-light);
|
||||
height: 60px;
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.eos-item {
|
||||
--border-style: none;
|
||||
--background: linear-gradient(45deg, var(--ion-color-dark) -380%, var(--ion-color-medium) 100%)
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.dim {
|
||||
font-weight: 300;
|
||||
color: var(--ion-color-dark-shade);
|
||||
}
|
||||
@@ -1,146 +1,53 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { MarketplacePkg } from 'src/app/services/api/api.types'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { MarketplaceService } from '../marketplace.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import Fuse from 'fuse.js/dist/fuse.min.js'
|
||||
import { exists, isEmptyObject, PackageState } from '@start9labs/shared'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { filter, first } from 'rxjs/operators'
|
||||
import { Component } from '@angular/core'
|
||||
import { defer, Observable } from 'rxjs'
|
||||
import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators'
|
||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
LocalPkg,
|
||||
MarketplacePkg,
|
||||
spreadProgress,
|
||||
} from '@start9labs/marketplace'
|
||||
|
||||
const defaultOps = {
|
||||
isCaseSensitive: false,
|
||||
includeScore: true,
|
||||
shouldSort: true,
|
||||
includeMatches: false,
|
||||
findAllMatches: false,
|
||||
minMatchCharLength: 1,
|
||||
location: 0,
|
||||
threshold: 0.6,
|
||||
distance: 100,
|
||||
useExtendedSearch: false,
|
||||
ignoreLocation: false,
|
||||
ignoreFieldNorm: false,
|
||||
keys: [
|
||||
'manifest.id',
|
||||
'manifest.title',
|
||||
'manifest.description.short',
|
||||
'manifest.description.long',
|
||||
],
|
||||
}
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-list',
|
||||
templateUrl: './marketplace-list.page.html',
|
||||
styleUrls: ['./marketplace-list.page.scss'],
|
||||
})
|
||||
export class MarketplaceListPage {
|
||||
PackageState = PackageState
|
||||
readonly localPkgs$: Observable<Record<string, LocalPkg>> = defer(() =>
|
||||
this.patch.watch$('package-data'),
|
||||
).pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
tap(pkgs => Object.values(pkgs).forEach(spreadProgress)),
|
||||
startWith({}),
|
||||
)
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
readonly categories$ = this.marketplaceService
|
||||
.getCategories()
|
||||
.pipe(
|
||||
map(categories => new Set(['featured', 'updates', ...categories, 'all'])),
|
||||
)
|
||||
|
||||
pkgs: MarketplacePkg[] = []
|
||||
categories: string[]
|
||||
localPkgs: Record<string, PackageDataEntry> = {}
|
||||
category = 'featured'
|
||||
query: string
|
||||
loading = true
|
||||
readonly pkgs$: Observable<MarketplacePkg[]> = defer(() =>
|
||||
this.patch.watch$('server-info'),
|
||||
).pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
first(),
|
||||
switchMapTo(this.marketplaceService.getPackages()),
|
||||
)
|
||||
|
||||
subs: Subscription[] = []
|
||||
readonly name$: Observable<string> = this.marketplaceService
|
||||
.getMarketplace()
|
||||
.pipe(map(({ name }) => name))
|
||||
|
||||
constructor(
|
||||
private readonly errToast: ErrorToastService,
|
||||
public readonly patch: PatchDbService,
|
||||
public readonly marketplaceService: MarketplaceService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.subs = [
|
||||
this.patch
|
||||
.watch$('package-data')
|
||||
.pipe(filter(data => exists(data) && !isEmptyObject(data)))
|
||||
.subscribe(pkgs => {
|
||||
this.localPkgs = pkgs
|
||||
Object.values(this.localPkgs).forEach(pkg => {
|
||||
pkg['install-progress'] = { ...pkg['install-progress'] }
|
||||
})
|
||||
}),
|
||||
]
|
||||
|
||||
this.patch
|
||||
.watch$('server-info')
|
||||
.pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
first(),
|
||||
)
|
||||
.subscribe(async _ => {
|
||||
try {
|
||||
if (!this.marketplaceService.pkgs.length) {
|
||||
await this.marketplaceService.load()
|
||||
}
|
||||
|
||||
// category should start as first item in array
|
||||
// remove here then add at beginning
|
||||
const filterdCategories =
|
||||
this.marketplaceService.data.categories.filter(
|
||||
cat => this.category !== cat,
|
||||
)
|
||||
this.categories = [this.category, 'updates']
|
||||
.concat(filterdCategories)
|
||||
.concat(['all'])
|
||||
|
||||
this.filterPkgs()
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
search(): void {
|
||||
if (this.query) {
|
||||
this.category = undefined
|
||||
}
|
||||
this.filterPkgs()
|
||||
}
|
||||
|
||||
switchCategory(category: string): void {
|
||||
this.category = category
|
||||
this.query = undefined
|
||||
this.filterPkgs()
|
||||
}
|
||||
|
||||
private filterPkgs(): void {
|
||||
if (this.category === 'updates') {
|
||||
this.pkgs = this.marketplaceService.pkgs.filter(pkg => {
|
||||
const { id, version } = pkg.manifest
|
||||
return (
|
||||
this.localPkgs[id] && version !== this.localPkgs[id].manifest.version
|
||||
)
|
||||
})
|
||||
} else if (this.query) {
|
||||
const fuse = new Fuse(this.marketplaceService.pkgs, defaultOps)
|
||||
this.pkgs = fuse.search(this.query).map(p => p.item)
|
||||
} else {
|
||||
const pkgsToSort = this.marketplaceService.pkgs.filter(p => {
|
||||
return this.category === 'all' || p.categories.includes(this.category)
|
||||
})
|
||||
|
||||
const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 })
|
||||
this.pkgs = fuse
|
||||
.search(this.category !== 'all' ? this.category || '' : 'bit')
|
||||
.map(p => p.item)
|
||||
}
|
||||
get loaded(): boolean {
|
||||
return this.patch.loaded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,24 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'browse',
|
||||
loadChildren: () => import('./marketplace-list/marketplace-list.module').then(m => m.MarketplaceListPageModule),
|
||||
loadChildren: () =>
|
||||
import('./marketplace-list/marketplace-list.module').then(
|
||||
m => m.MarketplaceListPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId',
|
||||
loadChildren: () => import('./marketplace-show/marketplace-show.module').then(m => m.MarketplaceShowPageModule),
|
||||
loadChildren: () =>
|
||||
import('./marketplace-show/marketplace-show.module').then(
|
||||
m => m.MarketplaceShowPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/notes',
|
||||
loadChildren: () => import('./app-release-notes/app-release-notes.module').then(m => m.ReleaseNotesModule),
|
||||
loadChildren: () =>
|
||||
import('./release-notes/release-notes.module').then(
|
||||
m => m.ReleaseNotesPageModule,
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -25,4 +34,4 @@ const routes: Routes = [
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class MarketplaceRoutingModule { }
|
||||
export class MarketplaceRoutingModule {}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- release notes -->
|
||||
<ion-item-divider>
|
||||
New in {{ pkg.manifest.version | displayEmver }}
|
||||
<ion-button routerLink="notes" class="all-notes" fill="clear" color="dark">
|
||||
All Release Notes
|
||||
<ion-icon slot="end" name="arrow-forward-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div
|
||||
class="release-notes"
|
||||
[innerHTML]="pkg.manifest['release-notes'] | markdown"
|
||||
></div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- description -->
|
||||
<ion-item-divider>Description</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div class="release-notes">{{ pkg.manifest.description.long }}</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,9 @@
|
||||
.all-notes {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.release-notes {
|
||||
overflow: auto;
|
||||
max-height: 120px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-about',
|
||||
templateUrl: 'marketplace-show-about.component.html',
|
||||
styleUrls: ['marketplace-show-about.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowAboutComponent {
|
||||
@Input()
|
||||
pkg: MarketplacePkg
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<ion-item-divider>Additional Info</ion-item-divider>
|
||||
<ion-card>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col sizeSm="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item button detail="false" (click)="presentAlertVersions()">
|
||||
<ion-label>
|
||||
<h2>Other Versions</h2>
|
||||
<p>Click to view other versions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item button detail="false" (click)="presentModalMd('license')">
|
||||
<ion-label>
|
||||
<h2>License</h2>
|
||||
<p>{{ pkg.manifest.license }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward-outline"></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-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
<ion-col sizeSm="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
[href]="pkg.manifest['upstream-repo']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Source Repository</h2>
|
||||
<p>{{ pkg.manifest['upstream-repo'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="pkg.manifest['wrapper-repo']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Wrapper Repository</h2>
|
||||
<p>{{ pkg.manifest['wrapper-repo'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="pkg.manifest['support-site']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Support Site</h2>
|
||||
<p>{{ pkg.manifest['support-site'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card>
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { AlertController, ModalController } from '@ionic/angular'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { displayEmver, Emver } from '@start9labs/shared'
|
||||
|
||||
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-additional',
|
||||
templateUrl: 'marketplace-show-additional.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowAdditionalComponent {
|
||||
@Input()
|
||||
pkg: MarketplacePkg
|
||||
|
||||
@Output()
|
||||
version = new EventEmitter<string>()
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly emver: Emver,
|
||||
) {}
|
||||
|
||||
async presentAlertVersions() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Versions',
|
||||
inputs: this.pkg.versions
|
||||
.sort((a, b) => -1 * this.emver.compare(a, b))
|
||||
.map(v => ({
|
||||
name: v, // for CSS
|
||||
type: 'radio',
|
||||
label: displayEmver(v), // appearance on screen
|
||||
value: v, // literal SEM version value
|
||||
checked: this.pkg.manifest.version === v,
|
||||
})),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: (version: string) => this.version.emit(version),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentModalMd(title: string) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title,
|
||||
contentUrl: `/marketplace${this.pkg[title]}`,
|
||||
},
|
||||
component: MarkdownPage,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<ng-container *ngIf="localPkg; else install">
|
||||
<!-- not installing, updating, or removing -->
|
||||
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
||||
<ion-button
|
||||
*ngIf="(version | compareEmver: pkg.manifest.version) === -1"
|
||||
expand="block"
|
||||
(click)="presentModal('update')"
|
||||
>
|
||||
Update
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="(version | compareEmver: pkg.manifest.version) === 1"
|
||||
expand="block"
|
||||
color="warning"
|
||||
(click)="presentModal('downgrade')"
|
||||
>
|
||||
Downgrade
|
||||
</ion-button>
|
||||
<ng-container *ngIf="localStorageService.showDevTools$ | async">
|
||||
<ion-button
|
||||
*ngIf="(version | compareEmver: pkg.manifest.version) === 0"
|
||||
expand="block"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
Reinstall
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #install>
|
||||
<ion-button expand="block" (click)="tryInstall()">Install</ion-button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
MarketplacePkg,
|
||||
LocalPkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { pauseFor, PackageState } from '@start9labs/shared'
|
||||
|
||||
import { Manifest } from 'src/app/services/patch-db/data-model'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-controls',
|
||||
templateUrl: 'marketplace-show-controls.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowControlsComponent {
|
||||
@Input()
|
||||
pkg: MarketplacePkg
|
||||
|
||||
@Input()
|
||||
localPkg: LocalPkg
|
||||
|
||||
readonly PackageState = PackageState
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
public readonly localStorageService: LocalStorageService,
|
||||
) {}
|
||||
|
||||
get version(): string {
|
||||
return this.localPkg?.manifest.version || ''
|
||||
}
|
||||
|
||||
async tryInstall() {
|
||||
const { id, title, version, alerts } = this.pkg.manifest
|
||||
|
||||
if (!alerts.install) {
|
||||
this.marketplaceService.install(id, version).subscribe()
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: title,
|
||||
subHeader: version,
|
||||
message: alerts.install,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Install',
|
||||
handler: () =>
|
||||
this.marketplaceService.install(id, version).subscribe(),
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
async presentModal(action: 'update' | 'downgrade') {
|
||||
// TODO: Fix type
|
||||
const { id, title, version, dependencies, alerts } = this.pkg
|
||||
.manifest as Manifest
|
||||
const value = {
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
serviceRequirements: dependencies,
|
||||
installAlert: alerts.install,
|
||||
}
|
||||
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
action === 'update'
|
||||
? this.wizardBaker.update(value)
|
||||
: this.wizardBaker.downgrade(value),
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
await pauseFor(250)
|
||||
this.navCtrl.back()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<ng-container *ngIf="!(dependencies | empty)">
|
||||
<ion-item-divider>Dependencies</ion-item-divider>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col
|
||||
*ngFor="let dep of dependencies | keyvalue"
|
||||
sizeSm="12"
|
||||
sizeMd="6"
|
||||
>
|
||||
<ion-item [routerLink]="['/marketplace', dep.key]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="getImg(dep.key) | trust" />
|
||||
</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>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
|
||||
import { DependencyInfo, Manifest } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-dependencies',
|
||||
templateUrl: 'marketplace-show-dependencies.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowDependenciesComponent {
|
||||
@Input()
|
||||
pkg: MarketplacePkg
|
||||
|
||||
get dependencies(): DependencyInfo {
|
||||
// TODO: Fix type
|
||||
return (this.pkg.manifest as Manifest).dependencies
|
||||
}
|
||||
|
||||
getImg(key: string): string {
|
||||
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- auto-config -->
|
||||
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
|
||||
<ion-label>
|
||||
<h2 class="heading">
|
||||
<ion-text class="title">
|
||||
{{ title }}
|
||||
</ion-text>
|
||||
</h2>
|
||||
<p>
|
||||
<ion-text color="dark">
|
||||
{{ dependentInfo.title }} requires an install of {{ title }} satisfying
|
||||
{{ dependentInfo.version }}.
|
||||
<br />
|
||||
<br />
|
||||
<span
|
||||
*ngIf="version | satisfiesEmver: dependentInfo.version"
|
||||
class="text"
|
||||
>
|
||||
{{ title }} version {{ version | displayEmver }} is compatible.
|
||||
</span>
|
||||
<span
|
||||
*ngIf="!(version | satisfiesEmver: dependentInfo.version)"
|
||||
class="text text_error"
|
||||
>
|
||||
{{ title }} version {{ version | displayEmver }} is NOT compatible.
|
||||
</span>
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,18 @@
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 5px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-style: italic;
|
||||
|
||||
&_error {
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { DependentInfo } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-dependent',
|
||||
templateUrl: 'marketplace-show-dependent.component.html',
|
||||
styleUrls: ['marketplace-show-dependent.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowDependentComponent {
|
||||
@Input()
|
||||
pkg: MarketplacePkg
|
||||
|
||||
readonly dependentInfo?: DependentInfo = history.state?.dependentInfo
|
||||
|
||||
get title(): string {
|
||||
return this.pkg?.manifest.title || ''
|
||||
}
|
||||
|
||||
get version(): string {
|
||||
return this.pkg?.manifest.version || ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="marketplace"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Marketplace Listing</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-header',
|
||||
templateUrl: 'marketplace-show-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowHeaderComponent {}
|
||||
@@ -2,15 +2,23 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
||||
import {
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
|
||||
|
||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
||||
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
|
||||
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
|
||||
import { MarketplaceShowDependenciesComponent } from './marketplace-show-dependencies/marketplace-show-dependencies.component'
|
||||
import { MarketplaceShowAdditionalComponent } from './marketplace-show-additional/marketplace-show-additional.component'
|
||||
import { MarketplaceShowAboutComponent } from './marketplace-show-about/marketplace-show-about.component'
|
||||
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -29,8 +37,26 @@ const routes: Routes = [
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
MarketplacePipesModule,
|
||||
MarketplaceStatusModule,
|
||||
InstallWizardComponentModule,
|
||||
],
|
||||
declarations: [MarketplaceShowPage],
|
||||
declarations: [
|
||||
MarketplaceShowPage,
|
||||
MarketplaceShowHeaderComponent,
|
||||
MarketplaceShowControlsComponent,
|
||||
MarketplaceShowDependentComponent,
|
||||
MarketplaceShowAboutComponent,
|
||||
MarketplaceShowDependenciesComponent,
|
||||
MarketplaceShowAdditionalComponent,
|
||||
],
|
||||
exports: [
|
||||
MarketplaceShowPage,
|
||||
MarketplaceShowHeaderComponent,
|
||||
MarketplaceShowControlsComponent,
|
||||
MarketplaceShowDependentComponent,
|
||||
MarketplaceShowAboutComponent,
|
||||
MarketplaceShowDependenciesComponent,
|
||||
MarketplaceShowAdditionalComponent,
|
||||
],
|
||||
})
|
||||
export class MarketplaceShowPageModule {}
|
||||
|
||||
@@ -1,68 +1,21 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="marketplace"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Marketplace Listing</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<marketplace-show-header></marketplace-show-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<text-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
text="Loading Package"
|
||||
></text-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<ng-container *ngIf="pkg$ | async as pkg else loading">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9">
|
||||
<div class="header">
|
||||
<img [src]="('data:image/png;base64,' + pkg.icon) | trust" />
|
||||
<img alt="" [src]="getIcon(pkg.icon) | trust" />
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
|
||||
<p class="header-version">
|
||||
{{ pkg.manifest.version | displayEmver }}
|
||||
</p>
|
||||
<div class="header-status">
|
||||
<!-- no localPkg -->
|
||||
<p *ngIf="!localPkg; else local">Not Installed</p>
|
||||
<!-- localPkg -->
|
||||
<ng-template #local>
|
||||
<!-- installed -->
|
||||
<p *ngIf="localPkg.state === PackageState.Installed">
|
||||
<ion-text
|
||||
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0"
|
||||
color="success"
|
||||
>Installed</ion-text
|
||||
>
|
||||
<ion-text
|
||||
*ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1"
|
||||
color="warning"
|
||||
>Update Available</ion-text
|
||||
>
|
||||
</p>
|
||||
<!-- installing, updating -->
|
||||
<p
|
||||
*ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"
|
||||
>
|
||||
<ion-text
|
||||
color="primary"
|
||||
*ngIf="(localPkg['install-progress'] | installProgress) as progress"
|
||||
>
|
||||
Installing
|
||||
<span class="loading-dots"></span>{{ progress }}
|
||||
</ion-text>
|
||||
</p>
|
||||
<!-- removing -->
|
||||
<p *ngIf="localPkg.state === PackageState.Removing">
|
||||
<ion-text color="danger">
|
||||
Removing
|
||||
<span class="loading-dots"></span>
|
||||
</ion-text>
|
||||
</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
<marketplace-status
|
||||
class="header-status"
|
||||
[pkg]="localPkg$ | async"
|
||||
></marketplace-status>
|
||||
</div>
|
||||
</div>
|
||||
</ion-col>
|
||||
@@ -74,41 +27,13 @@
|
||||
sizeXs="12"
|
||||
class="ion-align-self-center"
|
||||
>
|
||||
<!-- no localPkg -->
|
||||
<ion-button *ngIf="!localPkg" expand="block" (click)="tryInstall()">
|
||||
Install
|
||||
</ion-button>
|
||||
<!-- localPkg -->
|
||||
<ng-container *ngIf="localPkg">
|
||||
<!-- not installing, updating, or removing -->
|
||||
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
||||
<ion-button
|
||||
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === -1"
|
||||
expand="block"
|
||||
(click)="presentModal('update')"
|
||||
>
|
||||
Update
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 0 && (localStorageService.showDevTools$ | async)"
|
||||
expand="block"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
Reinstall
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 1"
|
||||
expand="block"
|
||||
color="warning"
|
||||
(click)="presentModal('downgrade')"
|
||||
>
|
||||
Downgrade
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<marketplace-show-controls
|
||||
[pkg]="pkg"
|
||||
[localPkg]="localPkg$ | async"
|
||||
></marketplace-show-controls>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row *ngIf="localPkg">
|
||||
<ion-row *ngIf="localPkg$ | async">
|
||||
<ion-col
|
||||
sizeXl="3"
|
||||
sizeLg="3"
|
||||
@@ -129,189 +54,23 @@
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- auto-config -->
|
||||
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
|
||||
<ion-label>
|
||||
<h2 style="display: flex; align-items: center">
|
||||
<ion-text
|
||||
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
|
||||
>{{ pkg.manifest.title }}</ion-text
|
||||
>
|
||||
</h2>
|
||||
<p>
|
||||
<ion-text color="dark">
|
||||
{{ dependentInfo.title }} requires an install of {{
|
||||
pkg.manifest.title }} satisfying {{ dependentInfo.version }}.
|
||||
<br />
|
||||
<br />
|
||||
<span
|
||||
*ngIf="pkg.manifest.version | satisfiesEmver: dependentInfo.version"
|
||||
class="recommendation-text"
|
||||
>{{ pkg.manifest.title }} version {{ pkg.manifest.version |
|
||||
displayEmver }} is compatible.</span
|
||||
>
|
||||
<span
|
||||
*ngIf="!(pkg.manifest.version | satisfiesEmver: dependentInfo.version)"
|
||||
class="recommendation-text recommendation-error"
|
||||
>{{ pkg.manifest.title }} version {{ pkg.manifest.version |
|
||||
displayEmver }} is NOT compatible.</span
|
||||
>
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
|
||||
|
||||
<ion-item-group>
|
||||
<!-- release notes -->
|
||||
<ion-item-divider>
|
||||
New in {{ pkg.manifest.version | displayEmver }}
|
||||
<ion-button
|
||||
[routerLink]="['notes']"
|
||||
style="position: absolute; right: 10px"
|
||||
fill="clear"
|
||||
color="dark"
|
||||
>
|
||||
All Release Notes
|
||||
<ion-icon slot="end" name="arrow-forward-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div
|
||||
id="release-notes"
|
||||
[innerHTML]="pkg.manifest['release-notes'] | markdown"
|
||||
></div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- description -->
|
||||
<ion-item-divider>Description</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div id="release-notes" class="release-notes">
|
||||
{{ pkg.manifest.description.long }}
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- dependencies -->
|
||||
<ng-container *ngIf="!(pkg.manifest.dependencies | empty)">
|
||||
<ion-item-divider>Dependencies</ion-item-divider>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col
|
||||
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
|
||||
sizeSm="12"
|
||||
sizeMd="6"
|
||||
>
|
||||
<ion-item [routerLink]="['/marketplace', dep.key]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img
|
||||
[src]="('data:image/png;base64,' + pkg['dependency-metadata'][dep.key].icon) | trust"
|
||||
/>
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{ pkg['dependency-metadata'][dep.key].title }}
|
||||
<span *ngIf="dep.value.requirement.type === 'required'">
|
||||
(required)</span
|
||||
>
|
||||
<span *ngIf="dep.value.requirement.type === 'opt-out'">
|
||||
(required by default)</span
|
||||
>
|
||||
<span *ngIf="dep.value.requirement.type === 'opt-in'">
|
||||
(optional)</span
|
||||
>
|
||||
</h2>
|
||||
<p style="font-size: small">
|
||||
{{ dep.value.version | displayEmver }}
|
||||
</p>
|
||||
<p>{{ dep.value.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-container>
|
||||
<marketplace-show-about [pkg]="pkg"></marketplace-show-about>
|
||||
|
||||
<marketplace-show-dependencies
|
||||
[pkg]="pkg"
|
||||
></marketplace-show-dependencies>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-divider>Additional Info</ion-item-divider>
|
||||
<ion-card>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col sizeSm="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item button detail="false" (click)="presentAlertVersions()">
|
||||
<ion-label>
|
||||
<h2>Other Versions</h2>
|
||||
<p>Click to view other versions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
button
|
||||
detail="false"
|
||||
(click)="presentModalMd('license')"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>License</h2>
|
||||
<p>{{ pkg.manifest.license }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward-outline"></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-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
<ion-col sizeSm="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
[href]="pkg.manifest['upstream-repo']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Source Repository</h2>
|
||||
<p>{{ pkg.manifest['upstream-repo'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="pkg.manifest['wrapper-repo']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Wrapper Repository</h2>
|
||||
<p>{{ pkg.manifest['wrapper-repo'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="pkg.manifest['support-site']"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Support Site</h2>
|
||||
<p>{{ pkg.manifest['support-site'] }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card>
|
||||
<marketplace-show-additional
|
||||
[pkg]="pkg"
|
||||
(version)="loadVersion$.next($event)"
|
||||
></marketplace-show-additional>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Loading Package"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
.header {
|
||||
font-family: 'Montserrat';
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
padding: 2%;
|
||||
|
||||
img {
|
||||
min-width: 15%;
|
||||
max-width: 18%;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
margin-left: 5%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
.header-title {
|
||||
margin: 0 0 0 -2px;
|
||||
font-size: calc(20px + 3vw)
|
||||
font-size: calc(20px + 3vw);
|
||||
}
|
||||
|
||||
.header-version {
|
||||
padding: 4px 0 12px 0;
|
||||
margin: 0;
|
||||
font-size: calc(10px + 1vw)
|
||||
font-size: calc(10px + 1vw);
|
||||
}
|
||||
|
||||
.header-status {
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: calc(16px + 1vw)
|
||||
}
|
||||
font-size: calc(16px + 1vw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommendation-text {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.recommendation-error {
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
|
||||
#release-notes {
|
||||
overflow: auto;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
@@ -1,223 +1,72 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import {
|
||||
AlertController,
|
||||
IonContent,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import {
|
||||
displayEmver,
|
||||
Emver,
|
||||
DependentInfo,
|
||||
pauseFor,
|
||||
PackageState,
|
||||
} from '@start9labs/shared'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
LocalPkg,
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
spreadProgress,
|
||||
} from '@start9labs/marketplace'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { MarketplaceService } from '../marketplace.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MarketplacePkg } from 'src/app/services/api/api.types'
|
||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
import { BehaviorSubject, defer, Observable, of } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show',
|
||||
templateUrl: './marketplace-show.page.html',
|
||||
styleUrls: ['./marketplace-show.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
loading = true
|
||||
pkgId: string
|
||||
pkg: MarketplacePkg
|
||||
localPkg: PackageDataEntry
|
||||
PackageState = PackageState
|
||||
dependentInfo: DependentInfo
|
||||
subs: Subscription[] = []
|
||||
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
|
||||
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
||||
|
||||
readonly localPkg$ = defer(() =>
|
||||
this.patch.watch$('package-data', this.pkgId),
|
||||
).pipe(
|
||||
filter<LocalPkg>(Boolean),
|
||||
tap(spreadProgress),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly pkg$: Observable<MarketplacePkg> = this.loadVersion$.pipe(
|
||||
switchMap(version =>
|
||||
this.marketplaceService
|
||||
.getPackage(this.pkgId, version)
|
||||
.pipe(startWith(null)),
|
||||
),
|
||||
// TODO: Better fallback
|
||||
catchError(e => this.errToast.present(e) && of({} as MarketplacePkg)),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
public readonly localStorageService: LocalStorageService,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.dependentInfo =
|
||||
history.state && (history.state.dependentInfo as DependentInfo)
|
||||
|
||||
this.subs = [
|
||||
this.patch.watch$('package-data', this.pkgId).subscribe(pkg => {
|
||||
if (!pkg) return
|
||||
this.localPkg = pkg
|
||||
this.localPkg['install-progress'] = {
|
||||
...this.localPkg['install-progress'],
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
try {
|
||||
if (!this.marketplaceService.pkgs.length) {
|
||||
await this.marketplaceService.load()
|
||||
}
|
||||
this.pkg = this.marketplaceService.pkgs.find(
|
||||
pkg => pkg.manifest.id === this.pkgId,
|
||||
)
|
||||
if (!this.pkg) {
|
||||
throw new Error(`Service with ID "${this.pkgId}" not found.`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
getIcon(icon: string): string {
|
||||
return `data:image/png;base64,${icon}`
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
async presentAlertVersions() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Versions',
|
||||
inputs: this.pkg.versions
|
||||
.sort((a, b) => -1 * this.emver.compare(a, b))
|
||||
.map(v => {
|
||||
return {
|
||||
name: v, // for CSS
|
||||
type: 'radio',
|
||||
label: displayEmver(v), // appearance on screen
|
||||
value: v, // literal SEM version value
|
||||
checked: this.pkg.manifest.version === v,
|
||||
}
|
||||
}),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: (version: string) => {
|
||||
this.getPkg(version)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentModalMd(title: string) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title,
|
||||
contentUrl: `/marketplace${this.pkg[title]}`,
|
||||
},
|
||||
component: MarkdownPage,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async tryInstall() {
|
||||
const { id, title, version, alerts } = this.pkg.manifest
|
||||
|
||||
if (!alerts.install) {
|
||||
await this.install(id, version)
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: title,
|
||||
subHeader: version,
|
||||
message: alerts.install,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Install',
|
||||
handler: () => {
|
||||
this.install(id, version)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
async presentModal(action: 'update' | 'downgrade') {
|
||||
const { id, title, version, dependencies, alerts } = this.pkg.manifest
|
||||
const value = {
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
serviceRequirements: dependencies,
|
||||
installAlert: alerts.install,
|
||||
}
|
||||
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
action === 'update'
|
||||
? this.wizardBaker.update(value)
|
||||
: this.wizardBaker.downgrade(value),
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
await pauseFor(250)
|
||||
this.navCtrl.back()
|
||||
}
|
||||
|
||||
private async getPkg(version?: string): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
await pauseFor(100)
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
private async install(id: string, version?: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Beginning Installation',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
loader.present()
|
||||
|
||||
try {
|
||||
await this.marketplaceService.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
// async getPkg(version: string): Promise<void> {
|
||||
// this.loading = true
|
||||
// try {
|
||||
// this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
|
||||
// } catch (e) {
|
||||
// this.errToast.present(e)
|
||||
// } finally {
|
||||
// await pauseFor(100)
|
||||
// this.loading = false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<ng-container *ngIf="pkg; else none" [ngSwitch]="pkg.state">
|
||||
<div *ngSwitchCase="PackageState.Installed">
|
||||
<ion-text *ngIf="(version | compareEmver: version) === 0" color="success">
|
||||
Installed
|
||||
</ion-text>
|
||||
<ion-text *ngIf="(version | compareEmver: version) === 1" color="warning">
|
||||
Update Available
|
||||
</ion-text>
|
||||
</div>
|
||||
<div *ngSwitchCase="PackageState.Removing">
|
||||
<ion-text color="danger">
|
||||
Removing
|
||||
<span class="loading-dots"></span>
|
||||
</ion-text>
|
||||
</div>
|
||||
<div *ngSwitchDefault>
|
||||
<ion-text
|
||||
*ngIf="pkg['install-progress'] | installProgress as progress"
|
||||
color="primary"
|
||||
>
|
||||
Installing
|
||||
<span class="loading-dots"></span>{{ progress }}
|
||||
</ion-text>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #none>
|
||||
<div>Not Installed</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { LocalPkg } from '@start9labs/marketplace'
|
||||
import { PackageState } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-status',
|
||||
templateUrl: 'marketplace-status.component.html',
|
||||
})
|
||||
export class MarketplaceStatusComponent {
|
||||
@Input()
|
||||
pkg?: LocalPkg
|
||||
|
||||
PackageState = PackageState
|
||||
|
||||
get version(): string {
|
||||
return this.pkg?.manifest.version || ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { EmverPipesModule } from '@start9labs/shared'
|
||||
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||
|
||||
import { MarketplaceStatusComponent } from './marketplace-status.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
EmverPipesModule,
|
||||
MarketplacePipesModule,
|
||||
],
|
||||
declarations: [MarketplaceStatusComponent],
|
||||
exports: [MarketplaceStatusComponent],
|
||||
})
|
||||
export class MarketplaceStatusModule {}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import {
|
||||
MarketplaceData,
|
||||
MarketplacePkg,
|
||||
RR,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MarketplaceService {
|
||||
data: MarketplaceData
|
||||
pkgs: MarketplacePkg[] = []
|
||||
releaseNotes: {
|
||||
[id: string]: {
|
||||
[version: string]: string
|
||||
}
|
||||
} = {}
|
||||
marketplace: {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
init(): Subscription {
|
||||
return this.patch.watch$('ui', 'marketplace').subscribe(marketplace => {
|
||||
if (!marketplace || !marketplace['selected-id']) {
|
||||
this.marketplace = this.config.marketplace
|
||||
} else {
|
||||
this.marketplace =
|
||||
marketplace['known-hosts'][marketplace['selected-id']]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
try {
|
||||
const [data, pkgs] = await Promise.all([
|
||||
this.getMarketplaceData({
|
||||
'server-id': this.patch.getData()['server-info'].id,
|
||||
}),
|
||||
this.getMarketplacePkgs({ page: 1, 'per-page': 100 }),
|
||||
])
|
||||
this.data = data
|
||||
this.pkgs = pkgs
|
||||
if (this.patch.getData().ui.marketplace?.['selected-id']) {
|
||||
const { 'selected-id': selectedId, 'known-hosts': knownHosts } =
|
||||
this.patch.getData().ui.marketplace
|
||||
if (knownHosts[selectedId].name !== this.data.name) {
|
||||
this.api.setDbValue({
|
||||
pointer: `/marketplace/known-hosts/${selectedId}/name`,
|
||||
value: this.data.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.data = undefined
|
||||
this.pkgs = []
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async installPackage(req: Omit<RR.InstallPackageReq, 'marketplace-url'>) {
|
||||
req['marketplace-url'] = this.marketplace.url
|
||||
return this.api.installPackage(req as RR.InstallPackageReq)
|
||||
}
|
||||
|
||||
async getUpdates(
|
||||
localPkgs: Record<string, PackageDataEntry>,
|
||||
): Promise<MarketplacePkg[]> {
|
||||
const id = this.patch.getData().ui.marketplace?.['selected-id']
|
||||
const url = id
|
||||
? this.patch.getData().ui.marketplace['known-hosts'][id].url
|
||||
: this.config.marketplace.url
|
||||
|
||||
const idAndCurrentVersions = Object.keys(localPkgs)
|
||||
.map(key => ({
|
||||
id: key,
|
||||
version: localPkgs[key].manifest.version,
|
||||
marketplaceUrl: localPkgs[key].installed['marketplace-url'],
|
||||
}))
|
||||
.filter(pkg => {
|
||||
return pkg.marketplaceUrl === url
|
||||
})
|
||||
const latestPkgs = await this.getMarketplacePkgs({
|
||||
ids: idAndCurrentVersions,
|
||||
})
|
||||
|
||||
return latestPkgs.filter(latestPkg => {
|
||||
const latestVersion = latestPkg.manifest.version
|
||||
const curVersion = localPkgs[latestPkg.manifest.id]?.manifest.version
|
||||
return !!curVersion && this.emver.compare(latestVersion, curVersion) === 1
|
||||
})
|
||||
}
|
||||
|
||||
async getPkg(id: string, version = '*'): Promise<MarketplacePkg> {
|
||||
const pkgs = await this.getMarketplacePkgs({
|
||||
ids: [{ id, version }],
|
||||
})
|
||||
const pkg = pkgs.find(pkg => pkg.manifest.id == id)
|
||||
|
||||
if (!pkg) {
|
||||
throw new Error(`No results for ${id}${version ? ' ' + version : ''}`)
|
||||
} else {
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
|
||||
async cacheReleaseNotes(id: string): Promise<void> {
|
||||
this.releaseNotes[id] = await this.getReleaseNotes({ id })
|
||||
}
|
||||
|
||||
async getMarketplaceData(
|
||||
params: RR.GetMarketplaceDataReq,
|
||||
url?: string,
|
||||
): Promise<RR.GetMarketplaceDataRes> {
|
||||
url = url || this.marketplace.url
|
||||
return this.api.marketplaceProxy('/package/v0/info', params, url)
|
||||
}
|
||||
|
||||
async getMarketplacePkgs(
|
||||
params: Omit<RR.GetMarketplacePackagesReq, 'eos-version-compat'>,
|
||||
): Promise<RR.GetMarketplacePackagesRes> {
|
||||
if (params.query) delete params.category
|
||||
if (params.ids) params.ids = JSON.stringify(params.ids) as any
|
||||
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...params,
|
||||
'eos-version-compat':
|
||||
this.patch.getData()['server-info']['eos-version-compat'],
|
||||
}
|
||||
|
||||
return this.api.marketplaceProxy(
|
||||
'/package/v0/index',
|
||||
qp,
|
||||
this.marketplace.url,
|
||||
)
|
||||
}
|
||||
|
||||
async getReleaseNotes(
|
||||
params: RR.GetReleaseNotesReq,
|
||||
): Promise<RR.GetReleaseNotesRes> {
|
||||
return this.api.marketplaceProxy(
|
||||
`/package/v0/release-notes/${params.id}`,
|
||||
{},
|
||||
this.marketplace.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="href"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Release Notes</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'release-notes-header',
|
||||
templateUrl: 'release-notes-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesHeaderComponent {
|
||||
readonly href = `/marketplace/${this.route.snapshot.paramMap.get('pkgId')}`
|
||||
|
||||
constructor(private readonly route: ActivatedRoute) {}
|
||||
}
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
TextSpinnerComponentModule,
|
||||
ElementModule,
|
||||
} from '@start9labs/shared'
|
||||
import { AppReleaseNotes } from './app-release-notes.page'
|
||||
import { MarketplacePipesModule } from '../pipes/marketplace-pipes.module'
|
||||
import { MarketplacePipesModule } from '@start9labs/marketplace'
|
||||
|
||||
import { ReleaseNotesPage } from './release-notes.page'
|
||||
import { ReleaseNotesHeaderComponent } from './release-notes-header/release-notes-header.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppReleaseNotes,
|
||||
component: ReleaseNotesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -26,7 +29,9 @@ const routes: Routes = [
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
MarketplacePipesModule,
|
||||
ElementModule,
|
||||
],
|
||||
declarations: [AppReleaseNotes],
|
||||
declarations: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
|
||||
exports: [ReleaseNotesPage, ReleaseNotesHeaderComponent],
|
||||
})
|
||||
export class ReleaseNotesModule {}
|
||||
export class ReleaseNotesPageModule {}
|
||||
@@ -0,0 +1,34 @@
|
||||
<release-notes-header></release-notes-header>
|
||||
|
||||
<ion-content>
|
||||
<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
|
||||
elementRef
|
||||
#element="elementRef"
|
||||
class="panel"
|
||||
color="light"
|
||||
[id]="note.key"
|
||||
[style.maxHeight.px]="getDocSize(note.key, element)"
|
||||
>
|
||||
<ion-text
|
||||
id="release-notes"
|
||||
[innerHTML]="note.value | markdown"
|
||||
></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,19 @@
|
||||
.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,38 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
|
||||
@Component({
|
||||
selector: 'release-notes',
|
||||
templateUrl: './release-notes.page.html',
|
||||
styleUrls: ['./release-notes.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesPage {
|
||||
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
) {}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
|
||||
@@ -20,16 +20,12 @@
|
||||
</ion-item>
|
||||
|
||||
<ion-item
|
||||
[button]="mp.id !== selectedId"
|
||||
detail="false"
|
||||
*ngFor="let mp of marketplaces"
|
||||
detail="false"
|
||||
[button]="mp.id !== selectedId"
|
||||
(click)="presentAction(mp.id)"
|
||||
>
|
||||
<div
|
||||
*ngIf="mp.id !== selectedId"
|
||||
slot="start"
|
||||
style="padding-right: 32px"
|
||||
></div>
|
||||
<div *ngIf="mp.id !== selectedId" slot="start" class="padding"></div>
|
||||
<ion-icon
|
||||
*ngIf="mp.id === selectedId"
|
||||
slot="start"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
.skeleton-parts {
|
||||
padding-bottom: 6px;
|
||||
|
||||
ion-button::part(native) {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 0;
|
||||
};
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user