mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
rename frontend to web and update contributing guide (#2509)
* rename frontend to web and update contributing guide * rename this time * fix build * restructure rust code * update documentation * update descriptions * Update CONTRIBUTING.md Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com> --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
pkg!: MarketplacePkg
|
||||
}
|
||||
20
web/projects/marketplace/src/pages/list/item/item.module.ts
Normal file
20
web/projects/marketplace/src/pages/list/item/item.module.ts
Normal 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 {}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.column {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 { ResponsiveColModule } from '@start9labs/shared'
|
||||
|
||||
import { SearchComponent } from './search.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [IonicModule, FormsModule, CommonModule, ResponsiveColModule],
|
||||
declarations: [SearchComponent],
|
||||
exports: [SearchComponent],
|
||||
})
|
||||
export class SearchModule {}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-skeleton',
|
||||
templateUrl: 'skeleton.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SkeletonComponent {}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ResponsiveColModule } from '@start9labs/shared'
|
||||
|
||||
import { SkeletonComponent } from './skeleton.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, ResponsiveColModule],
|
||||
declarations: [SkeletonComponent],
|
||||
exports: [SkeletonComponent],
|
||||
})
|
||||
export class SkeletonModule {}
|
||||
@@ -0,0 +1,32 @@
|
||||
<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"
|
||||
[innerHTML]="note.value | markdown"
|
||||
></ion-text>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Loading Release Notes"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
) {}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiElementModule } from '@taiga-ui/cdk'
|
||||
|
||||
import { ReleaseNotesComponent } from './release-notes.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TextSpinnerComponentModule,
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
TuiElementModule,
|
||||
],
|
||||
declarations: [ReleaseNotesComponent],
|
||||
exports: [ReleaseNotesComponent],
|
||||
})
|
||||
export class ReleaseNotesModule {}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- release notes -->
|
||||
<ion-item-divider>
|
||||
New in {{ pkg.manifest.version | displayEmver }}
|
||||
</ion-item-divider>
|
||||
<ion-item lines="none" color="transparent">
|
||||
<ion-label>
|
||||
<div [innerHTML]="pkg.manifest['release-notes'] | markdown"></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>
|
||||
@@ -0,0 +1,4 @@
|
||||
.all-notes {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
@@ -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()
|
||||
pkg!: MarketplacePkg
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared'
|
||||
|
||||
import { AboutComponent } from './about.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
IonicModule,
|
||||
MarkdownPipeModule,
|
||||
EmverPipesModule,
|
||||
],
|
||||
declarations: [AboutComponent],
|
||||
exports: [AboutComponent],
|
||||
})
|
||||
export class AboutModule {}
|
||||
@@ -0,0 +1,106 @@
|
||||
<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)="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()">
|
||||
<ion-label>
|
||||
<h2>Other Versions</h2>
|
||||
<p>Click to view other versions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
</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>
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
AlertController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import {
|
||||
copyToClipboard,
|
||||
displayEmver,
|
||||
Emver,
|
||||
MarkdownComponent,
|
||||
} from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional',
|
||||
templateUrl: 'additional.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AdditionalComponent {
|
||||
@Input()
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
@Output()
|
||||
version = new EventEmitter<string>()
|
||||
|
||||
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly emver: Emver,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
async copy(address: string): Promise<void> {
|
||||
const success = await copyToClipboard(address)
|
||||
const message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async presentAlertVersions() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Versions',
|
||||
inputs: this.pkg.versions
|
||||
.sort((a, b) => -1 * (this.emver.compare(a, b) || 0))
|
||||
.map(v => ({
|
||||
name: v, // for CSS
|
||||
type: 'radio',
|
||||
label: displayEmver(v), // appearance on screen
|
||||
value: v, // literal SEM version value
|
||||
checked: this.pkg.manifest.version === v,
|
||||
})),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: (version: string) => this.version.emit(version),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentModalMd(title: string) {
|
||||
const content = this.marketplaceService.fetchStatic$(
|
||||
this.pkg.manifest.id,
|
||||
title,
|
||||
this.url,
|
||||
)
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { title, content },
|
||||
component: MarkdownComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarkdownModule, ResponsiveColModule } from '@start9labs/shared'
|
||||
|
||||
import { AdditionalComponent } from './additional.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, MarkdownModule, ResponsiveColModule],
|
||||
declarations: [AdditionalComponent],
|
||||
exports: [AdditionalComponent],
|
||||
})
|
||||
export class AdditionalModule {}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { DependenciesComponent } from './dependencies.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
IonicModule,
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
declarations: [DependenciesComponent],
|
||||
exports: [DependenciesComponent],
|
||||
})
|
||||
export class DependenciesModule {}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="header montserrat">
|
||||
<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 class="published">
|
||||
Released: {{ pkg['published-at'] | date: 'medium' }}
|
||||
</p>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
pkg!: MarketplacePkg
|
||||
}
|
||||
@@ -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 {}
|
||||
85
web/projects/marketplace/src/pipes/filter-packages.pipe.ts
Normal file
85
web/projects/marketplace/src/pipes/filter-packages.pipe.ts
Normal 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 {}
|
||||
32
web/projects/marketplace/src/pipes/mime-type.pipe.ts
Normal file
32
web/projects/marketplace/src/pipes/mime-type.pipe.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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.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 {}
|
||||
29
web/projects/marketplace/src/public-api.ts
Normal file
29
web/projects/marketplace/src/public-api.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 './services/marketplace.service'
|
||||
|
||||
export * from './types'
|
||||
29
web/projects/marketplace/src/services/marketplace.service.ts
Normal file
29
web/projects/marketplace/src/services/marketplace.service.ts
Normal 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>
|
||||
}
|
||||
87
web/projects/marketplace/src/types.ts
Normal file
87
web/projects/marketplace/src/types.ts
Normal 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: MarketplaceManifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
'dependency-metadata': {
|
||||
[id: string]: DependencyMetadata
|
||||
}
|
||||
'published-at': string
|
||||
}
|
||||
|
||||
export interface DependencyMetadata {
|
||||
title: string
|
||||
icon: Url
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export interface MarketplaceManifest<T = unknown> {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
'git-hash'?: string
|
||||
description: {
|
||||
short: string
|
||||
long: string
|
||||
}
|
||||
assets: {
|
||||
icon: string // ie. icon.png
|
||||
}
|
||||
replaces?: string[]
|
||||
'release-notes': string
|
||||
license: string // type 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<T>>
|
||||
}
|
||||
|
||||
export interface Dependency<T> {
|
||||
version: string
|
||||
requirement:
|
||||
| {
|
||||
type: 'opt-in'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'opt-out'
|
||||
how: string
|
||||
}
|
||||
| {
|
||||
type: 'required'
|
||||
}
|
||||
description: string | null
|
||||
config: T
|
||||
}
|
||||
Reference in New Issue
Block a user