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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sonarlint.connectedMode.project": {
"connectionId": "http-localhost-9000",
"projectKey": "open-time-tracker"
"projectKey": "altaskur_OpenTimeTracker"
},
"editor.formatOnSave": true,
"[html]": {
Expand Down
116 changes: 116 additions & 0 deletions electron/src/interfaces/update.interface.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, it, expect } from 'vitest';
import {
UpdateInfo,
DownloadProgress,
UpdateSettings,
UpdateStatus,
UpdateError,
} from './update.interface.js';

describe('Update interfaces', () => {
describe('UpdateInfo', () => {
it('should create valid UpdateInfo with all properties', () => {
const updateInfo: UpdateInfo = {
version: '2.0.0',
releaseDate: '2026-02-01',
releaseName: 'Version 2.0.0',
releaseNotes: 'New features',
size: 1024000,
};

expect(updateInfo.version).toBe('2.0.0');
expect(updateInfo.releaseDate).toBe('2026-02-01');
expect(updateInfo.releaseName).toBe('Version 2.0.0');
expect(updateInfo.releaseNotes).toBe('New features');
expect(updateInfo.size).toBe(1024000);
});

it('should create valid UpdateInfo with only required properties', () => {
const updateInfo: UpdateInfo = {
version: '2.0.0',
releaseDate: '2026-02-01',
};

expect(updateInfo.version).toBe('2.0.0');
expect(updateInfo.releaseDate).toBe('2026-02-01');
expect(updateInfo.releaseName).toBeUndefined();
expect(updateInfo.releaseNotes).toBeUndefined();
expect(updateInfo.size).toBeUndefined();
});
});

describe('DownloadProgress', () => {
it('should create valid DownloadProgress', () => {
const progress: DownloadProgress = {
bytesPerSecond: 1024000,
percent: 45.67,
transferred: 5000000,
total: 10000000,
};

expect(progress.bytesPerSecond).toBe(1024000);
expect(progress.percent).toBe(45.67);
expect(progress.transferred).toBe(5000000);
expect(progress.total).toBe(10000000);
});
});

describe('UpdateSettings', () => {
it('should create valid UpdateSettings with all properties', () => {
const settings: UpdateSettings = {
autoCheckEnabled: true,
lastCheckDate: new Date('2026-02-01'),
};

expect(settings.autoCheckEnabled).toBe(true);
expect(settings.lastCheckDate).toBeInstanceOf(Date);
});

it('should create valid UpdateSettings with only required properties', () => {
const settings: UpdateSettings = {
autoCheckEnabled: false,
};

expect(settings.autoCheckEnabled).toBe(false);
expect(settings.lastCheckDate).toBeUndefined();
});
});

describe('UpdateStatus', () => {
it('should have correct enum values', () => {
expect(UpdateStatus.Idle).toBe('idle');
expect(UpdateStatus.Checking).toBe('checking');
expect(UpdateStatus.Available).toBe('available');
expect(UpdateStatus.NotAvailable).toBe('not-available');
expect(UpdateStatus.Downloading).toBe('downloading');
expect(UpdateStatus.Downloaded).toBe('downloaded');
expect(UpdateStatus.Error).toBe('error');
});

it('should be assignable to variables', () => {
const status: UpdateStatus = UpdateStatus.Checking;
expect(status).toBe(UpdateStatus.Checking);
});
});

describe('UpdateError', () => {
it('should create valid UpdateError with all properties', () => {
const error: UpdateError = {
message: 'Update failed',
code: 'ERR_UPDATE_FAILED',
};

expect(error.message).toBe('Update failed');
expect(error.code).toBe('ERR_UPDATE_FAILED');
});

it('should create valid UpdateError with only required properties', () => {
const error: UpdateError = {
message: 'Update failed',
};

expect(error.message).toBe('Update failed');
expect(error.code).toBeUndefined();
});
});
});
49 changes: 49 additions & 0 deletions electron/src/interfaces/update.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Information about an available update.
*/
export interface UpdateInfo {
version: string;
releaseDate: string;
releaseName?: string;
releaseNotes?: string;
size?: number;
}

/**
* Update download progress information.
*/
export interface DownloadProgress {
bytesPerSecond: number;
percent: number;
transferred: number;
total: number;
}

/**
* Update settings and preferences.
*/
export interface UpdateSettings {
autoCheckEnabled: boolean;
lastCheckDate?: Date;
}

/**
* Update status enum.
*/
export enum UpdateStatus {
Idle = 'idle',
Checking = 'checking',
Available = 'available',
NotAvailable = 'not-available',
Downloading = 'downloading',
Downloaded = 'downloaded',
Error = 'error',
}

