Skip to content
Merged
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
5 changes: 5 additions & 0 deletions apps/desktop/src/app/api/main.preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const api: Desktop = {
read: (path: string) => ipcRenderer.invoke('Exif.read', path)
},

CacheManager: {
clear: () => ipcRenderer.invoke('CacheManager.clear'),
cacheSize: () => ipcRenderer.invoke('CacheManager.cacheSize')
},

// TODO: Deprecated methods, should be moved into a handler class in the future
openFolder: () => ipcRenderer.invoke('openFolder'),
openFile: () => ipcRenderer.invoke('openFile'),
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/app/events/electron.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export default class ElectronEvents {
injector.inject('Dialogs'),
injector.inject('Exif'),
injector.inject('FileSystem'),
injector.inject('ProcessManager')
injector.inject('ProcessManager'),
injector.inject('CacheManager')
]
);

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/cache-manager/cache-manager.migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const getMigrationRecords = `SELECT * FROM migrations ORDER BY rowid;`;

export const insertMigrationRecord = `INSERT INTO migrations (name) VALUES (:name);`;

export const clearCacheRecords = `DELETE FROM cache_files;`;

export const insertCacheFile = `
INSERT INTO cache_files (
source_name,
Expand Down
38 changes: 35 additions & 3 deletions apps/desktop/src/cache-manager/cache-manager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { app } from 'electron';
import { nanoid } from 'nanoid/non-secure';
import { copyFile, mkdir, stat } from 'node:fs/promises';
import { extname } from 'node:path';
import { copyFile, mkdir, readdir, rm, stat } from 'node:fs/promises';
import { dirname, extname, join } from 'node:path';
import { DatabaseSync, SQLOutputValue } from 'node:sqlite';
import { dirname, join } from 'path';
import { IpcHandler } from '../app/decorators/ipc-handler';
import {
clearCacheRecords,
createMigrationTable,
dbMigrations,
findCacheFile,
Expand All @@ -26,6 +27,15 @@ export class CacheManager {
this.init();
}

@IpcHandler({ name: 'CacheManager.clear' })
readonly clear = async () => {
this.db.exec(clearCacheRecords);
await rm(this.cacheDir, { recursive: true });
};

@IpcHandler({ name: 'CacheManager.cacheSize' })
readonly cacheSize = async (): Promise<number> => this.getDirSize(this.cacheDir);

readonly exists = (sourceName: string, sourceTag: string): Record<string, SQLOutputValue> | null => {
const row = this.db.prepare(findCacheFile).get({ sourceName, sourceTag });
return row ? row : null;
Expand Down Expand Up @@ -104,4 +114,26 @@ export class CacheManager {

return targetPath;
};

private readonly getDirSize = async (directory: string): Promise<number> => {
let size = 0;

try {
const files = await readdir(directory, { withFileTypes: true });

for (const file of files) {
const filePath = join(directory, file.name);

if (file.isDirectory()) {
size += await this.getDirSize(filePath);
} else if (file.isFile()) {
size += (await stat(filePath)).size;
}
}
} catch {
return 0;
}

return size;
};
}
9 changes: 9 additions & 0 deletions apps/web/src/ipc/cache-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Injectable } from '@angular/core';
import { DesktopCacheManager } from 'internal-api';

@Injectable({ providedIn: 'root' })
export class CacheManager implements DesktopCacheManager {
readonly clear = () => window.desktop.CacheManager.clear();

readonly cacheSize = () => window.desktop.CacheManager.cacheSize();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@let totalCacheSize = cacheSize();

<ui-dialog>
<header>
<ui-title>Light Matter Settings</ui-title>
</header>
<section>
<ui-toolbar>
<ui-text [multiLine]="true"
[important]="true">
Cache size &ndash; {{ totalCacheSize | byteSize }}
</ui-text>

<aside>
<ui-flat-button variant="warn"
[disabled]="totalCacheSize === 0"
(pressed)="deleteCache()">
Delete cache
</ui-flat-button>
</aside>
</ui-toolbar>
</section>
<footer>
<ui-action-button (pressed)="dialogRef.close(false)">
Close
</ui-action-button>
</footer>
</ui-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:host {
display: block;
width: 90vw;
max-width: 560px;
}

section {
margin: var(--dim-large) 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DialogRef } from '@angular/cdk/dialog';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CacheManager } from '../../../ipc/cache-manager';

import { SettingsDialogComponent } from './settings-dialog.component';

describe('SettingsDialogComponent', () => {
let component: SettingsDialogComponent;
let fixture: ComponentFixture<SettingsDialogComponent>;

const dialogRef = {};

const cacheManager = {
cacheSize: jest.fn().mockResolvedValue(0)
};

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ SettingsDialogComponent ],
providers: [
{ provide: DialogRef, useValue: dialogRef },
{ provide: CacheManager, useValue: cacheManager }
]
})
.compileComponents();

fixture = TestBed.createComponent(SettingsDialogComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Meta, StoryObj } from '@storybook/angular';
import { SettingsDialogComponent } from './settings-dialog.component';

const meta: Meta<SettingsDialogComponent> = {
title: 'Components/SettingsDialog',
component: SettingsDialogComponent,

args: {},

argTypes: {}
};

export default meta;

type Story = StoryObj<SettingsDialogComponent>;

export const Primary: Story = {
render: props => {
return {
props,
template: `<app-settings-dialog></app-settings-dialog>`
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Dialog, DialogRef } from '@angular/cdk/dialog';
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import {
ActionButtonComponent,
DialogComponent,
FlatButtonComponent,
TextComponent,
TitleComponent,
ToolbarComponent
} from '@light-matter/ui';
import { CacheManager } from '../../../ipc/cache-manager';
import { ByteSizePipe } from '../../../ui/pipes/byte-size/byte-size.pipe';

@Component({
selector: 'app-settings-dialog',
imports: [
ActionButtonComponent,
DialogComponent,
TextComponent,
TitleComponent,
FlatButtonComponent,
ToolbarComponent,
ByteSizePipe
],
templateUrl: './settings-dialog.component.html',
styleUrl: './settings-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SettingsDialogComponent {
/* DI */
readonly dialogRef = inject<DialogRef<boolean>>(DialogRef);

readonly cacheManager = inject(CacheManager);

/* State */
protected readonly cacheSize = signal(0);

/* Constructor */
constructor() {
this.updateCacheSize().then();
}

/* State modifiers */
private readonly updateCacheSize = async () => this.cacheSize.set(await this.cacheManager.cacheSize());

/* Event handlers */
protected readonly deleteCache = async () => {
await this.cacheManager.clear();
await this.updateCacheSize();
};

/* Static methods */
static readonly open = (dialog: Dialog) => dialog.open(SettingsDialogComponent).closed;
}
14 changes: 14 additions & 0 deletions apps/web/src/ui/pipes/byte-size/byte-size.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'byteSize' })
export class ByteSizePipe implements PipeTransform {
transform(value: number): string {
if (value === 0) {
return '0 B';
}

const exponent = Math.floor(Math.log(value) / Math.log(1024));
const decimal = (value / Math.pow(1024, exponent)).toFixed(exponent ? 2 : 0);
return `${decimal} ${exponent ? `${'kMGTPEZY'[exponent - 1]}B` : 'B'}`;
}
}
33 changes: 25 additions & 8 deletions apps/web/src/welcome/pages/gallery/gallery.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,31 @@
<ui-side-panel>

<aside>
@if (galleryRoot !== null) {
<ui-tree #tree
[root]="galleryRoot"
[menu]="galleryMenu"
[enableLoading]="true"
(loadRequested)="loadTreeNode($event)"
(selected)="selectLocation($event)" />
}
<ui-vertical-stack>
<section>
@if (galleryRoot !== null) {
<ui-tree #tree
[root]="galleryRoot"
[menu]="galleryMenu"
[enableLoading]="true"
(loadRequested)="loadTreeNode($event)"
(selected)="selectLocation($event)" />
}
</section>

<footer>
<ui-toolbar>
<aside>
<ui-action-button (pressed)="openSettings()">
<ui-icon icon="settings"
size="large"
thickness="thin"
[inherit]="true" />
</ui-action-button>
</aside>
</ui-toolbar>
</footer>
</ui-vertical-stack>
</aside>

<app-image-grid [selectedLocation]="selectedLocation"
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/welcome/pages/gallery/gallery.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@
justify-content: stretch;
align-items: stretch;
}

ui-side-panel {
aside {
section {
padding: var(--dim-small);
}
}
}
11 changes: 9 additions & 2 deletions apps/web/src/welcome/pages/gallery/gallery.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ import {
PopupMenuComponent,
SidePanelComponent,
TextComponent,
ToolbarComponent,
TreeComponent,
TreeLoadRequest,
TreeNode
TreeNode,
VerticalStackComponent
} from '@light-matter/ui';
import { FileInfo, SortDirection, SortType } from 'internal-api';
import { ImageGridComponent } from '../../../gallery/components/image-grid/image-grid.component';
import { GalleryLocations } from '../../../gallery/services/gallery-locations/gallery-locations';
import { GalleryState } from '../../../gallery/services/gallery-state/gallery-state';
import { defaultSortMode, treeNode } from '../../../gallery/services/gallery-state/gallery-state.types';
import { Dialogs } from '../../../ipc/dialogs';
import { SettingsDialogComponent } from '../../../settings/components/settings-dialog/settings-dialog.component';
import { DefaultPipe } from '../../../system/pipes/default/default.pipe';
import { ImageView } from '../../../viewer/pages/image-view/image-view';
import { ViewNavigator } from '../../../viewer/services/view-navigator/view-navigator';
Expand All @@ -38,7 +41,9 @@ import { ViewNavigator } from '../../../viewer/services/view-navigator/view-navi
IconComponent,
PopupMenuComponent,
TextComponent,
DefaultPipe
DefaultPipe,
VerticalStackComponent,
ToolbarComponent
],
templateUrl: './gallery.page.html',
styleUrl: './gallery.page.scss',
Expand Down Expand Up @@ -97,6 +102,8 @@ export class GalleryPage {
)
.subscribe(() => this.galleryLocations.removeLocation(data.id));

protected readonly openSettings = () => SettingsDialogComponent.open(this.dialog);

protected readonly loadTreeNode = async (state: TreeLoadRequest<string>) => {
const result = await this.galleryState.getDirContents(state.node.id);
const node = structuredClone(state.node);
Expand Down
6 changes: 6 additions & 0 deletions libs/internal-api/src/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@ export interface DesktopExif {
readonly read: (path: string) => Promise<ApiResponse<ExifTags>>;
}

export interface DesktopCacheManager {
readonly clear: () => Promise<void>;
readonly cacheSize: () => Promise<number>;
}

export interface Desktop {
readonly ProcessManager: DesktopProcessManager;
readonly FileSystem: DesktopFileSystem;
readonly Dialogs: DesktopDialogs;
readonly Exif: DesktopExif;
readonly CacheManager: DesktopCacheManager;

openFolder: () => Promise<FileListing>;
openFile: () => Promise<FileListing>;
Expand Down
1 change: 1 addition & 0 deletions libs/ui/icons.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"scaleFill": "@fluentui/svg-icons/icons/scale_fill_20_regular.svg",
"scaleFit": "@fluentui/svg-icons/icons/scale_fit_16_regular.svg",
"scanCamera": "@fluentui/svg-icons/icons/scan_camera_16_regular.svg",
"settings": "@fluentui/svg-icons/icons/settings_16_regular.svg",
"text": "@fluentui/svg-icons/icons/text_16_regular.svg",
"zoomFitFilled": "@fluentui/svg-icons/icons/zoom_fit_16_filled.svg",
"zoomFit": "@fluentui/svg-icons/icons/zoom_fit_16_regular.svg",
Expand Down
2 changes: 1 addition & 1 deletion libs/ui/src/content/components/icon/icon.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
path {
fill: var(--color);
stroke: var(--color);
stroke-width: 0.5px;
stroke-width: var(--stroke-width);
}
}
}
Expand Down
Loading