Hard code registry icons (#1951)

* component for store icons

* order registries and abstract registry urls

* use helper functionm
This commit is contained in:
Matt Hill
2022-11-17 12:23:27 -07:00
committed by Aiden McClelland
parent 4f9fe7245b
commit eec8c41e20
26 changed files with 223 additions and 149 deletions

View File

@@ -6,6 +6,11 @@
"url": "rpc", "url": "rpc",
"version": "v1" "version": "v1"
}, },
"marketplace": {
"start9": "https://registry.start9.com/",
"community": "https://community-registry.start9.com/",
"beta": "https://beta-registry.start9.com/"
},
"mocks": { "mocks": {
"maskAs": "tor", "maskAs": "tor",
"skipStartupAlerts": true "skipStartupAlerts": true

View File

@@ -1,16 +1,10 @@
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { import { MarketplacePkg, Marketplace, StoreData, StoreIdentity } from '../types'
MarketplacePkg,
Marketplace,
StoreURL,
StoreData,
StoreIdentifier,
} from '../types'
export abstract class AbstractMarketplaceService { export abstract class AbstractMarketplaceService {
abstract getKnownHosts$(): Observable<Record<StoreURL, StoreIdentifier>> abstract getKnownHosts$(): Observable<StoreIdentity[]>
abstract getSelectedHost$(): Observable<StoreIdentifier & { url: string }> abstract getSelectedHost$(): Observable<StoreIdentity>
abstract getMarketplace$(): Observable<Marketplace> abstract getMarketplace$(): Observable<Marketplace>

View File

@@ -2,13 +2,11 @@ import { Url } from '@start9labs/shared'
export type StoreURL = string export type StoreURL = string
export type StoreName = string export type StoreName = string
export type StoreIcon = string // base64
export interface StoreIdentifier { export interface StoreIdentity {
url: StoreURL
name?: StoreName name?: StoreName
icon?: StoreIcon // base64
} }
export type Marketplace = Record<StoreURL, StoreData | null> export type Marketplace = Record<StoreURL, StoreData | null>
export interface StoreData { export interface StoreData {
@@ -18,7 +16,6 @@ export interface StoreData {
export interface StoreInfo { export interface StoreInfo {
name: StoreName name: StoreName
icon?: StoreIcon
categories: string[] categories: string[]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -8,6 +8,11 @@ export type WorkspaceConfig = {
url: string url: string
version: string version: string
} }
marketplace: {
start9: 'https://registry.start9.com/'
community: 'https://community-registry.start9.com/'
beta: 'https://beta-registry.start9.com/'
}
mocks: { mocks: {
maskAs: 'tor' | 'lan' maskAs: 'tor' | 'lan'
skipStartupAlerts: boolean skipStartupAlerts: boolean

View File

@@ -0,0 +1,4 @@
<img *ngIf="url | getIcon as icon; else noIcon" [src]="icon" alt="" />
<ng-template #noIcon>
<ion-icon name="storefront-outline"></ion-icon>
</ng-template>

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { GetIconPipe, StoreIconComponent } from './store-icon.component'
@NgModule({
declarations: [StoreIconComponent, GetIconPipe],
imports: [CommonModule, IonicModule],
exports: [StoreIconComponent],
})
export class StoreIconComponentModule {}

View File

@@ -0,0 +1,36 @@
import {
ChangeDetectionStrategy,
Component,
Input,
Pipe,
PipeTransform,
} from '@angular/core'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'store-icon',
templateUrl: './store-icon.component.html',
styleUrls: ['./store-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StoreIconComponent {
@Input() url: string = ''
}
@Pipe({
name: 'getIcon',
})
export class GetIconPipe implements PipeTransform {
constructor(private readonly config: ConfigService) {}
transform(url: string): string | null {
const { start9, community } = this.config.marketplace
if (url === start9) {
return 'assets/img/icon.png'
} else if (url === community) {
return 'assets/img/community-store.png'
}
return null
}
}

View File

@@ -3,9 +3,15 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { MarketplaceSettingsPage } from './marketplace-settings.page' import { MarketplaceSettingsPage } from './marketplace-settings.page'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
@NgModule({ @NgModule({
imports: [CommonModule, IonicModule, SharedPipesModule], imports: [
CommonModule,
IonicModule,
SharedPipesModule,
StoreIconComponentModule,
],
declarations: [MarketplaceSettingsPage], declarations: [MarketplaceSettingsPage],
}) })
export class MarketplaceSettingsPageModule {} export class MarketplaceSettingsPageModule {}

View File

@@ -10,23 +10,23 @@
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<ion-item-group *ngIf="marketplace$ | async as m"> <ion-item-group *ngIf="stores$ | async as stores">
<ion-item-divider>Default Registries</ion-item-divider> <ion-item-divider>Default Registries</ion-item-divider>
<ion-item <ion-item
*ngFor="let s of m.standard" *ngFor="let s of stores.standard"
detail="false" detail="false"
[button]="s.url !== m.selected" [button]="!s.selected"
(click)="s.url === m.selected ? '' : presentAction(s)" (click)="s.selected ? '' : presentAction(s)"
> >
<ion-avatar slot="start"> <ion-avatar slot="start">
<img [src]="'data:image/png;base64,' + s.icon | trustUrl" alt="" /> <store-icon [url]="s.url"></store-icon>
</ion-avatar> </ion-avatar>
<ion-label> <ion-label>
<h2>{{ s.name }}</h2> <h2>{{ s.name }}</h2>
<p>{{ s.url }}</p> <p>{{ s.url }}</p>
</ion-label> </ion-label>
<ion-icon <ion-icon
*ngIf="s.url === m.selected" *ngIf="s.selected"
slot="end" slot="end"
size="large" size="large"
name="checkmark" name="checkmark"
@@ -45,18 +45,22 @@
</ion-item> </ion-item>
<ion-item <ion-item
*ngFor="let a of m.alt" *ngFor="let a of stores.alt"
detail="false" detail="false"
[button]="a.url !== m.selected" [button]="!a.selected"
(click)="a.url === m.selected ? '' : presentAction(a, true)" (click)="a.selected ? '' : presentAction(a, true)"
> >
<ion-icon slot="start" name="storefront-outline"></ion-icon> <store-icon
slot="start"
[url]="a.url"
style="font-size: 36px"
></store-icon>
<ion-label> <ion-label>
<h2>{{ a.name }}</h2> <h2>{{ a.name }}</h2>
<p>{{ a.url }}</p> <p>{{ a.url }}</p>
</ion-label> </ion-label>
<ion-icon <ion-icon
*ngIf="a.url === m.selected" *ngIf="a.selected"
slot="end" slot="end"
size="large" size="large"
name="checkmark" name="checkmark"

View File

@@ -7,18 +7,15 @@ import {
} from '@ionic/angular' } from '@ionic/angular'
import { ActionSheetButton } from '@ionic/core' import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import { import { AbstractMarketplaceService } from '@start9labs/marketplace'
AbstractMarketplaceService,
StoreIdentifier,
} from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types' import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { firstValueFrom } from 'rxjs' import { combineLatest, firstValueFrom } from 'rxjs'
@Component({ @Component({
selector: 'marketplace-settings', selector: 'marketplace-settings',
@@ -27,24 +24,21 @@ import { firstValueFrom } from 'rxjs'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class MarketplaceSettingsPage { export class MarketplaceSettingsPage {
marketplace$ = this.patch.watch$('ui', 'marketplace').pipe( stores$ = combineLatest([
map(m => { this.marketplaceService.getKnownHosts$(),
const selected = m['selected-url'] this.marketplaceService.getSelectedHost$(),
const hosts = Object.entries(m['known-hosts']) ]).pipe(
map(([stores, selected]) => {
const hmmm = stores.map(s => ({
...s,
selected: s.url === selected.url,
}))
// 0 and 1 are prod and community
const standard = hmmm.slice(0, 2)
// 2 and beyond are alts
const alt = hmmm.slice(2)
const standard = hosts return { standard, alt }
.map(([url, info]) => {
return { url, ...info }
})
.slice(0, 2) // 0 and 1 will always be prod and community
const alt = hosts
.map(([url, info]) => {
return { url, ...info }
})
.slice(2) // 2 and beyond will always be alts
return { selected, standard, alt }
}), }),
) )
@@ -207,16 +201,16 @@ export class MarketplaceSettingsPage {
loader.message = 'Validating marketplace...' loader.message = 'Validating marketplace...'
await loader.present() await loader.present()
const { name, icon } = await firstValueFrom( const { name } = await firstValueFrom(
this.marketplaceService.fetchInfo$(url), this.marketplaceService.fetchInfo$(url),
) )
// Save // Save
loader.message = 'Saving...' loader.message = 'Saving...'
await this.api.setDbValue<StoreIdentifier>( await this.api.setDbValue<{ name: string }>(
['marketplace', 'known-hosts', url], ['marketplace', 'known-hosts', url],
{ name, icon }, { name },
) )
} }
@@ -230,7 +224,7 @@ export class MarketplaceSettingsPage {
this.patch.watch$('ui', 'marketplace', 'known-hosts'), this.patch.watch$('ui', 'marketplace', 'known-hosts'),
) )
const filtered: { [url: string]: StoreIdentifier } = Object.keys(hosts) const filtered: { [url: string]: UIStore } = Object.keys(hosts)
.filter(key => key !== url) .filter(key => key !== url)
.reduce((prev, curr) => { .reduce((prev, curr) => {
const name = hosts[curr] const name = hosts[curr]
@@ -241,7 +235,7 @@ export class MarketplaceSettingsPage {
}, {}) }, {})
try { try {
await this.api.setDbValue<{ [url: string]: StoreIdentifier }>( await this.api.setDbValue<{ [url: string]: UIStore }>(
['marketplace', 'known-hosts'], ['marketplace', 'known-hosts'],
filtered, filtered,
) )

View File

@@ -15,6 +15,7 @@ import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/b
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceListPage } from './marketplace-list.page' import { MarketplaceListPage } from './marketplace-list.page'
import { MarketplaceSettingsPageModule } from 'src/app/modals/marketplace-settings/marketplace-settings.module' import { MarketplaceSettingsPageModule } from 'src/app/modals/marketplace-settings/marketplace-settings.module'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -39,6 +40,7 @@ const routes: Routes = [
SearchModule, SearchModule,
SkeletonModule, SkeletonModule,
MarketplaceSettingsPageModule, MarketplaceSettingsPageModule,
StoreIconComponentModule,
], ],
declarations: [MarketplaceListPage], declarations: [MarketplaceListPage],
exports: [MarketplaceListPage], exports: [MarketplaceListPage],

View File

@@ -24,14 +24,7 @@
<ion-row> <ion-row>
<ion-col size="12"> <ion-col size="12">
<div class="heading"> <div class="heading">
<img <store-icon class="icon" [url]="details.url"></store-icon>
*ngIf="details.icon; else noIcon"
[src]="'data:image/png;base64,' + details.icon | trustUrl"
alt=""
/>
<ng-template #noIcon>
<ion-icon name="storefront-outline"></ion-icon>
</ng-template>
<h1 class="montserrat ion-text-center">{{ details.name }}</h1> <h1 class="montserrat ion-text-center">{{ details.name }}</h1>
</div> </div>
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()"> <ion-button fill="clear" (click)="presentModalMarketplaceSettings()">

View File

@@ -1,15 +1,14 @@
.heading { .heading {
$icon-size: 64px;
margin-top: 32px; margin-top: 32px;
img {
max-width: $icon-size;
}
h1 { h1 {
font-size: 42px; font-size: 42px;
} }
ion-icon { }
font-size: $icon-size;
} .icon {
display: inline-block;
max-width: 80px;
font-size: 80px;
} }
.divider { .divider {

View File

@@ -4,6 +4,7 @@ import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { filter, map } from 'rxjs' import { filter, map } from 'rxjs'
import { MarketplaceSettingsPage } from 'src/app/modals/marketplace-settings/marketplace-settings.page' import { MarketplaceSettingsPage } from 'src/app/modals/marketplace-settings/marketplace-settings.page'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -30,25 +31,26 @@ export class MarketplaceListPage {
readonly localPkgs$ = this.patch.watch$('package-data') readonly localPkgs$ = this.patch.watch$('package-data')
readonly details$ = this.marketplaceService.getSelectedHost$().pipe( readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
map(({ url, name, icon }) => { map(({ url, name }) => {
const { start9, community, beta } = this.config.marketplace
let color: string let color: string
let description: string let description: string
switch (url) { switch (url) {
case 'https://registry.start9.com/': case start9:
color = 'success' color = 'success'
description = description =
'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.' 'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.'
break break
case 'https://beta-registry.start9.com/': case community:
color = 'primary'
description =
'Services from this registry are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
break
case 'https://community-registry.start9.com/':
color = 'tertiary' color = 'tertiary'
description = description =
'Services from this registry are packaged and maintained by members of the Start9 community. <b>Install at your own risk</b>. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.' 'Services from this registry are packaged and maintained by members of the Start9 community. <b>Install at your own risk</b>. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.'
break break
case beta:
color = 'primary'
description =
'Services from this registry are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
break
default: default:
// alt marketplace // alt marketplace
color = 'warning' color = 'warning'
@@ -59,7 +61,6 @@ export class MarketplaceListPage {
return { return {
name, name,
url, url,
icon,
color, color,
description, description,
} }
@@ -71,6 +72,7 @@ export class MarketplaceListPage {
@Inject(AbstractMarketplaceService) @Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService, private readonly marketplaceService: MarketplaceService,
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly config: ConfigService,
) {} ) {}
category = 'featured' category = 'featured'

View File

@@ -8,6 +8,7 @@ import { MarkdownPipeModule, SharedPipesModule } from '@start9labs/shared'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
import { RoundProgressModule } from 'angular-svg-round-progressbar' import { RoundProgressModule } from 'angular-svg-round-progressbar'
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -27,6 +28,7 @@ const routes: Routes = [
MarkdownPipeModule, MarkdownPipeModule,
RoundProgressModule, RoundProgressModule,
InstallProgressPipeModule, InstallProgressPipeModule,
StoreIconComponentModule,
], ],
declarations: [UpdatesPage, FilterUpdatesPipe], declarations: [UpdatesPage, FilterUpdatesPipe],
}) })

View File

@@ -9,26 +9,24 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<ion-item-group *ngIf="data$ | async as data"> <ion-item-group *ngIf="data$ | async as data">
<ng-container *ngFor="let host of data.hosts | keyvalue"> <ng-container *ngFor="let host of data.hosts">
<ion-item-divider> <ion-item-divider>
{{ host.value.name }} &nbsp; {{ host.name }} &nbsp;
<img <div style="max-width: 16px">
style="max-width: 24px" <store-icon [url]="host.url"></store-icon>
[src]="'data:image/png;base64,' + host.value.icon | trustUrl" </div>
alt=""
/>
</ion-item-divider> </ion-item-divider>
<div class="ion-padding-start ion-padding-bottom"> <div class="ion-padding-start ion-padding-bottom">
<ion-item *ngIf="data.errors.includes(host.key)"> <ion-item *ngIf="data.errors.includes(host.url)">
<ion-text color="danger">Request Failed</ion-text> <ion-text color="danger">Request Failed</ion-text>
</ion-item> </ion-item>
<ng-container <ng-container
*ngIf="data.marketplace[host.key]?.packages as packages else loading" *ngIf="data.marketplace[host.url]?.packages as packages else loading"
> >
<ng-container <ng-container
*ngIf="packages | filterUpdates : data.localPkgs : host.key as updates" *ngIf="packages | filterUpdates : data.localPkgs : host.url as updates"
> >
<ion-item *ngFor="let pkg of updates"> <ion-item *ngFor="let pkg of updates">
<ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local"> <ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local">
@@ -64,7 +62,7 @@
></ion-spinner> ></ion-spinner>
<ng-template #updateBtn> <ng-template #updateBtn>
<ion-button <ion-button
(click)="update(pkg.manifest.id, host.key)" (click)="update(pkg.manifest.id, host.url)"
color="dark" color="dark"
strong strong
> >

View File

@@ -12,7 +12,7 @@ import {
Marketplace, Marketplace,
MarketplaceManifest, MarketplaceManifest,
MarketplacePkg, MarketplacePkg,
StoreIdentifier, StoreIdentity,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { Emver } from '@start9labs/shared' import { Emver } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@@ -20,7 +20,7 @@ import { combineLatest, Observable } from 'rxjs'
import { PrimaryRendering } from '../../services/pkg-status-rendering.service' import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
interface UpdatesData { interface UpdatesData {
hosts: Record<string, StoreIdentifier> hosts: StoreIdentity[]
marketplace: Marketplace marketplace: Marketplace
localPkgs: Record<string, PackageDataEntry> localPkgs: Record<string, PackageDataEntry>
errors: string[] errors: string[]

File diff suppressed because one or more lines are too long

View File

@@ -22,8 +22,6 @@ import { PatchDB, pathFromArray, Update } from 'patch-db-client'
@Injectable() @Injectable()
export class LiveApiService extends ApiService { export class LiveApiService extends ApiService {
readonly eosMarketplaceUrl = 'https://registry.start9.com/'
constructor( constructor(
@Inject(DOCUMENT) private readonly document: Document, @Inject(DOCUMENT) private readonly document: Document,
private readonly http: HttpService, private readonly http: HttpService,
@@ -136,7 +134,7 @@ export class LiveApiService extends ApiService {
async updateServer(url?: string): Promise<RR.UpdateServerRes> { async updateServer(url?: string): Promise<RR.UpdateServerRes> {
const params = { const params = {
'marketplace-url': url || this.eosMarketplaceUrl, 'marketplace-url': url || this.config.marketplace.start9,
} }
return this.rpcRequest({ method: 'server.update', params }) return this.rpcRequest({ method: 'server.update', params })
} }
@@ -180,7 +178,7 @@ export class LiveApiService extends ApiService {
return this.marketplaceProxy( return this.marketplaceProxy(
'/eos/v0/latest', '/eos/v0/latest',
params, params,
this.eosMarketplaceUrl, this.config.marketplace.start9,
) )
} }

View File

@@ -38,7 +38,6 @@ import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { AuthService } from '../auth.service' import { AuthService } from '../auth.service'
import { ConnectionService } from '../connection.service' import { ConnectionService } from '../connection.service'
import { StoreInfo } from '@start9labs/marketplace' import { StoreInfo } from '@start9labs/marketplace'
import { COMMUNITY_REGISTRY, START9_REGISTRY } from './api-icons'
const PROGRESS: InstallProgress = { const PROGRESS: InstallProgress = {
size: 120, size: 120,
@@ -284,7 +283,6 @@ export class MockApiService extends ApiService {
if (path === '/package/v0/info') { if (path === '/package/v0/info') {
const info: StoreInfo = { const info: StoreInfo = {
name: 'Start9 Registry', name: 'Start9 Registry',
icon: START9_REGISTRY,
categories: [ categories: [
'bitcoin', 'bitcoin',
'lightning', 'lightning',

View File

@@ -7,24 +7,19 @@ import {
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { COMMUNITY_REGISTRY, START9_REGISTRY } from './api-icons'
import { Mock } from './api.fixures' import { Mock } from './api.fixures'
export const mockPatchData: DataModel = { export const mockPatchData: DataModel = {
ui: { ui: {
name: `Matt's Embassy`, name: `Matt's Embassy`,
'pkg-order': [],
'ack-welcome': '1.0.0', 'ack-welcome': '1.0.0',
marketplace: { marketplace: {
'selected-url': 'https://registry.start9.com/', 'selected-url': 'https://registry.start9.com/',
'known-hosts': { 'known-hosts': {
'https://registry.start9.com/': { 'https://registry.start9.com/': {
name: 'Start9 Registry', name: 'Start9 Registry',
icon: START9_REGISTRY,
},
'https://community-registry.start9.com/': {
icon: COMMUNITY_REGISTRY,
}, },
'https://community-registry.start9.com/': {},
'https://dark9-marketplace.com/': { 'https://dark9-marketplace.com/': {
name: 'Dark9', name: 'Dark9',
}, },

View File

@@ -12,7 +12,7 @@ const {
targetArch, targetArch,
gitHash, gitHash,
useMocks, useMocks,
ui: { api, mocks }, ui: { api, marketplace, mocks },
} = require('../../../../../config.json') as WorkspaceConfig } = require('../../../../../config.json') as WorkspaceConfig
@Injectable({ @Injectable({
@@ -28,6 +28,7 @@ export class ConfigService {
targetArch = targetArch targetArch = targetArch
gitHash = gitHash gitHash = gitHash
api = api api = api
marketplace = marketplace
skipStartupAlerts = useMocks && mocks.skipStartupAlerts skipStartupAlerts = useMocks && mocks.skipStartupAlerts
isConsulate = (window as any)['platform'] === 'ios' isConsulate = (window as any)['platform'] === 'ios'
supportsWebSockets = !!window.WebSocket || this.isConsulate supportsWebSockets = !!window.WebSocket || this.isConsulate

View File

@@ -5,7 +5,7 @@ import {
StoreData, StoreData,
Marketplace, Marketplace,
StoreInfo, StoreInfo,
StoreIdentifier, StoreIdentity,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { import {
BehaviorSubject, BehaviorSubject,
@@ -19,7 +19,7 @@ import {
} from 'rxjs' } from 'rxjs'
import { RR } from 'src/app/services/api/api.types' import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
catchError, catchError,
@@ -29,35 +29,53 @@ import {
shareReplay, shareReplay,
startWith, startWith,
switchMap, switchMap,
take,
tap,
} from 'rxjs/operators' } from 'rxjs/operators'
import { getNewEntries } from '@start9labs/shared' import { ConfigService } from './config.service'
@Injectable() @Injectable()
export class MarketplaceService implements AbstractMarketplaceService { export class MarketplaceService implements AbstractMarketplaceService {
private readonly knownHosts$ = this.patch.watch$( private readonly knownHosts$: Observable<StoreIdentity[]> = this.patch
'ui', .watch$('ui', 'marketplace', 'known-hosts')
'marketplace', .pipe(
'known-hosts', map(hosts => {
) const { start9, community } = this.config.marketplace
let arr = [
toStoreIdentity(start9, hosts[start9]),
toStoreIdentity(community, hosts[community]),
]
private readonly selectedHost$ = this.patch.watch$('ui', 'marketplace').pipe( return arr.concat(
distinctUntilKeyChanged('selected-url'), Object.entries(hosts)
map(data => ({ .filter(([url, _]) => ![start9, community].includes(url as any))
url: data['selected-url'], .map(([url, store]) => toStoreIdentity(url, store)),
...data['known-hosts'][data['selected-url']], )
})), }),
shareReplay(1), )
)
private readonly selectedHost$: Observable<StoreIdentity> = this.patch
.watch$('ui', 'marketplace')
.pipe(
distinctUntilKeyChanged('selected-url'),
map(({ 'selected-url': url, 'known-hosts': hosts }) =>
toStoreIdentity(url, hosts[url]),
),
shareReplay(1),
)
private readonly marketplace$ = this.knownHosts$.pipe( private readonly marketplace$ = this.knownHosts$.pipe(
startWith<Record<string, StoreIdentifier>>({}), startWith<StoreIdentity[]>([]),
pairwise(), pairwise(),
mergeMap(([prev, curr]) => from(Object.entries(getNewEntries(prev, curr)))), mergeMap(([prev, curr]) =>
mergeMap(([url, registry]) => curr.filter(c => !prev.find(p => c.url === p.url)),
),
mergeMap(({ url, name }) =>
this.fetchStore$(url).pipe( this.fetchStore$(url).pipe(
tap(data => {
if (data?.info) this.updateStoreName(url, name, data.info.name)
}),
map<StoreData | null, [string, StoreData | null]>(data => { map<StoreData | null, [string, StoreData | null]>(data => {
if (data?.info) this.updateStoreIdentifier(url, registry, data.info)
return [url, data] return [url, data]
}), }),
startWith<[string, StoreData | null]>([url, null]), startWith<[string, StoreData | null]>([url, null]),
@@ -74,22 +92,30 @@ export class MarketplaceService implements AbstractMarketplaceService {
shareReplay(1), shareReplay(1),
) )
private readonly selectedStore$ = this.selectedHost$.pipe( private readonly selectedStore$: Observable<StoreData | null> =
switchMap(({ url }) => this.marketplace$.pipe(map(m => m[url]))), this.selectedHost$.pipe(
) switchMap(({ url }) =>
this.marketplace$.pipe(
filter(m => !!m[url]),
take(1),
map(m => m[url]),
),
),
)
private readonly requestErrors$ = new BehaviorSubject<string[]>([]) private readonly requestErrors$ = new BehaviorSubject<string[]>([])
constructor( constructor(
private readonly api: ApiService, private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
) {} ) {}
getKnownHosts$(): Observable<Record<string, StoreIdentifier>> { getKnownHosts$(): Observable<StoreIdentity[]> {
return this.knownHosts$ return this.knownHosts$
} }
getSelectedHost$(): Observable<StoreIdentifier & { url: string }> { getSelectedHost$(): Observable<StoreIdentity> {
return this.selectedHost$ return this.selectedHost$
} }
@@ -234,16 +260,23 @@ export class MarketplaceService implements AbstractMarketplaceService {
) )
} }
private async updateStoreIdentifier( private async updateStoreName(
url: string, url: string,
oldInfo: StoreIdentifier, oldName: string | undefined,
newInfo: StoreIdentifier, newName: string,
): Promise<void> { ): Promise<void> {
if (oldInfo.name !== newInfo.name || oldInfo.icon !== newInfo.icon) { if (oldName !== newName) {
this.api.setDbValue<StoreIdentifier>( this.api.setDbValue<string>(
['marketplace', 'known-hosts', url], ['marketplace', 'known-hosts', url, 'name'],
newInfo, newName,
) )
} }
} }
} }
function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity {
return {
url,
...uiStore,
}
}

View File

@@ -1,6 +1,6 @@
import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { Url } from '@start9labs/shared' import { Url } from '@start9labs/shared'
import { MarketplaceManifest, StoreIdentifier } from '@start9labs/marketplace' import { MarketplaceManifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
export interface DataModel { export interface DataModel {
@@ -11,7 +11,6 @@ export interface DataModel {
export interface UIData { export interface UIData {
name: string | null name: string | null
'pkg-order': string[]
'ack-welcome': string // eOS emver 'ack-welcome': string // eOS emver
marketplace: UIMarketplaceData marketplace: UIMarketplaceData
dev: DevData dev: DevData
@@ -26,12 +25,16 @@ export interface UIData {
export interface UIMarketplaceData { export interface UIMarketplaceData {
'selected-url': string 'selected-url': string
'known-hosts': { 'known-hosts': {
'https://registry.start9.com/': StoreIdentifier 'https://registry.start9.com/': UIStore
'https://community-registry.start9.com/': StoreIdentifier 'https://community-registry.start9.com/': UIStore
[url: string]: StoreIdentifier [url: string]: UIStore
} }
} }
export interface UIStore {
name?: string
}
export interface DevData { export interface DevData {
[id: string]: DevProjectData [id: string]: DevProjectData
} }