rename frontend to web

This commit is contained in:
Matt Hill
2023-11-13 15:59:16 -07:00
parent 09303ab2fb
commit 862ca375ee
869 changed files with 0 additions and 66 deletions

View File

@@ -0,0 +1,9 @@
<img
*ngIf="icon; else noIcon"
[style.max-width]="size || '100%'"
[src]="icon"
alt="Service Icon"
/>
<ng-template #noIcon>
<ion-icon name="storefront-outline" [style.font-size]="size"></ion-icon>
</ng-template>

View File

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

View File

@@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
@Component({
selector: 'store-icon',
templateUrl: './store-icon.component.html',
styleUrls: ['./store-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StoreIconComponent {
@Input()
url = ''
@Input()
size?: string
@Input({ required: true })
marketplace!: MarketplaceConfig
get icon() {
const { start9, community } = this.marketplace
if (sameUrl(this.url, start9)) {
return 'assets/img/icon_transparent.png'
} else if (sameUrl(this.url, community)) {
return 'assets/img/community-store.png'
}
return null
}
}

View File

@@ -0,0 +1,9 @@
<ion-button
*ngFor="let cat of categories"
fill="clear"
class="category"
[class.category_selected]="cat === category"
(click)="switchCategory(cat)"
>
{{ cat }}
</ion-button>

View File

@@ -0,0 +1,14 @@
:host {
display: block;
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
}
}

View File

@@ -0,0 +1,32 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
@Component({
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[] = []
@Input()
category = ''
@Output()
readonly categoryChange = new EventEmitter<string>()
switchCategory(category: string): void {
this.category = category
this.categoryChange.emit(category)
}
}

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { CategoriesComponent } from './categories.component'
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [CategoriesComponent],
exports: [CategoriesComponent],
})
export class CategoriesModule {}

View File

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

View File

