Feature/marketplace redesign (#2395)

* wip

* update marketplace categories styling

* update logo icons

* add sort pipe

* update search component styling

* clean up categories component

* cleanup and remove unnecessary sort pipe

* query packages in selected category

* fix search styling

* add reg icon and font, adjust category styles

* fix build from rebasing integration/refactors

* adjust marketplace types for icon with store data, plus formatting

* formatting

* update categories and search

* hover styling for categories

* category styling

* refactor for category as a behavior subject

* more category styling

* base functionality with new marketplace components

* styling cleanup

* misc style fixes and fix category selection from package page

* fixes from review feedback

* add and style additional details

* implement release notes modal

* fix menu when on service show page mobile to display change marketplace

* style and responsiveness fixes

* rename header to sidebar

* input icon config to sidebar

* add mime type pipe and type fn

* review feedback fixes

* skeleton text, more abstraction

* reorder categories, clean up a little

* audit sidebar, categories, store-icon, marketplace-sidebar, search

* finish code cleanup and fix few bugs

* misc fixes and cleanup

* fix broken styles and markdown

* bump shared marketplace version

* more cleanup

* sync package lock

* rename sidebar component to menu

* wip preview sidebar

* sync package lock

* breakout package show elements into components

* link to brochure in preview; custom taiga button styles

* move marketplace preview component into ui; open preview when viewing service in marketplace

* sync changes post file struture rename

* further cleanup

* create service for sidebar toggle and cleanup marketplace components

* bump shared marketplace version

* bump shared for new images needed for brochure marketplace

* cleanup

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Lucy
2023-12-08 15:12:38 -05:00
committed by GitHub
parent ad13b5eb4e
commit 0469aab433
115 changed files with 6434 additions and 5051 deletions

View File

@@ -18,6 +18,7 @@ tui-root {
}
.container {
max-width: 100%;
transition: filter 0.3s;
&_offline {

View File

@@ -15,6 +15,7 @@ import { ThemeSwitcherService } from './services/theme-switcher.service'
import { THEME } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { DataModel } from './services/patch-db/data-model'
import { slideInAnimation } from './route-animation'
function hasNavigation(url: string): boolean {
return (
@@ -28,13 +29,14 @@ function hasNavigation(url: string): boolean {
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
animations: [slideInAnimation],
})
export class AppComponent implements OnDestroy {
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
readonly theme$ = inject(THEME)
// @TODO theres a bug here disabling the side menu from appearing on first login; refresh fixes
readonly navigation$ = combineLatest([
this.authService.isVerified$,
this.router.events.pipe(map(() => hasNavigation(this.router.url))),

View File

@@ -13,7 +13,10 @@ import {
TUI_DATE_VALUE_TRANSFORMER,
} from '@taiga-ui/kit'
import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import {
AbstractCategoryService,
AbstractMarketplaceService,
} from '@start9labs/marketplace'
import { ApiService } from './services/api/embassy-api.service'
import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
@@ -25,6 +28,7 @@ import { DateTransformerService } from './services/date-transformer.service'
import { DatetimeTransformerService } from './services/datetime-transformer.service'
import { MarketplaceService } from './services/marketplace.service'
import { RoutingStrategyService } from './apps/portal/services/routing-strategy.service'
import { CategoryService } from './services/category.service'
const {
useMocks,
@@ -80,6 +84,10 @@ export const APP_PROVIDERS: Provider[] = [
provide: RouteReuseStrategy,
useExisting: RoutingStrategyService,
},
{
provide: AbstractCategoryService,
useClass: CategoryService,
},
]
export function appInitializer(

View File

@@ -6,8 +6,8 @@ import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { from } from 'rxjs'
import {
PackageDataEntry,
InstalledPackageInfo,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
@@ -92,8 +92,9 @@ export class ToMenuPipe implements PipeTransform {
name: 'Marketplace Listing',
description: `View ${manifest.title} on the Marketplace`,
action: () =>
this.router.navigate(['marketplace', manifest.id], {
queryParams: { url },
this.router.navigate(['marketplace'], {
relativeTo: this.route,
queryParams: { url, id: manifest.id },
}),
}
: {

View File

@@ -1,58 +0,0 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { EmverPipesModule } from '@start9labs/shared'
import {
TuiAvatarModule,
TuiCellModule,
TuiTitleModule,
} from '@taiga-ui/experimental'
@Component({
selector: 'sideload-dependencies',
template: `
<h3 class="g-title" [style.text-indent.rem]="1">Dependencies</h3>
<div *ngFor="let dep of package.manifest.dependencies | keyvalue" tuiCell>
<tui-avatar [src]="getImage(dep.key)"></tui-avatar>
<div tuiTitle>
<div>
<strong>{{ getTitle(dep.key) }}&nbsp;</strong>
<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>
</div>
<div tuiSubtitle [style.color]="'var(--tui-text-03)'">
{{ dep.value.version | displayEmver }}
</div>
<div tuiSubtitle>
{{ dep.value.description }}
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
TuiTitleModule,
EmverPipesModule,
TuiAvatarModule,
TuiCellModule,
],
})
export class SideloadDependenciesComponent {
@Input({ required: true })
package!: MarketplacePkg
getTitle(key: string): string {
return this.package['dependency-metadata'][key]?.title || key
}
getImage(key: string): string {
const icon = this.package['dependency-metadata'][key]?.icon
return icon ? `data:image/png;base64,${icon}` : key.substring(0, 2)
}
}

View File

@@ -4,8 +4,9 @@ import { Router, RouterLink } from '@angular/router'
import {
AboutModule,
AdditionalModule,
DependenciesModule,
MarketplacePackageHeroComponent,
MarketplacePkg,
PackageModule,
} from '@start9labs/marketplace'
import {
Emver,
@@ -23,31 +24,49 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ClientStorageService } from 'src/app/services/client-storage.service'
import { NavigationService } from '../../../services/navigation.service'
import { SideloadDependenciesComponent } from './dependencies.component'
@Component({
selector: 'sideload-package',
template: `
<ng-content></ng-content>
<marketplace-package *tuiLet="button$ | async as button" [pkg]="package">
<a
*ngIf="button !== null && button !== 'Install'"
tuiButton
appearance="secondary"
[routerLink]="'/portal/service/' + package.manifest.id"
<div class="grid gap-8 mb-16 p-4 lg:px-16 lg:pb-8 pt-14 justify-center">
<ng-content></ng-content>
<marketplace-package-hero
*tuiLet="button$ | async as button"
[pkg]="package"
>
View installed
</a>
<button *ngIf="button" tuiButton (click)="upload()">
{{ button }}
</button>
</marketplace-package>
<marketplace-about [pkg]="package"></marketplace-about>
<sideload-dependencies
*ngIf="!(package.manifest.dependencies | empty)"
[package]="package"
></sideload-dependencies>
<marketplace-additional [pkg]="package"></marketplace-additional>
<div class="flex justify-start">
<a
*ngIf="button !== null && button !== 'Install'"
tuiButton
appearance="secondary"
[routerLink]="'/portal/service/' + package.manifest.id"
>
View installed
</a>
<button *ngIf="button" tuiButton (click)="upload()">
{{ button }}
</button>
</div>
</marketplace-package-hero>
<marketplace-about [pkg]="package"></marketplace-about>
<div
*ngIf="!(package.manifest.dependencies | empty)"
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10"
>
<div class="lg:col-span-5 xl:col-span-4 bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps my-2 pb-3">Dependencies</h2>
<div class="grid grid-row-auto gap-3">
<div *ngFor="let dep of package.manifest.dependencies | keyvalue">
<marketplace-dependencies
[dep]="dep"
[pkg]="package"
></marketplace-dependencies>
</div>
</div>
</div>
</div>
<marketplace-additional [pkg]="package"></marketplace-additional>
</div>
`,
standalone: true,
imports: [
@@ -56,10 +75,10 @@ import { SideloadDependenciesComponent } from './dependencies.component'
SharedPipesModule,
AboutModule,
AdditionalModule,
PackageModule,
TuiButtonModule,
TuiLetModule,
SideloadDependenciesComponent,
MarketplacePackageHeroComponent,
DependenciesModule,
],
})
export class SideloadPackageComponent {

View File

@@ -29,6 +29,7 @@ import { SideloadPackageComponent } from './package.component'
[style.border-radius.%]="100"
[style.float]="'right'"
(click)="clear()"
class="justify-self-end"
>
Close
</button>

View File

@@ -83,8 +83,8 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
tuiLink
iconAlign="right"
icon="tuiIconExternalLink"
[routerLink]="'/marketplace/' + marketplacePkg.manifest.id"
[queryParams]="{ url: url }"
routerLink="/marketplace"
[queryParams]="{ url: url, id: marketplacePkg.manifest.id }"
>
View listing
</a>

View File

@@ -0,0 +1,134 @@
import { CommonModule, DOCUMENT } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
HostListener,
Inject,
Input,
inject,
} from '@angular/core'
import { TuiButtonModule } from '@taiga-ui/core'
import { TuiActiveZoneModule } from '@taiga-ui/cdk'
import { TuiSidebarModule } from '@taiga-ui/addon-mobile'
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
import { MarketplaceShowControlsComponent } from '../marketplace-show-preview/components/marketplace-show-controls.component'
import { MarketplaceShowPreviewModule } from '../marketplace-show-preview/marketplace-show-preview.module'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { BehaviorSubject, filter, Observable, shareReplay } from 'rxjs'
import { PatchDB } from 'patch-db-client'
import { SidebarService } from 'src/app/services/sidebar.service'
import { ActivatedRoute } from '@angular/router'
@Component({
selector: 'marketplace-item-toggle',
template: `
<div
[id]="pkg.manifest.id"
class="block h-full animate"
style="--animation-order: {{ index }}"
(click)="toggle(true)"
(tuiActiveZoneChange)="toggle($event)"
>
<marketplace-item [pkg]="pkg"></marketplace-item>
<marketplace-show-preview
[pkg]="pkg"
*tuiSidebar="
!!(sidebarService.getToggleState(pkg.manifest.id) | async);
direction: 'right';
autoWidth: true
"
class="overflow-y-auto max-w-full md:max-w-[30rem]"
>
<button
slot="close"
[style.--tui-padding]="0"
size="xs"
class="place-self-end"
tuiIconButton
type="button"
appearance="icon"
icon="tuiIconClose"
(tuiActiveZoneChange)="toggle($event)"
(click)="toggle(false)"
></button>
<marketplace-show-controls
slot="controls"
[pkg]="pkg"
[localPkg]="localPkg$ | async"
></marketplace-show-controls>
</marketplace-show-preview>
</div>
`,
styles: [
`
.animate {
animation-name: animateIn;
animation-duration: 400ms;
animation-delay: calc(var(--animation-order) * 200ms);
animation-fill-mode: both;
animation-timing-function: ease-in-out;
}
@keyframes animateIn {
0% {
opacity: 0;
transform: scale(0.6) translateY(-20px);
}
100% {
opacity: 1;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
TuiActiveZoneModule,
TuiButtonModule,
TuiSidebarModule,
MarketplaceShowPreviewModule,
ItemModule,
MarketplaceShowControlsComponent,
],
})
export class MarketplaceItemToggleComponent {
@Input({ required: true })
pkg!: MarketplacePkg
@Input({ required: true })
index!: number
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly activatedRoute: ActivatedRoute,
@Inject(DOCUMENT) private readonly document: Document,
) {}
readonly sidebarService = inject(SidebarService)
localPkg$!: Observable<PackageDataEntry>
pkgIdQueryParam = new BehaviorSubject<string>('')
readonly pkgId = this.activatedRoute.queryParamMap.subscribe(params => {
this.pkgIdQueryParam.next(params.get('id')!)
})
ngOnChanges() {
this.localPkg$ = this.patch
.watch$('package-data', this.pkg.manifest.id)
.pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true }))
}
@HostListener('animationend', ['$event.target'])
async onAnimationEnd(_target: EventTarget | null) {
if (this.pkgIdQueryParam.value === this.pkg.manifest.id) {
this.toggle(true)
}
}
toggle(open: boolean) {
this.sidebarService.toggleState(this.pkg.manifest.id, open)
}
}

View File

@@ -0,0 +1,29 @@
<menu [iconConfig]="marketplace">
<button
slot="desktop"
tuiIconButton
type="button"
appearance="icon"
icon="tuiIconRepeatLarge"
class="hover:opacity-70 bg-zinc-600 rounded-lg -mt-3"
(click)="presentModalMarketplaceSettings()"
></button>
<a
slot="mobile"
class="flex gap-2 relative hover:no-underline p-5"
(click)="presentModalMarketplaceSettings()"
>
<img
alt="Change Registry Icon"
width="24"
height="24"
class="opacity-70 invert"
src="svg/repeat-outline.svg"
/>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
>
Change Registry
</span>
</a>
</menu>

View File

@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { TuiDialogService } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { MarketplaceSettingsPage } from '../../marketplace-list/marketplace-settings/marketplace-settings.page'
@Component({
selector: 'marketplace-menu',
templateUrl: 'marketplace-menu.component.html',
styleUrls: ['./marketplace-menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceMenuComponent {
constructor(
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
readonly config: ConfigService,
) {}
readonly marketplace = this.config.marketplace
async presentModalMarketplaceSettings() {
this.dialogs
.open<MarketplaceSettingsPage>(
new PolymorpheusComponent(MarketplaceSettingsPage),
{
label: 'Change Registry',
},
)
.subscribe()
}
}

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core'
import { MarketplaceMenuComponent } from './marketplace-menu.component'
import { MenuModule } from '@start9labs/marketplace'
import { TuiButtonModule } from '@taiga-ui/core'
@NgModule({
imports: [MenuModule, TuiButtonModule],
exports: [MarketplaceMenuComponent],
declarations: [MarketplaceMenuComponent],
})
export class MarketplaceMenuModule {}

View File

@@ -1,26 +1,15 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { Routes, RouterModule } from '@angular/router'
import { RouterModule, Routes } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
SharedPipesModule,
EmverPipesModule,
ResponsiveColDirective,
} from '@start9labs/shared'
import {
FilterPackagesPipeModule,
CategoriesModule,
ItemModule,
SearchModule,
SkeletonModule,
StoreIconComponentModule,
} from '@start9labs/marketplace'
import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { ResponsiveColDirective, SharedPipesModule } from '@start9labs/shared'
import { FilterPackagesPipeModule } from '@start9labs/marketplace'
import { MarketplaceMenuModule } from '../components/marketplace-menu/marketplace-menu.module'
import { MarketplaceListPage } from './marketplace-list.page'
import { MarketplaceSettingsPageModule } from './marketplace-settings/marketplace-settings.module'
import { TuiNotificationModule } from '@taiga-ui/core'
import { TuiLetModule } from '@taiga-ui/cdk'
import { MarketplaceItemToggleComponent } from '../components/marketplace-item-toggle.component'
const routes: Routes = [
{
path: '',
@@ -32,20 +21,15 @@ const routes: Routes = [
imports: [
CommonModule,
IonicModule,
FormsModule,
RouterModule.forChild(routes),
SharedPipesModule,
EmverPipesModule,
FilterPackagesPipeModule,
MarketplaceStatusModule,
BadgeMenuComponentModule,
ItemModule,
CategoriesModule,
SearchModule,
SkeletonModule,
MarketplaceMenuModule,
MarketplaceSettingsPageModule,
StoreIconComponentModule,
TuiNotificationModule,
TuiLetModule,
ResponsiveColDirective,
MarketplaceItemToggleComponent,
],
declarations: [MarketplaceListPage],
exports: [MarketplaceListPage],

View File

@@ -1,90 +1,112 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start" *ngIf="back">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Marketplace</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding with-widgets">
<ng-container *ngIf="details$ | async as details">
<ion-item [color]="details.color">
<ion-icon slot="start" name="information-circle-outline"></ion-icon>
<ion-label>
<h2 style="font-weight: 600" [innerHTML]="details.description"></h2>
</ion-label>
</ion-item>
<ion-grid>
<ion-row>
<ion-col>
<div class="heading">
<store-icon
class="icon"
size="80px"
[url]="details.url"
[marketplace]="config.marketplace"
></store-icon>
<h1 class="montserrat">{{ details.name }}</h1>
</div>
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
<ion-icon slot="start" name="repeat-outline"></ion-icon>
Change
</ion-button>
<marketplace-search [(query)]="query"></marketplace-search>
</ion-col>
</ion-row>
<ion-row class="ion-align-items-center">
<ion-col>
<ng-container *ngIf="store$ | async as store; else loading">
<marketplace-categories
[categories]="store.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<div class="divider"></div>
<ion-grid
*ngIf="store.packages | filterPackages: query:category as filtered"
>
<ng-container *ngIf="filtered.length; else empty">
<ion-row *ngIf="localPkgs$ | async as localPkgs">
<ion-col
*ngFor="let pkg of filtered"
responsiveCol
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<marketplace-item [pkg]="pkg">
<marketplace-status
class="status"
[version]="pkg.manifest.version"
[localPkg]="localPkgs[pkg.manifest.id]"
></marketplace-status>
</marketplace-item>
</ion-col>
</ion-row>
</ng-container>
<ng-template #empty>
<div class="ion-padding">
<h2>No results</h2>
</div>
</ng-template>
</ion-grid>
</ng-container>
<ng-template #loading>
<marketplace-skeleton></marketplace-skeleton>
</ng-template>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ion-content>
<marketplace-menu></marketplace-menu>
<div
class="background sm:pl-[34vw] md:pl-[28vw] lg:pl-[22vw] 2xl:pl-[280px] min-h-screen flex justify-between overflow-auto scroll-smooth"
>
<main class="pt-24 sm:pt-3 md:pb-10 md:px-8">
<ng-container *ngIf="details$ | async as details">
<!-- icon as empty string displays no icon -->
<tui-notification
*ngIf="details.url === marketplace.start9"
status="success"
icon=""
class="m-4"
>
<p>
Services from this registry are packaged and maintained by the Start9
team. If you experience an issue or have questions related to a
service from this registry, one of our dedicated support staff will be
happy to assist you.
</p>
</tui-notification>
<tui-notification
*ngIf="details.url === marketplace.community"
status="info"
icon=""
class="m-4"
>
<p>
Services from this registry are packaged and maintained by members of
the Start9 community.
<strong>Install at your own risk</strong>
. 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.
</p>
</tui-notification>
<tui-notification
*ngIf="details.url.includes('beta')"
status="warning"
icon=""
class="m-4"
>
<p>
Services from this registry are undergoing
<strong>beta</strong>
testing and may contain bugs.
<strong>Install at your own risk</strong>
.
</p>
</tui-notification>
<tui-notification
*ngIf="details.url.includes('alpha')"
status="error"
icon=""
class="m-4"
>
<p>
Services from this registry are undergoing
<strong>alpha</strong>
testing. They are expected to contain bugs and could damage your
system.
<strong>Install at your own risk</strong>
.
</p>
</tui-notification>
<tui-notification
*ngIf="details.url !== marketplace.community && details.url !== marketplace.start9 && !details.url.includes('beta') && !details.url.includes('alpha')"
status="warning"
icon=""
class="m-4"
>
<p>
This is a Custom Registry. Start9 cannot verify the integrity or
functionality of services from this registry, and they could damage
your system.
<strong>Install at your own risk</strong>
.
</p>
</tui-notification>
</ng-container>
<div class="mt-8 px-6 mb-10">
<h1 class="text-4xl sm:text-5xl font-bold text-zinc-50/80 pb-6">
{{ category$ | async | titlecase }}
</h1>
</div>
<ng-container *ngIf="packages$ | async as packages; else loading">
<section
class="mt-10"
*ngIf="
packages | filterPackages : (query$ | async) : (category$ | async) as filtered
"
>
<ul
class="px-6 md:px-8 grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-16 list-none"
>
<li *ngFor="let pkg of filtered; index as i">
<marketplace-item-toggle
[pkg]="pkg"
[index]="i"
class="block h-full"
></marketplace-item-toggle>
</li>
</ul>
</section>
</ng-container>
<ng-template #loading>
<h1 class="text-xl pl-6">
Loading
<span class="loading-dots"></span>
</h1>
</ng-template>
</main>
</div>

View File

@@ -1,40 +1,4 @@
.heading {
margin-top: 32px;
h1 {
font-size: 42px;
margin-top: 0;
}
}
.icon {
display: inline-block;
margin-bottom: 14px;
}
.divider {
margin: 24px;
}
.ion-padding {
text-align: center;
}
.status {
font-size: 14px;
}
.description {
ion-icon {
padding-right: 8px;
}
@media (min-width: 1000px) {
ion-label {
::ng-deep p {
font-size: 1.1rem;
line-height: 25px;
}
}
}
.background {
background: url('/assets/img/background.png') no-repeat center center fixed;
z-index: -100;
}

View File

@@ -1,14 +1,18 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import {
AbstractCategoryService,
AbstractMarketplaceService,
} from '@start9labs/marketplace'
import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
import { MarketplaceSettingsPage } from './marketplace-settings/marketplace-settings.page'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { CategoryService } from 'src/app/services/category.service'
import { SidebarService } from 'src/app/services/sidebar.service'
import { MarketplaceSettingsPage } from './marketplace-settings/marketplace-settings.page'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
@Component({
selector: 'marketplace-list',
@@ -17,81 +21,37 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceListPage {
readonly back = !!this.route.snapshot.queryParamMap.get('back')
readonly store$ = this.marketplaceService.getSelectedStore$().pipe(
map(({ info, packages }) => {
const categories = new Set<string>()
if (info.categories.includes('featured')) categories.add('featured')
info.categories.forEach(c => categories.add(c))
categories.add('all')
return { categories: Array.from(categories), packages }
}),
)
readonly localPkgs$ = this.patch.watch$('package-data')
readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
map(({ url, name }) => {
const { start9, community } = this.config.marketplace
let color: string
let description: string
if (url === start9) {
color = 'success'
description =
'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.'
} else if (url === community) {
color = 'tertiary'
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.'
} else if (url.includes('beta')) {
color = 'warning'
description =
'Services from this registry are undergoing <b>beta</b> testing and may contain bugs. <b>Install at your own risk</b>.'
} else if (url.includes('alpha')) {
color = 'danger'
description =
'Services from this registry are undergoing <b>alpha</b> testing. They are expected to contain bugs and could damage your system. <b>Install at your own risk</b>.'
} else {
// alt marketplace
color = 'warning'
description =
'This is a Custom Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they could damage your system. <b>Install at your own risk</b>.'
}
return {
name,
url,
color,
description,
}
}),
)
constructor(
private readonly patch: PatchDB<DataModel>,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly dialogs: TuiDialogService,
@Inject(AbstractCategoryService)
private readonly categoryService: CategoryService,
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
readonly config: ConfigService,
private readonly route: ActivatedRoute,
readonly sidebarService: SidebarService,
) {}
category = 'featured'
query = ''
readonly packages$ = this.marketplaceService.getSelectedStore$().pipe(
map(({ packages }) => {
this.sidebarService.setMap(packages.map(p => p.manifest.id))
return packages
}),
)
readonly localPkgs$ = this.patch.watch$('package-data')
readonly category$ = this.categoryService.getCategory$()
readonly query$ = this.categoryService.getQuery$()
readonly details$ = this.marketplaceService.getSelectedHost$()
readonly marketplace = this.config.marketplace
presentModalMarketplaceSettings() {
async presentModalMarketplaceSettings() {
this.dialogs
.open(new PolymorpheusComponent(MarketplaceSettingsPage), {
label: 'Change Registry',
})
.open<MarketplaceSettingsPage>(
new PolymorpheusComponent(MarketplaceSettingsPage),
{
label: 'Change Registry',
},
)
.subscribe()
}
onCategoryChange(category: string): void {
this.category = category
this.query = ''
}
}

View File

@@ -25,6 +25,18 @@ import { ConfigService } from 'src/app/services/config.service'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceSettingsPage {
constructor(
private readonly api: ApiService,
private readonly loader: LoadingService,
private readonly formDialog: FormDialogService,
private readonly errorService: ErrorService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly patch: PatchDB<DataModel>,
private readonly dialogs: TuiDialogService,
readonly config: ConfigService,
) {}
stores$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getSelectedHost$(),
@@ -43,18 +55,6 @@ export class MarketplaceSettingsPage {
}),
)
constructor(
private readonly api: ApiService,
private readonly loader: LoadingService,
private readonly formDialog: FormDialogService,
private readonly errorService: ErrorService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly patch: PatchDB<DataModel>,
private readonly dialogs: TuiDialogService,
readonly config: ConfigService,
) {}
async presentModalAdd() {
const { name, spec } = getMarketplaceValueSpec()

View File

@@ -2,18 +2,27 @@ import {
ChangeDetectionStrategy,
Component,
Inject,
inject,
Input,
} from '@angular/core'
import {
AbstractMarketplaceService,
MarketplacePkg,
AboutModule,
AdditionalModule,
DependenciesModule,
} from '@start9labs/marketplace'
import {
Emver,
ErrorService,
isEmptyObject,
LoadingService,
pauseFor,
sameUrl,
EmverPipesModule,
MarkdownPipeModule,
SharedPipesModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { filter, firstValueFrom, of, Subscription, switchMap } from 'rxjs'
@@ -29,12 +38,99 @@ import { PatchDB } from 'patch-db-client'
import { getAllPackages } from 'src/app/util/get-package-data'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { dryUpdate } from 'src/app/util/dry-update'
import { Router } from '@angular/router'
import { SidebarService } from 'src/app/services/sidebar.service'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { TuiButtonModule } from '@taiga-ui/core'
@Component({
selector: 'marketplace-show-controls',
templateUrl: 'marketplace-show-controls.component.html',
styleUrls: ['./marketplace-show-controls.page.scss'],
template: `
<div class="flex justify-start">
<button
tuiButton
type="button"
class="mr-2"
appearance="primary"
*ngIf="localPkg"
(click)="showService()"
>
View Installed
</button>
<ng-container *ngIf="localPkg; else install">
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<button
tuiButton
type="button"
class="mr-2"
appearance="warning-solid"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === -1"
(click)="tryInstall()"
>
Update
</button>
<button
tuiButton
type="button"
class="mr-2"
appearance="secondary-solid"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 1"
(click)="tryInstall()"
>
Downgrade
</button>
<ng-container *ngIf="showDevTools$ | async">
<button
tuiButton
type="button"
class="mr-2"
appearance="tertiary-solid"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 0"
(click)="tryInstall()"
>
Reinstall
</button>
</ng-container>
</ng-container>
</ng-container>
<ng-template #install>
<button
tuiButton
type="button"
appearance="primary-solid"
(click)="tryInstall()"
>
Install
</button>
</ng-template>
</div>
`,
styles: [
`
button {
--tui-padding: 1.5rem;
}
`,
],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
IonicModule,
RouterModule,
TextSpinnerComponentModule,
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
AboutModule,
DependenciesModule,
AdditionalModule,
TuiButtonModule,
],
})
export class MarketplaceShowControlsComponent {
@Input()
@@ -47,8 +143,9 @@ export class MarketplaceShowControlsComponent {
localPkg!: PackageDataEntry | null
readonly showDevTools$ = this.ClientStorageService.showDevTools$
readonly PackageState = PackageState
private readonly router = inject(Router)
readonly sidebarService = inject(SidebarService)
constructor(
private readonly dialogs: TuiDialogService,
@@ -66,6 +163,7 @@ export class MarketplaceShowControlsComponent {
}
async tryInstall() {
this.sidebarService.toggleState(this.pkg.manifest.id, false)
const currentMarketplace = await firstValueFrom(
this.marketplaceService.getSelectedHost$(),
)
@@ -96,6 +194,13 @@ export class MarketplaceShowControlsComponent {
}
}
async showService() {
this.sidebarService.toggleState(this.pkg.manifest.id, false)
// @TODO code smell - needed to close preview - likely due to sidebar animation
await pauseFor(300)
this.router.navigate(['/services', this.pkg.manifest.id])
}
private async presentAlertDifferentMarketplace(
url: string,
originalUrl: string | null | undefined,
@@ -150,7 +255,7 @@ export class MarketplaceShowControlsComponent {
of(this.pkg.manifest.alerts.install)
.pipe(
switchMap(content =>
content
!content
? of(true)
: this.dialogs.open<boolean>(TUI_PROMPT, {
label: 'Alert',

View File

@@ -0,0 +1,47 @@
<div class="grid gap-8 p-7 justify-center">
<!-- close button -->
<ng-content select="[slot=close]"></ng-content>
<marketplace-package-hero [pkg]="pkg">
<!-- control buttons -->
<ng-content select="[slot=controls]"></ng-content>
</marketplace-package-hero>
<a
*ngIf="url$ | async as url"
href="{{ url + '/marketplace/' + pkg.manifest.id }}"
tuiButton
appearance="tertiary-solid"
type="button"
class="tui-space_right-3 tui-space_bottom-3"
iconRight="tuiIconExternalLink"
target="_blank"
style="margin: 0"
>
View more details
</a>
<div class="grid grid-cols-1 gap-x-8">
<marketplace-about [pkg]="pkg"></marketplace-about>
<div
*ngIf="!(pkg.manifest.dependencies | empty)"
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10 mt-6"
>
<div class="lg:col-span-5 xl:col-span-4 bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps my-2 pb-3">Dependencies</h2>
<div class="grid grid-row-auto gap-3">
<div *ngFor="let dep of pkg.manifest.dependencies | keyvalue">
<marketplace-dependencies
[dep]="dep"
[pkg]="pkg"
(click)="sidebarService.toggleState(dep.key, true)"
></marketplace-dependencies>
</div>
</div>
</div>
</div>
<release-notes [pkg]="pkg"></release-notes>
<marketplace-additional
class="mt-6"
[pkg]="pkg"
(version)="version$.next($event)"
></marketplace-additional>
</div>
</div>

View File

@@ -0,0 +1,59 @@
import {
ChangeDetectionStrategy,
Component,
Inject,
inject,
Input,
} from '@angular/core'
import { BehaviorSubject, map } from 'rxjs'
import {
TuiDialogContext,
TuiDialogService,
TuiDurationOptions,
tuiFadeIn,
} from '@taiga-ui/core'
import { tuiPure } from '@taiga-ui/cdk'
import { PolymorpheusContent } from '@tinkoff/ng-polymorpheus'
import { isPlatform } from '@ionic/angular'
import {
AbstractMarketplaceService,
MarketplacePkg,
} from '@start9labs/marketplace'
import { SidebarService } from 'src/app/services/sidebar.service'
@Component({
selector: 'marketplace-show-preview',
templateUrl: './marketplace-show-preview.component.html',
styleUrls: ['./marketplace-show-preview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [tuiFadeIn],
})
export class MarketplaceShowPreviewComponent {
@Input({ required: true })
pkg!: MarketplacePkg
constructor(
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
) {}
readonly sidebarService = inject(SidebarService)
private readonly marketplaceService = inject(AbstractMarketplaceService)
readonly version$ = new BehaviorSubject('*')
index = 0
speed = 1000
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
url$ = this.marketplaceService.getSelectedHost$().pipe(map(({ url }) => url))
@tuiPure
getAnimation(duration: number): TuiDurationOptions {
return { value: '', params: { duration } }
}
presentModalImg(content: PolymorpheusContent<TuiDialogContext>) {
this.dialogs
.open(content, {
size: 'l',
})
.subscribe()
}
}

View File

@@ -0,0 +1,35 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import {
SharedPipesModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import {
AboutModule,
AdditionalModule,
DependenciesModule,
MarketplacePackageHeroComponent,
ReleaseNotesModule,
} from '@start9labs/marketplace'
import { MarketplaceShowPreviewComponent } from './marketplace-show-preview.component'
import { TuiButtonModule } from '@taiga-ui/core'
import { RouterModule } from '@angular/router'
@NgModule({
declarations: [MarketplaceShowPreviewComponent],
exports: [MarketplaceShowPreviewComponent],
imports: [
CommonModule,
SharedPipesModule,
TextSpinnerComponentModule,
RouterModule,
DependenciesModule,
AdditionalModule,
ReleaseNotesModule,
TuiButtonModule,
AboutModule,
MarketplacePackageHeroComponent,
],
})
export class MarketplaceShowPreviewModule {}

View File

@@ -1,46 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import {
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import {
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
} from '@start9labs/marketplace'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
@NgModule({
declarations: [
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule,
TextSpinnerComponentModule,
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
PackageModule,
AboutModule,
DependenciesModule,
AdditionalModule,
],
exports: [
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
],
})
export class MarketplaceShowComponentsModule {}

View File

@@ -1,46 +0,0 @@
<div class="action-buttons">
<ion-button
*ngIf="localPkg"
expand="block"
color="primary"
[routerLink]="['/services', pkg.manifest.id]"
>
View Installed
</ion-button>
<ng-container *ngIf="localPkg; else install">
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === -1"
expand="block"
color="success"
(click)="tryInstall()"
>
Update
</ion-button>
<ion-button
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
>
Downgrade
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 0"
expand="block"
color="success"
(click)="tryInstall()"
>
Reinstall
</ion-button>
</ng-container>
</ng-container>
</ng-container>
<ng-template #install>
<ion-button expand="block" color="success" (click)="tryInstall()">
Install
</ion-button>
</ng-template>
</div>

View File

@@ -1,19 +0,0 @@
ion-button::part(native) {
font-size: 17px;
}
ion-button {
height: 44px;
margin: 16px;
}
@media (min-width: 1000px) {
.action-buttons {
display: flex;
}
ion-button {
width: 240px;
}
}

View File

@@ -1,30 +0,0 @@
<!-- auto-config -->
<ion-item *ngIf="dependentInfo" lines="none" class="rec-item">
<ion-label>
<h2 class="heading">
<ion-text class="montserrat 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>

View File

@@ -1,17 +0,0 @@
.heading {
display: flex;
align-items: center;
}
.title {
margin: 5px;
font-size: 18px;
}
.text {
font-style: italic;
&_error {
color: var(--ion-color-danger);
}
}

View File

@@ -1,33 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
} from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace'
import { DOCUMENT } from '@angular/common'
import { DependentInfo } from 'src/app/types/dependent-info'
@Component({
selector: 'marketplace-show-dependent',
templateUrl: 'marketplace-show-dependent.component.html',
styleUrls: ['marketplace-show-dependent.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowDependentComponent {
@Input({ required: true })
pkg!: MarketplacePkg
readonly dependentInfo?: DependentInfo =
this.document.defaultView?.history.state?.dependentInfo
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
get title(): string {
return this.pkg.manifest.title
}
get version(): string {
return this.pkg.manifest.version
}
}

View File

@@ -1,8 +0,0 @@
<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>

View File

@@ -1,8 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-show-header',
templateUrl: 'marketplace-show-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowHeaderComponent {}

View File

@@ -1,46 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { RouterModule, Routes } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
MarkdownPipeModule,
SharedPipesModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import {
AboutModule,
AdditionalModule,
DependenciesModule,
PackageModule,
} from '@start9labs/marketplace'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceShowPage } from './marketplace-show.page'
import { MarketplaceShowComponentsModule } from './components/marketplace-show-components.module'
const routes: Routes = [
{
path: '',
component: MarketplaceShowPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
TextSpinnerComponentModule,
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
MarketplaceStatusModule,
PackageModule,
AboutModule,
DependenciesModule,
AdditionalModule,
MarketplaceShowComponentsModule,
],
declarations: [MarketplaceShowPage],
})
export class MarketplaceShowPageModule {}

View File

@@ -1,50 +0,0 @@
<marketplace-show-header></marketplace-show-header>
<ion-content class="ion-padding with-widgets">
<ng-container *ngIf="pkg$ | async as pkg else loading">
<ng-container *ngIf="pkg | empty; else show">
<div
*ngIf="loadVersion$ | async as version"
class="ion-text-center"
style="padding-top: 64px"
>
<ion-icon
name="close-circle-outline"
style="font-size: 48px"
></ion-icon>
<h2>
{{ pkgId }} @{{ version === '*' ? 'latest' : version }} not found in
this registry
</h2>
</div>
</ng-container>
<ng-template #show>
<marketplace-package [pkg]="pkg"></marketplace-package>
<marketplace-show-controls
[url]="url"
[pkg]="pkg"
[localPkg]="localPkg$ | async"
></marketplace-show-controls>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
<ion-item-group>
<marketplace-about [pkg]="pkg"></marketplace-about>
<marketplace-dependencies
*ngIf="!(pkg.manifest.dependencies | empty)"
[pkg]="pkg"
></marketplace-dependencies>
</ion-item-group>
<marketplace-additional
[pkg]="pkg"
(version)="loadVersion$.next($event)"
></marketplace-additional>
</ng-template>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Package"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -1,3 +0,0 @@
.status {
font-size: calc(16px + 1vw);
}

View File

@@ -1,36 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
import { filter, shareReplay, switchMap, BehaviorSubject } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-show',
templateUrl: './marketplace-show.page.html',
styleUrls: ['./marketplace-show.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceShowPage {
readonly pkgId = getPkgId(this.route)
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
readonly loadVersion$ = new BehaviorSubject<string>('*')
readonly localPkg$ = this.patch
.watch$('package-data', this.pkgId)
.pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true }))
readonly pkg$ = this.loadVersion$.pipe(
switchMap(version =>
this.marketplaceService.getPackage$(this.pkgId, version, this.url),
),
)
constructor(
private readonly route: ActivatedRoute,
private readonly patch: PatchDB<DataModel>,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
}

View File

@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { RouterModule, Routes } from '@angular/router'
const routes: Routes = [
{
@@ -10,20 +10,6 @@ const routes: Routes = [
m => m.MarketplaceListPageModule,
),
},
{
path: ':pkgId',
loadChildren: () =>
import('./marketplace-show/marketplace-show.module').then(
m => m.MarketplaceShowPageModule,
),
},
{
path: ':pkgId/notes',
loadChildren: () =>
import('./release-notes/release-notes.module').then(
m => m.ReleaseNotesPageModule,
),
},
]
@NgModule({

View File

@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { ReleaseNotesModule } from '@start9labs/marketplace'
import { ReleaseNotesPage } from './release-notes.page'
const routes: Routes = [
{
path: '',
component: ReleaseNotesPage,
},
]
@NgModule({
imports: [IonicModule, ReleaseNotesModule, RouterModule.forChild(routes)],
declarations: [ReleaseNotesPage],
exports: [ReleaseNotesPage],
})
export class ReleaseNotesPageModule {}

View File

@@ -1,10 +0,0 @@
<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>
<release-notes></release-notes>

View File

@@ -1,13 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
@Component({
templateUrl: './release-notes.page.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesPage {
readonly href = `/marketplace/${getPkgId(this.route)}`
constructor(private readonly route: ActivatedRoute) {}
}

View File

@@ -6,4 +6,4 @@
font-weight: bold;
font-size: 2rem;
}
}
}

View File

@@ -1,3 +1,7 @@
.icon {
padding-right: 4px;
}
img {
border-radius: 100%;
}

View File

@@ -11,3 +11,7 @@
width: 54px;
margin: 0 16px;
}
img {
border-radius: 100%;
}

View File

@@ -77,7 +77,7 @@
*ngIf="pkg.installed?.['marketplace-url'] as url; else sideloaded"
button
detail
(click)="navigate('/marketplace/' + pkg.manifest.id, { url })"
(click)="navigate('/marketplace', { url, id: pkg.manifest.id })"
>
<ion-icon slot="start" name="storefront-outline"></ion-icon>
<ion-label>

View File

@@ -2,17 +2,17 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { SideloadPage } from './sideload.page'
import { Routes, RouterModule } from '@angular/router'
import { RouterModule, Routes } from '@angular/router'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { DragNDropDirective } from './dnd.directive'
import {
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
MarketplacePackageHeroComponent,
} from '@start9labs/marketplace'
// TODO: Find a way to not tie two routes together
import { MarketplaceShowComponentsModule } from '../../marketplace/marketplace-show/components/marketplace-show-components.module'
import { MarketplaceShowControlsComponent } from '../../marketplace/marketplace-show-preview/components/marketplace-show-controls.component'
const routes: Routes = [
{
@@ -28,11 +28,11 @@ const routes: Routes = [
RouterModule.forChild(routes),
SharedPipesModule,
EmverPipesModule,
PackageModule,
AboutModule,
AdditionalModule,
MarketplaceShowControlsComponent,
DependenciesModule,
MarketplaceShowComponentsModule,
MarketplacePackageHeroComponent,
],
declarations: [SideloadPage, DragNDropDirective],
})

View File

@@ -30,17 +30,29 @@
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</div>
<marketplace-package [pkg]="pkg"></marketplace-package>
<marketplace-show-controls [pkg]="pkg"></marketplace-show-controls>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
<ion-item-group>
<div class="grid gap-8 mb-16 p-4 lg:px-16 lg:pb-8 pt-14 justify-center">
<marketplace-package-hero [pkg]="pkg">
<marketplace-show-controls [pkg]="pkg"></marketplace-show-controls>
</marketplace-package-hero>
<marketplace-about [pkg]="pkg"></marketplace-about>
<marketplace-dependencies
<div
*ngIf="!(pkg.manifest.dependencies | empty)"
[pkg]="pkg"
></marketplace-dependencies>
</ion-item-group>
<marketplace-additional [pkg]="pkg"></marketplace-additional>
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10 mt-6"
>
<div class="lg:col-span-5 xl:col-span-4 bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps my-2 pb-3">Dependencies</h2>
<div class="grid grid-row-auto gap-3">
<div *ngFor="let dep of pkg.manifest.dependencies | keyvalue">
<marketplace-dependencies
[dep]="dep"
[pkg]="pkg"
></marketplace-dependencies>
</div>
</div>
</div>
</div>
<marketplace-additional [pkg]="pkg"></marketplace-additional>
</div>
</div>
<!-- empty -->
<ng-template #empty>

View File

@@ -10,7 +10,7 @@
<ion-content class="with-widgets">
<ion-item-group *ngIf="data$ | async as data">
<div *ngFor="let host of data.hosts">
<ion-item-divider class="inline">
<ion-item-divider>
<store-icon
[url]="host.url"
[marketplace]="config.marketplace"

View File

@@ -0,0 +1,33 @@
import {
animate,
group,
query,
style,
transition,
trigger,
} from '@angular/animations'
export const slideInAnimation = trigger('routeAnimations', [
transition('* => *', [
query(':enter, :leave', style({ position: 'fixed', width: '100%' }), {
optional: true,
}),
group([
query(
':enter',
[
style({ transform: 'translateX(-100%)' }),
animate('1s ease-in-out', style({ transform: 'translateX(0%)' })),
],
{ optional: true },
),
query(
':leave',
[
style({ transform: 'translateX(0%)' }),
animate('1s ease-in-out', style({ transform: 'translateX(100%)' })),
],
{ optional: true },
),
]),
]),
])

View File

@@ -6,17 +6,17 @@ import {
ServerStatusInfo,
} from 'src/app/services/patch-db/data-model'
import {
RR,
NotificationLevel,
ServerNotifications,
Metrics,
NotificationLevel,
RR,
ServerNotifications,
} from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
import {
DependencyMetadata,
MarketplacePkg,
Manifest,
MarketplacePkg,
} from '@start9labs/marketplace'
import { Log } from '@start9labs/shared'
import { unionSelectKey } from '@start9labs/start-sdk/lib/config/configTypes'
@@ -102,7 +102,8 @@ export module Mock {
assets: {
icon: 'icon.png',
},
'release-notes': 'Dual funded channels!',
'release-notes':
'Dual funded channels! And lots more amazing new features. Also includes several bugfixes and performance enhancements.',
license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -203,7 +204,7 @@ export module Mock {
...Mock.MockManifestBitcoind,
version: '0.19.0',
},
categories: ['bitcoin', 'cryptocurrency'],
categories: ['bitcoin', 'cryptocurrency', 'featured'],
versions: ['0.19.0', '0.20.0', '0.21.0'],
'dependency-metadata': {},
'published-at': new Date().toISOString(),
@@ -240,6 +241,7 @@ export module Mock {
icon: BTC_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
screenshots: ['one.png', 'two.png', 'three.png'],
manifest: {
...Mock.MockManifestBitcoind,
'release-notes':

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { AbstractCategoryService } from '@start9labs/marketplace'
@Injectable()
export class CategoryService extends AbstractCategoryService {
getCategory$(): Observable<string> {
return this.category$
}
changeCategory(category: string) {
this.category$.next(category)
}
setQuery(query: string) {
this.query$.next(query)
}
getQuery$(): Observable<string> {
return this.query$
}
resetQuery() {
this.query$.next('')
}
}

View File

@@ -1,27 +1,28 @@
import { Injectable } from '@angular/core'
import { sameUrl } from '@start9labs/shared'
import {
MarketplacePkg,
AbstractMarketplaceService,
StoreData,
Marketplace,
StoreInfo,
MarketplacePkg,
StoreData,
StoreIdentity,
StoreIdentityWithData,
StoreInfo,
} from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
import {
BehaviorSubject,
catchError,
combineLatest,
distinctUntilKeyChanged,
filter,
from,
map,
mergeMap,
Observable,
of,
scan,
catchError,
filter,
map,
pairwise,
scan,
shareReplay,
startWith,
switchMap,
@@ -162,6 +163,32 @@ export class MarketplaceService implements AbstractMarketplaceService {
return this.selectedStore$
}
getSelectedStoreWithCategories$() {
return this.selectedHost$.pipe(
switchMap(({ url }) =>
this.marketplace$.pipe(
map(m => m[url]),
filter(Boolean),
map(({ info, packages }) => {
const categories = new Set<string>()
if (info.categories.includes('featured')) categories.add('featured')
categories.add('all')
info.categories.forEach(c => categories.add(c))
return {
url,
info: {
...info,
categories: Array.from(categories),
},
packages,
}
}),
),
),
)
}
getPackage$(
id: string,
version: string,

View File

@@ -0,0 +1,21 @@
import { Injectable } from '@angular/core'
import { BehaviorSubject, Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class SidebarService {
openMap: Record<string, BehaviorSubject<boolean>> = {}
setMap(ids: string[]) {
ids.map(i => (this.openMap[i] = new BehaviorSubject(false)))
}
getToggleState(pkgId: string): Observable<boolean> {
return this.openMap[pkgId]
}
toggleState(pkgId: string, open: boolean) {
this.openMap[pkgId].next(open)
}
}