mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Merge pull request #2638 from Start9Labs/update/marketplace-for-brochure
misc fixes and backwards compatibility with new registry types for brochure
This commit is contained in:
@@ -48,7 +48,8 @@
|
||||
<br />
|
||||
|
||||
## Running StartOS
|
||||
There are multiple ways to get started with StartOS:
|
||||
> [!WARNING]
|
||||
> StartOS is in beta. It lacks features. It doesn't always work perfectly. Start9 servers are not plug and play. Using them properly requires some effort and patience. Please do not use StartOS or purchase a server if you are unable or unwilling to follow instructions and learn new concepts.
|
||||
|
||||
### 💰 Buy a Start9 server
|
||||
This is the most convenient option. Simply [buy a server](https://store.start9.com) from Start9 and plug it in.
|
||||
|
||||
@@ -74,12 +74,14 @@ async fn do_upload(
|
||||
mut url: Url,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
pkg_id: &str,
|
||||
body: Body,
|
||||
) -> Result<(), Error> {
|
||||
url.set_path("/admin/v0/upload");
|
||||
let req = httpc
|
||||
.post(url)
|
||||
.header(header::ACCEPT, "text/plain")
|
||||
.query(&[("id", pkg_id)])
|
||||
.basic_auth(user, Some(pass))
|
||||
.body(body)
|
||||
.build()?;
|
||||
@@ -198,6 +200,7 @@ pub async fn publish(
|
||||
registry.clone(),
|
||||
&user,
|
||||
&pass,
|
||||
&pkg.id,
|
||||
Body::wrap_stream(file_stream),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -169,8 +169,16 @@ echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/tor.key.gpg]
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o config/archives/docker.key
|
||||
echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/docker.key.gpg] https://download.docker.com/linux/debian ${IB_SUITE} stable" > config/archives/docker.list
|
||||
|
||||
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/Debian_Testing/Release.key | gpg --dearmor -o config/archives/podman.key
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/podman.key.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/Debian_Testing/ /" > config/archives/podman.list
|
||||
echo "deb http://deb.debian.org/debian/ trixie main contrib" > config/archives/trixie.list
|
||||
cat > config/archives/trixie.pref <<- EOF
|
||||
Package: *
|
||||
Pin: release n=trixie
|
||||
Pin-Priority: 100
|
||||
|
||||
Package: podman
|
||||
Pin: release n=trixie
|
||||
Pin-Priority: 600
|
||||
EOF
|
||||
|
||||
# Dependencies
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/marketplace",
|
||||
"version": "0.3.25",
|
||||
"version": "0.3.28",
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=13.2.0",
|
||||
"@angular/core": ">=13.2.0",
|
||||
|
||||
@@ -33,6 +33,7 @@ header {
|
||||
1;
|
||||
border-width: 0;
|
||||
border-right: 0.125rem solid;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1536px) {
|
||||
|
||||
@@ -66,6 +66,7 @@ export class MenuComponent implements OnDestroy {
|
||||
this.query = ''
|
||||
this.categoryService.resetQuery()
|
||||
this.categoryService.changeCategory(category)
|
||||
this.categoryService.handleNavigation()
|
||||
}
|
||||
|
||||
onQueryChange(query: string): void {
|
||||
|
||||
@@ -32,7 +32,7 @@ button {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
width: 120%;
|
||||
width: 108%;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export class CategoriesComponent {
|
||||
case 'all':
|
||||
return 'tuiIconGridLarge'
|
||||
case 'bitcoin':
|
||||
return 'tuiIconBitcoin'
|
||||
return 'assets/img/icons/logo-bitcoin.svg'
|
||||
case 'messaging':
|
||||
case 'communications':
|
||||
return 'tuiIconMessageCircleLarge'
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<div class="item-container box-shadow-lg">
|
||||
<div
|
||||
class="item-container box-shadow-lg"
|
||||
*tuiLet="marketplace$ | async as marketplace"
|
||||
>
|
||||
<!-- color background -->
|
||||
<div class="background">
|
||||
<img [src]="pkg.icon" alt="{{ pkg.manifest.title }} Icon" />
|
||||
<img
|
||||
[src]="determineIcon(marketplace)"
|
||||
alt="{{ pkg.manifest.title }} Icon"
|
||||
/>
|
||||
</div>
|
||||
<!-- darkening overlay -->
|
||||
<div class="overlay"></div>
|
||||
<!-- icon -->
|
||||
<img
|
||||
[src]="pkg.icon"
|
||||
class="icon box-shadow-lg"
|
||||
[src]="determineIcon(marketplace)"
|
||||
class="icon"
|
||||
alt="{{ pkg.manifest.title }} Icon"
|
||||
/>
|
||||
<div class="detail">
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
.item-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
border-radius: 1.5rem;
|
||||
padding: 5rem 2rem 2rem 2.5rem;
|
||||
gap: 1rem;
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 500ms;
|
||||
@@ -56,21 +53,20 @@
|
||||
top: -2.5rem;
|
||||
border-radius: 9999px;
|
||||
object-fit: cover;
|
||||
backdrop-filter: blur(24px);
|
||||
background-color: rgb(0 0 0 / 0.5);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin-top: 0.75rem;
|
||||
mix-blend-mode: plus-lighter;
|
||||
|
||||
|
||||
&-title {
|
||||
display: inline-block;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 400;
|
||||
will-change: transform, text-indent;
|
||||
}
|
||||
|
||||
&-description {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { MarketplacePkg, StoreIdentity } from '../../../types'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-item',
|
||||
@@ -10,4 +16,16 @@ import { MarketplacePkg } from '../../../types'
|
||||
export class ItemComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
private readonly marketplaceService = inject(AbstractMarketplaceService)
|
||||
readonly marketplace$ = this.marketplaceService.getSelectedHost$()
|
||||
|
||||
determineIcon(marketplace: StoreIdentity | null): string {
|
||||
try {
|
||||
const iconUrl = new URL(this.pkg.icon)
|
||||
return iconUrl.href
|
||||
} catch (e) {
|
||||
return `${marketplace?.url}package/v0/icon/${this.pkg.manifest.id}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,17 @@ import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharedPipesModule, TickerModule } from '@start9labs/shared'
|
||||
import { ItemComponent } from './item.component'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ItemComponent],
|
||||
exports: [ItemComponent],
|
||||
imports: [CommonModule, RouterModule, SharedPipesModule, TickerModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedPipesModule,
|
||||
TickerModule,
|
||||
TuiLetModule,
|
||||
],
|
||||
})
|
||||
export class ItemModule {}
|
||||
|
||||
@@ -39,7 +39,21 @@ export class ReleaseNotesComponent {
|
||||
}
|
||||
|
||||
asIsOrder(a: KeyValue<string, string>, b: KeyValue<string, string>) {
|
||||
return a.key > b.key ? -1 : b.key > a.key ? 1 : 0
|
||||
const a1 = a.key.split('.')
|
||||
const b1 = b.key.split('.')
|
||||
// contingency in case there's a 4th or 5th version
|
||||
const len = Math.min(a1.length, b1.length)
|
||||
// look through each version number and compare.
|
||||
for (let i = 0; i < len; i++) {
|
||||
const a2 = +a1[i] || 0
|
||||
const b2 = +b1[i] || 0
|
||||
|
||||
if (a2 !== b2) {
|
||||
// sort descending
|
||||
return a2 > b2 ? -1 : 1
|
||||
}
|
||||
}
|
||||
return a1.length - b1.length
|
||||
}
|
||||
|
||||
async showReleaseNotes(content: PolymorpheusContent<TuiDialogContext>) {
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { CommonModule, KeyValue } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { EmverPipesModule } from '@start9labs/shared'
|
||||
import { Dependency, MarketplacePkg } from '../../../types'
|
||||
import { Dependency, MarketplacePkg, StoreIdentity } from '../../../types'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { TuiAvatarModule, TuiLineClampModule } from '@taiga-ui/kit'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-dep-item',
|
||||
template: `
|
||||
<div class="outer-container">
|
||||
<div class="outer-container" *tuiLet="marketplace$ | async as marketplace">
|
||||
<tui-avatar
|
||||
class="dep-img"
|
||||
[rounded]="true"
|
||||
[size]="'l'"
|
||||
[avatarUrl]="getImage(dep.key)"
|
||||
[avatarUrl]="getImage(dep.key, marketplace)"
|
||||
></tui-avatar>
|
||||
<div>
|
||||
<tui-line-clamp
|
||||
@@ -103,6 +110,7 @@ import { TuiAvatarModule, TuiLineClampModule } from '@taiga-ui/kit'
|
||||
TuiAvatarModule,
|
||||
EmverPipesModule,
|
||||
TuiLineClampModule,
|
||||
TuiLetModule,
|
||||
],
|
||||
})
|
||||
export class MarketplaceDepItemComponent {
|
||||
@@ -112,11 +120,24 @@ export class MarketplaceDepItemComponent {
|
||||
@Input({ required: true })
|
||||
dep!: KeyValue<string, Dependency>
|
||||
|
||||
getImage(key: string): string {
|
||||
private readonly marketplaceService = inject(AbstractMarketplaceService)
|
||||
readonly marketplace$ = this.marketplaceService.getSelectedHost$()
|
||||
|
||||
getImage(key: string, marketplace: StoreIdentity | null) {
|
||||
const icon = this.pkg.dependencyMetadata[key]?.icon
|
||||
// @TODO fix when registry api is updated to include mimetype in icon url
|
||||
// return icon ? `data:image/png;base64,${icon}` : key.substring(0, 2)
|
||||
return icon ? icon : key.substring(0, 2)
|
||||
const camelToSnakeCase = (str: string) =>
|
||||
str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)
|
||||
|
||||
if (icon) {
|
||||
try {
|
||||
const iconUrl = new URL(icon)
|
||||
return iconUrl.href
|
||||
} catch (e) {
|
||||
return `${marketplace?.url}package/v0/icon/${camelToSnakeCase(key)}`
|
||||
}
|
||||
} else {
|
||||
return key.substring(0, 2)
|
||||
}
|
||||
}
|
||||
|
||||
getTitle(key: string): string {
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { SharedPipesModule, TickerModule } from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { MarketplacePkg, StoreIdentity } from '../../../types'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-package-hero',
|
||||
template: `
|
||||
<div class="outer-container">
|
||||
<div class="outer-container" *tuiLet="marketplace$ | async as marketplace">
|
||||
<div class="inner-container box-shadow-lg">
|
||||
<!-- icon -->
|
||||
<img
|
||||
[src]="pkg.icon | trustUrl"
|
||||
class="box-shadow-lg"
|
||||
[src]="determineIcon(marketplace) | trustUrl"
|
||||
alt="{{ pkg.manifest.title }} Icon"
|
||||
/>
|
||||
<!-- color background -->
|
||||
<div class="color-background">
|
||||
<img
|
||||
[src]="pkg.icon | trustUrl"
|
||||
[src]="determineIcon(marketplace) | trustUrl"
|
||||
alt="{{ pkg.manifest.title }} background image"
|
||||
/>
|
||||
</div>
|
||||
@@ -61,11 +67,11 @@ import { MarketplacePkg } from '../../../types'
|
||||
padding: 4rem 2rem 0 2rem;
|
||||
|
||||
@media (min-width: 376px) {
|
||||
min-height: 26vh;
|
||||
min-height: 20vh;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
min-height: 14rem;
|
||||
min-height: 11rem;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -75,8 +81,6 @@ import { MarketplacePkg } from '../../../types'
|
||||
border-radius: 9999px;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
backdrop-filter: blur(24px);
|
||||
background-color: rgb(0 0 0 / 0.5);
|
||||
top: -2.25rem;
|
||||
left: 1.75rem;
|
||||
z-index: 1;
|
||||
@@ -157,9 +161,21 @@ import { MarketplacePkg } from '../../../types'
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, SharedPipesModule, TickerModule],
|
||||
imports: [CommonModule, SharedPipesModule, TickerModule, TuiLetModule],
|
||||
})
|
||||
export class MarketplacePackageHeroComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
private readonly marketplaceService = inject(AbstractMarketplaceService)
|
||||
readonly marketplace$ = this.marketplaceService.getSelectedHost$()
|
||||
|
||||
determineIcon(marketplace: StoreIdentity | null) {
|
||||
try {
|
||||
const iconUrl = new URL(this.pkg.icon)
|
||||
return iconUrl.href
|
||||
} catch (e) {
|
||||
return `${marketplace?.url}package/v0/icon/${this.pkg.manifest.id}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ export abstract class AbstractCategoryService {
|
||||
abstract getQuery$(): Observable<string>
|
||||
|
||||
abstract resetQuery(): void
|
||||
|
||||
abstract handleNavigation(): void
|
||||
}
|
||||
|
||||
1
web/projects/shared/assets/img/icons/logo-bitcoin.svg
Normal file
1
web/projects/shared/assets/img/icons/logo-bitcoin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M410.47 279.2c-5-11.5-12.7-21.6-28.1-30.1a98.15 98.15 0 00-25.4-10 62.22 62.22 0 0016.3-11 56.37 56.37 0 0015.6-23.3 77.11 77.11 0 003.5-28.2c-1.1-16.8-4.4-33.1-13.2-44.8s-21.2-20.7-37.6-27c-12.6-4.8-25.5-7.8-45.5-8.9V32h-40v64h-32V32h-41v64H96v48h27.87c8.7 0 14.6.8 17.6 2.3a13.22 13.22 0 016.5 6c1.3 2.5 1.9 8.4 1.9 17.5V343c0 9-.6 14.8-1.9 17.4s-2 4.9-5.1 6.3-3.2 1.3-11.8 1.3h-26.4L96 416h87v64h41v-64h32v64h40v-64.4c26-1.3 44.5-4.7 59.4-10.3 19.3-7.2 34.1-17.7 44.7-31.5s14-34.9 14.93-51.2c.67-14.5-.03-33.2-4.56-43.4zM224 150h32v74h-32zm0 212v-90h32v90zm72-208.1c6 2.5 9.9 7.5 13.8 12.7 4.3 5.7 6.5 13.3 6.5 21.4 0 7.8-2.9 14.5-7.5 20.5-3.8 4.9-6.8 8.3-12.8 11.1zm28.8 186.7c-7.8 6.9-12.3 10.1-22.1 13.8a56.06 56.06 0 01-6.7 1.9v-82.8a40.74 40.74 0 0111.3 3.4c7.8 3.3 15.2 6.9 19.8 13.2a43.82 43.82 0 018 24.7c-.03 10.9-2.83 19.2-10.33 25.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 943 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/shared",
|
||||
"version": "0.3.12",
|
||||
"version": "0.3.13",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^17.0.6",
|
||||
"@angular/core": "^17.0.6",
|
||||
|
||||
@@ -59,7 +59,9 @@ export class ServiceBackupsComponent {
|
||||
)
|
||||
|
||||
readonly next = computed(() =>
|
||||
daysBetween(new Date(), new Date(this.pkg().nextBackup || new Date())),
|
||||
// TODO @lucy add this back in when types fixed for PackageDataEntry ie. when next/minor merge resolved
|
||||
// daysBetween(new Date(), new Date(this.pkg().nextBackup || new Date())),
|
||||
daysBetween(new Date(), new Date(new Date())),
|
||||
)
|
||||
|
||||
readonly ago = {
|
||||
|
||||
@@ -101,8 +101,7 @@ import { TuiScrollbarModule } from '@taiga-ui/core'
|
||||
&-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 4rem;
|
||||
list-style-type: none;
|
||||
gap: 4rem 3rem;
|
||||
padding: 1.5rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -111,9 +110,12 @@ import { TuiScrollbarModule } from '@taiga-ui/core'
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
@media (min-width: 1280px) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.tile-wrapper {
|
||||
display: block;
|
||||
|
||||
@@ -152,6 +152,10 @@ import { Router } from '@angular/router'
|
||||
height: 100%;
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
marketplace-additional {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { AbstractCategoryService } from '@start9labs/marketplace'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService extends AbstractCategoryService {
|
||||
private readonly router = inject(Router)
|
||||
|
||||
getCategory$(): Observable<string> {
|
||||
return this.category$
|
||||
}
|
||||
@@ -23,4 +26,8 @@ export class CategoryService extends AbstractCategoryService {
|
||||
resetQuery() {
|
||||
this.query$.next('')
|
||||
}
|
||||
|
||||
handleNavigation() {
|
||||
this.router.navigate([])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-unused-variable": true,
|
||||
"no-unused-expression": true,
|
||||
"semicolon": [true, "never"],
|
||||
"no-trailing-whitespace": true,
|
||||
|
||||
Reference in New Issue
Block a user