mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
feature/marketplace icons (#1921)
* add registry icons, update links, clean up code (#1913) * add registry icons, update links, clean up code * remove seeding of registry icon and name * fix install wizard copy Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * remove references to bep and chime * fix shutdown language and remove chime from initializing screen * fix type error Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
"marketplace": {
|
"marketplace": {
|
||||||
"selected-url": "https://registry.start9.com/",
|
"selected-url": "https://registry.start9.com/",
|
||||||
"known-hosts": {
|
"known-hosts": {
|
||||||
"https://registry.start9.com/": "Start9 Marketplace",
|
"https://registry.start9.com/": {},
|
||||||
"https://community-registry.start9.com/": "Community Marketplace"
|
"https://community-registry.start9.com/": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dev": {},
|
"dev": {},
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class HomePage {
|
|||||||
code: 2,
|
code: 2,
|
||||||
problem: 'Filesystem I/O error.',
|
problem: 'Filesystem I/O error.',
|
||||||
solution:
|
solution:
|
||||||
'Repairing the disk could help resolve this issue. This will occur on a restart between the bep and chime. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
|
'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
|
||||||
details: error.data?.details,
|
details: error.data?.details,
|
||||||
}
|
}
|
||||||
// disk management error - disk needs repair
|
// disk management error - disk needs repair
|
||||||
@@ -69,7 +69,7 @@ export class HomePage {
|
|||||||
code: 48,
|
code: 48,
|
||||||
problem: 'Disk management error.',
|
problem: 'Disk management error.',
|
||||||
solution:
|
solution:
|
||||||
'Repairing the disk could help resolve this issue. This will occur on a restart between the bep and chime. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
|
'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
|
||||||
details: error.data?.details,
|
details: error.data?.details,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -139,7 +139,7 @@ export class HomePage {
|
|||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Warning',
|
header: 'Warning',
|
||||||
message:
|
message:
|
||||||
'This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot (between the bep and chime), such as losing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem *will* be in an unrecoverable state. Please proceed with caution.',
|
'This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot, such as losing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem *will* be in an unrecoverable state. Please proceed with caution.',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
|
|||||||
@@ -43,15 +43,21 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template swiperSlide>
|
<ng-template swiperSlide>
|
||||||
<ion-item button (click)="tryInstall(false)">
|
<ion-item
|
||||||
|
*ngIf="selectedDisk?.['embassy-data']"
|
||||||
|
button
|
||||||
|
(click)="tryInstall(false)"
|
||||||
|
>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
color="dark"
|
color="dark"
|
||||||
slot="start"
|
slot="start"
|
||||||
name="medkit-outline"
|
name="medkit-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2><ion-text color="success">Light Install</ion-text></h2>
|
<h2>
|
||||||
<p>Reinstall embassyOS but keep your existing data</p>
|
<ion-text color="success">Re-Install embassyOS</ion-text>
|
||||||
|
</h2>
|
||||||
|
<h4>Will preserve existing embassyOS data</h4>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button lines="none" (click)="tryInstall(true)">
|
<ion-item button lines="none" (click)="tryInstall(true)">
|
||||||
@@ -61,10 +67,14 @@
|
|||||||
name="download-outline"
|
name="download-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2><ion-text color="warning">Full Install</ion-text></h2>
|
<h2>
|
||||||
<p>
|
<ion-text
|
||||||
Install embassyOS and delete all existing data on disk
|
[color]="selectedDisk?.['embassy-data'] ? 'danger' : 'success'"
|
||||||
</p>
|
>{{ selectedDisk?.['embassy-data'] ? 'Factory Reset' :
|
||||||
|
'Install embassyOS' }}</ion-text
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<h4>Will delete existing data on disk</h4>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ export class HomePage {
|
|||||||
|
|
||||||
private async presentAlertDanger(logicalname: string, embassyData: boolean) {
|
private async presentAlertDanger(logicalname: string, embassyData: boolean) {
|
||||||
const message = embassyData
|
const message = embassyData
|
||||||
? 'This action COMPLETELY erases your existing Embassy data'
|
? 'This action will COMPLETELY erase your existing Embassy data'
|
||||||
: `This action COMPLETELY erases the disk ${logicalname} and installs embassyOS`
|
: `This action will COMPLETELY erase the disk <b>${logicalname}</b> and install embassyOS!`
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Warning',
|
header: 'Warning',
|
||||||
@@ -93,7 +93,7 @@ export class HomePage {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cssClass: 'alert-warning-message',
|
cssClass: 'alert-danger-message',
|
||||||
})
|
})
|
||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ export class HomePage {
|
|||||||
private async presentAlertReboot() {
|
private async presentAlertReboot() {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Install Success',
|
header: 'Install Success',
|
||||||
message: 'Reboot your device to begin using your new Emabssy',
|
message: 'Reboot your device to begin using your new Embassy',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Reboot',
|
text: 'Reboot',
|
||||||
@@ -110,7 +110,7 @@ export class HomePage {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cssClass: 'alert-warning-message',
|
cssClass: 'alert-success-message',
|
||||||
})
|
})
|
||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,6 @@ export class HomePage {
|
|||||||
header: 'Rebooting',
|
header: 'Rebooting',
|
||||||
message: 'Please wait for embassyOS to restart, then refresh this page',
|
message: 'Please wait for embassyOS to restart, then refresh this page',
|
||||||
buttons: ['OK'],
|
buttons: ['OK'],
|
||||||
cssClass: 'alert-warning-message',
|
|
||||||
})
|
})
|
||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,3 +39,21 @@
|
|||||||
--spinner-color: var(--ion-color-warning) !important;
|
--spinner-color: var(--ion-color-warning) !important;
|
||||||
z-index: 40000 !important;
|
z-index: 40000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-danger-message {
|
||||||
|
.alert-title {
|
||||||
|
color: var(--ion-color-danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success-message {
|
||||||
|
.alert-title {
|
||||||
|
color: var(--ion-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-alert {
|
||||||
|
.alert-button {
|
||||||
|
color: var(--ion-color-dark) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,26 @@
|
|||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col sizeXs="12" sizeMd="6">
|
<ion-col sizeXs="12" sizeMd="6">
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<ion-item button detail="false" (click)="copy(manifest['git-hash'])">
|
<ion-item
|
||||||
|
*ngIf="manifest['git-hash'] as gitHash; else noHash"
|
||||||
|
button
|
||||||
|
detail="false"
|
||||||
|
(click)="copy(gitHash)"
|
||||||
|
>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>Git Hash</h2>
|
<h2>Git Hash</h2>
|
||||||
<p>{{ manifest['git-hash'] }}</p>
|
<p>{{ gitHash }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-icon slot="end" name="copy-outline"></ion-icon>
|
<ion-icon slot="end" name="copy-outline"></ion-icon>
|
||||||
</ion-item>
|
</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-item button detail="false" (click)="presentAlertVersions()">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>Other Versions</h2>
|
<h2>Other Versions</h2>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { Observable } from 'rxjs'
|
|||||||
import {
|
import {
|
||||||
MarketplacePkg,
|
MarketplacePkg,
|
||||||
Marketplace,
|
Marketplace,
|
||||||
MarketplaceURL,
|
StoreURL,
|
||||||
MarketplaceName,
|
|
||||||
StoreData,
|
StoreData,
|
||||||
|
StoreIdentifier,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
export abstract class AbstractMarketplaceService {
|
export abstract class AbstractMarketplaceService {
|
||||||
abstract getKnownHosts$(): Observable<Record<MarketplaceURL, MarketplaceName>>
|
abstract getKnownHosts$(): Observable<Record<StoreURL, StoreIdentifier>>
|
||||||
|
|
||||||
abstract getSelectedHost$(): Observable<{ url: string; name: string }>
|
abstract getSelectedHost$(): Observable<StoreIdentifier & { url: string }>
|
||||||
|
|
||||||
abstract getMarketplace$(): Observable<Marketplace>
|
abstract getMarketplace$(): Observable<Marketplace>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Url } from '@start9labs/shared'
|
import { Url } from '@start9labs/shared'
|
||||||
|
|
||||||
export type MarketplaceURL = string
|
export type StoreURL = string
|
||||||
|
export type StoreName = string
|
||||||
|
export type StoreIcon = string // base64
|
||||||
|
|
||||||
export type MarketplaceName = string
|
export interface StoreIdentifier {
|
||||||
|
name?: StoreName
|
||||||
|
icon?: StoreIcon // base64
|
||||||
|
}
|
||||||
|
|
||||||
export type Marketplace = Record<MarketplaceURL, StoreData | null>
|
export type Marketplace = Record<StoreURL, StoreData | null>
|
||||||
|
|
||||||
export interface StoreData {
|
export interface StoreData {
|
||||||
info: StoreInfo
|
info: StoreInfo
|
||||||
@@ -12,7 +17,8 @@ export interface StoreData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreInfo {
|
export interface StoreInfo {
|
||||||
name: MarketplaceName
|
name: StoreName
|
||||||
|
icon?: StoreIcon
|
||||||
categories: string[]
|
categories: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +42,7 @@ export interface MarketplaceManifest<T = unknown> {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
version: string
|
version: string
|
||||||
'git-hash': string
|
'git-hash'?: string
|
||||||
description: {
|
description: {
|
||||||
short: string
|
short: string
|
||||||
long: string
|
long: string
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
can bypass this warning on most browsers. The warning will go
|
can bypass this warning on most browsers. The warning will go
|
||||||
away after you
|
away after you
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
|
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="inline"
|
class="inline"
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<b>Important!</b>
|
<b>Important!</b>
|
||||||
This address will only work from a
|
This address will only work from a
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
|
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="inline"
|
class="inline"
|
||||||
@@ -185,9 +185,9 @@
|
|||||||
any Tor-enabled browser.
|
any Tor-enabled browser.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For a list of recommended browsers, click
|
For more detailed instructions, click
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
|
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
><b>here</b></a
|
><b>here</b></a
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<p>
|
<p>
|
||||||
For step-by-step instructions, click
|
For step-by-step instructions, click
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
|
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
><b>here</b></a
|
><b>here</b></a
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function getErrorMessage(
|
|||||||
): string | IonicSafeString {
|
): string | IonicSafeString {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
message = 'Unknown Error.'
|
message = 'Unknown Error.'
|
||||||
link = 'https://start9.com/latest/support/FAQ'
|
link = 'https://docs.start9.com/latest/support/faq'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
|
|||||||
@@ -32,19 +32,8 @@ export type RPCResponse<T> = RPCSuccessRes<T> | RPCErrorRes
|
|||||||
|
|
||||||
export interface RPCOptions {
|
export interface RPCOptions {
|
||||||
method: string
|
method: string
|
||||||
headers?: {
|
headers?: Record<string, string | string[]>
|
||||||
[header: string]: string | string[]
|
params: Record<string, any>
|
||||||
}
|
|
||||||
params: {
|
|
||||||
[param: string]:
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| object
|
|
||||||
| string[]
|
|
||||||
| number[]
|
|
||||||
| null
|
|
||||||
}
|
|
||||||
timeout?: number
|
timeout?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class SnekDirective {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.setDbValue(
|
await this.embassyApi.setDbValue<number>(
|
||||||
['gaming', 'snake', 'high-score'],
|
['gaming', 'snake', 'high-score'],
|
||||||
data.highScore,
|
data.highScore,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
a folder on another computer that is connected to the same network
|
a folder on another computer that is connected to the same network
|
||||||
as your Embassy. View the
|
as your Embassy. View the
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/backups/cifs-setup"
|
href="https://docs.start9.com/latest/user-manual/backups/backup-create"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
noreferrer
|
noreferrer
|
||||||
style="text-decoration: none"
|
style="text-decoration: none"
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
a physcial drive that is plugged directly into your Embassy. View
|
a physcial drive that is plugged directly into your Embassy. View
|
||||||
the
|
the
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/backups/backups-create/#backup-using-a-physical-drive"
|
href="https://docs.start9.com/latest/user-manual/backups/backup-setup/backup-physical"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
noreferrer
|
noreferrer
|
||||||
style="text-decoration: none"
|
style="text-decoration: none"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>Marketplace Settings</ion-title>
|
<ion-title>Change Registry</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<ion-button (click)="dismiss()">
|
<ion-button (click)="dismiss()">
|
||||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||||
@@ -11,19 +11,20 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<ion-item-group *ngIf="marketplace$ | async as m">
|
<ion-item-group *ngIf="marketplace$ | async as m">
|
||||||
<ion-item>
|
<ion-item-divider>Default Registries</ion-item-divider>
|
||||||
<ion-label>
|
|
||||||
Connect to a standard marketplaces or an alternative marketplace.
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item-divider>Standard Marketplaces</ion-item-divider>
|
|
||||||
<ion-item
|
<ion-item
|
||||||
*ngFor="let s of m.standard"
|
*ngFor="let s of m.standard"
|
||||||
detail="false"
|
detail="false"
|
||||||
[button]="s.url !== m.selected"
|
[button]="s.url !== m.selected"
|
||||||
(click)="s.url === m.selected ? '' : presentAction(s)"
|
(click)="s.url === m.selected ? '' : presentAction(s)"
|
||||||
>
|
>
|
||||||
|
<ion-avatar slot="start">
|
||||||
|
<img [src]="'data:image/png;base64,' + s.icon | trustUrl" alt="" />
|
||||||
|
</ion-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ s.name }}</h2>
|
||||||
|
<p>{{ s.url }}</p>
|
||||||
|
</ion-label>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="s.url === m.selected"
|
*ngIf="s.url === m.selected"
|
||||||
slot="end"
|
slot="end"
|
||||||
@@ -31,18 +32,14 @@
|
|||||||
name="checkmark"
|
name="checkmark"
|
||||||
color="success"
|
color="success"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
|
||||||
<h2>{{ s.name }}</h2>
|
|
||||||
<p>{{ s.url }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item-divider>Alt Marketplaces</ion-item-divider>
|
<ion-item-divider>Custom Registries</ion-item-divider>
|
||||||
<ion-item button detail="false" (click)="presentModalAdd()">
|
<ion-item button detail="false" (click)="presentModalAdd()">
|
||||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<ion-text color="dark">
|
<ion-text color="dark">
|
||||||
<b>Add Alt Marketplace</b>
|
<b>Add custom registry</b>
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -53,6 +50,11 @@
|
|||||||
[button]="a.url !== m.selected"
|
[button]="a.url !== m.selected"
|
||||||
(click)="a.url === m.selected ? '' : presentAction(a, true)"
|
(click)="a.url === m.selected ? '' : presentAction(a, true)"
|
||||||
>
|
>
|
||||||
|
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ a.name }}</h2>
|
||||||
|
<p>{{ a.url }}</p>
|
||||||
|
</ion-label>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="a.url === m.selected"
|
*ngIf="a.url === m.selected"
|
||||||
slot="end"
|
slot="end"
|
||||||
@@ -60,10 +62,6 @@
|
|||||||
name="checkmark"
|
name="checkmark"
|
||||||
color="success"
|
color="success"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
|
||||||
<h2>{{ a.name }}</h2>
|
|
||||||
<p>{{ a.url }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { ActionSheetButton } from '@ionic/core'
|
import { ActionSheetButton } from '@ionic/core'
|
||||||
import { ErrorToastService } from '@start9labs/shared'
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
import {
|
||||||
|
AbstractMarketplaceService,
|
||||||
|
StoreIdentifier,
|
||||||
|
} from '@start9labs/marketplace'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
@@ -30,14 +33,14 @@ export class MarketplaceSettingsPage {
|
|||||||
const hosts = Object.entries(m['known-hosts'])
|
const hosts = Object.entries(m['known-hosts'])
|
||||||
|
|
||||||
const standard = hosts
|
const standard = hosts
|
||||||
.map(([url, name]) => {
|
.map(([url, info]) => {
|
||||||
return { url, name }
|
return { url, ...info }
|
||||||
})
|
})
|
||||||
.slice(0, 2) // 0 and 1 will always be prod and community
|
.slice(0, 2) // 0 and 1 will always be prod and community
|
||||||
|
|
||||||
const alt = hosts
|
const alt = hosts
|
||||||
.map(([url, name]) => {
|
.map(([url, info]) => {
|
||||||
return { url, name }
|
return { url, ...info }
|
||||||
})
|
})
|
||||||
.slice(2) // 2 and beyond will always be alts
|
.slice(2) // 2 and beyond will always be alts
|
||||||
|
|
||||||
@@ -91,7 +94,7 @@ export class MarketplaceSettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async presentAction(
|
async presentAction(
|
||||||
{ url, name }: { url: string; name: string },
|
{ url, name }: { url: string; name?: string },
|
||||||
canDelete = false,
|
canDelete = false,
|
||||||
) {
|
) {
|
||||||
const buttons: ActionSheetButton[] = [
|
const buttons: ActionSheetButton[] = [
|
||||||
@@ -108,7 +111,7 @@ export class MarketplaceSettingsPage {
|
|||||||
text: 'Delete',
|
text: 'Delete',
|
||||||
role: 'destructive',
|
role: 'destructive',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.presentAlertDelete(url, name)
|
this.presentAlertDelete(url, name!)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -146,7 +149,7 @@ export class MarketplaceSettingsPage {
|
|||||||
url: string,
|
url: string,
|
||||||
loader?: HTMLIonLoadingElement,
|
loader?: HTMLIonLoadingElement,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const message = 'Changing Marketplace...'
|
const message = 'Changing Registry...'
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
loader = await this.loadingCtrl.create({ message })
|
loader = await this.loadingCtrl.create({ message })
|
||||||
await loader.present()
|
await loader.present()
|
||||||
@@ -155,7 +158,7 @@ export class MarketplaceSettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue(['marketplace', 'selected-url'], url)
|
await this.api.setDbValue<string>(['marketplace', 'selected-url'], url)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -204,14 +207,17 @@ export class MarketplaceSettingsPage {
|
|||||||
loader.message = 'Validating marketplace...'
|
loader.message = 'Validating marketplace...'
|
||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
const { name } = await firstValueFrom(
|
const { name, icon } = await firstValueFrom(
|
||||||
this.marketplaceService.fetchInfo$(url),
|
this.marketplaceService.fetchInfo$(url),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
loader.message = 'Saving...'
|
loader.message = 'Saving...'
|
||||||
|
|
||||||
await this.api.setDbValue(['marketplace', 'known-hosts', url], name)
|
await this.api.setDbValue<StoreIdentifier>(
|
||||||
|
['marketplace', 'known-hosts', url],
|
||||||
|
{ name, icon },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async delete(url: string): Promise<void> {
|
private async delete(url: string): Promise<void> {
|
||||||
@@ -224,7 +230,7 @@ export class MarketplaceSettingsPage {
|
|||||||
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
|
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const filtered = Object.keys(hosts)
|
const filtered: { [url: string]: StoreIdentifier } = Object.keys(hosts)
|
||||||
.filter(key => key !== url)
|
.filter(key => key !== url)
|
||||||
.reduce((prev, curr) => {
|
.reduce((prev, curr) => {
|
||||||
const name = hosts[curr]
|
const name = hosts[curr]
|
||||||
@@ -235,7 +241,10 @@ export class MarketplaceSettingsPage {
|
|||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue(['marketplace', 'known-hosts'], filtered)
|
await this.api.setDbValue<{ [url: string]: StoreIdentifier }>(
|
||||||
|
['marketplace', 'known-hosts'],
|
||||||
|
filtered,
|
||||||
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -247,12 +256,12 @@ export class MarketplaceSettingsPage {
|
|||||||
function getMarketplaceValueSpec(): ValueSpecObject {
|
function getMarketplaceValueSpec(): ValueSpecObject {
|
||||||
return {
|
return {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
name: 'Add Marketplace',
|
name: 'Add Custom Registry',
|
||||||
spec: {
|
spec: {
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'URL',
|
name: 'URL',
|
||||||
description: 'The fully-qualified URL of the alt marketplace.',
|
description: 'A fully-qualified URL of the custom registry',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
masked: false,
|
masked: false,
|
||||||
copyable: false,
|
copyable: false,
|
||||||
|
|||||||
@@ -24,10 +24,12 @@
|
|||||||
</p>
|
</p>
|
||||||
<h6>Highlights</h6>
|
<h6>Highlights</h6>
|
||||||
<ul class="spaced-list">
|
<ul class="spaced-list">
|
||||||
<li>Kiosk mode</li>
|
|
||||||
<li>x86_64 architecture compatibility</li>
|
<li>x86_64 architecture compatibility</li>
|
||||||
<li>Community marketplaces</li>
|
<li>Kiosk mode - use your Embassy with monitor, keyboard, and mouse</li>
|
||||||
<li>New update all tab</li>
|
<li>Community Registry now included in Marketplace</li>
|
||||||
|
<li>
|
||||||
|
"Updates" tab - view all service updates from all registries in one place
|
||||||
|
</li>
|
||||||
<li>Various UI/UX improvements</li>
|
<li>Various UI/UX improvements</li>
|
||||||
<li>Various bugfixes and optimizations</li>
|
<li>Various bugfixes and optimizations</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export class AppActionsPage {
|
|||||||
try {
|
try {
|
||||||
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
||||||
this.embassyApi
|
this.embassyApi
|
||||||
.setDbValue(['ack-instructions', this.pkgId], false)
|
.setDbValue<boolean>(['ack-instructions', this.pkgId], false)
|
||||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||||
this.navCtrl.navigateRoot('/services')
|
this.navCtrl.navigateRoot('/services')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -10,13 +10,26 @@
|
|||||||
<p>{{ manifest.version | displayEmver }}</p>
|
<p>{{ manifest.version | displayEmver }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button detail="false" (click)="copy(manifest['git-hash'])">
|
<ion-item
|
||||||
|
*ngIf="manifest['git-hash'] as gitHash; else noHash"
|
||||||
|
button
|
||||||
|
detail="false"
|
||||||
|
(click)="copy(gitHash)"
|
||||||
|
>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>Git Hash</h2>
|
<h2>Git Hash</h2>
|
||||||
<p>{{ manifest['git-hash'] }}</p>
|
<p>{{ gitHash }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-icon slot="end" name="copy-outline"></ion-icon>
|
<ion-icon slot="end" name="copy-outline"></ion-icon>
|
||||||
</ion-item>
|
</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)="presentModalLicense()">
|
<ion-item button detail="false" (click)="presentModalLicense()">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>License</h2>
|
<h2>License</h2>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
|
|
||||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||||
this.apiService
|
this.apiService
|
||||||
.setDbValue(['ack-instructions', pkg.manifest.id], true)
|
.setDbValue<boolean>(['ack-instructions', pkg.manifest.id], true)
|
||||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
@@ -130,19 +130,19 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
const queryParams = url ? { url } : {}
|
const queryParams = url ? { url } : {}
|
||||||
|
|
||||||
let button: Button = {
|
let button: Button = {
|
||||||
title: 'Marketplace',
|
title: 'Marketplace Listing',
|
||||||
icon: 'storefront-outline',
|
icon: 'storefront-outline',
|
||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
|
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
|
||||||
queryParams,
|
queryParams,
|
||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
description: 'View service in marketplace',
|
description: 'View service in the marketplace',
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
button.disabled = true
|
button.disabled = true
|
||||||
button.description = 'This package was not installed from a marketplace.'
|
button.description = 'This package was not installed from the marketplace'
|
||||||
button.action = () => {}
|
button.action = () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ export class DevConfigPage {
|
|||||||
async save() {
|
async save() {
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue(['dev', this.projectId, 'config'], this.code)
|
await this.api.setDbValue<string>(
|
||||||
|
['dev', this.projectId, 'config'],
|
||||||
|
this.code,
|
||||||
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class DevInstructionsPage {
|
|||||||
async save() {
|
async save() {
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue(
|
await this.api.setDbValue<string>(
|
||||||
['dev', this.projectId, 'instructions'],
|
['dev', this.projectId, 'instructions'],
|
||||||
this.code,
|
this.code,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -148,7 +148,11 @@ export class DeveloperListPage {
|
|||||||
.replace(/warning:/g, '# Optional\n warning:')
|
.replace(/warning:/g, '# Optional\n warning:')
|
||||||
|
|
||||||
const def = { name, config, instructions: SAMPLE_INSTUCTIONS }
|
const def = { name, config, instructions: SAMPLE_INSTUCTIONS }
|
||||||
await this.api.setDbValue(['dev', id], def)
|
await this.api.setDbValue<{
|
||||||
|
name: string
|
||||||
|
config: string
|
||||||
|
instructions: string
|
||||||
|
}>(['dev', id], def)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -184,7 +188,7 @@ export class DeveloperListPage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue(['dev', id, 'name'], newName)
|
await this.api.setDbValue<string>(['dev', id, 'name'], newName)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -201,7 +205,7 @@ export class DeveloperListPage {
|
|||||||
try {
|
try {
|
||||||
const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData))
|
const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData))
|
||||||
delete devDataToSave[id]
|
delete devDataToSave[id]
|
||||||
await this.api.setDbValue(['dev'], devDataToSave)
|
await this.api.setDbValue<DevData>(['dev'], devDataToSave)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class DeveloperMenuPage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue(
|
await this.api.setDbValue<BasicInfo>(
|
||||||
['dev', this.projectId, 'basic-info'],
|
['dev', this.projectId, 'basic-info'],
|
||||||
basicInfo,
|
basicInfo,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'Support Site',
|
name: 'Support Site',
|
||||||
description: 'URL to the support site / channel for the project',
|
description: 'URL to the support site / channel for the project',
|
||||||
placeholder: 'e.g. www.start9labs.com',
|
placeholder: 'e.g. start9.com/support',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
masked: false,
|
masked: false,
|
||||||
copyable: false,
|
copyable: false,
|
||||||
@@ -161,7 +161,7 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'Marketing Site',
|
name: 'Marketing Site',
|
||||||
description: 'URL to the marketing site / channel for the project',
|
description: 'URL to the marketing site / channel for the project',
|
||||||
placeholder: 'e.g. www.start9labs.com',
|
placeholder: 'e.g. start9.com',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
masked: false,
|
masked: false,
|
||||||
copyable: false,
|
copyable: false,
|
||||||
|
|||||||
@@ -23,10 +23,20 @@
|
|||||||
</ion-row>
|
</ion-row>
|
||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col size="12">
|
<ion-col size="12">
|
||||||
<h1 class="heading montserrat ion-text-center">{{ details.name }}</h1>
|
<div class="heading">
|
||||||
|
<img
|
||||||
|
*ngIf="details.icon; else noIcon"
|
||||||
|
[src]="'data:image/png;base64,' + details.icon | trustUrl"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<ng-template #noIcon>
|
||||||
|
<ion-icon name="storefront-outline"></ion-icon>
|
||||||
|
</ng-template>
|
||||||
|
<h1 class="montserrat ion-text-center">{{ details.name }}</h1>
|
||||||
|
</div>
|
||||||
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
|
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
|
||||||
<ion-icon slot="start" name="repeat-outline"></ion-icon>
|
<ion-icon slot="start" name="repeat-outline"></ion-icon>
|
||||||
Switch Marketplaces
|
Change
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<marketplace-search [(query)]="query"></marketplace-search>
|
<marketplace-search [(query)]="query"></marketplace-search>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
.heading {
|
.heading {
|
||||||
font-size: 42px;
|
$icon-size: 64px;
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
|
img {
|
||||||
|
max-width: $icon-size;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 42px;
|
||||||
|
}
|
||||||
|
ion-icon {
|
||||||
|
font-size: $icon-size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
|||||||
@@ -30,35 +30,36 @@ export class MarketplaceListPage {
|
|||||||
readonly localPkgs$ = this.patch.watch$('package-data')
|
readonly localPkgs$ = this.patch.watch$('package-data')
|
||||||
|
|
||||||
readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
|
readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
|
||||||
map(({ url, name }) => {
|
map(({ url, name, icon }) => {
|
||||||
let color: string
|
let color: string
|
||||||
let description: string
|
let description: string
|
||||||
switch (url) {
|
switch (url) {
|
||||||
case 'https://registry.start9.com/':
|
case 'https://registry.start9.com/':
|
||||||
color = 'success'
|
color = 'success'
|
||||||
description =
|
description =
|
||||||
'Services in this marketplace are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service in this marketplace, one of our dedicated support staff will be happy to assist you.'
|
'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.'
|
||||||
break
|
break
|
||||||
case 'https://beta-registry-0-3.start9labs.com/':
|
case 'https://beta-registry.start9.com/':
|
||||||
color = 'primary'
|
color = 'primary'
|
||||||
description =
|
description =
|
||||||
'Services in this marketplace are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
|
'Services from this registry are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
|
||||||
break
|
break
|
||||||
case 'https://community.start9labs.com/':
|
case 'https://community-registry.start9.com/':
|
||||||
color = 'tertiary'
|
color = 'tertiary'
|
||||||
description =
|
description =
|
||||||
'Services in this marketplace 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.'
|
'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.'
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
// alt marketplace
|
// alt marketplace
|
||||||
color = 'warning'
|
color = 'warning'
|
||||||
description =
|
description =
|
||||||
'Warning. This is an <b>Alternative</b> Marketplace. Start9 cannot verify the integrity or functionality of services in this marketplace, and they may cause harm to your system. <b>Install at your own risk</b>.'
|
'Warning. This is a <b>Custom</b> Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they may cause harm to your system. <b>Install at your own risk</b>.'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
|
icon,
|
||||||
color,
|
color,
|
||||||
description,
|
description,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,11 +99,12 @@ export class MarketplaceShowControlsComponent {
|
|||||||
this.patch.watch$('ui', 'marketplace'),
|
this.patch.watch$('ui', 'marketplace'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const name = marketplaces['known-hosts'][url] || url
|
const name: string = marketplaces['known-hosts'][url]?.name || url
|
||||||
|
|
||||||
let originalName: string | undefined
|
let originalName: string | undefined
|
||||||
if (originalUrl) {
|
if (originalUrl) {
|
||||||
originalName = marketplaces['known-hosts'][originalUrl] || originalUrl
|
originalName =
|
||||||
|
marketplaces['known-hosts'][originalUrl]?.name || originalUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
View the full
|
View the full
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
|
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>instructions</a
|
>instructions</a
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export class ServerShowPage {
|
|||||||
async presentAlertRepairDisk() {
|
async presentAlertRepairDisk() {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Warning',
|
header: 'Warning',
|
||||||
message: `<p>This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action.</p><p>If anything happens to the device during the reboot (between the bep and chime), such as losing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem <i>will</i> be in an unrecoverable state. Please proceed with caution.</p>`,
|
message: `<p>This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action.</p><p>If anything happens to the device during the reboot, such as losing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem <i>will</i> be in an unrecoverable state. Please proceed with caution.</p>`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
@@ -223,7 +223,7 @@ export class ServerShowPage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.setDbValue([key], value)
|
await this.embassyApi.setDbValue<string>([key], value)
|
||||||
} finally {
|
} finally {
|
||||||
loader.dismiss()
|
loader.dismiss()
|
||||||
}
|
}
|
||||||
@@ -333,7 +333,7 @@ export class ServerShowPage {
|
|||||||
private async presentAlertInProgress(verb: string, message: string) {
|
private async presentAlertInProgress(verb: string, message: string) {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: `${verb} In Progress...`,
|
header: `${verb} In Progress...`,
|
||||||
message: `Stopping all services gracefully. This can take a while.<br /><br />Your Embassy will then <b>♫ play a melody ♫</b> and become unreachable${message}`,
|
message: `Stopping all services gracefully. This can take a while.<br /><br />If you have a speaker, your Embassy will <b>♫ play a melody ♫</b> before shutting down. Your Embassy will then become unreachable${message}`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'OK',
|
text: 'OK',
|
||||||
@@ -484,7 +484,7 @@ export class ServerShowPage {
|
|||||||
icon: 'map-outline',
|
icon: 'map-outline',
|
||||||
action: () =>
|
action: () =>
|
||||||
window.open(
|
window.open(
|
||||||
'https://start9.com/latest/user-manual/',
|
'https://docs.start9.com/latest/user-manual',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noreferrer',
|
'noreferrer',
|
||||||
),
|
),
|
||||||
@@ -497,7 +497,7 @@ export class ServerShowPage {
|
|||||||
icon: 'chatbubbles-outline',
|
icon: 'chatbubbles-outline',
|
||||||
action: () =>
|
action: () =>
|
||||||
window.open(
|
window.open(
|
||||||
'https://start9.com/latest/support/contact/',
|
'https://docs.start9.com/latest/support/contact',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noreferrer',
|
'noreferrer',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
export class SSHKeysPage {
|
export class SSHKeysPage {
|
||||||
loading = true
|
loading = true
|
||||||
sshKeys: SSHKey[] = []
|
sshKeys: SSHKey[] = []
|
||||||
readonly docsUrl = 'https://start9.com/latest/user-manual/ssh'
|
readonly docsUrl = 'https://docs.start9.com/latest/user-manual/ssh'
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
Ethernet cable and move the device anywhere you want. Embassy will
|
Ethernet cable and move the device anywhere you want. Embassy will
|
||||||
automatically connect to available networks.
|
automatically connect to available networks.
|
||||||
<a
|
<a
|
||||||
href="https://start9.com/latest/user-manual/wifi"
|
href="https://docs.start9.com/latest/user-manual/wifi"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>View instructions</a
|
>View instructions</a
|
||||||
|
|||||||
@@ -10,7 +10,14 @@
|
|||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<ion-item-group *ngIf="data$ | async as data">
|
<ion-item-group *ngIf="data$ | async as data">
|
||||||
<ng-container *ngFor="let host of data.hosts | keyvalue">
|
<ng-container *ngFor="let host of data.hosts | keyvalue">
|
||||||
<ion-item-divider> {{ host.value }} </ion-item-divider>
|
<ion-item-divider>
|
||||||
|
{{ host.value.name }}
|
||||||
|
<img
|
||||||
|
style="max-width: 24px"
|
||||||
|
[src]="'data:image/png;base64,' + host.value.icon | trustUrl"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</ion-item-divider>
|
||||||
|
|
||||||
<div class="ion-padding-start ion-padding-bottom">
|
<div class="ion-padding-start ion-padding-bottom">
|
||||||
<ion-item *ngIf="data.errors.includes(host.key)">
|
<ion-item *ngIf="data.errors.includes(host.key)">
|
||||||
@@ -25,7 +32,7 @@
|
|||||||
>
|
>
|
||||||
<ion-item *ngFor="let pkg of updates">
|
<ion-item *ngFor="let pkg of updates">
|
||||||
<ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local">
|
<ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local">
|
||||||
<ion-avatar slot="start">
|
<ion-avatar slot="start" class="service-avatar">
|
||||||
<img [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
|
<img [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ion-avatar {
|
.service-avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Marketplace,
|
Marketplace,
|
||||||
MarketplaceManifest,
|
MarketplaceManifest,
|
||||||
MarketplacePkg,
|
MarketplacePkg,
|
||||||
|
StoreIdentifier,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { Emver } from '@start9labs/shared'
|
import { Emver } from '@start9labs/shared'
|
||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
@@ -19,7 +20,7 @@ import { combineLatest, Observable } from 'rxjs'
|
|||||||
import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
|
import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
|
||||||
|
|
||||||
interface UpdatesData {
|
interface UpdatesData {
|
||||||
hosts: Record<string, string>
|
hosts: Record<string, StoreIdentifier>
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
localPkgs: Record<string, PackageDataEntry>
|
localPkgs: Record<string, PackageDataEntry>
|
||||||
errors: string[]
|
errors: string[]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -348,7 +348,6 @@ export module Mock {
|
|||||||
id: 'lnd',
|
id: 'lnd',
|
||||||
title: 'Lightning Network Daemon',
|
title: 'Lightning Network Daemon',
|
||||||
version: '0.11.1',
|
version: '0.11.1',
|
||||||
'git-hash': 'lalalalalala',
|
|
||||||
description: {
|
description: {
|
||||||
short: 'A bolt spec compliant client.',
|
short: 'A bolt spec compliant client.',
|
||||||
long: 'More info about LND. More info about LND. More info about LND.',
|
long: 'More info about LND. More info about LND. More info about LND.',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export module RR {
|
|||||||
|
|
||||||
export type GetDumpRes = Dump<DataModel>
|
export type GetDumpRes = Dump<DataModel>
|
||||||
|
|
||||||
export type SetDBValueReq = { pointer: string; value: any } // db.put.ui
|
export type SetDBValueReq<T> = { pointer: string; value: T } // db.put.ui
|
||||||
export type SetDBValueRes = null
|
export type SetDBValueRes = null
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
abstract setDbValue(
|
abstract setDbValue<T>(
|
||||||
pathArr: Array<string | number>,
|
pathArr: Array<string | number>,
|
||||||
value: any,
|
value: T,
|
||||||
): Promise<RR.SetDBValueRes>
|
): Promise<RR.SetDBValueRes>
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
async setDbValue(
|
async setDbValue<T>(
|
||||||
pathArr: Array<string | number>,
|
pathArr: Array<string | number>,
|
||||||
value: any,
|
value: T,
|
||||||
): Promise<RR.SetDBValueRes> {
|
): Promise<RR.SetDBValueRes> {
|
||||||
const pointer = pathFromArray(pathArr)
|
const pointer = pathFromArray(pathArr)
|
||||||
const params: RR.SetDBValueReq = { pointer, value }
|
const params: RR.SetDBValueReq<T> = { pointer, value }
|
||||||
return this.rpcRequest({ method: 'db.put.ui', params })
|
return this.rpcRequest({ method: 'db.put.ui', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { mockPatchData } from './mock-patch'
|
|||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import { AuthService } from '../auth.service'
|
import { AuthService } from '../auth.service'
|
||||||
import { ConnectionService } from '../connection.service'
|
import { ConnectionService } from '../connection.service'
|
||||||
|
import { StoreInfo } from '@start9labs/marketplace'
|
||||||
|
import { COMMUNITY_REGISTRY, START9_REGISTRY } from './api-icons'
|
||||||
|
|
||||||
const PROGRESS: InstallProgress = {
|
const PROGRESS: InstallProgress = {
|
||||||
size: 120,
|
size: 120,
|
||||||
@@ -93,12 +95,12 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
async setDbValue(
|
async setDbValue<T>(
|
||||||
pathArr: Array<string | number>,
|
pathArr: Array<string | number>,
|
||||||
value: any,
|
value: T,
|
||||||
): Promise<RR.SetDBValueRes> {
|
): Promise<RR.SetDBValueRes> {
|
||||||
const pointer = pathFromArray(pathArr)
|
const pointer = pathFromArray(pathArr)
|
||||||
const params: RR.SetDBValueReq = { pointer, value }
|
const params: RR.SetDBValueReq<T> = { pointer, value }
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const patch = [
|
const patch = [
|
||||||
{
|
{
|
||||||
@@ -280,8 +282,9 @@ export class MockApiService extends ApiService {
|
|||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
if (path === '/package/v0/info') {
|
if (path === '/package/v0/info') {
|
||||||
return {
|
const info: StoreInfo = {
|
||||||
name: 'Dark69',
|
name: 'Start9 Registry',
|
||||||
|
icon: START9_REGISTRY,
|
||||||
categories: [
|
categories: [
|
||||||
'bitcoin',
|
'bitcoin',
|
||||||
'lightning',
|
'lightning',
|
||||||
@@ -292,6 +295,7 @@ export class MockApiService extends ApiService {
|
|||||||
'alt coin',
|
'alt coin',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
return info
|
||||||
} else if (path === '/package/v0/index') {
|
} else if (path === '/package/v0/index') {
|
||||||
return Mock.MarketplacePkgsList
|
return Mock.MarketplacePkgsList
|
||||||
} else if (path.startsWith('/package/v0/release-notes')) {
|
} else if (path.startsWith('/package/v0/release-notes')) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
|
import { COMMUNITY_REGISTRY, START9_REGISTRY } from './api-icons'
|
||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
|
|
||||||
export const mockPatchData: DataModel = {
|
export const mockPatchData: DataModel = {
|
||||||
@@ -17,9 +18,16 @@ export const mockPatchData: DataModel = {
|
|||||||
marketplace: {
|
marketplace: {
|
||||||
'selected-url': 'https://registry.start9.com/',
|
'selected-url': 'https://registry.start9.com/',
|
||||||
'known-hosts': {
|
'known-hosts': {
|
||||||
'https://registry.start9.com/': 'Start9 Marketplace',
|
'https://registry.start9.com/': {
|
||||||
'https://community-registry.start9.com/': 'Community Marketplace',
|
name: 'Start9 Registry',
|
||||||
'https://dark9-marketplace.com/': 'Dark9',
|
icon: START9_REGISTRY,
|
||||||
|
},
|
||||||
|
'https://community-registry.start9.com/': {
|
||||||
|
icon: COMMUNITY_REGISTRY,
|
||||||
|
},
|
||||||
|
'https://dark9-marketplace.com/': {
|
||||||
|
name: 'Dark9',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dev: {},
|
dev: {},
|
||||||
@@ -453,7 +461,6 @@ export const mockPatchData: DataModel = {
|
|||||||
id: 'lnd',
|
id: 'lnd',
|
||||||
title: 'Lightning Network Daemon',
|
title: 'Lightning Network Daemon',
|
||||||
version: '0.11.1',
|
version: '0.11.1',
|
||||||
'git-hash': 'lalalalalala',
|
|
||||||
description: {
|
description: {
|
||||||
short: 'A bolt spec compliant client.',
|
short: 'A bolt spec compliant client.',
|
||||||
long: 'More info about LND. More info about LND. More info about LND.',
|
long: 'More info about LND. More info about LND. More info about LND.',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const {
|
|||||||
})
|
})
|
||||||
export class ConfigService {
|
export class ConfigService {
|
||||||
origin = removePort(removeProtocol(window.origin))
|
origin = removePort(removeProtocol(window.origin))
|
||||||
version = require('../../../../../package.json').version
|
version = require('../../../../../package.json').version as string
|
||||||
useMocks = useMocks
|
useMocks = useMocks
|
||||||
mocks = mocks
|
mocks = mocks
|
||||||
targetArch = targetArch
|
targetArch = targetArch
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
StoreData,
|
StoreData,
|
||||||
Marketplace,
|
Marketplace,
|
||||||
StoreInfo,
|
StoreInfo,
|
||||||
|
StoreIdentifier,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -28,7 +29,6 @@ import {
|
|||||||
shareReplay,
|
shareReplay,
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators'
|
} from 'rxjs/operators'
|
||||||
import { getNewEntries } from '@start9labs/shared'
|
import { getNewEntries } from '@start9labs/shared'
|
||||||
|
|
||||||
@@ -44,19 +44,19 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
|||||||
distinctUntilKeyChanged('selected-url'),
|
distinctUntilKeyChanged('selected-url'),
|
||||||
map(data => ({
|
map(data => ({
|
||||||
url: data['selected-url'],
|
url: data['selected-url'],
|
||||||
name: data['known-hosts'][data['selected-url']],
|
...data['known-hosts'][data['selected-url']],
|
||||||
})),
|
})),
|
||||||
shareReplay(1),
|
shareReplay(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly marketplace$ = this.knownHosts$.pipe(
|
private readonly marketplace$ = this.knownHosts$.pipe(
|
||||||
startWith<Record<string, string>>({}),
|
startWith<Record<string, StoreIdentifier>>({}),
|
||||||
pairwise(),
|
pairwise(),
|
||||||
mergeMap(([prev, curr]) => from(Object.entries(getNewEntries(prev, curr)))),
|
mergeMap(([prev, curr]) => from(Object.entries(getNewEntries(prev, curr)))),
|
||||||
mergeMap(([url, name]) =>
|
mergeMap(([url, registry]) =>
|
||||||
this.fetchStore$(url).pipe(
|
this.fetchStore$(url).pipe(
|
||||||
map<StoreData, [string, StoreData | null]>(data => {
|
map<StoreData, [string, StoreData | null]>(data => {
|
||||||
if (data.info) this.updateName(url, name, data.info.name)
|
if (data.info) this.updateStoreIdentifier(url, registry, data.info)
|
||||||
|
|
||||||
return [url, data]
|
return [url, data]
|
||||||
}),
|
}),
|
||||||
@@ -85,11 +85,11 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
|||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getKnownHosts$(): Observable<Record<string, string>> {
|
getKnownHosts$(): Observable<Record<string, StoreIdentifier>> {
|
||||||
return this.knownHosts$
|
return this.knownHosts$
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedHost$(): Observable<{ url: string; name: string }> {
|
getSelectedHost$(): Observable<StoreIdentifier & { url: string }> {
|
||||||
return this.selectedHost$
|
return this.selectedHost$
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,13 +233,16 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateName(
|
private async updateStoreIdentifier(
|
||||||
url: string,
|
url: string,
|
||||||
name: string,
|
oldInfo: StoreIdentifier,
|
||||||
newName: string,
|
newInfo: StoreIdentifier,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (name !== newName) {
|
if (oldInfo.name !== newInfo.name || oldInfo.icon !== newInfo.icon) {
|
||||||
this.api.setDbValue(['marketplace', 'known-hosts', url], newName)
|
this.api.setDbValue<StoreIdentifier>(
|
||||||
|
['marketplace', 'known-hosts', url],
|
||||||
|
newInfo,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ export class PatchDataService extends Observable<DataModel> {
|
|||||||
backdropDismiss: false,
|
backdropDismiss: false,
|
||||||
})
|
})
|
||||||
modal.onWillDismiss().then(() => {
|
modal.onWillDismiss().then(() => {
|
||||||
this.embassyApi.setDbValue(['ack-welcome'], this.config.version).catch()
|
this.embassyApi
|
||||||
|
.setDbValue<string>(['ack-welcome'], this.config.version)
|
||||||
|
.catch()
|
||||||
})
|
})
|
||||||
|
|
||||||
await modal.present()
|
await modal.present()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
import { Url } from '@start9labs/shared'
|
import { Url } from '@start9labs/shared'
|
||||||
import { MarketplaceManifest } from '@start9labs/marketplace'
|
import { MarketplaceManifest, StoreIdentifier } from '@start9labs/marketplace'
|
||||||
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
|
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
|
||||||
|
|
||||||
export interface DataModel {
|
export interface DataModel {
|
||||||
@@ -26,9 +26,9 @@ export interface UIData {
|
|||||||
export interface UIMarketplaceData {
|
export interface UIMarketplaceData {
|
||||||
'selected-url': string
|
'selected-url': string
|
||||||
'known-hosts': {
|
'known-hosts': {
|
||||||
'https://registry.start9.com/': string
|
'https://registry.start9.com/': StoreIdentifier
|
||||||
'https://community-registry.start9.com/': string
|
'https://community-registry.start9.com/': StoreIdentifier
|
||||||
[url: string]: string
|
[url: string]: StoreIdentifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,5 @@
|
|||||||
<body>
|
<body>
|
||||||
<h2 class="header-title"><span>embassyOS Initializing</span></h2>
|
<h2 class="header-title"><span>embassyOS Initializing</span></h2>
|
||||||
<p style="padding: 20px;">This process can take up to several minutes to complete.</p>
|
<p style="padding: 20px;">This process can take up to several minutes to complete.</p>
|
||||||
<p>Please wait to refresh the page until you hear a chime from your device.</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user