mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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
8164
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
1
web/projects/marketplace/index.ts
Normal file
1
web/projects/marketplace/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src/public-api'
|
||||
@@ -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": {
|
||||
|
||||
145
web/projects/marketplace/src/components/menu/menu.component.html
Normal file
145
web/projects/marketplace/src/components/menu/menu.component.html
Normal 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>
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<img
|
||||
*ngIf="icon; else noIcon"
|
||||
class="rounded-full"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt="Service Icon"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.column {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { THEME } from '@start9labs/shared'
|
||||
})
|
||||
export class SearchComponent {
|
||||
@Input()
|
||||
query = ''
|
||||
query?: string | null = ''
|
||||
|
||||
@Output()
|
||||
readonly queryChange = new EventEmitter<string>()
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
-
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.radio {
|
||||
display: block;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
16
web/projects/marketplace/src/services/category.service.ts
Normal file
16
web/projects/marketplace/src/services/category.service.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]
|
||||
|
||||
11
web/projects/marketplace/tailwind.config.js
Normal file
11
web/projects/marketplace/tailwind.config.js
Normal 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.
BIN
web/projects/shared/assets/img/background.png
Normal file
BIN
web/projects/shared/assets/img/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
BIN
web/projects/shared/assets/img/icon_transparent.png
Normal file
BIN
web/projects/shared/assets/img/icon_transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
5
web/projects/shared/assets/img/icon_transparent.svg
Normal file
5
web/projects/shared/assets/img/icon_transparent.svg
Normal 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 |
BIN
web/projects/shared/assets/img/temp/one.png
Normal file
BIN
web/projects/shared/assets/img/temp/one.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
BIN
web/projects/shared/assets/img/temp/three.png
Normal file
BIN
web/projects/shared/assets/img/temp/three.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 329 KiB |
BIN
web/projects/shared/assets/img/temp/two.png
Normal file
BIN
web/projects/shared/assets/img/temp/two.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -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",
|
||||
|
||||
34
web/projects/shared/src/pipes/shared/sort.pipe.ts
Normal file
34
web/projects/shared/src/pipes/shared/sort.pipe.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -18,6 +18,7 @@ tui-root {
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
transition: filter 0.3s;
|
||||
|
||||
&_offline {
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -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) }} </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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SideloadPackageComponent } from './package.component'
|
||||
[style.border-radius.%]="100"
|
||||
[style.float]="'right'"
|
||||
(click)="clear()"
|
||||
class="justify-self-end"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
.status {
|
||||
font-size: calc(16px + 1vw);
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
img {
|
||||
border-radius: 100%;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user