Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions src/app/apps/apps.component.html
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
<div class="w-100 h-100 d-flex flex-column overflow-hidden" xmlns="http://www.w3.org/1999/html">
<div class="w-100 h-100 d-flex flex-column overflow-hidden position-relative" xmlns="http://www.w3.org/1999/html"
[class.drag-over]="dragOver">
<nav class="manager-toolbar navbar bg-panel border-bottom px-2">
<div class="d-flex flex-row w-100">
<select class="form-select w-auto" [value]="$safeNavigationMigration(device?.name)" (change)="deviceManager.setDefault(deviceSelect.value)"
#deviceSelect>
@for (dev of devices$ | async; track dev.name) {
<option [ngValue]="dev.name" [selected]="dev.name === device?.name">{{ dev.name }}</option>
}
</select>
<div class="d-flex flex-row w-100 align-items-center">
<ul ngbNav #nav="ngbNav" class="nav-pills flex-nowrap ms-2" [(activeId)]="tabId">
<li ngbNavItem="installed">
<a ngbNavLink>Installed</a>
<ng-template ngbNavContent>
<app-installed [device]="device" [installed$]="packages$"></app-installed>
<app-installed [device]="device"/>
</ng-template>
</li>
<li ngbNavItem="channel">
<a ngbNavLink>Available</a>
<ng-template ngbNavContent>
<app-channel [installed]="instPackages"></app-channel>
<app-channel/>
</ng-template>
</li>
</ul>
<button class="btn btn-primary text-nowrap ms-auto" (click)="openInstallChooser()">
<i class="bi bi-folder-fill me-2"></i>Install
</button>
@if (tabId === 'installed') {
<button class="btn btn-primary text-nowrap ms-auto" (click)="openInstallChooser()">
<i class="bi bi-folder-fill me-2"></i>Install
</button>
}
</div>
</nav>
<div class="p-3 flex-fill overflow-auto" [ngbNavOutlet]="nav"></div>
<div class="stat-bar border-top bottom-0 mt-auto d-flex flex-row flex-shrink-0 align-items-center">
<app-stat-storage-info class="ms-auto me-3 storage-info-bar" [device]="device" #storageInfo></app-stat-storage-info>
</div>
<div class="flex-fill d-flex flex-column overflow-hidden" [ngbNavOutlet]="nav"></div>
@if (dragOver) {
<div class="drop-overlay">
<div class="drop-overlay-inner">
<i class="bi bi-download me-2"></i>Drop .ipk to install
</div>
</div>
}
</div>
19 changes: 19 additions & 0 deletions src/app/apps/apps.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.drop-overlay {
position: absolute;
inset: 0;
background: rgba(13, 110, 253, 0.15);
border: 3px dashed rgba(13, 110, 253, 0.75);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 1050;
}

