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

@@ -28,7 +28,7 @@
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"input": "node_modules/ionicons/dist/svg",
"output": "./svg"
},
{

8164
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -46,14 +46,14 @@
"@start9labs/argon2": "^0.1.0",
"@start9labs/emver": "^0.1.5",
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
"@taiga-ui/addon-charts": "3.56.0",
"@taiga-ui/addon-mobile": "3.56.0",
"@taiga-ui/cdk": "3.56.0",
"@taiga-ui/core": "3.56.0",
"@taiga-ui/experimental": "3.56.0",
"@taiga-ui/icons": "3.56.0",
"@taiga-ui/kit": "3.56.0",
"@taiga-ui/styles": "3.56.0",
"@taiga-ui/addon-charts": "3.57.0",
"@taiga-ui/addon-mobile": "3.57.0",
"@taiga-ui/cdk": "3.57.0",
"@taiga-ui/core": "3.57.0",
"@taiga-ui/experimental": "3.57.0",
"@taiga-ui/icons": "3.57.0",
"@taiga-ui/kit": "3.57.0",
"@taiga-ui/styles": "3.57.0",
"@tinkoff/ng-dompurify": "4.0.0",
"@tinkoff/ng-event-plugins": "3.1.0",
"ansi-to-html": "^0.7.2",
@@ -77,6 +77,7 @@
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
"swiper": "^8.2.4",
"tailwindcss": "^3.3.3",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",
"uuid": "^8.3.2",
@@ -97,10 +98,12 @@
"@types/node-jose": "^1.1.10",
"@types/pbkdf2": "^3.1.0",
"@types/uuid": "^8.3.1",
"autoprefixer": "^10.4.14",
"husky": "^4.3.8",
"lint-staged": "^13.2.0",
"ng-packagr": "^16.1.0",
"node-html-parser": "^5.3.3",
"postcss": "^8.4.21",
"prettier": "^2.6.1",
"raw-loader": "^4.0.2",
"ts-node": "^10.7.0",

View File

@@ -0,0 +1 @@
export * from './src/public-api'

View File

@@ -1,13 +1,14 @@
{
"name": "@start9labs/marketplace",
"version": "0.3.12",
"version": "0.3.16",
"peerDependencies": {
"@angular/common": ">=13.2.0",
"@angular/core": ">=13.2.0",
"@ionic/angular": ">=6.0.0",
"@start9labs/shared": ">=0.3.0",
"@start9labs/shared": ">=0.3.2",
"@taiga-ui/cdk": ">=3.0.0",
"@tinkoff/ng-dompurify": ">=4.0.0",
"tailwindcss": "^3.3.2",
"fuse.js": "^6.4.6"
},
"dependencies": {

View File

@@ -0,0 +1,145 @@
<header
class="z-50 overflow-hidden w-full fixed sm:w-[34vw] md:w-[28vw] lg:w-[22vw] 2xl:w-[280px] sm:bg-zinc-700/90 sm:backdrop-blur-2xl sm:min-h-screen overflow-y-auto flex flex-col sm:py-6 sm:after:block sm:after:absolute sm:after:top-0 sm:after:bottom-0 sm:after:right-0 sm:after:w-0.5 after:h-[calc(100vh - 20px)] sm:after:bg-gradient-to-b from-zinc-700 to-zinc-400"
>
<ng-container *tuiLet="store$ | async as store">
<div class="hidden sm:flex flex-col mx-6 pb-3 items-center">
<div
class="mb-3 rounded-full"
[class.tui-skeleton]="!store"
[class.tui-skeleton_rounded]="!store"
>
<store-icon
size="64px"
[url]="store?.url || ''"
[marketplace]="iconConfig"
></store-icon>
</div>
<h1
class="font-semibold text-2xl text-zinc-100 text-center mb-3"
[class.tui-skeleton]="!store"
>
{{ store?.info?.name || 'Loading store' }}
</h1>
<!-- change registry modal -->
<ng-content select="[slot=desktop]"></ng-content>
</div>
<!-- mobile nav -->
<div class="sm:hidden bg-zinc-700/90 backdrop-blur-3xl">
<div class="flex justify-between items-center py-4 px-4 w-[100vw]">
<!-- mobile search -->
<marketplace-search
class="max-w-fit"
[(query)]="query"
(queryChange)="onQueryChange($event)"
></marketplace-search>
<button
tuiButton
type="button"
appearance="flat"
[pseudoActive]="false"
(click)="toggleMenu(true)"
(tuiActiveZoneChange)="toggleMenu($event)"
[style.--tui-padding]="'1rem'"
>
<store-icon
size="48px"
[url]="store?.url || ''"
[marketplace]="iconConfig"
class="rounded-full"
[class.tui-skeleton]="!store"
[class.tui-skeleton_rounded]="!store"
></store-icon>
<nav
*tuiSidebar="open; direction: 'right'; autoWidth: true"
class="bg-zinc-700/90 h-screen w-[70vw]"
>
<div class="flex flex-col divide-y divide-zinc-500 h-full">
<div class="flex items-center p-4">
<h1
class="font-semibold text-xl text-zinc-200 flex-grow"
[class.tui-skeleton]="!store"
>
{{ store?.info?.name }}
</h1>
<button
[style.--tui-padding]="0"
class="place-self-end"
tuiButton
type="button"
appearance="icon"
icon="tuiIconClose"
(tuiActiveZoneChange)="toggleMenu($event)"
(click)="toggleMenu(false)"
></button>
</div>
<!-- change registry modal -->
<ng-content select="[slot=mobile]"></ng-content>
<div
class="flex flex-col justify-between divide-y divide-zinc-500 h-full"
>
<marketplace-categories
[categories]="store?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event); toggleMenu(false)"
class="grow pt-5 pl-5 pr-5"
></marketplace-categories>
<a
class="flex relative gap-2 hover:no-underline p-5"
target="_blank"
href="https://github.com/Start9Labs/service-pipeline#readme"
>
<img
alt="Launch icon"
width="24"
height="24"
class="opacity-70 invert"
src="svg/rocket-outline.svg"
/>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
>
Launch your project
</span>
</a>
</div>
</div>
</nav>
</button>
</div>
</div>
<!-- desktop nav -->
<nav class="hidden sm:flex grow flex-col py-3 px-4">
<!-- desktop search -->
<marketplace-search
class="place-self-center md:pb-8"
[query]="query"
(queryChange)="onQueryChange($event)"
></marketplace-search>
<div class="flex grow flex-col justify-between">
<marketplace-categories
[categories]="store?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<a
class="flex gap-2 p-2 hover:no-underline sm:hover:bg-[#222428] hover:cursor-pointer sm:w-[120%] z-50 rounded-l-lg ease-in-out delay-75 duration-300"
target="_blank"
href="https://github.com/Start9Labs/service-pipeline#readme"
>
<img
alt="Rocket Icon"
width="24"
height="24"
class="opacity-70 invert"
src="svg/rocket-outline.svg"
/>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
>
Launch your project
</span>
</a>
</div>
</nav>
</ng-container>
</header>

View File

@@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared'
import { MenuComponent } from './menu.component'
import { TuiButtonModule, TuiLoaderModule } from '@taiga-ui/core'
import { TuiActiveZoneModule, TuiLetModule } from '@taiga-ui/cdk'
import { TuiSidebarModule } from '@taiga-ui/addon-mobile'
import { SearchModule } from '../../pages/list/search/search.module'
import { CategoriesModule } from '../../pages/list/categories/categories.module'
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
@NgModule({
imports: [
CommonModule,
SharedPipesModule,
SearchModule,
CategoriesModule,
TuiActiveZoneModule,
TuiSidebarModule,
TuiLoaderModule,
TuiButtonModule,
CategoriesModule,
StoreIconComponentModule,
TuiLetModule,
],
declarations: [MenuComponent],
exports: [MenuComponent],
})
export class MenuModule {}

View File

@@ -0,0 +1,89 @@
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
OnDestroy,
} from '@angular/core'
import { combineLatest, map, Subject, takeUntil } from 'rxjs'
import { StoreIdentity } from '../../types'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
import { AbstractCategoryService } from '../../services/category.service'
import { Router } from '@angular/router'
import { MarketplaceConfig } from '@start9labs/shared'
@Component({
selector: 'menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MenuComponent implements OnDestroy {
@Input({ required: true })
iconConfig!: MarketplaceConfig
constructor(private readonly router: Router) {}
private destroy$ = new Subject<void>()
private readonly marketplaceService = inject(AbstractMarketplaceService)
private readonly categoryService = inject(AbstractCategoryService)
readonly store$ = this.marketplaceService.getSelectedStoreWithCategories$()
readonly alt$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getSelectedHost$(),
]).pipe(
map(([stores, selected]) =>
stores.filter(({ url }) => url != selected.url),
),
)
private hosts?: StoreIdentity[]
category = ''
query = ''
open = false
ngOnInit() {
this.categoryService
.getQuery$()
.pipe(takeUntil(this.destroy$))
.subscribe(val => {
this.query = val
})
this.categoryService
.getCategory$()
.pipe(takeUntil(this.destroy$))
.subscribe(val => {
this.category = val
})
this.marketplaceService
.getKnownHosts$()
.pipe(takeUntil(this.destroy$))
.subscribe(hosts => {
this.hosts = hosts
})
}
onCategoryChange(category: string): void {
this.category = category
this.query = ''
this.categoryService.resetQuery()
this.categoryService.changeCategory(category)
this.router.navigate(['/marketplace'], { replaceUrl: true })
}
onQueryChange(query: string): void {
this.query = query
this.categoryService.setQuery(query)
this.router.navigate(['/marketplace'], { replaceUrl: true })
}
toggleMenu(open: boolean): void {
this.open = open
}
ngOnDestroy(): void {
this.destroy$.next()
this.destroy$.complete()
}
}

View File

@@ -1,5 +1,6 @@
<img
*ngIf="icon; else noIcon"
class="rounded-full"
[style.max-width]="size || '100%'"
[src]="icon"
alt="Service Icon"

View File

@@ -1,9 +1,28 @@
<ion-button
*ngFor="let cat of categories"
fill="clear"
class="category"
[class.category_selected]="cat === category"
<button
*ngFor="let cat of categories || ['', '', '', '', '', '']"
class="flex relative gap-2 hover:no-underline hover:cursor-pointer sm:hover:bg-[#222428] sm:hover:opacity-90 sm:w-[120%] rounded-l-lg ease-in-out delay-75 duration-300 mb-5 sm:p-2 sm:mb-3 z-50"
routerLink="/marketplace"
(click)="switchCategory(cat)"
[class.category_selected]="cat === category"
>
{{ cat }}
</ion-button>
<div
class="relative flex"
[class.tui-skeleton]="!categories"
[class.tui-skeleton_rounded]="!categories"
>
<img
alt="Category Icon"
width="24"
height="24"
class="opacity-70 invert"
src="/svg/{{ determineIcon(cat) }}.svg"
/>
</div>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
[class.tui-skeleton]="!categories"
[class.tui-skeleton_rounded]="!categories"
>
{{ (cat | titlecase) || 'loading category...' }}
</span>
</button>

View File

@@ -1,14 +1,12 @@
:host {
display: block;
.category_selected {
font-weight: bold;
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
@media (min-width: 600px) {
.category_selected {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
background-color: #222428;
opacity: 90;
}
}
}

View File

@@ -10,14 +10,11 @@ import {
selector: 'marketplace-categories',
templateUrl: 'categories.component.html',
styleUrls: ['categories.component.scss'],
host: {
class: 'hidden-scrollbar ion-text-center',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CategoriesComponent {
@Input()
categories: readonly string[] = []
categories?: string[]
@Input()
category = ''
@@ -29,4 +26,31 @@ export class CategoriesComponent {
this.category = category
this.categoryChange.emit(category)
}
determineIcon(category: string): string {
switch (category.toLowerCase()) {
case 'all':
return 'apps-outline'
case 'bitcoin':
return 'logo-bitcoin'
case 'communications':
return 'chatbubbles-outline'
case 'data':
return 'document-outline'
case 'developer tools':
return 'code-slash-outline'
case 'featured':
return 'star-outline'
case 'lightning':
return 'flash-outline'
case 'media':
return 'play-outline'
case 'networking':
return 'globe-outline'
case 'social':
return 'people-outline'
default:
return 'cube-outline'
}
}
}

View File

@@ -1,12 +1,33 @@
<ion-item class="service-card" [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start">
<img alt="" [src]="pkg | mimeType | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2 class="montserrat">
<strong>{{ pkg.manifest.title }}</strong>
</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<ng-content></ng-content>
</ion-label>
</ion-item>
<div
class="h-full relative rounded-3xl pt-20 pb-10 px-8 gap-4 shadow-lg hover:scale-90 transition duration-500 ease-in-out min-w-[300px]"
>
<!-- color background -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 -z-50 rounded-3xl bg-zinc-800"
>
<img
[src]="'data:image/png;base64,' + pkg.icon | trustUrl"
class="absolute object-cover pointer-events-none w-[150%] h-[150%] max-w-[200%] blur-[100px]"
alt="{{ pkg.manifest.title }} Icon"
/>
</div>
<!-- darkening overlay -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 -z-50 rounded-3xl bg-zinc-800 opacity-40"
></div>
<!-- icon -->
<img
[src]="'data:image/png;base64,' + pkg.icon | trustUrl"
class="w-[5.5rem] h-[5.5rem] pointer-events-none absolute -top-10 rounded-full object-cover shadow-lg z-10"
alt="{{ pkg.manifest.title }} Icon"
style="transform: none"
/>
<div class="mt-3 text-zinc-5 mix-blend-plus-lighter">
<span class="block text-2xl font-medium line-clamp-1 mb-1">
{{ pkg.manifest.title }}
</span>
<span class="block text-base h-12 line-clamp-2">
{{ pkg.manifest.description.short }}
</span>
</div>
</div>

View File

@@ -1,20 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({
declarations: [ItemComponent],
exports: [ItemComponent],
imports: [
CommonModule,
IonicModule,
RouterModule,
SharedPipesModule,
MimeTypePipeModule,
],
imports: [CommonModule, RouterModule, SharedPipesModule],
})
export class ItemModule {}

View File

@@ -1,14 +1,20 @@
<ion-grid>
<ion-row>
<ion-col responsiveCol class="column" sizeSm="8" sizeLg="6">
<ion-toolbar color="transparent" class="ion-text-left">
<ion-searchbar
[color]="(theme$ | async) === 'Light' ? 'light' : 'dark'"
debounce="250"
[ngModel]="query"
(ngModelChange)="onModelChange($event)"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>
<div class="text-zinc-100 bg-zinc-500 px-3 rounded-full">
<div class="relative flex items-center">
<img
alt="Search icon"
width="24"
height="24"
class="invert"
src="svg/search.svg"
/>
<input
type="text"
name="search"
class="mt-1 bg-transparent placeholder-zinc-300 focus:outline-none px-3 py-2"
placeholder="Search..."
autocomplete="off"
[ngModel]="query"
(ngModelChange)="onModelChange($event)"
/>
</div>
</div>

View File

@@ -1,8 +0,0 @@
:host {
display: block;
padding-bottom: 32px;
}
.column {
margin: 0 auto;
}

View File

@@ -16,7 +16,7 @@ import { THEME } from '@start9labs/shared'
})
export class SearchComponent {
@Input()
query = ''
query?: string | null = ''
@Output()
readonly queryChange = new EventEmitter<string>()

View File

@@ -1,13 +1,11 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { ResponsiveColDirective } from '@start9labs/shared'
import { SearchComponent } from './search.component'
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { ResponsiveColDirective } from "@start9labs/shared";
import { SearchComponent } from "./search.component";
@NgModule({
imports: [IonicModule, FormsModule, CommonModule, ResponsiveColDirective],
imports: [FormsModule, CommonModule, ResponsiveColDirective],
declarations: [SearchComponent],
exports: [SearchComponent],
})

View File

@@ -1,4 +1,4 @@
<div class="hidden-scrollbar ion-text-center">
<div class="">
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
<ion-skeleton-text
animated
@@ -6,17 +6,47 @@
></ion-skeleton-text>
</ion-button>
</div>
<div class="hidden sm:flex flex-col mx-6 pb-3 items-center">
<div class="pb-3">
<ion-skeleton-text style="border-radius: 100%" animated></ion-skeleton-text>
</div>
<div class="divider" style="margin: 24px 0"></div>
<ng-content select="[slot=desktop]"></ng-content>
</div>
<ion-row>
<ion-col
*ngFor="let cat of ['', '', '', '', '', '', '']"
fill="clear"
responsiveCol
sizeXs="12"
sizeSm="12"
sizeMd="3"
>
<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-label>
</ion-item>
</ion-col>
</ion-row>
<ion-grid>
<ion-grid class="p-20">
<ion-row>
<ion-col
*ngFor="let pkg of ['', '', '', '']"
responsiveCol
sizeXs="12"
sizeSm="12"
sizeMd="6"
sizeMd="3"
>
<ion-item>
<ion-thumbnail slot="start">

View File

@@ -1,33 +1,58 @@
<ion-content class="with-widgets">
<ng-container *ngIf="notes$ | async as notes; else loading">
<div *ngFor="let note of notes | keyvalue : asIsOrder">
<ion-button
expand="full"
color="light"
class="version-button"
[class.ion-activated]="isSelected(note.key)"
(click)="setSelected(note.key)"
<div
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="bg-zinc-800 rounded-xl p-7 grid grid-flow-row items-center gap-6">
<div class="block">
<h3 class="text-lg font-bold small-caps">What's new</h3>
<p
*ngIf="pkg['published-at'] as published"
class="text-base font-light mb-1 text-zinc-300"
>
<p class="version">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
tuiElement
#element="elementRef"
class="panel"
color="light"
[id]="note.key"
[style.maxHeight.px]="getDocSize(note.key, element)"
>
<ion-text
id="release-notes"
safeLinks
[innerHTML]="note.value | markdown | dompurify"
></ion-text>
</ion-card>
<span class="small-caps">Latest Release</span>
&nbsp;-&nbsp;
<span class="text-sm">{{ published | date : 'medium' }}</span>
</p>
</div>
<div class="flex flex-wrap justify-between gap-6">
<div class="text-base">
<p>Version {{ pkg.manifest.version }}</p>
<p
safeLinks
class="flex-wrap mt-1"
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
></p>
</div>
<button
tuiButton
type="button"
appearance="secondary"
size="m"
class="mt-3 place-self-end sm:place-self-start md:place-self-start lg:place-self-end"
(click)="showReleaseNotes(template)"
>
Previous releases
</button>
</div>
</div>
</div>
<ng-template #template let-observer>
<ng-container *ngIf="notes$ | async as notes; else loading">
<tui-accordion
class="max-w-lg"
[closeOthers]="false"
*ngFor="let note of notes | keyvalue : asIsOrder"
>
<tui-accordion-item class="my-1">
{{ note.key | displayEmver }}
<ng-template tuiAccordionItemContent>
<p safeLinks [innerHTML]="note.value | markdown | dompurify"></p>
</ng-template>
</tui-accordion-item>
</tui-accordion>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>
</ng-template>

View File

@@ -1,23 +0,0 @@
:host {
flex: 1 1 0;
}
.panel {
margin: 0;
padding: 0 24px;
transition: max-height 0.2s ease-out;
}
.active {
border: 5px solid #4d4d4d;
}
.version-button {
height: 50px;
margin: 1px;
}
.version {
position: absolute;
left: 10px;
}

View File

@@ -1,7 +1,15 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
import { PolymorpheusContent } from '@tinkoff/ng-polymorpheus'
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
import { MarketplacePkg } from '../../types'
import { Observable } from 'rxjs'
@Component({
selector: 'release-notes',
@@ -13,27 +21,29 @@ export class ReleaseNotesComponent {
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
) {}
private readonly pkgId = getPkgId(this.route)
@Input({ required: true })
pkg!: MarketplacePkg
private selected: string | null = null
notes$!: Observable<Record<string, string>>
readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId)
isSelected(key: string): boolean {
return this.selected === key
}
setSelected(selected: string) {
this.selected = this.isSelected(selected) ? null : selected
}
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
return this.isSelected(key) ? nativeElement.scrollHeight : 0
ngOnChanges() {
this.notes$ = this.marketplaceService.fetchReleaseNotes$(
this.pkg.manifest.id,
)
}
asIsOrder(a: any, b: any) {
return 0
}
async showReleaseNotes(content: PolymorpheusContent<TuiDialogContext>) {
this.dialogs
.open(content, {
label: 'Previous Release Notes',
})
.subscribe()
}
}

View File

@@ -1,27 +1,26 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import {
EmverPipesModule,
MarkdownPipeModule,
SafeLinksDirective,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import { TuiElementModule } from '@taiga-ui/cdk'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { ReleaseNotesComponent } from './release-notes.component'
} from "@start9labs/shared";
import { NgDompurifyModule } from "@tinkoff/ng-dompurify";
import { ReleaseNotesComponent } from "./release-notes.component";
import { TuiButtonModule } from "@taiga-ui/core";
import { TuiAccordionModule } from "@taiga-ui/kit";
@NgModule({
imports: [
CommonModule,
IonicModule,
TextSpinnerComponentModule,
EmverPipesModule,
MarkdownPipeModule,
TuiElementModule,
NgDompurifyModule,
SafeLinksDirective,
TuiButtonModule,
TuiAccordionModule,
],
declarations: [ReleaseNotesComponent],
exports: [ReleaseNotesComponent],

View File

@@ -1,32 +1,21 @@
<!-- release notes -->
<ion-item-divider>
New in {{ pkg.manifest.version | displayEmver }}
</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div
safeLinks
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
></div>
</ion-label>
</ion-item>
<ion-button routerLink="notes" fill="clear" strong>
Past Release Notes
<ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>
<!-- description -->
<ion-item-divider>Description</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<h2>{{ pkg.manifest.description.long }}</h2>
</ion-label>
</ion-item>
<div
*ngIf="pkg.manifest['marketing-site'] as url"
style="padding: 4px 0 10px 14px"
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10"
>
<ion-button [href]="url" target="_blank" rel="noreferrer" color="tertiary">
View website
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
<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 pb-3">Description</h2>
<p class="text-base mb-3">
{{ pkg.manifest.description.long }}
</p>
<ng-container *ngIf="pkg.manifest.replaces as replaces">
<div *ngIf="replaces.length">
<h2 class="text-lg font-bold small-caps pb-3">Intended to replace</h2>
<tui-tag
*ngFor="let app; index as i; of: replaces"
size="l"
[class]="i > 0 ? 'ml-1.5 mt-2' : 'mt-2'"
[value]="app"
></tui-tag>
</div>
</ng-container>
</div>
</div>

View File

@@ -1,23 +1,16 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
MarkdownPipeModule,
SafeLinksDirective,
} from '@start9labs/shared'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { AboutComponent } from './about.component'
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { AboutComponent } from "./about.component";
import { TuiTagModule } from "@taiga-ui/kit";
import { NgDompurifyModule } from "@tinkoff/ng-dompurify";
import { SafeLinksDirective } from "@start9labs/shared";
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
MarkdownPipeModule,
EmverPipesModule,
TuiTagModule,
NgDompurifyModule,
SafeLinksDirective,
],

View File

@@ -0,0 +1,12 @@
<a [href]="url" target="_blank" rel="noreferrer">
<div class="flex justify-between items-center">
<label [tuiLabel]="label" class="hover:cursor-pointer">
<tui-line-clamp [content]="url" [linesLimit]="1"></tui-line-clamp>
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/open-outline.svg"
/>
</div>
</a>

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { AdditionalLinkComponent } from './additional-link.component'
import { TuiLabelModule } from '@taiga-ui/core'
import { TuiLineClampModule } from '@taiga-ui/kit'
@NgModule({
imports: [CommonModule, TuiLabelModule, TuiLineClampModule],
declarations: [AdditionalLinkComponent],
exports: [AdditionalLinkComponent],
})
export class AdditionalLinkModule {}

View File

@@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { Url } from '@start9labs/shared'
@Component({
selector: 'marketplace-additional-link',
templateUrl: 'additional-link.component.html',
styleUrls: ['additional-link.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdditionalLinkComponent {
@Input({ required: true })
url!: Url
@Input({ required: true })
label!: string
}

View File

@@ -1,131 +1,128 @@
<ng-container *ngIf="pkg.manifest.replaces as replaces">
<div *ngIf="replaces.length" class="ion-padding-bottom">
<ion-item-divider>Intended to replace</ion-item-divider>
<ul>
<li *ngFor="let app of replaces">
{{ app }}
</li>
</ul>
</div>
</ng-container>
<div
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10"
>
<div class="bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps pb-3">Information</h2>
<div class="grid grid-flow-row divide-y divide-zinc-500">
<!-- git hash -->
<div
*ngIf="pkg.manifest['git-hash'] as gitHash; else noHash"
button
detail="false"
class="py-3 px-1 flex flex-wrap justify-between items-center"
(click)="copyService.copy(gitHash)"
>
<label tuiLabel="Git Hash">{{ gitHash }}</label>
<img
alt="Copy Icon"
class="block opacity-70 invert w-4 hover:cursor-copy"
src="svg/copy-outline.svg"
/>
</div>
<ng-template #noHash>
<div class="py-3 px-1">
<label tuiLabel="Git Hash">Unknown</label>
</div>
</ng-template>
<!-- license -->
<div
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
(click)="presentModalMd('License')"
>
<div class="flex flex-wrap justify-between items-center">
<label tuiLabel="License" class="hover:cursor-pointer">
{{ pkg.manifest.license }}
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/chevron-forward.svg"
/>
</div>
</div>
<!-- instructions -->
<div
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
(click)="presentModalMd('Instructions')"
>
<div class="flex flex-wrap justify-between items-center">
<label tuiLabel="Instructions" class="hover:cursor-pointer">
Click to view instructions
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/chevron-forward.svg"
/>
</div>
</div>
<ion-item-divider>Additional Info</ion-item-divider>
<ion-grid *ngIf="pkg.manifest as manifest">
<ion-row>
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
*ngIf="manifest['git-hash'] as gitHash; else noHash"
button
detail="false"
(click)="copyService.copy(gitHash)"
>
<ion-label>
<h2>Git Hash</h2>
<p>{{ gitHash }}</p>
</ion-label>
<ion-icon slot="end" name="copy-outline"></ion-icon>
</ion-item>
<ng-template #noHash>
<ion-item>
<ion-label>
<h2>Git Hash</h2>
<p>Unknown</p>
</ion-label>
</ion-item>
<!-- versions -->
<div
(click)="presentAlertVersions(version)"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
>
<div class="flex flex-wrap justify-between items-center">
<label tuiLabel="Other versions" class="hover:cursor-pointer">
Click to view other versions
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/chevron-forward.svg"
/>
</div>
<ng-template #version let-data="data" let-completeWith="completeWith">
<tui-radio-list
class="radio"
size="l"
[items]="data.items"
[itemContent]="displayEmver | tuiStringifyContent"
[(ngModel)]="data.value"
></tui-radio-list>
<footer class="buttons">
<button
tuiButton
appearance="secondary"
(click)="completeWith(null)"
>
Cancel
</button>
<button
tuiButton
appearance="secondary"
(click)="completeWith(data.value)"
>
Ok
</button>
</footer>
</ng-template>
<ion-item button detail="false" (click)="presentAlertVersions(version)">
<ion-label>
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
<ng-template #version let-data="data" let-completeWith="completeWith">
<tui-radio-list
class="radio"
size="l"
[items]="data.items"
[itemContent]="displayEmver | tuiStringifyContent"
[(ngModel)]="data.value"
></tui-radio-list>
<footer class="buttons">
<button
tuiButton
appearance="secondary"
(click)="completeWith(null)"
>
Cancel
</button>
<button
tuiButton
appearance="secondary"
(click)="completeWith(data.value)"
>
Ok
</button>
</footer>
</ng-template>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('License')">
<ion-label>
<h2>License</h2>
<p>{{ manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('Instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="manifest['upstream-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ manifest['upstream-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest['wrapper-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ manifest['wrapper-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest['support-site']"
[disabled]="!manifest['support-site']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ manifest['support-site'] || 'Not provided' }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid>
</div>
<!-- links -->
<marketplace-additional-link
[url]="pkg.manifest['marketing-site']"
*ngIf="pkg.manifest['marketing-site']"
label="Marketing Site"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
<marketplace-additional-link
[url]="pkg.manifest['upstream-repo']"
*ngIf="pkg.manifest['upstream-repo']"
label="Source Repository"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
<marketplace-additional-link
[url]="pkg.manifest['wrapper-repo']"
*ngIf="pkg.manifest['wrapper-repo']"
label="Wrapper Repository"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
<marketplace-additional-link
[url]="pkg.manifest['support-site']"
*ngIf="pkg.manifest['support-site']"
label="Support Site"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
</div>
</div>
</div>

View File

@@ -1,10 +0,0 @@
.radio {
display: block;
margin: 1rem 0;
}
.buttons {
display: flex;
justify-content: flex-end;
gap: 1rem;
}

View File

@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
EventEmitter,
inject,
Input,
Output,
TemplateRef,
@@ -38,13 +39,13 @@ export class AdditionalComponent {
version = new EventEmitter<string>()
readonly displayEmver = displayEmver
private readonly marketplaceService = inject(AbstractMarketplaceService)
constructor(
readonly copyService: CopyService,
private readonly alerts: TuiAlertService,
private readonly dialogs: TuiDialogService,
private readonly emver: Emver,
private readonly marketplaceService: AbstractMarketplaceService,
private readonly route: ActivatedRoute,
) {}

View File

@@ -1,26 +1,25 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { MarkdownModule, ResponsiveColDirective } from '@start9labs/shared'
import { AdditionalComponent } from './additional.component'
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { AdditionalComponent } from "./additional.component";
import {
TuiRadioListModule,
TuiStringifyContentPipeModule,
} from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms'
import { TuiButtonModule } from '@taiga-ui/core'
} from "@taiga-ui/kit";
import { FormsModule } from "@angular/forms";
import { TuiButtonModule, TuiLabelModule } from "@taiga-ui/core";
import { AdditionalLinkModule } from "./additional-link/additional-link.component.module";
import { ResponsiveColDirective } from "@start9labs/shared";
@NgModule({
imports: [
CommonModule,
IonicModule,
MarkdownModule,
ResponsiveColDirective,
TuiRadioListModule,
FormsModule,
TuiStringifyContentPipeModule,
TuiButtonModule,
TuiLabelModule,
AdditionalLinkModule,
],
declarations: [AdditionalComponent],
exports: [AdditionalComponent],

View File

@@ -1,35 +1,30 @@
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
responsiveCol
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img
alt=""
style="border-radius: 100%"
[src]="getImg(dep.key) | trustUrl"
/>
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
<div
class="bg-zinc-700/40 rounded-xl py-3 px-5 gap-2 drop-shadow-lg hover:bg-zinc-700/70"
>
<div class="flex items-center gap-6">
<tui-avatar
class="w-16 pointer-events-none rounded-full object-cover drop-shadow-lg"
[src]="getImage(dep.key)"
></tui-avatar>
<div class="mt-3">
<div class="flex flex-wrap items-center gap-1 mb-1">
<span class="block text-base font-medium text-zinc-50/90 line-clamp-1">
{{ getTitle(dep.key) }}
</span>
<p>
<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>
</p>
</div>
<span class="text-sm text-zinc-50/70 line-clamp-1">
{{ dep.value.version | displayEmver }}
</span>
<span class="text-sm text-zinc-50/70 h-11 line-clamp-2">
{{ dep.value.description }}
</span>
</div>
</div>
</div>

View File

@@ -1,17 +1,27 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types'
import { Dependency, MarketplacePkg } from '../../../types'
import { KeyValue } from '@angular/common'
@Component({
selector: 'marketplace-dependencies',
templateUrl: 'dependencies.component.html',
styleUrls: ['./dependencies.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DependenciesComponent {
@Input({ required: true })
pkg!: MarketplacePkg
getImg(key: string): string {
@Input({ required: true })
dep!: KeyValue<string, Dependency>
getImage(key: string): string {
const icon = this.pkg['dependency-metadata'][key]?.icon
// @TODO fix when registry api is updated to include mimetype in icon url
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
return icon ? `data:image/png;base64,${icon}` : key.substring(0, 2)
}
getTitle(key: string): string {
return this.pkg['dependency-metadata'][key]?.title || key
}
}

View File

@@ -1,23 +1,16 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
ResponsiveColDirective,
SharedPipesModule,
} from '@start9labs/shared'
import { EmverPipesModule, ResponsiveColDirective } from '@start9labs/shared'
import { DependenciesComponent } from './dependencies.component'
import { TuiAvatarModule } from '@taiga-ui/experimental'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
ResponsiveColDirective,
TuiAvatarModule,
EmverPipesModule,
],
declarations: [DependenciesComponent],
exports: [DependenciesComponent],

View File

@@ -0,0 +1,55 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared'
import { MarketplacePkg } from '../../../types'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@Component({
selector: 'marketplace-package-hero',
template: `
<div class="flex justify-center mt-10 md:mt-0 z-0">
<div
class="flex flex-col w-full h-[32vh] xs:h-[26vh] md:min-h-[14rem] relative rounded-3xl pt-16 px-8 shadow-lg"
>
<!-- icon -->
<img
[src]="pkg | mimeType | trustUrl"
class="w-24 h-24 pointer-events-none rounded-full object-cover shadow-lg absolute -top-9 left-7 z-10"
alt="{{ pkg.manifest.title }} Icon"
/>
<!-- color background -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 rounded-3xl bg-zinc-800"
>
<img
[src]="pkg | mimeType | trustUrl"
class="absolute object-cover pointer-events-none w-[200%] h-[200%] max-w-[200%] blur-[100px] saturate-150 rounded-full"
alt="{{ pkg.manifest.title }} background image"
/>
</div>
<!-- background darkening overlay -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 rounded-3xl bg-zinc-700 opacity-70"
></div>
<div class="my-4 text-zinc-50 mix-blend-plus-lighter">
<h2 class="text-2xl font-medium line-clamp-1 mb-1">
{{ pkg.manifest.title }}
</h2>
<p class="block text-base line-clamp-2">
{{ pkg.manifest.description.short }}
</p>
</div>
<!-- control buttons -->
<ng-content></ng-content>
</div>
</div>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, SharedPipesModule, MimeTypePipeModule],
})
export class MarketplacePackageHeroComponent {
@Input({ required: true })
pkg!: MarketplacePkg
}

View File

@@ -1,9 +0,0 @@
<img class="logo" alt="" [src]="pkg | mimeType | trustUrl" />
<div class="text">
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<p *ngIf="pkg['published-at'] as published" class="published">
Released: {{ published | date : 'medium' }}
</p>
<ng-content></ng-content>
</div>

View File

@@ -1,48 +0,0 @@
:host {
display: flex;
align-items: flex-start;
padding: 16px;
line-height: 2;
}
.text {
display: inline-block;
vertical-align: top;
overflow: hidden;
}
.logo {
width: 80px;
margin-right: 16px;
border-radius: 100%;
}
.title {
margin: 0 0 0 -2px;
font-size: 36px;
}
.version {
margin: 0;
font-size: 18px;
}
.published {
margin: 0;
padding: 4px 0 12px 0;
font-style: italic;
}
@media (min-width: 1000px) {
.logo {
width: 140px;
}
.title {
font-size: 64px;
}
.version {
font-size: 32px;
}
}

View File

@@ -1,13 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types'
@Component({
selector: 'marketplace-package',
templateUrl: 'package.component.html',
styleUrls: ['package.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PackageComponent {
@Input({ required: true })
pkg!: MarketplacePkg
}

View File

@@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
SharedPipesModule,
TickerModule,
} from '@start9labs/shared'
import { PackageComponent } from './package.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({
declarations: [PackageComponent],
exports: [PackageComponent],
imports: [
CommonModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
TickerModule,
MimeTypePipeModule,
],
})
export class PackageModule {}

View File

@@ -0,0 +1,97 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
} from '@angular/core'
import {
TuiButtonModule,
TuiDialogContext,
TuiDialogService,
} from '@taiga-ui/core'
import { TuiCarouselModule } from '@taiga-ui/kit'
import { MarketplacePkg } from '../../../types'
import { PolymorpheusContent } from '@tinkoff/ng-polymorpheus'
import { isPlatform } from '@ionic/angular'
@Component({
selector: 'marketplace-package-screenshots',
template: `
<div
*ngIf="pkg.screenshots"
tuiCarouselButtons
class="flex items-center content-center m-0 lg:-ml-14 lg:-mr-14 lg:min-h-80 lg:h-80 2xl:h-full"
>
<button
tuiIconButton
appearance="flat"
icon="tuiIconChevronLeftLarge"
title="Previous"
type="button"
(click)="carousel.prev()"
></button>
<tui-carousel
#carousel
[itemsCount]="isMobile ? 1 : 2"
[(index)]="index"
class="overflow-y-hidden overflow-x-scroll carousel overflow-hidden"
>
<ng-container *ngFor="let item of pkg.screenshots; let i = index">
<div
*tuiItem
draggable="false"
[class.item_active]="i === index + 1"
class="object-cover overflow-hidden rounded-lg md:rounded-xl border border-zinc-400/30 hover:cursor-pointer shadow-lg shadow-zinc-400/10"
>
<img
#template
alt="Service screenshot"
src="assets/img/temp/{{ item }}"
class="w-full h-full rounded-lg md:rounded-xl"
(click)="presentModalImg(dialogTemplate)"
/>
<ng-template #dialogTemplate let-observer>
<img
alt="Service screenshot"
src="assets/img/temp/{{ item }}"
class="rounded-none"
/>
</ng-template>
</div>
</ng-container>
</tui-carousel>
<button
tuiIconButton
appearance="flat"
type="button"
icon="tuiIconChevronRightLarge"
title="Next"
(click)="carousel.next()"
></button>
</div>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, TuiCarouselModule, TuiButtonModule],
})
export class MarketplacePackageScreenshotComponent {
@Input({ required: true })
pkg!: MarketplacePkg
constructor(
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
) {}
index = 0
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
presentModalImg(content: PolymorpheusContent<TuiDialogContext>) {
this.dialogs
.open(content, {
size: 'l',
})
.subscribe()
}
}

View File

@@ -8,8 +8,8 @@ import Fuse from 'fuse.js'
export class FilterPackagesPipe implements PipeTransform {
transform(
packages: MarketplacePkg[],
query: string,
category: string,
query: string | null,
category: string | null,
): MarketplacePkg[] {
// query
if (query) {
@@ -68,13 +68,14 @@ export class FilterPackagesPipe implements PipeTransform {
// category
return packages
.filter(p => category === 'all' || p.categories.includes(category))
.filter(p => category === 'all' || p.categories.includes(category!))
.sort((a, b) => {
return (
new Date(b['published-at']).valueOf() -
new Date(a['published-at']).valueOf()
)
})
.map(a => ({ ...a }))
}
}

View File

@@ -8,8 +8,6 @@ export * from './pages/list/item/item.component'
export * from './pages/list/item/item.module'
export * from './pages/list/search/search.component'
export * from './pages/list/search/search.module'
export * from './pages/list/skeleton/skeleton.component'
export * from './pages/list/skeleton/skeleton.module'
export * from './pages/release-notes/release-notes.component'
export * from './pages/release-notes/release-notes.module'
export * from './pages/show/about/about.component'
@@ -18,8 +16,8 @@ export * from './pages/show/additional/additional.component'
export * from './pages/show/additional/additional.module'
export * from './pages/show/dependencies/dependencies.component'
export * from './pages/show/dependencies/dependencies.module'
export * from './pages/show/package/package.component'
export * from './pages/show/package/package.module'
export * from './pages/show/screenshots/screenshots.component'
export * from './pages/show/hero/hero.component'
export * from './pipes/filter-packages.pipe'
export * from './pipes/mime-type.pipe'
@@ -27,7 +25,10 @@ export * from './pipes/mime-type.pipe'
export * from './components/store-icon/store-icon.component'
export * from './components/store-icon/store-icon.component.module'
export * from './components/store-icon/store-icon.component'
export * from './components/menu/menu.component.module'
export * from './components/menu/menu.component'
export * from './services/marketplace.service'
export * from './services/category.service'
export * from './types'

View File

@@ -0,0 +1,16 @@
import { BehaviorSubject, Observable } from 'rxjs'
export abstract class AbstractCategoryService {
readonly category$ = new BehaviorSubject<string>('all')
readonly query$ = new BehaviorSubject<string>('')
abstract getCategory$(): Observable<string>
abstract changeCategory(category: string): void
abstract setQuery(query: string): void
abstract getQuery$(): Observable<string>
abstract resetQuery(): void
}

View File

@@ -1,5 +1,11 @@
import { Observable } from 'rxjs'
import { MarketplacePkg, Marketplace, StoreData, StoreIdentity } from '../types'
import {
Marketplace,
MarketplacePkg,
StoreData,
StoreIdentity,
StoreIdentityWithData,
} from '../types'
export abstract class AbstractMarketplaceService {
abstract getKnownHosts$(): Observable<StoreIdentity[]>
@@ -10,6 +16,8 @@ export abstract class AbstractMarketplaceService {
abstract getSelectedStore$(): Observable<StoreData>
abstract getSelectedStoreWithCategories$(): Observable<StoreIdentityWithData>
abstract getPackage$(
id: string,
version: string,

View File

@@ -19,9 +19,12 @@ export interface StoreInfo {
categories: string[]
}
export type StoreIdentityWithData = StoreData & StoreIdentity
export interface MarketplacePkg {
icon: Url
license: Url
screenshots?: string[]
instructions: Url
manifest: Manifest
categories: string[]

View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,ts}'],
theme: {
extend: {},
fontFamily: {
sans: ['Montserrat', 'sans-serif'],
},
},
plugins: [],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,5 @@
<svg width="1369" height="1369" viewBox="0 0 1369 1369" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M245.229 1154.11C218.012 1203.36 279.4 1235.04 304.523 1188.79C315.173 1169.19 641.573 388.267 681.128 296.285L1061.75 1185.9C1087.02 1239.85 1150.2 1200.35 1126.87 1149.29L719.161 220.202C699.384 177.981 658.308 177.981 640.052 221.71C638.53 224.726 260.899 1125.74 245.229 1154.11Z" fill="#F0F0F0"/>
<path d="M173.678 1079.24C99.1497 984.791 52.8134 871.532 39.9236 752.308C27.0337 633.084 48.106 512.665 100.751 404.708C153.396 296.75 235.507 205.573 337.773 141.516C440.038 77.4585 558.367 43.0845 679.34 42.2916C800.313 41.4988 919.089 74.3187 1022.2 137.03C1125.31 199.741 1208.63 289.834 1262.71 397.092C1316.79 504.35 1339.47 624.482 1328.17 743.864C1316.88 863.247 1272.05 977.103 1198.79 1072.52L1197.54 1071.58C1270.62 976.389 1315.33 862.81 1326.6 743.718C1337.87 624.627 1315.25 504.788 1261.3 397.791C1207.35 290.794 1124.24 200.921 1021.38 138.362C918.516 75.804 800.028 43.064 679.35 43.8549C558.672 44.6459 440.632 78.9361 338.615 142.837C236.599 206.738 154.688 297.693 102.171 405.388C49.6544 513.082 28.6335 633.208 41.4919 752.141C54.3504 871.075 100.574 984.058 174.92 1078.28L173.678 1079.24Z" stroke="#F0F0F0" stroke-width="65.4186" stroke-linejoin="round"/>
<path d="M393.951 1258.16C484.768 1302.62 583.891 1326.42 685.181 1325.58C786.471 1324.73 886.158 1302.21 976.206 1256.23L976.206 1254.3C886.377 1300.17 786.211 1323.17 685.167 1324.01C584.124 1324.86 484.546 1300.59 393.951 1256.23L393.951 1258.16Z" stroke="#F0F0F0" stroke-width="65.4186" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -1,10 +1,10 @@
{
"name": "@start9labs/shared",
"version": "0.3.4",
"version": "0.3.5",
"peerDependencies": {
"@angular/common": ">=13.2.0",
"@angular/core": ">=13.2.0",
"@angular/router": ">=13.2.0",
"@angular/common": "^14.1.0",
"@angular/core": "^14.1.0",
"@angular/router": "^14.1.0",
"@ionic/angular": ">=6.0.0",
"@ng-web-apis/mutation-observer": ">=2.0.0",
"@ng-web-apis/resize-observer": ">=2.0.0",

View File

@@ -0,0 +1,34 @@
import { Pipe, PipeTransform } from '@angular/core'
@Pipe({
name: 'sort',
})
export class SortPipe implements PipeTransform {
transform(
value: any[],
column: string = '',
direction: string = 'asc',
): any[] {
// If the value is not an array or is empty, return the original value
if (!Array.isArray(value) || value.length === 0) {
return value
}
// Clone the array to avoid modifying the original value
const sortedValue = [...value]
// Define the sorting function based on the column and direction parameters
const sortingFn = (a: any, b: any): number => {
if (a[column] < b[column]) {
return direction === 'asc' ? -1 : 1
} else if (a[column] > b[column]) {
return direction === 'asc' ? 1 : -1
} else {
return 0
}
}
// Sort the array and return the result
return sortedValue.sort(sortingFn)
}
}

View File

@@ -30,6 +30,143 @@
[tuiAppearance][data-appearance='outline'] {
color: var(--tui-text-01);
}
[tuiWrapper][data-appearance='primary-solid'] {
background: #3880ff;
color: #fff;
.wrapper-hover {
background: #4c8dff;
}
;
.wrapper-active {
background: #3171e0;
}
;
.wrapper-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='secondary-solid'] {
background: #3dc2ff;
color: #fff;
.wrapper-hover {
background: #50c8ff;
}
;
.wrapper-active {
background: #36abe0;
}
;
.wrapper-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='tertiary-solid'] {
background: #5260ff;
color: #fff;
.wrapper-hover {
background: #6370ff;
}
;
.wrapper-active {
background: #4854e0;
}
;
.wrapper-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='success-solid'] {
background: #2dd36f;
color: #fff;
.wrapper-hover {
background: #42d77d;
}
;
.wrapper-active {
background: #28ba62;
}
;
.wrapper-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='warning-solid'] {
background: #ffc409;
color: #fff;
.wrapper-hover {
background: #ffca22;
}
;
.wrapper-active {
background: #e0ac08;
}
;
.wrapper-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='danger-solid'] {
background: #eb445a;
color: #fff;
.wrapper-hover {
background: #ed576b;
}
;
.wrapper-active {
background: #cf3c4f;
}
;
.wrapper-disabled {
background: #eaecee;
}
;
}
[tuiWrapper][data-appearance='input-file'] {
&:hover,

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) {}
}

Some files were not shown because too many files have changed in this diff Show More