@@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types'
@Component({
selector: 'marketplace-item',
templateUrl: 'item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemComponent {
@Input({ required: true })
pkg!: MarketplacePkg
}

View File

@@ -0,0 +1,20 @@
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,
],
})
export class ItemModule {}

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
inject,
Input,
Output,
} from '@angular/core'
import { THEME } from '@start9labs/shared'
@Component({
selector: 'marketplace-search',
templateUrl: 'search.component.html',
styleUrls: ['search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchComponent {
@Input()
query = ''
@Output()
readonly queryChange = new EventEmitter<string>()
readonly theme$ = inject(THEME)
onModelChange(query: string) {
this.query = query
this.queryChange.emit(query)
}
}

View File

@@ -0,0 +1,14 @@
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'
@NgModule({
imports: [IonicModule, FormsModule, CommonModule, ResponsiveColDirective],
declarations: [SearchComponent],
exports: [SearchComponent],
})
export class SearchModule {}

View File

@@ -0,0 +1,39 @@
<div class="hidden-scrollbar ion-text-center">
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
<ion-skeleton-text
animated
style="width: 80px; border-radius: 0"
></ion-skeleton-text>
</ion-button>
</div>
<div class="divider" style="margin: 24px 0"></div>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let pkg of ['', '', '', '']"
responsiveCol
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text
style="border-radius: 100%"
animated
></ion-skeleton-text>
</ion-thumbnail>
<ion-label>
<ion-skeleton-text
animated
style="width: 150px; height: 18px; margin-bottom: 8px"
></ion-skeleton-text>
<ion-skeleton-text animated style="width: 400px"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 100px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

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

View File

@@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { ResponsiveColDirective } from '@start9labs/shared'
import { SkeletonComponent } from './skeleton.component'
@NgModule({
imports: [CommonModule, IonicModule, ResponsiveColDirective],
declarations: [SkeletonComponent],
exports: [SkeletonComponent],
})
export class SkeletonModule {}

View File

@@ -0,0 +1,33 @@
<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)"
>
<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>
</div>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>

View File

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

View File

@@ -0,0 +1,39 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
@Component({
selector: 'release-notes',
templateUrl: './release-notes.component.html',
styleUrls: ['./release-notes.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesComponent {
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
private readonly pkgId = getPkgId(this.route)
private selected: string | null = null
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
}
asIsOrder(a: any, b: any) {
return 0
}
}

View File

@@ -0,0 +1,29 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
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'
@NgModule({
imports: [
CommonModule,
IonicModule,
TextSpinnerComponentModule,
EmverPipesModule,
MarkdownPipeModule,
TuiElementModule,
NgDompurifyModule,
SafeLinksDirective,
],
declarations: [ReleaseNotesComponent],
exports: [ReleaseNotesComponent],
})
export class ReleaseNotesModule {}

View File

@@ -0,0 +1,32 @@
<!-- 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"
>
<ion-button [href]="url" target="_blank" rel="noreferrer" color="tertiary">
View website
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
</div>

View File

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

View File

@@ -0,0 +1,27 @@
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'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
MarkdownPipeModule,
EmverPipesModule,
NgDompurifyModule,
SafeLinksDirective,
],
declarations: [AboutComponent],
exports: [AboutComponent],
})
export class AboutModule {}

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
TemplateRef,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import {
TuiAlertService,
TuiDialogContext,
TuiDialogService,
} from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import {
CopyService,
copyToClipboard,
displayEmver,
Emver,
MarkdownComponent,
} from '@start9labs/shared'
import { filter } from 'rxjs'
import { MarketplacePkg } from '../../../types'
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
@Component({
selector: 'marketplace-additional',
templateUrl: 'additional.component.html',
styleUrls: ['additional.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdditionalComponent {
@Input({ required: true })
pkg!: MarketplacePkg
@Output()
version = new EventEmitter<string>()
readonly displayEmver = displayEmver
constructor(
readonly copyService: CopyService,
private readonly alerts: TuiAlertService,
private readonly dialogs: TuiDialogService,
private readonly emver: Emver,
private readonly marketplaceService: AbstractMarketplaceService,
private readonly route: ActivatedRoute,
) {}
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
presentAlertVersions(version: TemplateRef<TuiDialogContext>) {
this.dialogs
.open<string>(version, {
label: 'Versions',
size: 's',
data: {
value: this.pkg.manifest.version,
items: this.pkg.versions.sort(
(a, b) => -1 * (this.emver.compare(a, b) || 0),
),
},
})
.pipe(filter(Boolean))
.subscribe(version => this.version.emit(version))
}
presentModalMd(label: string) {
this.dialogs
.open(new PolymorpheusComponent(MarkdownComponent), {
label,
size: 'l',
data: {
content: this.marketplaceService.fetchStatic$(
this.pkg.manifest.id,
label.toLowerCase(),
this.url,
),
},
})
.subscribe()
}
}

View File

@@ -0,0 +1,28 @@
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 {
TuiRadioListModule,
TuiStringifyContentPipeModule,
} from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms'
import { TuiButtonModule } from '@taiga-ui/core'
@NgModule({
imports: [
CommonModule,
IonicModule,
MarkdownModule,
ResponsiveColDirective,
TuiRadioListModule,
FormsModule,
TuiStringifyContentPipeModule,
TuiButtonModule,
],
declarations: [AdditionalComponent],
exports: [AdditionalComponent],
})
export class AdditionalModule {}

View File

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

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types'
@Component({
selector: 'marketplace-dependencies',
templateUrl: 'dependencies.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DependenciesComponent {
@Input({ required: true })
pkg!: MarketplacePkg
getImg(key: string): string {
// @TODO fix when registry api is updated to include mimetype in icon url
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
}
}

View File

@@ -0,0 +1,25 @@
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 { DependenciesComponent } from './dependencies.component'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
ResponsiveColDirective,
],
declarations: [DependenciesComponent],
exports: [DependenciesComponent],
})
export class DependenciesModule {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
import { NgModule, Pipe, PipeTransform } from '@angular/core'
import { MarketplacePkg } from '../types'
import Fuse from 'fuse.js'
@Pipe({
name: 'filterPackages',
})
export class FilterPackagesPipe implements PipeTransform {
transform(
packages: MarketplacePkg[],
query: string,
category: string,
): MarketplacePkg[] {
// query
if (query) {
let options: Fuse.IFuseOptions<MarketplacePkg> = {
includeScore: true,
includeMatches: true,
}
if (query.length < 4) {
options = {
...options,
threshold: 0.2,
location: 0,
distance: 16,
keys: [
{
name: 'manifest.title',
weight: 1,
},
{
name: 'manifest.id',
weight: 0.5,
},
],
}
} else {
options = {
...options,
ignoreLocation: true,
useExtendedSearch: true,
keys: [
{
name: 'manifest.title',
weight: 1,
},
{
name: 'manifest.id',
weight: 0.5,
},
{
name: 'manifest.description.short',
weight: 0.4,
},
{
name: 'manifest.description.long',
weight: 0.1,
},
],
}
query = `'${query}`
}
const fuse = new Fuse(packages, options)
return fuse.search(query).map(p => p.item)
}
// category
return packages
.filter(p => category === 'all' || p.categories.includes(category))
.sort((a, b) => {
return (
new Date(b['published-at']).valueOf() -
new Date(a['published-at']).valueOf()
)
})
}
}
@NgModule({
declarations: [FilterPackagesPipe],
exports: [FilterPackagesPipe],
})
export class FilterPackagesPipeModule {}

View File

@@ -0,0 +1,34 @@
import { NgModule, Pipe, PipeTransform } from '@angular/core'
import { MarketplacePkg } from '../types'
@Pipe({
name: 'mimeType',
})
export class MimeTypePipe implements PipeTransform {
transform(pkg: MarketplacePkg): string {
if (pkg.icon.startsWith('data:')) return pkg.icon
if (pkg.manifest.assets.icon) {
switch (pkg.manifest.assets.icon.split('.').pop()) {
case 'png':
return `data:image/png;base64,${pkg.icon}`
case 'jpeg':
case 'jpg':
return `data:image/jpeg;base64,${pkg.icon}`
case 'gif':
return `data:image/gif;base64,${pkg.icon}`
case 'svg':
return `data:image/svg+xml;base64,${pkg.icon}`
default:
return `data:image/png;base64,${pkg.icon}`
}
}
return `data:image/png;base64,${pkg.icon}`
}
}
@NgModule({
declarations: [MimeTypePipe],
exports: [MimeTypePipe],
})
export class MimeTypePipeModule {}

View File

@@ -0,0 +1,33 @@
/*
* Public API Surface of @start9labs/marketplace
*/
export * from './pages/list/categories/categories.component'
export * from './pages/list/categories/categories.module'
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'
export * from './pages/show/about/about.module'
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 './pipes/filter-packages.pipe'
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 './services/marketplace.service'
export * from './types'

View File

@@ -0,0 +1,29 @@
import { Observable } from 'rxjs'
import { MarketplacePkg, Marketplace, StoreData, StoreIdentity } from '../types'
export abstract class AbstractMarketplaceService {
abstract getKnownHosts$(): Observable<StoreIdentity[]>
abstract getSelectedHost$(): Observable<StoreIdentity>
abstract getMarketplace$(): Observable<Marketplace>
abstract getSelectedStore$(): Observable<StoreData>
abstract getPackage$(
id: string,
version: string,
url?: string,
): Observable<MarketplacePkg> // could be {} so need to check in show page
abstract fetchReleaseNotes$(
id: string,
url?: string,
): Observable<Record<string, string>>
abstract fetchStatic$(
id: string,
type: string,
url?: string,
): Observable<string>
}

View File

@@ -0,0 +1,87 @@
import { Url } from '@start9labs/shared'
export type StoreURL = string
export type StoreName = string
export interface StoreIdentity {
url: StoreURL
name?: StoreName
}
export type Marketplace = Record<StoreURL, StoreData | null>
export interface StoreData {
info: StoreInfo
packages: MarketplacePkg[]
}
export interface StoreInfo {
name: StoreName
categories: string[]
}
export interface MarketplacePkg {
icon: Url
license: Url
instructions: Url
manifest: Manifest
categories: string[]
versions: string[]
'dependency-metadata': {
[id: string]: DependencyMetadata
}
'published-at': string
}
export interface DependencyMetadata {
title: string
icon: Url
hidden: boolean
}
export interface Manifest {
id: string
title: string
version: string
'git-hash'?: string
description: {
short: string
long: string
}
assets: {
icon: Url // filename
}
replaces?: string[]
'release-notes': string
license: string // name of license
'wrapper-repo': Url
'upstream-repo': Url
'support-site': Url
'marketing-site': Url
'donation-url': Url | null
alerts: {
install: string | null
uninstall: string | null
restore: string | null
start: string | null
stop: string | null
}
dependencies: Record<string, Dependency>
'os-version': string
}
export interface Dependency {
version: string
requirement:
| {
type: 'opt-in'
how: string
}
| {
type: 'opt-out'
how: string
}
| {
type: 'required'
}
description: string | null
}