/**
* Update error information.
*/
export interface UpdateError {
message: string;
code?: string;
}
10 changes: 10 additions & 0 deletions electron/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { WindowManager } from './window.js';
import { DatabaseManager } from '../services/database/database.js';
import { setupIpcHandlers } from '../services/ipc/index.js';
import { BackupService } from '../services/backup/index.js';
import { UpdateManager } from '../services/updater/index.js';

let windowManager: WindowManager | null = null;
let dbManager: DatabaseManager | null = null;
let backupService: BackupService | null = null;
let updateManager: UpdateManager | null = null;

const initializeApp = async (): Promise<void> => {
backupService = new BackupService();
Expand All @@ -32,6 +34,14 @@ const initializeApp = async (): Promise<void> => {
setupIpcHandlers(dbManager, backupService);
windowManager = new WindowManager();
await windowManager.createMainWindow();

// Initialize update manager after window is created
updateManager = UpdateManager.getInstance();
const mainWindow = windowManager.getMainWindow();
if (mainWindow) {
updateManager.setMainWindow(mainWindow);
}
await updateManager.initialize();
};

app.whenReady().then(initializeApp);
Expand Down
75 changes: 74 additions & 1 deletion electron/src/preload/preload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer } from 'electron';
import { contextBridge, ipcRenderer, shell } from 'electron';

/**
* Type definitions for database entities
Expand Down Expand Up @@ -128,6 +128,34 @@ interface BackupResult {
path?: string;
}

interface UpdateInfo {
version: string;
releaseDate: string;
releaseName?: string;
releaseNotes?: string;
size?: number;
}

interface DownloadProgress {
bytesPerSecond: number;
percent: number;
transferred: number;
total: number;
}

interface UpdateSettings {
autoCheckEnabled: boolean;
lastCheckDate?: Date;
}

interface UpdateResult {
success: boolean;
error?: string;
settings?: UpdateSettings;
status?: string;
updateInfo?: UpdateInfo | null;
}

try {
const electronAPI = {
// Projects
Expand Down Expand Up @@ -393,6 +421,51 @@ try {
importBackup: (): Promise<BackupResult> =>
ipcRenderer.invoke('backup-import'),
getBackupDir: (): Promise<string> => ipcRenderer.invoke('backup-get-dir'),

// System
openExternal: (url: string): Promise<void> => shell.openExternal(url),

// Updates
checkForUpdates: (): Promise<UpdateResult> =>
ipcRenderer.invoke('update:check'),
downloadUpdate: (): Promise<UpdateResult> =>
ipcRenderer.invoke('update:download'),
installUpdate: (): Promise<UpdateResult> =>
ipcRenderer.invoke('update:install'),
getAppVersion: (): Promise<UpdateResult> =>
ipcRenderer.invoke('update:get-app-version'),
getUpdateSettings: (): Promise<UpdateResult> =>
ipcRenderer.invoke('update:get-settings'),
setUpdateSettings: (
settings: Partial<UpdateSettings>,
): Promise<UpdateResult> =>
ipcRenderer.invoke('update:set-settings', settings),
getUpdateStatus: (): Promise<UpdateResult> =>
ipcRenderer.invoke('update:get-status'),
onUpdateChecking: (callback: () => void): void => {
ipcRenderer.on('update:checking', () => callback());
},
onUpdateAvailable: (callback: (info: UpdateInfo) => void): void => {
ipcRenderer.on('update:available', (_event, info) => callback(info));
},
onUpdateNotAvailable: (
callback: (info: { version: string }) => void,
): void => {
ipcRenderer.on('update:not-available', (_event, info) => callback(info));
},
onDownloadProgress: (
callback: (progress: DownloadProgress) => void,
): void => {
ipcRenderer.on('update:download-progress', (_event, progress) =>
callback(progress),
);
},
onUpdateDownloaded: (callback: (info: UpdateInfo) => void): void => {
ipcRenderer.on('update:downloaded', (_event, info) => callback(info));
},
onUpdateError: (callback: (error: { message: string }) => void): void => {
ipcRenderer.on('update:error', (_event, error) => callback(error));
},
};

contextBridge.exposeInMainWorld('electronAPI', electronAPI);
Expand Down
2 changes: 2 additions & 0 deletions electron/src/services/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { setupDatabaseHandlers } from './database-handlers.js';
import { setupThemeHandlers } from './theme-handlers.js';
import { setupLanguageHandlers } from './language-handlers.js';
import { setupBackupHandlers } from './backup-handlers.js';
import { setupUpdateHandlers } from './update-handlers.js';

/**
* Sets up all IPC handlers
Expand All @@ -15,6 +16,7 @@ export const setupIpcHandlers = (
setupDatabaseHandlers(dbManager);
setupThemeHandlers(dbManager);
setupLanguageHandlers(dbManager);
setupUpdateHandlers();
if (backupService) {
setupBackupHandlers(backupService);
}
Expand Down
Loading