.drop-overlay-inner {
padding: 1rem 1.5rem;
background: var(--bs-body-bg);
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
font-size: 1.25rem;
}
102 changes: 48 additions & 54 deletions src/app/apps/apps.component.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {Component, Injector, OnDestroy, OnInit, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {ChangeDetectionStrategy, Component, Injector, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {noop, Observable, Subscription} from 'rxjs';
import {Device, PackageInfo, RawPackageInfo} from '../types';
import {noop, Observable, Subject, Subscription} from 'rxjs';
import {Device, RawPackageInfo} from '../types';
import {AppManagerService, DeviceManagerService, RepositoryItem} from '../core/services';
import {MessageDialogComponent} from '../shared/components/message-dialog/message-dialog.component';
import {ProgressDialogComponent} from '../shared/components/progress-dialog/progress-dialog.component';
import {keyBy} from 'lodash';
import {open as showOpenDialog} from '@tauri-apps/plugin-dialog';
import {basename, downloadDir} from "@tauri-apps/api/path";
import * as os from "@tauri-apps/plugin-os";
import {getCurrentWebview} from "@tauri-apps/api/webview";
import {APP_ID_HBCHANNEL} from "../shared/constants";
import {HbchannelRemoveComponent} from "./hbchannel-remove/hbchannel-remove.component";
import {StatStorageInfoComponent} from "../shared/components/stat-storage-info/stat-storage-info.component";
import {DetailsComponent} from "./details/details.component";

type UnlistenFn = () => void;

@Component({
selector: 'app-apps',
Expand All @@ -22,57 +23,59 @@ import {DetailsComponent} from "./details/details.component";
})
export class AppsComponent implements OnInit, OnDestroy {

packages$?: Observable<PackageInfo[] | null>;
instPackages?: Record<string, RawPackageInfo>;
device: Device | null = null;
devices$?: Observable<Device[]|null>;
devices$?: Observable<Device[] | null>;
tabId: string = 'installed';

@ViewChild('storageInfo') storageInfo?: StatStorageInfoComponent;
dragOver = false;
readonly storageChanged$ = new Subject<void>();

private deviceSubscription?: Subscription;
private packagesSubscription?: Subscription;
private unlistenDragDrop?: UnlistenFn;

constructor(
public deviceManager: DeviceManagerService,
private modalService: NgbModal,
private appManager: AppManagerService,
public deviceManager: DeviceManagerService,
private zone: NgZone,
) {
}

ngOnInit(): void {
this.devices$ = this.deviceManager.devices$;
this.deviceSubscription = this.deviceManager.selected$.subscribe((device) => {
this.device = device;
if (device) {
this.loadPackages();
} else {
this.packages$ = undefined;
this.packagesSubscription?.unsubscribe();
this.packagesSubscription = undefined;
}
this.deviceSubscription = this.devices$.subscribe(devices => {
this.device = devices?.find(d => d.default) ?? null;
});
this.setupDragDrop().catch(e => console.warn('Drag-drop listener failed:', e));
}

ngOnDestroy(): void {
this.deviceSubscription?.unsubscribe();
this.packagesSubscription?.unsubscribe();
this.packagesSubscription = undefined;
this.unlistenDragDrop?.();
}

loadPackages(): void {
const device = this.device;
if (!device) return;
this.packagesSubscription?.unsubscribe();
this.packages$ = this.appManager.packages$(device);
this.packagesSubscription = this.packages$.subscribe({
next: (pkgs) => {
if (pkgs?.length) {
this.instPackages = keyBy(pkgs, (pkg) => pkg.id);
private async setupDragDrop(): Promise<void> {
if (os.type() === 'android' || os.type() === 'ios') return;
const webview = getCurrentWebview();
this.unlistenDragDrop = await webview.onDragDropEvent(event => {
this.zone.run(() => {
switch (event.payload.type) {
case 'over':
case 'enter':
this.dragOver = true;
break;
case 'leave':
this.dragOver = false;
break;
case 'drop':
this.dragOver = false;
const ipks = event.payload.paths.filter(p => p.toLowerCase().endsWith('.ipk'));
for (const path of ipks) {
this.installFromPath(path).catch(noop);
}
break;
}
}, error: noop
});
});
this.appManager.load(device).catch(noop);
}

async openInstallChooser(): Promise<void> {
Expand All @@ -85,11 +88,17 @@ export class AppsComponent implements OnInit, OnDestroy {
if (!path) {
return;
}
await this.installFromPath(path);
}

private async installFromPath(path: string): Promise<void> {
if (!this.device) return;
const progress = ProgressDialogComponent.open(this.modalService);
const component = progress.componentInstance as ProgressDialogComponent;
try {
await this.appManager.installByPath(this.device, path,
(progress, statusText) => component.update(statusText, progress));
this.storageChanged$.next();
} catch (e) {
console.warn(e);
this.handleInstallationError(await basename(path), e as Error);
Expand Down Expand Up @@ -127,7 +136,7 @@ export class AppsComponent implements OnInit, OnDestroy {
const progress = ProgressDialogComponent.open(this.modalService);
try {
await this.appManager.remove(this.device, pkg.id);
this.storageInfo?.refresh();
this.storageChanged$.next();
return true;
} catch (e) {
MessageDialogComponent.open(this.modalService, {
Expand All @@ -141,8 +150,8 @@ export class AppsComponent implements OnInit, OnDestroy {
}
}

async installPackage(item: RepositoryItem, channel: 'stable' | 'beta' = 'stable'): Promise<boolean> {
const device = this.device;
async installPackage(item: RepositoryItem, channel: 'stable' | 'beta' = 'stable', deviceOverride?: Device): Promise<boolean> {
const device = deviceOverride ?? this.device;
if (!device) return false;
const progress = ProgressDialogComponent.open(this.modalService);
try {
Expand Down Expand Up @@ -181,7 +190,7 @@ export class AppsComponent implements OnInit, OnDestroy {
const component = progress.componentInstance as ProgressDialogComponent;
await this.appManager.installByManifest(device, manifest,
(progress, statusText) => component.update(statusText, progress));
this.storageInfo?.refresh();
this.storageChanged$.next();
return true;
} catch (e: any) {
this.handleInstallationError(item.title, e as Error);
Expand All @@ -191,21 +200,6 @@ export class AppsComponent implements OnInit, OnDestroy {
}
}

openDetails(item: RepositoryItem): void {
const modalRef = this.modalService.open(DetailsComponent, {
size: 'lg',
scrollable: true,
injector: Injector.create({
providers: [
{provide: RepositoryItem, useValue: item},
{provide: 'device', useValue: this.device},
],
}),
});
const component = modalRef.componentInstance as DetailsComponent;
component.parent = this;
}

private handleInstallationError(name: string, e: Error) {
MessageDialogComponent.open(this.modalService, {
title: `Failed to install ${name}`,
Expand Down
2 changes: 2 additions & 0 deletions src/app/apps/apps.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {InstalledComponent} from "./installed/installed.component";
import {NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbProgressbar} from "@ng-bootstrap/ng-bootstrap";
import {SharedModule} from "../shared/shared.module";
import {HbchannelRemoveComponent} from './hbchannel-remove/hbchannel-remove.component';
import {DetailsComponent as InstalledDetailsComponent} from "./installed/details/details.component";
import {FormsModule} from "@angular/forms";

@NgModule({
Expand All @@ -27,6 +28,7 @@ import {FormsModule} from "@angular/forms";
NgbProgressbar,
NgOptimizedImage,
FormsModule,
InstalledDetailsComponent,
]
})
export class AppsModule {
Expand Down
97 changes: 71 additions & 26 deletions src/app/apps/channel/channel.component.html
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
@let repoPage = repoPage$ | async;
@if (repoPage) {
<ul class="list-group list-group-flush">
@for (item of repoPage.packages; track item) {
<li class="list-group-item app-item d-flex flex-row align-items-center" (click)="parent.openDetails(item)"
>
@let manifest = item.manifest;
@if (manifest) {
<div class="app-desc flex-fill">
<img class="app-icon" [ngSrc]="item.iconUri" [alt]="item.title" width="48" height="48">
<div class="app-headline ms-3">
<div class="app-title">{{ item.title }}</div>
<div class="app-description">{{ manifest.appDescription }}</div>
<div class="p-3">
@let repoPage = repoPage$ | async;
@if (repoPage) {
@if (page === 1 && repoPage.packages.length > 2) {
<section class="featured-section mb-4">
<h6 class="text-body-secondary mb-2">Featured</h6>
<div class="featured-grid">
@let hero = repoPage.packages[0];
<button type="button" class="app-card app-card-hero" (click)="openDetails(hero)">
@switch (cardState(hero)) {
@case ('installed') { <span class="app-card-tag badge text-bg-secondary">Installed</span> }
@case ('update') { <span class="app-card-tag badge text-bg-warning">Update</span> }
}
<img class="app-card-icon" [ngSrc]="hero.detailIconUri ?? hero.iconUri" [alt]="hero.title" width="128" height="128">
<div class="app-card-text">
<div class="app-card-title">{{ hero.title }}</div>
<div class="app-card-version">v{{ hero.manifest?.version }}</div>
@if (hero.shortDescription || hero.manifest?.appDescription) {
<p class="app-card-desc mt-2 mb-0">{{ hero.shortDescription || hero.manifest?.appDescription }}</p>
}
</div>
</div>
<button class="btn btn-sm btn-primary ms-2 text-nowrap" aria-label="Details">Details</button>
}
</li>
</button>
@for (item of repoPage.packages.slice(1, 3); track item.id) {
<button type="button" class="app-card" (click)="openDetails(item)">
@switch (cardState(item)) {
@case ('installed') { <span class="app-card-tag badge text-bg-secondary">Installed</span> }
@case ('update') { <span class="app-card-tag badge text-bg-warning">Update</span> }
}
<img class="app-card-icon" [ngSrc]="item.iconUri" [alt]="item.title" width="64" height="64">
<div class="app-card-text">
<div class="app-card-title text-truncate">{{ item.title }}</div>
<div class="app-card-version">v{{ item.manifest?.version }}</div>
@if (item.shortDescription) {
<div class="app-card-desc text-truncate mt-1">{{ item.shortDescription }}</div>
}
</div>
</button>
}
</div>
</section>
}
</ul>
<div class="mt-3">
<ngb-pagination class="d-flex justify-content-center" [collectionSize]="repoPage.paging.maxPage" [(page)]="page"
[pageSize]="1" aria-label="Default pagination" (pageChange)="loadPage($event)">
</ngb-pagination>
</div>
} @else {
<app-loading-card/>
}
<section>
<h6 class="text-body-secondary mb-2">{{ page === 1 ? 'All apps' : ('Page ' + page) }}</h6>
<div class="app-grid">
@for (item of repoPage.packages; track item.id) {
@if (item.manifest) {
<button type="button" class="app-card" (click)="openDetails(item)">
@switch (cardState(item)) {
@case ('installed') { <span class="app-card-tag badge text-bg-secondary">Installed</span> }
@case ('update') { <span class="app-card-tag badge text-bg-warning">Update</span> }
}
<img class="app-card-icon" [ngSrc]="item.iconUri" [alt]="item.title" width="64" height="64">
<div class="app-card-text">
<div class="app-card-title text-truncate">{{ item.title }}</div>
<div class="app-card-version">v{{ item.manifest.version }}</div>
@if (item.shortDescription) {
<div class="app-card-desc text-truncate mt-1">{{ item.shortDescription }}</div>
}
</div>
</button>
}
}
</div>
</section>
<div class="mt-4">
<ngb-pagination class="d-flex justify-content-center" [collectionSize]="repoPage.paging.maxPage" [(page)]="page"
[pageSize]="1" aria-label="App pages" (pageChange)="loadPage($event)">
</ngb-pagination>
</div>
} @else {
<app-loading-card/>
}
</div>
Loading
Loading