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
4 changes: 4 additions & 0 deletions packages/uhk-agent/src/electron-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DeviceService } from './services/device.service';
import { ElectronLogService } from './services/logger.service';
import { AppUpdateService } from './services/app-update.service';
import { AppService } from './services/app.service';
import { FileDialogService } from './services/file-dialog.service';
import { SudoService } from './services/sudo.service';
import { SmartMacroDocService } from './services/smart-macro-doc.service';
import { TrayService } from './services/tray.service';
Expand Down Expand Up @@ -56,6 +57,7 @@ let uhkHidDeviceService: UhkHidDevice;
let uhkOperations: UhkOperations;
let appUpdateService: AppUpdateService;
let appService: AppService;
let fileDialogService: FileDialogService;
let sudoService: SudoService;
let packagesDir: string;
let smartMacroDocService: SmartMacroDocService;
Expand Down Expand Up @@ -133,6 +135,7 @@ async function createWindow() {
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, options, packagesDir);
appUpdateService = new AppUpdateService(logger, win, options);
appService = new AppService(logger, win, deviceService, options, packagesDir);
fileDialogService = new FileDialogService(logger, win);
sudoService = new SudoService(logger, options, deviceService, packagesDir);
// and load the index.html of the app.

Expand Down Expand Up @@ -223,6 +226,7 @@ async function windowClosed() {
deviceService = null;
appUpdateService = null;
appService = null;
fileDialogService = null;
await uhkHidDeviceService.close();
uhkHidDeviceService = null;
sudoService = null;
Expand Down
98 changes: 98 additions & 0 deletions packages/uhk-agent/src/services/file-dialog.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { dialog, ipcMain } from 'electron';
import settings from 'electron-settings';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import {
IpcEvents,
LogService,
OpenUserConfigDialogResult,
SaveUserConfigDialogData,
SaveUserConfigDialogResult,
} from 'uhk-common';

import { MainServiceBase } from './main-service-base';

const USER_CONFIG_FOLDER_SETTING = 'user-config-folder';

export class FileDialogService extends MainServiceBase {
constructor(protected logService: LogService,
protected win: Electron.BrowserWindow) {
super(logService, win);

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
ipcMain.handle(IpcEvents.fileDialog.openUserConfig, this.openUserConfig.bind(this));
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
ipcMain.handle(IpcEvents.fileDialog.saveUserConfig, this.saveUserConfig.bind(this));

logService.misc('[FileDialogService] init success');
}

private async openUserConfig(): Promise<OpenUserConfigDialogResult> {
const defaultPath = await this.getUserConfigFolder();
this.logService.misc(`[FileDialogService] open user configuration dialog, defaultPath: ${defaultPath}`);

const result = await dialog.showOpenDialog(this.win, {
defaultPath,
properties: ['openFile'],
filters: [
{ name: 'User configuration', extensions: ['json', 'bin'] },
{ name: 'All files', extensions: ['*'] },
],
});

if (result.canceled || result.filePaths.length === 0) {
return { canceled: true };
}

const filePath = result.filePaths[0];
await this.saveUserConfigFolder(filePath);

const buffer = await fs.readFile(filePath);
this.logService.misc(`[FileDialogService] opened user configuration: ${filePath}`);

return {
canceled: false,
filePath,
data: Array.from(buffer),
};
}

private async saveUserConfig(_event: Electron.IpcMainInvokeEvent,
data: SaveUserConfigDialogData): Promise<SaveUserConfigDialogResult> {
const folder = await this.getUserConfigFolder();
const defaultPath = folder ? join(folder, data.fileName) : data.fileName;
this.logService.misc(`[FileDialogService] save user configuration dialog, defaultPath: ${defaultPath}`);

const result = await dialog.showSaveDialog(this.win, {
defaultPath,
filters: [
{ name: 'User configuration', extensions: [data.fileName.split('.').pop()] },
{ name: 'All files', extensions: ['*'] },
],
});

if (result.canceled || !result.filePath) {
return { canceled: true };
}

await fs.writeFile(result.filePath, Buffer.from(data.content, 'base64'));
await this.saveUserConfigFolder(result.filePath);
this.logService.misc(`[FileDialogService] saved user configuration: ${result.filePath}`);

return {
canceled: false,
filePath: result.filePath,
};
}

private async getUserConfigFolder(): Promise<string | undefined> {
const folder = await settings.get(USER_CONFIG_FOLDER_SETTING);

return typeof folder === 'string' && folder.length > 0 ? folder : undefined;
}

private async saveUserConfigFolder(filePath: string): Promise<void> {
await settings.set(USER_CONFIG_FOLDER_SETTING, dirname(filePath));
}
}
18 changes: 18 additions & 0 deletions packages/uhk-common/src/models/file-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface OpenUserConfigDialogResult {
canceled: boolean;
filePath?: string;
data?: Array<number>;
}

export interface SaveUserConfigDialogData {
fileName: string;
/**
* Base64 encoded file content.
*/
content: string;
}

export interface SaveUserConfigDialogResult {
canceled: boolean;
filePath?: string;
}
1 change: 1 addition & 0 deletions packages/uhk-common/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './currently-updating-module-info.js';
export * from './device-module.js';
export * from './device-version-information.js';
export * from './dongle.js';
export * from './file-dialog.js';
export * from './firmware-json.js';
export * from './firmware-repo-info.js';
export * from './firmware-upgrade-connect-prompt.js';
Expand Down
6 changes: 6 additions & 0 deletions packages/uhk-common/src/util/ipcEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,15 @@ export class SmartMacroDoc {
public static readonly serviceListening = 'smart-macro-doc-service-listening';
}

export class FileDialog {
public static readonly openUserConfig = 'file-dialog-open-user-config';
public static readonly saveUserConfig = 'file-dialog-save-user-config';
}

export class IpcEvents {
public static readonly app = App;
public static readonly autoUpdater = AutoUpdate;
public static readonly device = Device;
public static readonly fileDialog = FileDialog;
public static readonly smartMacroDoc = SmartMacroDoc;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ <h1>
</button>
</li>
<li>
<file-upload (fileChanged)="changeFile($event)"
[disabled]="savingUserConfig"
label="Import device configuration">
</file-upload>
<button class="btn btn-primary"
[disabled]="savingUserConfig"
(click)="importUserConfiguration()">Import device configuration
</button>
</li>
<li *ngIf="(hasRecoverableLEDSpace$ | async)">
<span class="d-inline-block"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable, Subscription } from 'rxjs';
import { UploadFileData } from 'uhk-common';
import { faSlidersH } from '@fortawesome/free-solid-svg-icons';

import { AppState,
Expand All @@ -25,6 +24,7 @@ import {
GetUserConfigurationFromHistoryAction,
LoadUserConfigurationHistoryAction
} from '../../../store/actions/user-configuration-history.actions';
import { FileDialogService } from '../../../services/file-dialog.service';

@Component({
selector: 'device-settings',
Expand All @@ -44,6 +44,7 @@ export class DeviceConfigurationComponent implements OnInit, OnDestroy {
faSlidersH = faSlidersH;

private readonly store = inject<Store<AppState>>(Store);
private readonly fileDialogService = inject(FileDialogService);
private subscription = new Subscription();

constructor() {
Expand Down Expand Up @@ -87,14 +88,21 @@ export class DeviceConfigurationComponent implements OnInit, OnDestroy {
this.store.dispatch(new ChangeUserConfigurationHistoryTabAction(index));
}

changeFile(data: UploadFileData): void {
this.store.dispatch(new LoadUserConfigurationFromFileAction({
uploadFileData: {
...data,
saveInHistory: true
},
autoSave: false
}));
importUserConfiguration(): void {
this.fileDialogService.openUserConfigurationFile()
.subscribe(data => {
if (!data) {
return;
}

this.store.dispatch(new LoadUserConfigurationFromFileAction({
uploadFileData: {
...data,
saveInHistory: true
},
autoSave: false
}));
});
}

deleteUserConfigHistory(deviceUniqueId: number): void {
Expand Down
67 changes: 67 additions & 0 deletions packages/uhk-web/src/app/services/file-dialog.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';
import { saveAs } from 'file-saver';

import { UploadFileData } from 'uhk-common';

export interface SaveFileResult {
canceled: boolean;
filePath?: string;
}

/**
* Platform specific user configuration import/export file picker.
*
* The default implementation uses the browser file input and file-saver.
* The Electron implementation (see ElectronFileDialogService) uses the native
* dialogs and remembers the last used folder.
*/
@Injectable()
export class FileDialogService {
openUserConfigurationFile(): Observable<UploadFileData | null> {
return new Observable<UploadFileData | null>(subscriber => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.bin';
input.style.display = 'none';
document.body.appendChild(input);

const cleanup = () => input.remove();
input.onchange = () => {
const files = input.files;
if (!files || files.length === 0) {
cleanup();
subscriber.next(null);
subscriber.complete();
return;
}

const file = files[0];
const fileReader = new FileReader();
fileReader.onloadend = () => {
cleanup();
subscriber.next({
filename: file.name,
data: Array.from(new Uint8Array(fileReader.result as ArrayBuffer)),
saveInHistory: false,
});
subscriber.complete();
};
fileReader.onerror = () => {
cleanup();
subscriber.error(fileReader.error);
};
fileReader.readAsArrayBuffer(file);
};
input.click();
});
}

saveUserConfigurationFile(fileName: string, data: Uint8Array, mimeType: string): Observable<SaveFileResult> {
const arrayBuffer = new ArrayBuffer(data.byteLength);
new Uint8Array(arrayBuffer).set(data);
saveAs(new Blob([arrayBuffer], { type: mimeType }), fileName);

return from(Promise.resolve({ canceled: false }));
}
}
2 changes: 2 additions & 0 deletions packages/uhk-web/src/app/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import { KeymapEditGuard } from './components/keymap/edit';
import { MacroNotFoundGuard } from './components/macro/not-found';
import { DataStorageRepositoryService } from './services/datastorage-repository.service';
import { DefaultUserConfigurationService } from './services/default-user-configuration.service';
import { FileDialogService } from './services/file-dialog.service';
import { LogService } from 'uhk-common';
import { AutoUpdateSettings } from './components/auto-update-settings/auto-update-settings';
import { angularNotifierConfig } from './models/angular-notifier-config';
Expand Down Expand Up @@ -337,6 +338,7 @@ import appInitFactory from './services/app-init-factory';
CaptureService,
DataStorageRepositoryService,
DefaultUserConfigurationService,
FileDialogService,
LogService,
AppUpdateRendererService,
AppRendererService,
Expand Down
Loading
Loading