@let installed = installedInfo$ | async;
-
-
![App icon]()
-
-
{{ item.title }}
-
+
diff --git a/src/app/apps/details/details.component.scss b/src/app/apps/details/details.component.scss
index 5e537a6b..f75fd044 100644
--- a/src/app/apps/details/details.component.scss
+++ b/src/app/apps/details/details.component.scss
@@ -1,12 +1,21 @@
+app-channel-app-details {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+}
+
.full-description {
img {
max-width: 100%;
}
}
-.app-details-icon {
- max-width: 25vw;
- max-height: 25vw;
- height: auto;
- object-fit: contain;
+.hero-icon {
+ border-radius: 0.75rem;
+ flex: 0 0 auto;
+}
+
+.detail-footer {
+ flex: 0 0 auto;
}
diff --git a/src/app/apps/details/details.component.ts b/src/app/apps/details/details.component.ts
index 24fe8691..a4bc9084 100644
--- a/src/app/apps/details/details.component.ts
+++ b/src/app/apps/details/details.component.ts
@@ -1,10 +1,10 @@
-import {Component, ElementRef, Inject, OnDestroy, OnInit, Renderer2, ViewChild, ViewEncapsulation, ChangeDetectionStrategy} from '@angular/core';
-import {AppManagerService, IncompatibleReason, PackageManifest, RepositoryItem} from "../../core/services";
+import {ChangeDetectionStrategy, Component, ElementRef, Inject, OnDestroy, OnInit, Renderer2, ViewChild, ViewEncapsulation} from '@angular/core';
+import {AppManagerService, DeviceManagerService, IncompatibleReason, PackageManifest, RepositoryItem} from "../../core/services";
import {noop, Observable, of} from "rxjs";
-import { AsyncPipe, NgOptimizedImage } from "@angular/common";
+import {AsyncPipe, NgOptimizedImage} from "@angular/common";
import {open as openPath} from "@tauri-apps/plugin-shell";
import {
- NgbActiveModal,
+ NgbActiveOffcanvas,
NgbDropdown,
NgbDropdownItem,
NgbDropdownMenu,
@@ -19,15 +19,15 @@ import {ExternalLinkDirective} from "../../shared/directives";
@Component({
selector: 'app-channel-app-details',
imports: [
- AsyncPipe,
- NgOptimizedImage,
- NgbDropdown,
- NgbDropdownItem,
- NgbDropdownMenu,
- NgbDropdownToggle,
- SharedModule,
- ExternalLinkDirective
-],
+ AsyncPipe,
+ NgOptimizedImage,
+ NgbDropdown,
+ NgbDropdownItem,
+ NgbDropdownMenu,
+ NgbDropdownToggle,
+ SharedModule,
+ ExternalLinkDirective
+ ],
templateUrl: './details.component.html',
styleUrl: './details.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
@@ -38,27 +38,42 @@ export class DetailsComponent implements OnInit, OnDestroy {
fullDescriptionHtml$: Observable
;
installedInfo$?: Observable;
- incompatible$: Observable;
+ incompatible$!: Observable;
+ devices$: Observable;
+ selectedDevice: Device;
@ViewChild('fullDescription', {static: true})
fullDescription!: ElementRef;
- parent?: AppsComponent;
-
-
private unsubscribeClickListener!: () => void;
constructor(
- public modal: NgbActiveModal,
+ public offcanvas: NgbActiveOffcanvas,
public item: RepositoryItem,
@Inject('device') public device: Device,
+ @Inject('parent') private parent: AppsComponent,
private appManager: AppManagerService,
+ private deviceManager: DeviceManagerService,
private renderer2: Renderer2
) {
this.manifest = item.manifest!;
- this.incompatible$ = fromPromise(this.appManager.checkIncompatibility(device, item));
+ this.selectedDevice = device;
+ this.devices$ = this.deviceManager.devices$;
this.fullDescriptionHtml$ = item.fullDescriptionUrl ? fromPromise(fetch(item.fullDescriptionUrl)
.then(resp => resp.text())) : of('');
+ this.refreshForDevice();
+ }
+
+ onDeviceChange(name: string): void {
+ let next: Device | undefined;
+ this.devices$.subscribe(devices => next = devices?.find(d => d.name === name)).unsubscribe();
+ if (!next) return;
+ this.selectedDevice = next;
+ this.refreshForDevice();
+ }
+
+ private refreshForDevice(): void {
+ this.incompatible$ = fromPromise(this.appManager.checkIncompatibility(this.selectedDevice, this.item));
this.reloadInstalledInfo();
}
@@ -75,15 +90,20 @@ export class DetailsComponent implements OnInit, OnDestroy {
this.unsubscribeClickListener();
}
+ launchApp(id: string) {
+ this.parent.launchApp(id);
+ }
+
installPackage(item: RepositoryItem, channel: 'stable' | 'beta' = 'stable') {
- this.parent?.installPackage(item, channel).then((installed) => installed && this.reloadInstalledInfo());
+ this.parent.installPackage(item, channel, this.selectedDevice)
+ .then((installed) => installed && this.reloadInstalledInfo());
}
removePackage(item: PackageInfo) {
- this.parent?.removePackage(item).then((removed) => removed && this.reloadInstalledInfo());
+ this.parent.removePackage(item).then((removed) => removed && this.reloadInstalledInfo());
}
private reloadInstalledInfo(): void {
- this.installedInfo$ = fromPromise(this.appManager.info(this.device, this.item.id));
+ this.installedInfo$ = fromPromise(this.appManager.info(this.selectedDevice, this.item.id));
}
}
diff --git a/src/app/apps/installed/details/details.component.html b/src/app/apps/installed/details/details.component.html
new file mode 100644
index 00000000..6dbae694
--- /dev/null
+++ b/src/app/apps/installed/details/details.component.html
@@ -0,0 +1,68 @@
+
+
+
+ @if (pkg.appDescription) {
+
About
+
{{ pkg.appDescription }}
+ }
+
Details
+
+ - App ID
+ {{ pkg.id }}
+ @if (pkg.vendor) {
+ - Vendor
+ - {{ pkg.vendor }}
+ }
+ @if (pkg.type) {
+ - Type
+ - {{ pkg.type }}
+ }
+ - Path
+ {{ pkg.folderPath }}
+ - Disk usage
+ -
+ @let du = diskUsage$ | async;
+ @if (!du || (!du.result && !du.error)) {
+ Calculating…
+ } @else if (du.error) {
+ Unavailable
+ } @else if (du.result) {
+ {{ du.result.total * 1024 | filesize:sizeOptions }}
+ }
+
+ @if (sourceUrl) {
+ - Website
+ -
+ {{ sourceUrl }}
+
+ }
+
+
+
diff --git a/src/app/apps/installed/details/details.component.scss b/src/app/apps/installed/details/details.component.scss
new file mode 100644
index 00000000..d063d4a3
--- /dev/null
+++ b/src/app/apps/installed/details/details.component.scss
@@ -0,0 +1,10 @@
+:host {
+ display: block;
+ height: 100%;
+}
+
+.detail-icon {
+ width: 64px;
+ height: 64px;
+ border-radius: 0.75rem;
+}
diff --git a/src/app/apps/installed/details/details.component.spec.ts b/src/app/apps/installed/details/details.component.spec.ts
new file mode 100644
index 00000000..b76b5529
--- /dev/null
+++ b/src/app/apps/installed/details/details.component.spec.ts
@@ -0,0 +1,98 @@
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {Mock, vi} from 'vitest';
+
+import {DetailsComponent} from './details.component';
+import {AppManagerService, PackageManifest, RepositoryItem} from '../../../core/services';
+import {Device, PackageInfo} from '../../../types';
+import {AppsComponent} from '../../apps.component';
+
+describe('InstalledDetailsComponent', () => {
+ let component: DetailsComponent;
+ let fixture: ComponentFixture;
+ let parentSpy: {launchApp: Mock; removePackage: Mock; installPackage: Mock};
+
+ const device: Device = {
+ name: 'test', host: '192.168.1.1', port: 22, username: 'prisoner',
+ profile: 'ose', privateKey: {openSsh: 'test'},
+ };
+ const pkg: PackageInfo = {
+ id: 'com.example.app', title: 'Example', version: '1.0.0',
+ folderPath: '/media/developer/apps/usr/palm/applications/com.example.app',
+ iconUri: '', appDescription: '',
+ };
+
+ function setup(repoPackage: RepositoryItem | null = null) {
+ parentSpy = {
+ launchApp: vi.fn(),
+ removePackage: vi.fn().mockResolvedValue(true),
+ installPackage: vi.fn().mockResolvedValue(true),
+ };
+ const appManagerStub = {
+ appDiskUsage: () => Promise.reject(new Error('not under test')),
+ } as Partial;
+
+ TestBed.configureTestingModule({
+ imports: [DetailsComponent],
+ providers: [
+ {provide: AppManagerService, useValue: appManagerStub},
+ ],
+ });
+
+ fixture = TestBed.createComponent(DetailsComponent);
+ component = fixture.componentInstance;
+ component.pkg = pkg;
+ component.device = device;
+ // Only the three methods the details view calls are stubbed.
+ component.parent = parentSpy as unknown as AppsComponent;
+ component.repoPackage = repoPackage;
+ component.ngOnChanges({
+ pkg: {currentValue: pkg, previousValue: undefined, firstChange: true, isFirstChange: () => true},
+ device: {currentValue: device, previousValue: undefined, firstChange: true, isFirstChange: () => true},
+ });
+ fixture.detectChanges();
+ }
+
+ it('should create', () => {
+ setup();
+ expect(component).toBeTruthy();
+ });
+
+ it('Launch calls parent.launchApp', () => {
+ setup();
+ component.launch();
+ expect(parentSpy.launchApp).toHaveBeenCalledWith(pkg.id);
+ });
+
+ it('Uninstall calls parent.removePackage with the package', async () => {
+ setup();
+ const removed = await component.uninstall();
+ expect(parentSpy.removePackage).toHaveBeenCalledWith(pkg);
+ expect(removed).toBe(true);
+ });
+
+ it('Update calls parent.installPackage when a repo package is available', async () => {
+ const newer = new RepositoryItem({manifest: new PackageManifest({version: '2.0.0'})}, '');
+ setup(newer);
+ const installed = await component.update();
+ expect(parentSpy.installPackage).toHaveBeenCalledWith(newer);
+ expect(installed).toBe(true);
+ });
+
+ it('Update is a no-op without a repo package', async () => {
+ setup(null);
+ const installed = await component.update();
+ expect(installed).toBe(false);
+ expect(parentSpy.installPackage).not.toHaveBeenCalled();
+ });
+
+ it('hasUpdate is false when no repo package is provided', () => {
+ setup(null);
+ expect(component.hasUpdate).toBe(false);
+ });
+
+ it('hasUpdate reflects the repo manifest comparison', () => {
+ const newer = new RepositoryItem({manifest: new PackageManifest({version: '2.0.0'})}, '');
+ setup(newer);
+ expect(component.hasUpdate).toBe(true);
+ });
+});
diff --git a/src/app/apps/installed/details/details.component.ts b/src/app/apps/installed/details/details.component.ts
new file mode 100644
index 00000000..0ae9fad5
--- /dev/null
+++ b/src/app/apps/installed/details/details.component.ts
@@ -0,0 +1,65 @@
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {AsyncResult, Device, PackageInfo} from "../../../types";
+import {AppManagerService, PackageDiskUsage, RepositoryItem} from "../../../core/services";
+import {fromPromise} from "rxjs/internal/observable/innerFrom";
+import {Observable, of} from "rxjs";
+import {AsyncPipe} from "@angular/common";
+import {SharedModule} from "../../../shared/shared.module";
+import {FilesizePipe} from "../../../shared/pipes/filesize.pipe";
+import {FileSizeOptions} from "filesize";
+import {AppsComponent} from "../../apps.component";
+
+@Component({
+ selector: 'app-installed-details',
+ standalone: true,
+ imports: [
+ AsyncPipe,
+ SharedModule,
+ FilesizePipe
+ ],
+ templateUrl: './details.component.html',
+ styleUrl: './details.component.scss'
+})
+export class DetailsComponent implements OnChanges {
+
+ @Input() pkg!: PackageInfo;
+ @Input() device!: Device;
+ @Input() parent!: AppsComponent;
+ @Input() repoPackage: RepositoryItem | null = null;
+
+ diskUsage$: Observable> = of({});
+ sizeOptions: FileSizeOptions = {base: 2, standard: 'jedec'};
+
+ constructor(private appManager: AppManagerService) {
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['pkg'] || changes['device']) {
+ this.diskUsage$ = this.pkg && this.device
+ ? fromPromise(this.appManager.appDiskUsage(this.device, this.pkg.folderPath)
+ .then((result) => ({result})).catch((error) => ({error})))
+ : of({});
+ }
+ }
+
+ get hasUpdate(): boolean {
+ return this.repoPackage?.manifest?.hasUpdate(this.pkg.version) === true;
+ }
+
+ launch(): void {
+ this.parent.launchApp(this.pkg.id);
+ }
+
+ uninstall(): Promise {
+ return this.parent.removePackage(this.pkg);
+ }
+
+ update(): Promise {
+ if (!this.repoPackage) return Promise.resolve(false);
+ return this.parent.installPackage(this.repoPackage);
+ }
+
+ get sourceUrl(): string | undefined {
+ return this.repoPackage?.manifest?.sourceUrl;
+ }
+}
diff --git a/src/app/apps/installed/installed.component.html b/src/app/apps/installed/installed.component.html
index 62aa3c8e..fe412b89 100644
--- a/src/app/apps/installed/installed.component.html
+++ b/src/app/apps/installed/installed.component.html
@@ -1,35 +1,65 @@
-@let installed = installed$ | async;
-@if (installedError) {
-
-
-} @else if (installed) {
-
- @for (pkg of installed; track pkg.id) {
- -
-
-
-
![]()
-
-
{{ pkg.title }}
-
- v{{ pkg.version }}
- @let rpkg = repoPackages && repoPackages[pkg.id];
- @if (rpkg && rpkg.manifest?.hasUpdate(pkg.version)) {
- › v{{ rpkg.manifest?.version }}
- }
-
-
-
- @if (repoPackages?.[pkg.id]?.manifest?.hasUpdate(pkg.version)) {
-
- }
-
+
+
+
+ @if (selectedPkg && device) {
+
+ } @else {
+
+
+
Select an installed app to see details
+
}
-
-} @else {
-
-}
+
+
+
+
+
+
diff --git a/src/app/apps/installed/installed.component.scss b/src/app/apps/installed/installed.component.scss
index e69de29b..71ebfa3f 100644
--- a/src/app/apps/installed/installed.component.scss
+++ b/src/app/apps/installed/installed.component.scss
@@ -0,0 +1,40 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+}
+
+.installed-body {
+ min-height: 0;
+}
+
+.installed-list-pane {
+ width: 320px;
+ flex: 0 0 auto;
+}
+
+.installed-row {
+ cursor: pointer;
+ border-left: 3px solid transparent;
+
+ &.active {
+ background: var(--bs-primary-bg-subtle);
+ border-left-color: var(--bs-primary);
+ color: inherit;
+ }
+}
+
+.installed-row-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 0.4rem;
+}
+
+.installed-row-title {
+ font-weight: 500;
+}
+
+.installed-empty {
+ padding: 2rem;
+}
diff --git a/src/app/apps/installed/installed.component.ts b/src/app/apps/installed/installed.component.ts
index 6bb117c2..6d93a903 100644
--- a/src/app/apps/installed/installed.component.ts
+++ b/src/app/apps/installed/installed.component.ts
@@ -1,8 +1,10 @@
-import {Component, Host, Input, OnDestroy, ChangeDetectionStrategy} from '@angular/core';
+import {ChangeDetectionStrategy, Component, Host, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild} from '@angular/core';
import {AppsComponent} from '../apps.component';
import {Device, PackageInfo} from "../../types";
import {Observable, Subscription} from "rxjs";
-import {AppsRepoService, RepositoryItem} from "../../core/services";
+import {AppManagerService, AppsRepoService, RepositoryItem} from "../../core/services";
+import {fromPromise} from "rxjs/internal/observable/innerFrom";
+import {StatStorageInfoComponent} from "../../shared/components/stat-storage-info/stat-storage-info.component";
@Component({
selector: 'app-installed',
@@ -11,49 +13,79 @@ import {AppsRepoService, RepositoryItem} from "../../core/services";
changeDetection: ChangeDetectionStrategy.Eager,
standalone: false
})
-export class InstalledComponent implements OnDestroy {
+export class InstalledComponent implements OnChanges, OnInit, OnDestroy {
- @Input()
- device: Device | null = null;
+ @Input() device: Device | null = null;
+
+ installed$: Observable | undefined;
installedError?: Error;
repoPackages?: Record;
- private subscription?: Subscription;
- private installedField?: Observable;
+ selectedPkg: PackageInfo | null = null;
+ filterText = '';
- constructor(@Host() public parent: AppsComponent, private appsRepo: AppsRepoService) {
- }
+ @ViewChild('storageInfo') storageInfo?: StatStorageInfoComponent;
- @Input()
- set installed$(value: Observable | undefined) {
- this.subscription?.unsubscribe();
- this.subscription = value?.subscribe({
- next: (pkgs) => {
- this.installedError = undefined;
+ private storageSubscription?: Subscription;
- const strings: string[] = pkgs?.map((pkg) => pkg.id) ?? [];
- this.appsRepo.showApps(...strings).then(apps => this.repoPackages = apps);
- },
- error: (error) => {
- console.log('installed apps', error);
- return this.installedError = error;
- }
- });
- this.installedField = value;
+ constructor(@Host() public parent: AppsComponent,
+ private appManager: AppManagerService, private appsRepo: AppsRepoService) {
}
- get installed$(): Observable | undefined {
- return this.installedField;
+ ngOnInit(): void {
+ this.storageSubscription = this.parent.storageChanged$.subscribe(() => {
+ this.storageInfo?.refresh();
+ this.loadPackages();
+ });
}
ngOnDestroy(): void {
- this.subscription?.unsubscribe();
+ this.storageSubscription?.unsubscribe();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['device']) {
+ this.selectedPkg = null;
+ this.loadPackages();
+ }
}
loadPackages(): void {
+ const device = this.device;
this.installedError = undefined;
- this.parent.loadPackages();
+ this.repoPackages = undefined;
+ if (!device) {
+ this.installed$ = undefined;
+ return;
+ }
+ this.installed$ = fromPromise(this.appManager.load(device).then(packages => {
+ this.appsRepo.showApps(...packages.map(p => p.id))
+ .then(repo => this.repoPackages = repo)
+ .catch(() => undefined);
+ this.reconcileSelection(packages);
+ return packages;
+ }));
+ }
+
+ selectPackage(pkg: PackageInfo): void {
+ this.selectedPkg = pkg;
+ }
+
+ matchesFilter(pkg: PackageInfo): boolean {
+ if (!this.filterText) return true;
+ const q = this.filterText.toLowerCase();
+ return pkg.title.toLowerCase().includes(q) || pkg.id.toLowerCase().includes(q);
+ }
+
+ hasUpdate(pkg: PackageInfo): boolean {
+ return this.repoPackages?.[pkg.id]?.manifest?.hasUpdate(pkg.version) === true;
+ }
+
+ private reconcileSelection(packages: PackageInfo[]): void {
+ if (!this.selectedPkg) return;
+ const match = packages.find(p => p.id === this.selectedPkg!.id);
+ this.selectedPkg = match ?? null;
}
}
diff --git a/src/app/core/services/app-manager.service.ts b/src/app/core/services/app-manager.service.ts
index fb321fe6..788748a0 100644
--- a/src/app/core/services/app-manager.service.ts
+++ b/src/app/core/services/app-manager.service.ts
@@ -1,6 +1,6 @@
import {Injectable} from '@angular/core';
import {BehaviorSubject, catchError, firstValueFrom, lastValueFrom, mergeMap, noop, Observable, Subject} from 'rxjs';
-import {Device, PackageInfo, RawPackageInfo} from '../../types';
+import {Device, DeviceLike, PackageInfo, RawPackageInfo} from '../../types';
import {
LunaResponse,
LunaResponseError,
@@ -279,6 +279,37 @@ export class AppManagerService {
);
}
+ async appDiskUsage(device: DeviceLike, appDir: string): Promise {
+ const appIndex = appDir.indexOf('/usr/palm/applications/');
+ if (appIndex < 0) {
+ throw new Error('Not accepted appDir format: ' + appDir);
+ }
+ const installBase = appDir.substring(0, appIndex);
+ const appId = appDir.substring(appIndex + 23);
+ const pkgInfo = JSON.parse(await this.file.read(device, `${installBase}/usr/palm/packages/${appId}/packageinfo.json`, undefined, 'utf-8')) as PkgInfo;
+ const dirs = [appDir];
+ pkgInfo.services?.forEach(service => {
+ const serviceDir = `${installBase}/usr/palm/services/${service}`;
+ dirs.push(serviceDir);
+ });
+ return await this.cmd.exec(device, `xargs du -d 0 -c`, 'utf-8', dirs.join('\n'))
+ .then(stdout => (Object.fromEntries(stdout.split('\n').map(line => line.match(/(\d+)\t(.+)/))
+ .map((match): [keyof PackageDiskUsage, number] | null => {
+ if (!match) {
+ return null;
+ }
+ const size = parseInt(match[1] ?? '0');
+ const path = match[2] ?? '';
+ if (path === 'total') {
+ return ['total', size];
+ } else if (path.includes('/usr/palm/applications')) {
+ return ['application', size];
+ } else if (path.includes('/usr/palm/services')) {
+ return [path.substring(path.lastIndexOf('/')), size];
+ }
+ return null;
+ }).filter(v => v) as Iterable<[keyof PackageDiskUsage, number]>) as unknown as PackageDiskUsage));
+ }
}
function mapAppinstalldResponse(v: LunaResponse, expectResult: string | RegExp): boolean {
@@ -301,6 +332,10 @@ function mapAppinstalldResponse(v: LunaResponse, expectResult: string | RegExp):
return false;
}
+interface PkgInfo {
+ services?: string[];
+}
+
export interface InstallProgressHandler {
(progress?: number, statusText?: string): void;
}
@@ -314,3 +349,10 @@ export class InstallError extends Error {
return new InstallError('Can\'t install because of insufficient space', details);
}
}
+
+export interface PackageDiskUsage {
+ application: number;
+ total: number;
+
+ [service: string]: number;
+}
diff --git a/src/app/files/files.module.ts b/src/app/files/files.module.ts
index 53296b53..9813047f 100644
--- a/src/app/files/files.module.ts
+++ b/src/app/files/files.module.ts
@@ -15,6 +15,7 @@ import {FilesTableComponent} from './files-table/files-table.component';
import {SharedModule} from "../shared/shared.module";
import {CreateDirectoryMessageComponent} from './create-directory-message/create-directory-message.component';
import {ReactiveFormsModule} from "@angular/forms";
+import {FilesizePipe} from "../shared/pipes/filesize.pipe";
@NgModule({
@@ -24,17 +25,18 @@ import {ReactiveFormsModule} from "@angular/forms";
FilesTableComponent,
CreateDirectoryMessageComponent,
],
- imports: [
- CommonModule,
- FilesRoutingModule,
- NgbTooltipModule,
- SharedModule,
- NgbDropdown,
- NgbDropdownItem,
- NgbDropdownMenu,
- NgbDropdownToggle,
- ReactiveFormsModule,
- ]
+ imports: [
+ CommonModule,
+ FilesRoutingModule,
+ NgbTooltipModule,
+ SharedModule,
+ NgbDropdown,
+ NgbDropdownItem,
+ NgbDropdownMenu,
+ NgbDropdownToggle,
+ ReactiveFormsModule,
+ FilesizePipe,
+ ]
})
export class FilesModule {
}
diff --git a/src/app/shared/pipes/filesize.pipe.ts b/src/app/shared/pipes/filesize.pipe.ts
index a6c9d08a..34bc576b 100644
--- a/src/app/shared/pipes/filesize.pipe.ts
+++ b/src/app/shared/pipes/filesize.pipe.ts
@@ -1,13 +1,13 @@
import {Pipe, PipeTransform} from "@angular/core";
-import {filesize, FileSizeOptionsBase} from 'filesize';
+import {filesize, FileSizeOptions} from 'filesize';
@Pipe({
name: 'filesize',
- standalone: false
+ standalone: true
})
export class FilesizePipe implements PipeTransform {
- transform(bytes: number, options: Partial): string {
+ transform(bytes: number, options: Partial): string {
return filesize(bytes, {output: "string", ...options});
}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index a95a83d4..45be2a06 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -17,7 +17,6 @@ import {FilesizePipe} from "./pipes/filesize.pipe";
declarations: [
PageNotFoundComponent,
TrustUriPipe,
- FilesizePipe,
MessageDialogComponent,
ProgressDialogComponent,
MessageTraceComponent,
@@ -26,11 +25,10 @@ import {FilesizePipe} from "./pipes/filesize.pipe";
StatStorageInfoComponent,
],
imports: [CommonModule, FormsModule, NgbModule,
- ExternalLinkDirective],
+ ExternalLinkDirective, FilesizePipe],
exports: [
PageNotFoundComponent,
TrustUriPipe,
- FilesizePipe,
MessageDialogComponent,
ProgressDialogComponent,
MessageTraceComponent,
diff --git a/src/app/types/index.ts b/src/app/types/index.ts
index 76b77faf..c908fe04 100644
--- a/src/app/types/index.ts
+++ b/src/app/types/index.ts
@@ -1,3 +1,8 @@
export * from './device-manager';
export * from './file-session';
export * from './device';
+
+export interface AsyncResult {
+ result?: T;
+ error?: E;
+}
diff --git a/src/styles.scss b/src/styles.scss
index ffabc992..21b05453 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -33,3 +33,20 @@ body {
.bg-panel {
@extend .bg-dark-subtle;
}
+
+app-apps .tab-content > .tab-pane.active {
+ flex: 1 1 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.offcanvas.app-detail-offcanvas {
+ --bs-offcanvas-width: 480px;
+}
+
+@media (max-width: 575.98px) {
+ .offcanvas.app-detail-offcanvas {
+ --bs-offcanvas-width: 100%;
+ }
+}
diff --git a/src/styles/shared.scss b/src/styles/shared.scss
index ac8bad63..b67c3281 100644
--- a/src/styles/shared.scss
+++ b/src/styles/shared.scss
@@ -14,6 +14,7 @@
.storage-info-bar {
width: 25%;
+ height: 15px;
max-width: 110px;
}