diff --git a/electron/src/interfaces/update.interface.spec.ts b/electron/src/interfaces/update.interface.spec.ts deleted file mode 100644 index 8b16e04..0000000 --- a/electron/src/interfaces/update.interface.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -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(); - }); - }); -}); diff --git a/electron/src/interfaces/update.interface.ts b/electron/src/interfaces/update.interface.ts deleted file mode 100644 index 6d0a03d..0000000 --- a/electron/src/interfaces/update.interface.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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; -} diff --git a/electron/src/main/main.ts b/electron/src/main/main.ts index 9b50bf1..430b4d4 100644 --- a/electron/src/main/main.ts +++ b/electron/src/main/main.ts @@ -3,12 +3,10 @@ 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 => { backupService = new BackupService(); @@ -34,14 +32,6 @@ const initializeApp = async (): Promise => { 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); diff --git a/electron/src/preload/preload.ts b/electron/src/preload/preload.ts index c9d696a..49f8a54 100644 --- a/electron/src/preload/preload.ts +++ b/electron/src/preload/preload.ts @@ -128,34 +128,6 @@ 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 @@ -424,48 +396,6 @@ try { // System openExternal: (url: string): Promise => shell.openExternal(url), - - // Updates - checkForUpdates: (): Promise => - ipcRenderer.invoke('update:check'), - downloadUpdate: (): Promise => - ipcRenderer.invoke('update:download'), - installUpdate: (): Promise => - ipcRenderer.invoke('update:install'), - getAppVersion: (): Promise => - ipcRenderer.invoke('update:get-app-version'), - getUpdateSettings: (): Promise => - ipcRenderer.invoke('update:get-settings'), - setUpdateSettings: ( - settings: Partial, - ): Promise => - ipcRenderer.invoke('update:set-settings', settings), - getUpdateStatus: (): Promise => - 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); diff --git a/electron/src/services/ipc/index.ts b/electron/src/services/ipc/index.ts index a24c1eb..26ed1ed 100644 --- a/electron/src/services/ipc/index.ts +++ b/electron/src/services/ipc/index.ts @@ -4,7 +4,6 @@ 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 @@ -16,7 +15,6 @@ export const setupIpcHandlers = ( setupDatabaseHandlers(dbManager); setupThemeHandlers(dbManager); setupLanguageHandlers(dbManager); - setupUpdateHandlers(); if (backupService) { setupBackupHandlers(backupService); } diff --git a/electron/src/services/ipc/update-handlers.spec.ts b/electron/src/services/ipc/update-handlers.spec.ts deleted file mode 100644 index fe500b0..0000000 --- a/electron/src/services/ipc/update-handlers.spec.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { - describe, - it, - expect, - beforeEach, - vi, - type Mock, - type Mocked, -} from 'vitest'; -import { app, ipcMain } from 'electron'; -import { setupUpdateHandlers } from './update-handlers.js'; -import { UpdateManager } from '../updater/update-manager.js'; -import { - UpdateSettings, - UpdateStatus, - UpdateInfo, -} from '../../interfaces/update.interface.js'; - -vi.mock('electron-updater', () => ({ - default: { - autoUpdater: { - checkForUpdates: vi.fn(), - downloadUpdate: vi.fn(), - quitAndInstall: vi.fn(), - on: vi.fn(), - removeAllListeners: vi.fn(), - setFeedURL: vi.fn(), - logger: null, - autoDownload: false, - autoInstallOnAppQuit: false, - allowPrerelease: false, - }, - }, -})); -vi.mock('../updater/update-manager.js'); - -describe('Update IPC Handlers', () => { - let mockUpdateManager: Mocked; - let ipcHandlers: Map; - - const mockUpdateSettings: UpdateSettings = { - autoCheckEnabled: true, - }; - - const mockUpdateInfo: UpdateInfo = { - version: '2.0.0', - releaseName: 'Version 2.0.0', - releaseNotes: 'New features', - releaseDate: '2026-02-01', - }; - - beforeEach(() => { - vi.clearAllMocks(); - ipcHandlers = new Map(); - - mockUpdateManager = { - checkForUpdates: vi.fn().mockResolvedValue(undefined), - downloadUpdate: vi.fn().mockResolvedValue(undefined), - quitAndInstall: vi.fn(), - getSettings: vi.fn().mockReturnValue(mockUpdateSettings), - setSettings: vi.fn().mockResolvedValue(undefined), - getStatus: vi.fn().mockReturnValue(UpdateStatus.Idle), - getUpdateInfo: vi.fn().mockReturnValue(null), - } as unknown as Mocked; - - (UpdateManager.getInstance as Mock).mockReturnValue(mockUpdateManager); - - (ipcMain.handle as Mock).mockImplementation( - (channel: string, handler: Mock) => { - ipcHandlers.set(channel, handler); - }, - ); - }); - - describe('setupUpdateHandlers', () => { - it('should register all update IPC handlers', () => { - setupUpdateHandlers(); - - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:check', - expect.any(Function), - ); - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:download', - expect.any(Function), - ); - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:install', - expect.any(Function), - ); - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:get-settings', - expect.any(Function), - ); - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:set-settings', - expect.any(Function), - ); - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:get-status', - expect.any(Function), - ); - expect(ipcMain.handle).toHaveBeenCalledWith( - 'update:get-app-version', - expect.any(Function), - ); - }); - - it('should initialize UpdateManager instance', () => { - setupUpdateHandlers(); - - expect(UpdateManager.getInstance).toHaveBeenCalled(); - }); - }); - - describe('update:check handler', () => { - it('should check for updates successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:check')!; - - const result = await handler(); - - expect(mockUpdateManager.checkForUpdates).toHaveBeenCalled(); - expect(result).toEqual({ success: true }); - }); - - it('should handle check for updates error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:check')!; - const error = new Error('Network error'); - mockUpdateManager.checkForUpdates.mockRejectedValue(error); - - const result = await handler(); - - expect(result).toEqual({ - success: false, - error: 'Network error', - }); - }); - - it('should handle non-Error objects', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:check')!; - mockUpdateManager.checkForUpdates.mockRejectedValue('String error'); - - const result = await handler(); - - expect(result).toEqual({ - success: false, - error: 'Unknown error', - }); - }); - }); - - describe('update:download handler', () => { - it('should download update successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:download')!; - - const result = await handler(); - - expect(mockUpdateManager.downloadUpdate).toHaveBeenCalled(); - expect(result).toEqual({ success: true }); - }); - - it('should handle download error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:download')!; - const error = new Error('Download failed'); - mockUpdateManager.downloadUpdate.mockRejectedValue(error); - - const result = await handler(); - - expect(result).toEqual({ - success: false, - error: 'Download failed', - }); - }); - }); - - describe('update:install handler', () => { - it('should install update successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:install')!; - - const result = await handler(); - - expect(mockUpdateManager.quitAndInstall).toHaveBeenCalled(); - expect(result).toEqual({ success: true }); - }); - - it('should handle install error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:install')!; - const error = new Error('Install failed'); - mockUpdateManager.quitAndInstall.mockImplementation(() => { - throw error; - }); - - const result = await handler(); - - expect(result).toEqual({ - success: false, - error: 'Install failed', - }); - }); - }); - - describe('update:get-settings handler', () => { - it('should get settings successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-settings')!; - - const result = await handler(); - - expect(mockUpdateManager.getSettings).toHaveBeenCalled(); - expect(result).toEqual({ - success: true, - settings: mockUpdateSettings, - }); - }); - - it('should handle get settings error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-settings')!; - const error = new Error('Settings error'); - mockUpdateManager.getSettings.mockImplementation(() => { - throw error; - }); - - const result = await handler(); - - expect(result).toEqual({ - success: false, - error: 'Settings error', - }); - }); - }); - - describe('update:set-settings handler', () => { - it('should set settings successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:set-settings')!; - const newSettings: Partial = { - autoCheckEnabled: false, - }; - - const result = await handler({}, newSettings); - - expect(mockUpdateManager.setSettings).toHaveBeenCalledWith(newSettings); - expect(result).toEqual({ success: true }); - }); - - it('should handle set settings error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:set-settings')!; - const error = new Error('Update failed'); - mockUpdateManager.setSettings.mockRejectedValue(error); - - const result = await handler({}, {}); - - expect(result).toEqual({ - success: false, - error: 'Update failed', - }); - }); - }); - - describe('update:get-status handler', () => { - it('should get status successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-status')!; - mockUpdateManager.getStatus.mockReturnValue(UpdateStatus.Checking); - mockUpdateManager.getUpdateInfo.mockReturnValue(mockUpdateInfo); - - const result = await handler(); - - expect(mockUpdateManager.getStatus).toHaveBeenCalled(); - expect(mockUpdateManager.getUpdateInfo).toHaveBeenCalled(); - expect(result).toEqual({ - success: true, - status: UpdateStatus.Checking, - updateInfo: mockUpdateInfo, - }); - }); - - it('should get status with null updateInfo', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-status')!; - mockUpdateManager.getStatus.mockReturnValue(UpdateStatus.Idle); - mockUpdateManager.getUpdateInfo.mockReturnValue(null); - - const result = await handler(); - - expect(result).toEqual({ - success: true, - status: UpdateStatus.Idle, - updateInfo: null, - }); - }); - - it('should handle get status error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-status')!; - const error = new Error('Status error'); - mockUpdateManager.getStatus.mockImplementation(() => { - throw error; - }); - - const result = await handler(); - - expect(result).toEqual({ - success: false, - error: 'Status error', - }); - }); - }); - - describe('update:get-app-version handler', () => { - it('should return app version successfully', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-app-version')!; - - const result = await handler(); - - expect(result).toEqual({ success: true, version: '1.0.0' }); - }); - - it('should handle get version error', async () => { - setupUpdateHandlers(); - const handler = ipcHandlers.get('update:get-app-version')!; - (app.getVersion as Mock).mockImplementation(() => { - throw new Error('Version error'); - }); - - const result = await handler(); - - expect(result).toEqual({ success: false, error: 'Version error' }); - }); - }); -}); diff --git a/electron/src/services/ipc/update-handlers.ts b/electron/src/services/ipc/update-handlers.ts deleted file mode 100644 index aad5650..0000000 --- a/electron/src/services/ipc/update-handlers.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { app, ipcMain } from 'electron'; -import { UpdateManager } from '../updater/update-manager.js'; -import { UpdateSettings } from '../../interfaces/update.interface.js'; - -let updateManager: UpdateManager | null = null; - -/** - * Sets up update-related IPC handlers. - * Manages update checking, downloading, and installation between main and renderer processes. - */ -export const setupUpdateHandlers = (): void => { - updateManager = UpdateManager.getInstance(); - - /** - * Trigger manual update check. - */ - ipcMain.handle('update:check', async () => { - try { - await updateManager?.checkForUpdates(); - return { success: true }; - } catch (error) { - console.error('[IPC] Update check failed:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); - - /** - * Download available update. - */ - ipcMain.handle('update:download', async () => { - try { - await updateManager?.downloadUpdate(); - return { success: true }; - } catch (error) { - console.error('[IPC] Update download failed:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); - - /** - * Install downloaded update and restart app. - */ - ipcMain.handle('update:install', async () => { - try { - updateManager?.quitAndInstall(); - return { success: true }; - } catch (error) { - console.error('[IPC] Update installation failed:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); - - /** - * Get current update settings. - */ - ipcMain.handle('update:get-settings', async () => { - try { - const settings = updateManager?.getSettings(); - return { success: true, settings }; - } catch (error) { - console.error('[IPC] Failed to get update settings:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); - - /** - * Update update settings. - */ - ipcMain.handle( - 'update:set-settings', - async (_event, settings: Partial) => { - try { - await updateManager?.setSettings(settings); - return { success: true }; - } catch (error) { - console.error('[IPC] Failed to set update settings:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }, - ); - - /** - * Get current update status. - */ - ipcMain.handle('update:get-status', async () => { - try { - const status = updateManager?.getStatus(); - const updateInfo = updateManager?.getUpdateInfo(); - return { - success: true, - status, - updateInfo, - }; - } catch (error) { - console.error('[IPC] Failed to get update status:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); - - /** - * Get current application version. - */ - ipcMain.handle('update:get-app-version', async () => { - try { - return { success: true, version: app.getVersion() }; - } catch (error) { - console.error('[IPC] Failed to get app version:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); -}; diff --git a/electron/src/services/menu/menu-manager.ts b/electron/src/services/menu/menu-manager.ts index 4079304..eee7519 100644 --- a/electron/src/services/menu/menu-manager.ts +++ b/electron/src/services/menu/menu-manager.ts @@ -54,7 +54,6 @@ const menuTranslations: Record> = { tags: 'Etiquetas', dayTypes: 'Tipos de Día', taskStatuses: 'Estados de Tarea', - checkForUpdates: 'Buscar Actualizaciones...', }, en: { home: 'Home', @@ -93,7 +92,6 @@ const menuTranslations: Record> = { tags: 'Tags', dayTypes: 'Day Types', taskStatuses: 'Task Statuses', - checkForUpdates: 'Check for Updates...', }, }; @@ -305,13 +303,6 @@ export class MenuManager { }, }, { type: 'separator' }, - { - label: this.t('checkForUpdates'), - click: (): void => { - this.navigateTo('/settings/updates'); - }, - }, - { type: 'separator' }, { label: this.t('reportIssue'), click: (): void => { diff --git a/electron/src/services/updater/index.ts b/electron/src/services/updater/index.ts deleted file mode 100644 index de4d121..0000000 --- a/electron/src/services/updater/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UpdateManager } from './update-manager.js'; diff --git a/electron/src/services/updater/update-manager.spec.ts b/electron/src/services/updater/update-manager.spec.ts deleted file mode 100644 index e08d942..0000000 --- a/electron/src/services/updater/update-manager.spec.ts +++ /dev/null @@ -1,691 +0,0 @@ -import { - describe, - it, - expect, - beforeEach, - afterEach, - vi, - type Mock, - type Mocked, -} from 'vitest'; -import { BrowserWindow, app } from 'electron'; -import { UpdateManager } from './update-manager.js'; -import electronUpdater from 'electron-updater'; -import { UpdateStatus } from '../../interfaces/update.interface.js'; -import * as fs from 'node:fs'; -import { BackupService } from '../backup/backup.service.js'; - -vi.mock('electron'); -vi.mock('electron-updater', () => ({ - default: { - autoUpdater: { - checkForUpdates: vi.fn(), - downloadUpdate: vi.fn(), - quitAndInstall: vi.fn(), - on: vi.fn(), - removeAllListeners: vi.fn(), - setFeedURL: vi.fn(), - logger: null, - autoDownload: false, - autoInstallOnAppQuit: false, - allowPrerelease: false, - }, - }, -})); -vi.mock('node:fs'); -const createBackupMock = vi.hoisted(() => vi.fn()); - -vi.mock('../backup/backup.service.js', () => ({ - BackupService: class BackupServiceMock { - createBackup = createBackupMock; - }, -})); - -const { autoUpdater } = electronUpdater; - -describe('UpdateManager', () => { - let updateManager: UpdateManager; - let mockWindow: Mocked; - let mockBackupService: Mocked; - - const mockUpdateInfo = { - version: '2.0.0', - releaseDate: '2026-02-01', - releaseName: 'Version 2.0.0', - releaseNotes: 'New features', - }; - - beforeEach(() => { - /* Clear only call history, not implementations. */ - (autoUpdater.on as Mock).mockClear(); - (autoUpdater.checkForUpdates as Mock).mockClear(); - (autoUpdater.downloadUpdate as Mock).mockClear(); - (autoUpdater.quitAndInstall as Mock).mockClear(); - - /* Reset singleton instance before each test. */ - ( - UpdateManager as unknown as Record - ).instance = undefined; - - mockWindow = { - webContents: { - send: vi.fn(), - }, - isDestroyed: vi.fn().mockReturnValue(false), - } as unknown as Mocked; - - createBackupMock.mockReset(); - createBackupMock.mockResolvedValue({ - success: true, - backup: { filename: 'backup.db' }, - }); - - mockBackupService = { - createBackup: createBackupMock, - } as unknown as Mocked; - - (app.getPath as Mock).mockReturnValue('/test/path'); - (app.getVersion as Mock).mockReturnValue('1.0.0'); - (app.isPackaged as unknown) = false; - - (fs.existsSync as Mock).mockReturnValue(false); - (fs.readFileSync as Mock).mockReturnValue('{}'); - (fs.writeFileSync as Mock).mockReturnValue(undefined); - - (autoUpdater.on as Mock).mockReturnValue(autoUpdater); - (autoUpdater.checkForUpdates as Mock).mockResolvedValue({}); - (autoUpdater.downloadUpdate as Mock).mockResolvedValue([]); - (autoUpdater.quitAndInstall as Mock).mockReturnValue(undefined); - - /* AutoUpdater logger setter. */ - Object.defineProperty(autoUpdater, 'logger', { - set: vi.fn(), - get: vi.fn(), - }); - }); - - afterEach(() => { - /* Reset singleton instance after each test. */ - ( - UpdateManager as unknown as Record - ).instance = undefined; - vi.restoreAllMocks(); - }); - - describe('getInstance', () => { - it('should return singleton instance', () => { - const instance1 = UpdateManager.getInstance(); - const instance2 = UpdateManager.getInstance(); - - expect(instance1).toBe(instance2); - }); - - it('should setup auto updater on first instantiation', () => { - UpdateManager.getInstance(); - - expect(autoUpdater.on).toHaveBeenCalledWith( - 'checking-for-update', - expect.any(Function), - ); - expect(autoUpdater.on).toHaveBeenCalledWith( - 'update-available', - expect.any(Function), - ); - expect(autoUpdater.on).toHaveBeenCalledWith( - 'update-not-available', - expect.any(Function), - ); - expect(autoUpdater.on).toHaveBeenCalledWith( - 'download-progress', - expect.any(Function), - ); - expect(autoUpdater.on).toHaveBeenCalledWith( - 'update-downloaded', - expect.any(Function), - ); - expect(autoUpdater.on).toHaveBeenCalledWith( - 'error', - expect.any(Function), - ); - }); - }); - - describe('initialization', () => { - it('should load settings from file if exists', () => { - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ - autoCheckEnabled: false, - checkOnStartup: false, - autoDownload: true, - }), - ); - - const manager = UpdateManager.getInstance(); - const settings = manager.getSettings(); - - expect(settings.autoCheckEnabled).toBe(false); - }); - - it('should use default settings if file does not exist', () => { - (fs.existsSync as Mock).mockReturnValue(false); - - const manager = UpdateManager.getInstance(); - const settings = manager.getSettings(); - - expect(settings.autoCheckEnabled).toBe(true); - }); - - it('should handle settings load error gracefully', () => { - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockImplementation(() => { - throw new Error('Read error'); - }); - - const manager = UpdateManager.getInstance(); - const settings = manager.getSettings(); - - // Should fallback to defaults - expect(settings.autoCheckEnabled).toBe(true); - }); - }); - - describe('setMainWindow', () => { - it('should set main window reference', () => { - updateManager = UpdateManager.getInstance(); - updateManager.setMainWindow(mockWindow); - - // Verify window is set by triggering an event - const checkingHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'checking-for-update', - )?.[1]; - - if (checkingHandler) { - checkingHandler(); - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:checking', - undefined, - ); - } - }); - }); - - describe('initialize', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('should initialize update manager', async () => { - updateManager = UpdateManager.getInstance(); - - await updateManager.initialize(); - - expect(updateManager).toBeDefined(); - }); - - it('should check for updates on startup if enabled', async () => { - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ - autoCheckEnabled: true, - checkOnStartup: true, - }), - ); - (app.isPackaged as unknown) = true; - - updateManager = UpdateManager.getInstance(); - const checkSpy = vi - .spyOn(updateManager, 'checkForUpdates') - .mockResolvedValue(); - - await updateManager.initialize(); - vi.advanceTimersByTime(5000); - - await vi.runAllTimersAsync(); - - expect(checkSpy).toHaveBeenCalled(); - }); - - it('should not check for updates if auto-check disabled', async () => { - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ - autoCheckEnabled: false, - checkOnStartup: false, - }), - ); - - updateManager = UpdateManager.getInstance(); - const checkSpy = vi - .spyOn(updateManager, 'checkForUpdates') - .mockResolvedValue(); - - await updateManager.initialize(); - vi.advanceTimersByTime(5000); - - expect(checkSpy).not.toHaveBeenCalled(); - }); - }); - - describe('checkForUpdates', () => { - beforeEach(() => { - updateManager = UpdateManager.getInstance(); - }); - - it('should skip check in development mode', async () => { - (app.isPackaged as unknown) = false; - updateManager.setMainWindow(mockWindow); - - await updateManager.checkForUpdates(); - - expect(autoUpdater.checkForUpdates).not.toHaveBeenCalled(); - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:not-available', - { version: '1.0.0' }, - ); - }); - - it('should check for updates in production mode', async () => { - (app.isPackaged as unknown) = true; - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ autoCheckEnabled: true }), - ); - - await updateManager.checkForUpdates(); - - expect(autoUpdater.checkForUpdates).toHaveBeenCalled(); - expect(fs.writeFileSync).toHaveBeenCalled(); - }); - - it('should allow manual check when auto-check is disabled', async () => { - (app.isPackaged as unknown) = true; - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ autoCheckEnabled: false }), - ); - - await updateManager.checkForUpdates(); - - expect(autoUpdater.checkForUpdates).toHaveBeenCalled(); - }); - - it('should not check if already checking', async () => { - (app.isPackaged as unknown) = true; - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ autoCheckEnabled: true }), - ); - - // Simulate checking state - const checkingHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'checking-for-update', - )?.[1]; - if (checkingHandler) { - checkingHandler(); - } - - await updateManager.checkForUpdates(); - - // Should only be called once from the event - expect(autoUpdater.checkForUpdates).not.toHaveBeenCalled(); - }); - - it('should handle check error', async () => { - (app.isPackaged as unknown) = true; - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ autoCheckEnabled: true }), - ); - const error = new Error('Network error'); - (autoUpdater.checkForUpdates as Mock).mockRejectedValue(error); - - await expect(updateManager.checkForUpdates()).rejects.toThrow( - 'Network error', - ); - }); - }); - - describe('downloadUpdate', () => { - beforeEach(() => { - updateManager = UpdateManager.getInstance(); - }); - - it('should throw error in development mode', async () => { - (app.isPackaged as unknown) = false; - - await expect(updateManager.downloadUpdate()).rejects.toThrow( - 'Updates not available in development mode', - ); - }); - - it('should throw error if no update available', async () => { - (app.isPackaged as unknown) = true; - - await expect(updateManager.downloadUpdate()).rejects.toThrow( - 'No update available to download', - ); - }); - - it('should download update if available', async () => { - (app.isPackaged as unknown) = true; - - // Trigger update available event - const availableHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-available', - )?.[1]; - - if (availableHandler) { - availableHandler(mockUpdateInfo); - } - - await updateManager.downloadUpdate(); - - expect(autoUpdater.downloadUpdate).toHaveBeenCalled(); - }); - - it('should handle download error', async () => { - (app.isPackaged as unknown) = true; - - // Trigger update available event - const availableHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-available', - )?.[1]; - - if (availableHandler) { - availableHandler(mockUpdateInfo); - } - - const error = new Error('Download failed'); - (autoUpdater.downloadUpdate as Mock).mockRejectedValue(error); - - await expect(updateManager.downloadUpdate()).rejects.toThrow( - 'Download failed', - ); - }); - }); - - describe('quitAndInstall', () => { - beforeEach(() => { - vi.useFakeTimers(); - updateManager = UpdateManager.getInstance(); - updateManager.setMainWindow(mockWindow); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('should do nothing in development mode', async () => { - (app.isPackaged as unknown) = false; - - await updateManager.quitAndInstall(); - - expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled(); - }); - - it('should not install if update not downloaded', async () => { - (app.isPackaged as unknown) = true; - - await updateManager.quitAndInstall(); - - expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled(); - }); - - it('should create backup and install update', async () => { - (app.isPackaged as unknown) = true; - - // Trigger update downloaded event - const downloadedHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-downloaded', - )?.[1]; - - if (downloadedHandler) { - downloadedHandler(mockUpdateInfo); - } - - const installPromise = updateManager.quitAndInstall(); - await installPromise; - - vi.advanceTimersByTime(1000); - - expect(mockBackupService.createBackup).toHaveBeenCalledWith( - 'before-restore', - ); - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:installing', - undefined, - ); - expect(autoUpdater.quitAndInstall).toHaveBeenCalledWith(false, true); - }); - - it('should continue install even if backup fails', async () => { - (app.isPackaged as unknown) = true; - mockBackupService.createBackup.mockResolvedValue({ - success: false, - error: 'Backup error', - }); - - // Trigger update downloaded event - const downloadedHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-downloaded', - )?.[1]; - - if (downloadedHandler) { - downloadedHandler(mockUpdateInfo); - } - - const installPromise = updateManager.quitAndInstall(); - await installPromise; - - vi.advanceTimersByTime(1000); - - expect(autoUpdater.quitAndInstall).toHaveBeenCalled(); - }); - - it('should continue install even if backup throws', async () => { - (app.isPackaged as unknown) = true; - mockBackupService.createBackup.mockRejectedValue( - new Error('Backup error'), - ); - - // Trigger update downloaded event - const downloadedHandler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-downloaded', - )?.[1]; - - if (downloadedHandler) { - downloadedHandler(mockUpdateInfo); - } - - const installPromise = updateManager.quitAndInstall(); - await installPromise; - - vi.advanceTimersByTime(1000); - - expect(autoUpdater.quitAndInstall).toHaveBeenCalled(); - }); - }); - - describe('event handlers', () => { - beforeEach(() => { - updateManager = UpdateManager.getInstance(); - updateManager.setMainWindow(mockWindow); - }); - - it('should handle checking-for-update event', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'checking-for-update', - )?.[1]; - - handler(); - - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:checking', - undefined, - ); - expect(updateManager.getStatus()).toBe(UpdateStatus.Checking); - }); - - it('should handle update-available event', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-available', - )?.[1]; - - handler(mockUpdateInfo); - - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:available', - expect.objectContaining({ version: '2.0.0' }), - ); - expect(updateManager.getStatus()).toBe(UpdateStatus.Available); - }); - - it('should handle update-not-available event', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-not-available', - )?.[1]; - - handler({ version: '1.0.0' }); - - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:not-available', - { version: '1.0.0' }, - ); - expect(updateManager.getStatus()).toBe(UpdateStatus.NotAvailable); - }); - - it('should handle download-progress event', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'download-progress', - )?.[1]; - - const progress = { - bytesPerSecond: 1024000, - percent: 45.67, - transferred: 5000000, - total: 10000000, - }; - - handler(progress); - - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:download-progress', - progress, - ); - expect(updateManager.getStatus()).toBe(UpdateStatus.Downloading); - }); - - it('should handle update-downloaded event', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-downloaded', - )?.[1]; - - handler(mockUpdateInfo); - - expect(mockWindow.webContents.send).toHaveBeenCalledWith( - 'update:downloaded', - expect.objectContaining({ version: '2.0.0' }), - ); - expect(updateManager.getStatus()).toBe(UpdateStatus.Downloaded); - }); - - it('should handle error event', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'error', - )?.[1]; - - const error = new Error('Update error'); - handler(error); - - expect(mockWindow.webContents.send).toHaveBeenCalledWith('update:error', { - message: 'Update error', - }); - expect(updateManager.getStatus()).toBe(UpdateStatus.Error); - }); - - it('should not send events if window is destroyed', () => { - mockWindow.isDestroyed.mockReturnValue(true); - - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'checking-for-update', - )?.[1]; - - handler(); - - expect(mockWindow.webContents.send).not.toHaveBeenCalled(); - }); - }); - - describe('settings management', () => { - beforeEach(() => { - updateManager = UpdateManager.getInstance(); - }); - - it('should get settings with last check date', () => { - (fs.existsSync as Mock).mockReturnValue(true); - (fs.readFileSync as Mock).mockReturnValue( - JSON.stringify({ - autoCheckEnabled: true, - lastCheckDate: '2026-02-01T00:00:00.000Z', - }), - ); - - const settings = updateManager.getSettings(); - - expect(settings.autoCheckEnabled).toBe(true); - expect(settings.lastCheckDate).toBeInstanceOf(Date); - }); - - it('should set settings', async () => { - await updateManager.setSettings({ autoCheckEnabled: false }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - const settings = updateManager.getSettings(); - expect(settings.autoCheckEnabled).toBe(false); - }); - - it('should handle save settings error', async () => { - (fs.writeFileSync as Mock).mockImplementation(() => { - throw new Error('Write error'); - }); - - await expect( - updateManager.setSettings({ autoCheckEnabled: false }), - ).resolves.not.toThrow(); - }); - }); - - describe('getStatus and getUpdateInfo', () => { - beforeEach(() => { - updateManager = UpdateManager.getInstance(); - }); - - it('should return current status', () => { - expect(updateManager.getStatus()).toBe(UpdateStatus.Idle); - }); - - it('should return update info when available', () => { - const handler = (autoUpdater.on as Mock).mock.calls.find( - (call) => call[0] === 'update-available', - )?.[1]; - - handler(mockUpdateInfo); - - const info = updateManager.getUpdateInfo(); - expect(info).toEqual( - expect.objectContaining({ - version: '2.0.0', - }), - ); - }); - - it('should return null if no update info', () => { - expect(updateManager.getUpdateInfo()).toBeNull(); - }); - }); -}); diff --git a/electron/src/services/updater/update-manager.ts b/electron/src/services/updater/update-manager.ts deleted file mode 100644 index 8f03052..0000000 --- a/electron/src/services/updater/update-manager.ts +++ /dev/null @@ -1,404 +0,0 @@ -import electronUpdater from 'electron-updater'; -import { app, BrowserWindow } from 'electron'; -import { - UpdateInfo, - UpdateSettings, - UpdateStatus, - UpdateError, - DownloadProgress, -} from '../../interfaces/update.interface.js'; - -const { autoUpdater } = electronUpdater; -type ElectronUpdateInfo = electronUpdater.UpdateInfo; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { BackupService } from '../backup/backup.service.js'; - -/** - * Update manager configuration. - */ -export interface UpdateManagerConfig { - autoCheckEnabled: boolean; - checkOnStartup: boolean; - autoDownload: boolean; -} - -/** - * Service for managing application updates using electron-updater. - * Provides functionality for checking, downloading, and installing updates from GitHub Releases. - */ -export class UpdateManager { - private static instance: UpdateManager; - private mainWindow: BrowserWindow | null = null; - private readonly config: UpdateManagerConfig; - private currentStatus: UpdateStatus = UpdateStatus.Idle; - private updateInfo: UpdateInfo | null = null; - private readonly settingsPath: string; - private backupService: BackupService | null = null; - - private constructor() { - this.settingsPath = path.join( - app.getPath('userData'), - 'update-settings.json', - ); - this.config = this.loadSettings(); - this.setupAutoUpdater(); - this.attachEventHandlers(); - } - - /** - * Gets the singleton instance of UpdateManager. - */ - static getInstance(): UpdateManager { - if (!UpdateManager.instance) { - UpdateManager.instance = new UpdateManager(); - } - return UpdateManager.instance; - } - - /** - * Sets the main window reference for sending IPC events. - */ - setMainWindow(window: BrowserWindow): void { - this.mainWindow = window; - } - - /** - * Initializes the update manager. - * Should be called after app is ready. - */ - async initialize(): Promise { - console.log('[UpdateManager] Initialized'); - - if (this.config.checkOnStartup && this.config.autoCheckEnabled) { - // Delay initial check to allow app to fully load - setTimeout(() => { - this.checkForUpdates().catch((error) => { - console.error('[UpdateManager] Initial check failed:', error); - }); - }, 5000); - } - } - - /** - * Configures electron-updater settings. - */ - private setupAutoUpdater(): void { - // Configure auto-updater - autoUpdater.autoDownload = this.config.autoDownload; - autoUpdater.autoInstallOnAppQuit = true; - - // Set logger for debugging - autoUpdater.logger = { - info: (msg: string) => console.log('[AutoUpdater]', msg), - warn: (msg: string) => console.warn('[AutoUpdater]', msg), - error: (msg: string) => console.error('[AutoUpdater]', msg), - debug: (msg: string) => console.debug('[AutoUpdater]', msg), - }; - - // In development, use generic versioning server for testing - if (!app.isPackaged) { - console.log('[UpdateManager] Running in development mode'); - // Updates won't work in dev mode without mock server - autoUpdater.forceDevUpdateConfig = true; - } - } - - /** - * Attaches event handlers to autoUpdater events. - */ - private attachEventHandlers(): void { - autoUpdater.on('checking-for-update', () => { - console.log('[UpdateManager] Checking for updates...'); - this.currentStatus = UpdateStatus.Checking; - this.sendToRenderer('update:checking'); - }); - - autoUpdater.on('update-available', (info: ElectronUpdateInfo) => { - console.log('[UpdateManager] Update available:', info.version); - this.currentStatus = UpdateStatus.Available; - this.updateInfo = this.mapUpdateInfo(info); - this.sendToRenderer('update:available', this.updateInfo); - }); - - autoUpdater.on('update-not-available', (info: ElectronUpdateInfo) => { - console.log( - '[UpdateManager] Update not available. Current version:', - info.version, - ); - this.currentStatus = UpdateStatus.NotAvailable; - this.sendToRenderer('update:not-available', { version: info.version }); - }); - - autoUpdater.on('download-progress', (progressObj) => { - const progress: DownloadProgress = { - bytesPerSecond: progressObj.bytesPerSecond, - percent: progressObj.percent, - transferred: progressObj.transferred, - total: progressObj.total, - }; - console.log( - `[UpdateManager] Download progress: ${progress.percent.toFixed(2)}%`, - ); - this.currentStatus = UpdateStatus.Downloading; - this.sendToRenderer('update:download-progress', progress); - }); - - autoUpdater.on('update-downloaded', (info: ElectronUpdateInfo) => { - console.log('[UpdateManager] Update downloaded:', info.version); - this.currentStatus = UpdateStatus.Downloaded; - this.updateInfo = this.mapUpdateInfo(info); - this.sendToRenderer('update:downloaded', this.updateInfo); - }); - - autoUpdater.on('error', (error: Error) => { - console.error('[UpdateManager] Error:', error); - this.currentStatus = UpdateStatus.Error; - const updateError: UpdateError = { - message: error.message, - }; - this.sendToRenderer('update:error', updateError); - }); - } - - /** - * Sends IPC event to renderer process. - */ - private sendToRenderer(channel: string, data?: unknown): void { - if (this.mainWindow && !this.mainWindow.isDestroyed()) { - this.mainWindow.webContents.send(channel, data); - } - } - - /** - * Maps electron-updater UpdateInfo to our UpdateInfo interface. - */ - private mapUpdateInfo(info: ElectronUpdateInfo): UpdateInfo { - return { - version: info.version, - releaseDate: info.releaseDate, - releaseName: info.releaseName ?? undefined, - releaseNotes: info.releaseNotes as string | undefined, - }; - } - - /** - * Checks for available updates. - */ - async checkForUpdates(): Promise { - try { - if (this.currentStatus === UpdateStatus.Checking) { - console.log('[UpdateManager] Already checking for updates'); - return; - } - - if (!this.config.autoCheckEnabled) { - console.log('[UpdateManager] Auto-check is disabled'); - return; - } - - // In development mode, skip actual update check - if (!app.isPackaged) { - console.log( - '[UpdateManager] Skipping update check in development mode', - ); - this.currentStatus = UpdateStatus.NotAvailable; - this.sendToRenderer('update:not-available', { - version: app.getVersion(), - }); - return; - } - - console.log('[UpdateManager] Checking for updates...'); - await autoUpdater.checkForUpdates(); - - // Update last check date - this.updateLastCheckDate(); - } catch (error) { - console.error('[UpdateManager] Check for updates failed:', error); - throw error; - } - } - - /** - * Downloads the available update. - */ - async downloadUpdate(): Promise { - try { - if (!app.isPackaged) { - console.log( - '[UpdateManager] Download not available in development mode', - ); - throw new Error('Updates not available in development mode'); - } - - if (this.currentStatus !== UpdateStatus.Available) { - throw new Error('No update available to download'); - } - - console.log('[UpdateManager] Starting download...'); - await autoUpdater.downloadUpdate(); - } catch (error) { - console.error('[UpdateManager] Download failed:', error); - throw error; - } - } - - /** - * Quits the application and installs the downloaded update. - */ - async quitAndInstall(): Promise { - if (!app.isPackaged) { - console.log('[UpdateManager] Install not available in development mode'); - return; - } - - if (this.currentStatus !== UpdateStatus.Downloaded) { - console.error('[UpdateManager] No update downloaded to install'); - return; - } - - console.log('[UpdateManager] Creating backup before update...'); - - // Create backup before installing update - if (!this.backupService) { - this.backupService = new BackupService(); - } - - try { - const backupResult = - await this.backupService.createBackup('before-restore'); - if (backupResult.success) { - console.log( - '[UpdateManager] Backup created successfully:', - backupResult.backup?.filename, - ); - } else { - console.warn( - '[UpdateManager] Backup failed, continuing with update:', - backupResult.error, - ); - } - } catch (error) { - console.warn( - '[UpdateManager] Backup error, continuing with update:', - error, - ); - } - - console.log('[UpdateManager] Quitting and installing update...'); - - // Notify renderer to allow cleanup - this.sendToRenderer('update:installing'); - - // Give renderer time to cleanup - setTimeout(() => { - autoUpdater.quitAndInstall(false, true); - }, 1000); - } - - /** - * Gets current update settings. - */ - getSettings(): UpdateSettings { - return { - autoCheckEnabled: this.config.autoCheckEnabled, - lastCheckDate: this.loadLastCheckDate(), - }; - } - - /** - * Updates update settings. - */ - async setSettings(settings: Partial): Promise { - if (settings.autoCheckEnabled !== undefined) { - this.config.autoCheckEnabled = settings.autoCheckEnabled; - } - - this.saveSettings(); - console.log('[UpdateManager] Settings updated:', this.config); - } - - /** - * Gets current update status. - */ - getStatus(): UpdateStatus { - return this.currentStatus; - } - - /** - * Gets current update info if available. - */ - getUpdateInfo(): UpdateInfo | null { - return this.updateInfo; - } - - /** - * Loads settings from disk. - */ - private loadSettings(): UpdateManagerConfig { - try { - if (fs.existsSync(this.settingsPath)) { - const data = fs.readFileSync(this.settingsPath, 'utf-8'); - const saved = JSON.parse(data); - return { - autoCheckEnabled: saved.autoCheckEnabled ?? true, - checkOnStartup: saved.checkOnStartup ?? true, - autoDownload: saved.autoDownload ?? false, - }; - } - } catch (error) { - console.error('[UpdateManager] Failed to load settings:', error); - } - - // Default settings - return { - autoCheckEnabled: true, - checkOnStartup: true, - autoDownload: false, - }; - } - - /** - * Saves settings to disk. - */ - private saveSettings(): void { - try { - fs.writeFileSync(this.settingsPath, JSON.stringify(this.config, null, 2)); - } catch (error) { - console.error('[UpdateManager] Failed to save settings:', error); - } - } - - /** - * Loads last check date from settings. - */ - private loadLastCheckDate(): Date | undefined { - try { - if (fs.existsSync(this.settingsPath)) { - const data = fs.readFileSync(this.settingsPath, 'utf-8'); - const saved = JSON.parse(data); - return saved.lastCheckDate ? new Date(saved.lastCheckDate) : undefined; - } - } catch (error) { - console.error('[UpdateManager] Failed to load last check date:', error); - } - return undefined; - } - - /** - * Updates last check date in settings. - */ - private updateLastCheckDate(): void { - try { - const currentSettings = this.loadSettings(); - const updated = { - ...currentSettings, - lastCheckDate: new Date().toISOString(), - }; - fs.writeFileSync(this.settingsPath, JSON.stringify(updated, null, 2)); - } catch (error) { - console.error('[UpdateManager] Failed to update last check date:', error); - } - } -} diff --git a/package-lock.json b/package-lock.json index 2e6f603..ccd5e09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@prisma/adapter-better-sqlite3": "^7.1.0", "@prisma/client": "^7.1.0", "better-sqlite3": "^12.5.0", - "electron-updater": "^6.3.9", "primeflex": "^4.0.0", "primeicons": "^7.0.0", "primeng": "^20.3.0", @@ -7670,6 +7669,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -8118,6 +8118,7 @@ "version": "9.5.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -9119,6 +9120,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -9894,57 +9896,6 @@ "dev": true, "license": "ISC" }, - "node_modules/electron-updater": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz", - "integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==", - "license": "MIT", - "dependencies": { - "builder-util-runtime": "9.5.1", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "~7.7.3", - "tiny-typed-emitter": "^2.1.0" - } - }, - "node_modules/electron-updater/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-updater/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-updater/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/electron/node_modules/@types/node": { "version": "22.19.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", @@ -11518,6 +11469,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/grammex": { @@ -12353,6 +12305,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -12951,6 +12904,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, "license": "MIT" }, "node_modules/levn": { @@ -13233,19 +13187,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -13916,6 +13857,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/msgpackr": { @@ -16202,6 +16144,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -17263,12 +17206,6 @@ "semver": "bin/semver" } }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index f55f71e..25f129d 100644 --- a/package.json +++ b/package.json @@ -131,12 +131,6 @@ "path": "/Applications" } ] - }, - "publish": { - "provider": "github", - "owner": "altaskur", - "repo": "OpenTimeTracker", - "releaseType": "release" } }, "prettier": { @@ -165,7 +159,6 @@ "@prisma/adapter-better-sqlite3": "^7.1.0", "@prisma/client": "^7.1.0", "better-sqlite3": "^12.5.0", - "electron-updater": "^6.3.9", "primeflex": "^4.0.0", "primeicons": "^7.0.0", "primeng": "^20.3.0", diff --git a/src/app/app.html b/src/app/app.html index 88c6a86..1cbfd09 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -3,26 +3,3 @@
- - - - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index e5f3f99..3d5a1b6 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -49,13 +49,6 @@ export const routes: Routes = [ (m) => m.OpenSettingsStatusesComponent, ), }, - { - path: 'settings/updates', - loadComponent: () => - import('./pages/open-settings-updates/open-settings-updates').then( - (m) => m.OpenSettingsUpdatesComponent, - ), - }, { path: '**', redirectTo: '', diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index ea1e94f..21ad9f9 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -3,12 +3,9 @@ import { App } from './app'; import { TranslateModule } from '@ngx-translate/core'; import { MessageService } from 'primeng/api'; import { ActionHistoryService } from './services/action-history.service'; -import { UpdateService } from './services/update/update.service'; -import { signal } from '@angular/core'; describe('App', () => { let mockHistoryService: jasmine.SpyObj; - let mockUpdateService: jasmine.SpyObj; let mockElectronAPI: { onUndoAction: jasmine.Spy; onRedoAction: jasmine.Spy; @@ -52,28 +49,6 @@ describe('App', () => { }), ); - // Mock UpdateService with signals - mockUpdateService = jasmine.createSpyObj('UpdateService', [ - 'checkForUpdates', - 'downloadUpdate', - 'installUpdate', - ]); - Object.defineProperty(mockUpdateService, 'updateAvailable', { - get: () => signal(null), - }); - Object.defineProperty(mockUpdateService, 'downloadProgress', { - get: () => signal(null), - }); - Object.defineProperty(mockUpdateService, 'isChecking', { - get: () => signal(false), - }); - Object.defineProperty(mockUpdateService, 'isDownloading', { - get: () => signal(false), - }); - Object.defineProperty(mockUpdateService, 'updateDownloaded', { - get: () => signal(false), - }); - mockElectronAPI = { onUndoAction: jasmine .createSpy('onUndoAction') @@ -96,7 +71,6 @@ describe('App', () => { providers: [ MessageService, { provide: ActionHistoryService, useValue: mockHistoryService }, - { provide: UpdateService, useValue: mockUpdateService }, ], }).compileComponents(); }); @@ -181,41 +155,6 @@ describe('App', () => { expect(messageService.add).not.toHaveBeenCalled(); })); - - it('should handle download update', async () => { - const fixture = TestBed.createComponent(App); - const app = fixture.componentInstance; - fixture.detectChanges(); - - mockUpdateService.downloadUpdate.and.returnValue(Promise.resolve()); - - await app.onDownloadUpdate(); - - expect(mockUpdateService.downloadUpdate).toHaveBeenCalled(); - }); - - it('should handle install update', async () => { - const fixture = TestBed.createComponent(App); - const app = fixture.componentInstance; - fixture.detectChanges(); - - mockUpdateService.installUpdate.and.returnValue(Promise.resolve()); - - await app.onInstallUpdate(); - - expect(mockUpdateService.installUpdate).toHaveBeenCalled(); - }); - - it('should close update dialog', () => { - const fixture = TestBed.createComponent(App); - const app = fixture.componentInstance; - fixture.detectChanges(); - - app.updateDialogVisible.set(true); - app.onCloseUpdateDialog(); - - expect(app.updateDialogVisible()).toBe(false); - }); }); describe('App without electronAPI', () => { @@ -235,66 +174,3 @@ describe('App without electronAPI', () => { expect(app).toBeTruthy(); }); }); - -describe('App update notifications', () => { - it('should show notification when update becomes available', fakeAsync(() => { - const updateSignal = signal<{ - version: string; - releaseName?: string; - releaseNotes?: string; - releaseDate: string; - } | null>(null); - - const mockUpdateService = jasmine.createSpyObj('UpdateService', [ - 'checkForUpdates', - 'downloadUpdate', - 'installUpdate', - ]); - Object.defineProperty(mockUpdateService, 'updateAvailable', { - get: () => updateSignal, - }); - Object.defineProperty(mockUpdateService, 'downloadProgress', { - get: () => signal(null), - }); - Object.defineProperty(mockUpdateService, 'isChecking', { - get: () => signal(false), - }); - Object.defineProperty(mockUpdateService, 'isDownloading', { - get: () => signal(false), - }); - Object.defineProperty(mockUpdateService, 'updateDownloaded', { - get: () => signal(false), - }); - - TestBed.configureTestingModule({ - imports: [App, TranslateModule.forRoot()], - providers: [ - MessageService, - { provide: UpdateService, useValue: mockUpdateService }, - ], - }); - - const fixture = TestBed.createComponent(App); - const messageService = TestBed.inject(MessageService); - spyOn(messageService, 'add'); - fixture.detectChanges(); - - // Trigger the effect by setting update info - updateSignal.set({ - version: '2.0.0', - releaseName: 'Version 2.0.0', - releaseNotes: 'New features', - releaseDate: '2026-02-01', - }); - fixture.detectChanges(); - tick(); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ - severity: 'info', - sticky: true, - data: { action: 'viewUpdate' }, - }), - ); - })); -}); diff --git a/src/app/app.ts b/src/app/app.ts index 7550317..53492bb 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,12 +1,4 @@ -import { - Component, - inject, - OnInit, - NgZone, - OnDestroy, - signal, - effect, -} from '@angular/core'; +import { Component, inject, OnInit, NgZone, OnDestroy } from '@angular/core'; import { RouterOutlet, Router } from '@angular/router'; import { ToastModule } from 'primeng/toast'; import { MessageService } from 'primeng/api'; @@ -17,8 +9,6 @@ import { TranslationService, } from './services'; import { ActionHistoryService } from './services/action-history.service'; -import { UpdateService } from './services/update/update.service'; -import { OpenUpdateDialogComponent } from './components/open-update-dialog/open-update-dialog'; /** * Root component of the application. @@ -26,7 +16,7 @@ import { OpenUpdateDialogComponent } from './components/open-update-dialog/open- */ @Component({ selector: 'app-root', - imports: [RouterOutlet, ToastModule, OpenUpdateDialogComponent], + imports: [RouterOutlet, ToastModule], templateUrl: './app.html', styleUrl: './app.scss', }) @@ -37,34 +27,11 @@ export class App implements OnInit, OnDestroy { private readonly themeService = inject(ThemeService); private readonly translationService = inject(TranslationService); private readonly historyService = inject(ActionHistoryService); - readonly updateService = inject(UpdateService); private readonly messageService = inject(MessageService); private readonly translate = inject(TranslateService); private readonly ngZone = inject(NgZone); private readonly router = inject(Router); - readonly updateDialogVisible = signal(false); - - constructor() { - // Setup effect for update notifications - // Must be in constructor to run in injection context - effect(() => { - const updateInfo = this.updateService.updateAvailable(); - if (updateInfo) { - this.ngZone.run(() => { - this.messageService.add({ - severity: 'info', - summary: this.translate.instant('update.availableTitle'), - detail: `${this.translate.instant('update.newVersion')}: ${updateInfo.version}`, - sticky: true, - closable: true, - data: { action: 'viewUpdate' }, - }); - }); - } - }); - } - ngOnInit(): void { this.setupHistoryListeners(); } @@ -108,25 +75,4 @@ export class App implements OnInit, OnDestroy { }); } } - - /** - * Handles download update action - */ - async onDownloadUpdate(): Promise { - await this.updateService.downloadUpdate(); - } - - /** - * Handles install update action - */ - async onInstallUpdate(): Promise { - await this.updateService.installUpdate(); - } - - /** - * Closes update dialog - */ - onCloseUpdateDialog(): void { - this.updateDialogVisible.set(false); - } } diff --git a/src/app/components/open-update-dialog/open-update-dialog.html b/src/app/components/open-update-dialog/open-update-dialog.html deleted file mode 100644 index 1a3fa99..0000000 --- a/src/app/components/open-update-dialog/open-update-dialog.html +++ /dev/null @@ -1,101 +0,0 @@ - -
- @if (updateInfo()) { -
-
- - - {{ "update.newVersion" | translate }}: - {{ updateInfo()?.version }} - -
- - @if (updateInfo()?.releaseName) { -
- {{ updateInfo()?.releaseName }} -
- } - - @if (updateInfo()?.releaseDate) { -
- {{ "update.releaseDate" | translate }}: - {{ updateInfo()?.releaseDate | date: "medium" }} -
- } - - @if (updateInfo()?.releaseNotes) { -
- {{ "update.releaseNotes" | translate }}: -
- {{ updateInfo()?.releaseNotes }} -
-
- } -
- } - - @if (isDownloading()) { -
-

{{ "update.downloading" | translate }}

- -

{{ downloadProgress() }}%

-
- } - - @if (isDownloaded()) { -
- -

{{ "update.readyToInstall" | translate }}

-
- } -
- - - @if (isDownloaded()) { - - - } @else if (isDownloading()) { - - } @else { - - - } - -
diff --git a/src/app/components/open-update-dialog/open-update-dialog.scss b/src/app/components/open-update-dialog/open-update-dialog.scss deleted file mode 100644 index 2224ff4..0000000 --- a/src/app/components/open-update-dialog/open-update-dialog.scss +++ /dev/null @@ -1,85 +0,0 @@ -.update-dialog { - &__info { - display: flex; - flex-direction: column; - gap: 1rem; - margin-bottom: 1rem; - } - - &__version { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.1rem; - - i { - color: var(--primary-color); - font-size: 1.5rem; - } - } - - &__release-name { - font-size: 1rem; - color: var(--text-color-secondary); - font-style: italic; - } - - &__date { - font-size: 0.9rem; - color: var(--text-color-secondary); - } - - &__notes { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - &__notes-content { - padding: 0.75rem; - background: var(--surface-ground); - border-radius: var(--border-radius); - border-left: 3px solid var(--primary-color); - max-height: 200px; - overflow-y: auto; - white-space: pre-wrap; - word-wrap: break-word; - font-size: 0.9rem; - line-height: 1.5; - } - - &__progress { - display: flex; - flex-direction: column; - gap: 0.75rem; - text-align: center; - - p { - margin: 0; - } - } - - &__progress-text { - font-weight: bold; - color: var(--primary-color); - } - - &__ready { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; - padding: 1rem; - text-align: center; - - p { - margin: 0; - font-size: 1.1rem; - } - } - - &__ready-icon { - font-size: 3rem; - color: var(--green-500); - } -} diff --git a/src/app/components/open-update-dialog/open-update-dialog.spec.ts b/src/app/components/open-update-dialog/open-update-dialog.spec.ts deleted file mode 100644 index ec6eba7..0000000 --- a/src/app/components/open-update-dialog/open-update-dialog.spec.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; -import { OpenUpdateDialogComponent } from './open-update-dialog'; -import { UpdateInfo } from '../../../types/electron'; - -describe('OpenUpdateDialogComponent', () => { - let component: OpenUpdateDialogComponent; - let fixture: ComponentFixture; - - const mockUpdateInfo: UpdateInfo = { - version: '2.0.0', - releaseName: 'Major Update', - releaseNotes: 'New features and improvements', - releaseDate: '2026-02-01T00:00:00Z', - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [OpenUpdateDialogComponent, TranslateModule.forRoot()], - providers: [provideNoopAnimations()], - }).compileComponents(); - - fixture = TestBed.createComponent(OpenUpdateDialogComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.componentRef.setInput('visible', false); - expect(component).toBeTruthy(); - }); - - describe('dialogHeader computed', () => { - it('should return availableTitle when not downloading and not downloaded', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', false); - fixture.componentRef.setInput('isDownloaded', false); - - expect(component.dialogHeader()).toBe('update.availableTitle'); - }); - - it('should return downloadingTitle when downloading', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', true); - fixture.componentRef.setInput('isDownloaded', false); - - expect(component.dialogHeader()).toBe('update.downloadingTitle'); - }); - - it('should return readyToInstallTitle when downloaded', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', false); - fixture.componentRef.setInput('isDownloaded', true); - - expect(component.dialogHeader()).toBe('update.readyToInstallTitle'); - }); - }); - - describe('output events', () => { - it('should emit download event when onDownload is called', () => { - fixture.componentRef.setInput('visible', true); - let emitted = false; - component.download.subscribe(() => { - emitted = true; - }); - - component.onDownload(); - - expect(emitted).toBe(true); - }); - - it('should emit install event when onInstall is called', () => { - fixture.componentRef.setInput('visible', true); - let emitted = false; - component.install.subscribe(() => { - emitted = true; - }); - - component.onInstall(); - - expect(emitted).toBe(true); - }); - - it('should emit closed event when onClose is called', () => { - fixture.componentRef.setInput('visible', true); - let emitted = false; - component.closed.subscribe(() => { - emitted = true; - }); - - component.onClose(); - - expect(emitted).toBe(true); - }); - }); - - describe('rendering', () => { - it('should display update info when provided', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('updateInfo', mockUpdateInfo); - fixture.detectChanges(); - - const compiled = fixture.nativeElement; - expect(compiled.textContent).toContain('2.0.0'); - }); - - it('should display progress bar when downloading', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', true); - fixture.componentRef.setInput('downloadProgress', 50); - fixture.detectChanges(); - - const progressBar = fixture.nativeElement.querySelector('p-progressBar'); - expect(progressBar).toBeTruthy(); - }); - - it('should show download button when not downloading and not downloaded', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', false); - fixture.componentRef.setInput('isDownloaded', false); - fixture.detectChanges(); - - const buttons = fixture.nativeElement.querySelectorAll('p-button'); - expect(buttons.length).toBeGreaterThan(0); - }); - - it('should show install button when downloaded', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', false); - fixture.componentRef.setInput('isDownloaded', true); - fixture.detectChanges(); - - const buttons = fixture.nativeElement.querySelectorAll('p-button'); - expect(buttons.length).toBeGreaterThan(0); - }); - - it('should show disabled button when downloading in progress', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', true); - fixture.detectChanges(); - - const buttons = fixture.nativeElement.querySelectorAll('p-button'); - expect(buttons.length).toBeGreaterThan(0); - }); - }); - - describe('inputs', () => { - it('should accept visible input', () => { - fixture.componentRef.setInput('visible', true); - expect(component.visible()).toBe(true); - - fixture.componentRef.setInput('visible', false); - expect(component.visible()).toBe(false); - }); - - it('should accept updateInfo input', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('updateInfo', mockUpdateInfo); - expect(component.updateInfo()).toEqual(mockUpdateInfo); - }); - - it('should accept downloadProgress input', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('downloadProgress', 75); - expect(component.downloadProgress()).toBe(75); - }); - - it('should accept isDownloading input', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloading', true); - expect(component.isDownloading()).toBe(true); - }); - - it('should accept isDownloaded input', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('isDownloaded', true); - expect(component.isDownloaded()).toBe(true); - }); - - it('should handle null updateInfo', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('updateInfo', null); - expect(component.updateInfo()).toBeNull(); - }); - }); - - describe('dialog states', () => { - it('should handle initial state (available update)', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('updateInfo', mockUpdateInfo); - fixture.componentRef.setInput('isDownloading', false); - fixture.componentRef.setInput('isDownloaded', false); - fixture.detectChanges(); - - expect(component.dialogHeader()).toBe('update.availableTitle'); - expect(component.visible()).toBe(true); - }); - - it('should handle downloading state', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('updateInfo', mockUpdateInfo); - fixture.componentRef.setInput('isDownloading', true); - fixture.componentRef.setInput('isDownloaded', false); - fixture.componentRef.setInput('downloadProgress', 30); - fixture.detectChanges(); - - expect(component.dialogHeader()).toBe('update.downloadingTitle'); - expect(component.downloadProgress()).toBe(30); - }); - - it('should handle downloaded state', () => { - fixture.componentRef.setInput('visible', true); - fixture.componentRef.setInput('updateInfo', mockUpdateInfo); - fixture.componentRef.setInput('isDownloading', false); - fixture.componentRef.setInput('isDownloaded', true); - fixture.componentRef.setInput('downloadProgress', 100); - fixture.detectChanges(); - - expect(component.dialogHeader()).toBe('update.readyToInstallTitle'); - expect(component.isDownloaded()).toBe(true); - }); - }); -}); diff --git a/src/app/components/open-update-dialog/open-update-dialog.ts b/src/app/components/open-update-dialog/open-update-dialog.ts deleted file mode 100644 index 5035c1f..0000000 --- a/src/app/components/open-update-dialog/open-update-dialog.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Component, input, output, computed } from '@angular/core'; -import { DatePipe } from '@angular/common'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { ProgressBarModule } from 'primeng/progressbar'; -import { TranslateModule } from '@ngx-translate/core'; -import { UpdateInfo } from '../../../types/electron'; - -/** - * Update notification dialog component. - * Displays information about available updates and allows downloading/installing them. - */ -@Component({ - selector: 'app-open-update-dialog', - imports: [ - DatePipe, - ButtonModule, - DialogModule, - ProgressBarModule, - TranslateModule, - ], - templateUrl: './open-update-dialog.html', - styleUrl: './open-update-dialog.scss', -}) -export class OpenUpdateDialogComponent { - /** Whether the dialog is visible */ - visible = input.required(); - - /** Information about the available update */ - updateInfo = input(null); - - /** Download progress percentage (0-100) */ - downloadProgress = input(0); - - /** Whether update is currently being downloaded */ - isDownloading = input(false); - - /** Whether update has been downloaded and ready to install */ - isDownloaded = input(false); - - /** Event emitted when user wants to download the update */ - download = output(); - - /** Event emitted when user wants to install the update */ - install = output(); - - /** Event emitted when dialog is closed */ - closed = output(); - - /** - * Computed property for dialog header based on state. - */ - readonly dialogHeader = computed(() => { - if (this.isDownloading()) { - return 'update.downloadingTitle'; - } - if (this.isDownloaded()) { - return 'update.readyToInstallTitle'; - } - return 'update.availableTitle'; - }); - - /** - * Handles the download action. - */ - onDownload(): void { - this.download.emit(); - } - - /** - * Handles the install and restart action. - */ - onInstall(): void { - this.install.emit(); - } - - /** - * Handles the cancel/close action. - */ - onClose(): void { - this.closed.emit(); - } -} diff --git a/src/app/pages/open-settings-updates/open-settings-updates.html b/src/app/pages/open-settings-updates/open-settings-updates.html deleted file mode 100644 index f7de3a3..0000000 --- a/src/app/pages/open-settings-updates/open-settings-updates.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - -
-

- - {{ "update.checkForUpdates" | translate }} -

-
-
- -
- -
-
- -
- {{ "update.currentVersion" | translate }}: - {{ currentVersion() }} -
-
-
- - - - -
-
-
- - {{ "update.autoCheckEnabled" | translate }} -
- -
-
- - - - -
- -
- - @if (updateAvailable()) { -
- - - {{ "update.newVersion" | translate }}: - {{ updateAvailable()?.version }} - - -
- } - - @if (errorMessage()) { -
- - {{ "update.errorChecking" | translate }}: {{ errorMessage() }} -
- } - - - - -
- -
- - @if (settings().lastCheckDate) { -
- {{ "Last checked" }}: {{ settings().lastCheckDate | date: "medium" }} -
- } -
- - - -
diff --git a/src/app/pages/open-settings-updates/open-settings-updates.scss b/src/app/pages/open-settings-updates/open-settings-updates.scss deleted file mode 100644 index 556b2a0..0000000 --- a/src/app/pages/open-settings-updates/open-settings-updates.scss +++ /dev/null @@ -1,109 +0,0 @@ -.settings-updates-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem; - - &__title { - display: flex; - align-items: center; - gap: 0.75rem; - margin: 0; - font-size: 1.5rem; - - i { - color: var(--primary-color); - } - } -} - -.settings-updates { - padding: 1.5rem; - max-width: 800px; - margin: 0 auto; - - &__section { - display: flex; - flex-direction: column; - gap: 1rem; - padding: 1rem 0; - } - - &__info { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - background: var(--surface-ground); - border-radius: var(--border-radius); - border-left: 3px solid var(--primary-color); - - i { - font-size: 1.5rem; - color: var(--primary-color); - } - } - - &__setting { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem; - background: var(--surface-ground); - border-radius: var(--border-radius); - } - - &__setting-label { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1rem; - - i { - color: var(--text-color-secondary); - } - } - - &__update-info { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - background: var(--blue-50); - border-radius: var(--border-radius); - border-left: 3px solid var(--blue-500); - margin-top: 1rem; - - i { - font-size: 1.5rem; - color: var(--blue-500); - } - - span { - flex: 1; - } - } - - &__error-info { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - background: var(--red-50); - border-radius: var(--border-radius); - border-left: 3px solid var(--red-500); - margin-top: 1rem; - - i { - font-size: 1.5rem; - color: var(--red-500); - } - } - - &__last-check { - text-align: center; - font-size: 0.875rem; - color: var(--text-color-secondary); - margin-top: 1rem; - } -} diff --git a/src/app/pages/open-settings-updates/open-settings-updates.spec.ts b/src/app/pages/open-settings-updates/open-settings-updates.spec.ts deleted file mode 100644 index 9523a99..0000000 --- a/src/app/pages/open-settings-updates/open-settings-updates.spec.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, -} from '@angular/core/testing'; -import { signal, WritableSignal } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; -import { MessageService } from 'primeng/api'; -import { UpdateService } from '../../services/update/update.service'; -import { OpenSettingsUpdatesComponent } from './open-settings-updates'; -import { UpdateInfo, UpdateSettings } from '../../../types/electron'; - -describe('OpenSettingsUpdatesComponent', () => { - let fixture: ComponentFixture; - let component: OpenSettingsUpdatesComponent; - let mockUpdateService: jasmine.SpyObj & { - updateAvailable: WritableSignal; - isChecking: WritableSignal; - isDownloading: WritableSignal; - downloadProgress: WritableSignal; - updateDownloaded: WritableSignal; - settings: WritableSignal; - errorMessage: WritableSignal; - }; - let messageService: MessageService; - - const updateInfo: UpdateInfo = { - version: '2.0.0', - releaseName: 'v2', - releaseNotes: 'notes', - releaseDate: '2026-02-01', - }; - - beforeEach(async () => { - mockUpdateService = Object.assign( - jasmine.createSpyObj('UpdateService', [ - 'getSettings', - 'getAppVersion', - 'setAutoCheck', - 'checkForUpdates', - 'downloadUpdate', - 'installUpdate', - ]), - { - updateAvailable: signal(null), - isChecking: signal(false), - isDownloading: signal(false), - downloadProgress: signal(0), - updateDownloaded: signal(false), - settings: signal({ autoCheckEnabled: true }), - errorMessage: signal(null), - }, - ); - - mockUpdateService.getSettings.and.returnValue( - Promise.resolve({ autoCheckEnabled: true }), - ); - mockUpdateService.getAppVersion.and.returnValue(Promise.resolve('1.2.3')); - mockUpdateService.setAutoCheck.and.returnValue(Promise.resolve()); - mockUpdateService.checkForUpdates.and.returnValue(Promise.resolve()); - mockUpdateService.downloadUpdate.and.returnValue(Promise.resolve()); - mockUpdateService.installUpdate.and.returnValue(Promise.resolve()); - - await TestBed.configureTestingModule({ - imports: [OpenSettingsUpdatesComponent, TranslateModule.forRoot()], - providers: [ - provideNoopAnimations(), - { provide: UpdateService, useValue: mockUpdateService }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(OpenSettingsUpdatesComponent); - component = fixture.componentInstance; - messageService = fixture.debugElement.injector.get(MessageService); - spyOn(messageService, 'add'); - }); - - afterEach(() => { - delete (window as { electronAPI?: unknown }).electronAPI; - }); - - it('should load settings and version on init', fakeAsync(() => { - fixture.detectChanges(); - tick(); - - expect(mockUpdateService.getSettings).toHaveBeenCalled(); - expect(mockUpdateService.getAppVersion).toHaveBeenCalled(); - expect(component.currentVersion()).toBe('1.2.3'); - })); - - it('should fallback to unknown version when getAppVersion fails', fakeAsync(() => { - mockUpdateService.getAppVersion.and.returnValue(Promise.reject('fail')); - - ( - component as unknown as { loadCurrentVersion: () => Promise } - ).loadCurrentVersion(); - tick(); - - expect(component.currentVersion()).toBe('Unknown'); - })); - - it('should show error toast when loadSettings fails', fakeAsync(() => { - mockUpdateService.getSettings.and.returnValue(Promise.reject('error')); - - component.loadSettings(); - tick(); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'error' }), - ); - })); - - it('should update auto-check and show success toast', fakeAsync(() => { - component.onAutoCheckToggle(true); - tick(); - - expect(mockUpdateService.setAutoCheck).toHaveBeenCalledWith(true); - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'success' }), - ); - })); - - it('should show error toast when auto-check update fails', fakeAsync(() => { - mockUpdateService.setAutoCheck.and.returnValue(Promise.reject('error')); - - component.onAutoCheckToggle(false); - tick(); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'error' }), - ); - })); - - it('should open dialog when update is available after check', fakeAsync(() => { - mockUpdateService.updateAvailable.set(updateInfo); - - component.checkForUpdates(); - tick(1000); - - expect(component.dialogVisible()).toBeTrue(); - })); - - it('should show info toast when no updates are available', fakeAsync(() => { - mockUpdateService.updateAvailable.set(null); - mockUpdateService.isChecking.set(false); - mockUpdateService.errorMessage.set(null); - - component.checkForUpdates(); - tick(1000); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'info' }), - ); - expect(component.dialogVisible()).toBeFalse(); - })); - - it('should show error toast when update check fails', fakeAsync(() => { - mockUpdateService.checkForUpdates.and.returnValue(Promise.reject('error')); - - component.checkForUpdates(); - tick(); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'error' }), - ); - })); - - it('should show error toast when download fails', fakeAsync(() => { - mockUpdateService.downloadUpdate.and.returnValue(Promise.reject('error')); - - component.onDownload(); - tick(); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'error' }), - ); - })); - - it('should show error toast when install fails', fakeAsync(() => { - mockUpdateService.installUpdate.and.returnValue(Promise.reject('error')); - - component.onInstall(); - tick(); - - expect(messageService.add).toHaveBeenCalledWith( - jasmine.objectContaining({ severity: 'error' }), - ); - })); - - it('should open releases page using electron API when available', async () => { - const openExternal = jasmine - .createSpy('openExternal') - .and.returnValue(Promise.resolve()); - Object.defineProperty(window, 'electronAPI', { - configurable: true, - writable: true, - value: { openExternal }, - }); - - await component.openReleasesPage(); - - expect(openExternal).toHaveBeenCalledWith( - 'https://github.com/altaskur/OpenTimeTracker/releases', - ); - }); - - it('should fallback to window.open when electron API is not available', () => { - delete (window as { electronAPI?: unknown }).electronAPI; - const openSpy = spyOn(window, 'open'); - - component.openReleasesPage(); - - expect(openSpy).toHaveBeenCalledWith( - 'https://github.com/altaskur/OpenTimeTracker/releases', - '_blank', - 'noopener,noreferrer', - ); - }); -}); diff --git a/src/app/pages/open-settings-updates/open-settings-updates.ts b/src/app/pages/open-settings-updates/open-settings-updates.ts deleted file mode 100644 index 67b7f66..0000000 --- a/src/app/pages/open-settings-updates/open-settings-updates.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { - Component, - OnInit, - inject, - signal, - ChangeDetectionStrategy, -} from '@angular/core'; -import { CommonModule, DatePipe } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { ButtonModule } from 'primeng/button'; -import { ToggleSwitch } from 'primeng/toggleswitch'; -import { DividerModule } from 'primeng/divider'; -import { TooltipModule } from 'primeng/tooltip'; -import { TranslateModule } from '@ngx-translate/core'; -import { MessageService } from 'primeng/api'; -import { ToastModule } from 'primeng/toast'; -import { UpdateService } from '../../services/update/update.service'; -import { OpenLayoutComponent } from '../../components/open-layout/open-layout'; -import { OpenUpdateDialogComponent } from '../../components/open-update-dialog/open-update-dialog'; - -/** - * Settings page for managing application updates. - */ -@Component({ - selector: 'app-open-settings-updates', - standalone: true, - imports: [ - CommonModule, - DatePipe, - FormsModule, - ButtonModule, - ToggleSwitch, - DividerModule, - TooltipModule, - TranslateModule, - ToastModule, - OpenLayoutComponent, - OpenUpdateDialogComponent, - ], - providers: [MessageService], - templateUrl: './open-settings-updates.html', - styleUrl: './open-settings-updates.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class OpenSettingsUpdatesComponent implements OnInit { - private readonly updateService = inject(UpdateService); - private readonly messageService = inject(MessageService); - - readonly loading = signal(false); - readonly dialogVisible = signal(false); - readonly currentVersion = signal('Unknown'); - readonly updateAvailable = this.updateService.updateAvailable; - readonly isChecking = this.updateService.isChecking; - readonly isDownloading = this.updateService.isDownloading; - readonly downloadProgress = this.updateService.downloadProgress; - readonly updateDownloaded = this.updateService.updateDownloaded; - readonly settings = this.updateService.settings; - readonly errorMessage = this.updateService.errorMessage; - - ngOnInit(): void { - void this.loadSettings(); - void this.loadCurrentVersion(); - } - - private async loadCurrentVersion(): Promise { - try { - const version = await this.updateService.getAppVersion(); - this.currentVersion.set(version); - } catch { - this.currentVersion.set('Unknown'); - } - } - - async loadSettings(): Promise { - this.loading.set(true); - try { - await this.updateService.getSettings(); - } catch { - this.messageService.add({ - severity: 'error', - summary: '', - detail: 'Error loading update settings', - life: 3000, - }); - } finally { - this.loading.set(false); - } - } - - async onAutoCheckToggle(enabled: boolean): Promise { - try { - await this.updateService.setAutoCheck(enabled); - this.messageService.add({ - severity: 'success', - summary: '', - detail: enabled ? 'Auto-check enabled' : 'Auto-check disabled', - life: 3000, - }); - } catch { - this.messageService.add({ - severity: 'error', - summary: '', - detail: 'Error updating settings', - life: 3000, - }); - } - } - - async checkForUpdates(): Promise { - try { - await this.updateService.checkForUpdates(); - - /* Wait a moment for the update check to complete. */ - setTimeout(() => { - if (this.updateAvailable()) { - this.dialogVisible.set(true); - } else if (!this.isChecking() && !this.errorMessage()) { - this.messageService.add({ - severity: 'info', - summary: '', - detail: 'No updates available', - life: 3000, - }); - } - }, 1000); - } catch { - this.messageService.add({ - severity: 'error', - summary: '', - detail: 'Error checking for updates', - life: 3000, - }); - } - } - - async onDownload(): Promise { - try { - await this.updateService.downloadUpdate(); - } catch { - this.messageService.add({ - severity: 'error', - summary: '', - detail: 'Error downloading update', - life: 3000, - }); - } - } - - async onInstall(): Promise { - try { - await this.updateService.installUpdate(); - /* App will restart, no need for toast. */ - } catch { - this.messageService.add({ - severity: 'error', - summary: '', - detail: 'Error installing update', - life: 3000, - }); - } - } - - onDialogClosed(): void { - this.dialogVisible.set(false); - } - - openReleasesPage(): void { - /* Open GitHub releases page in system browser. */ - const url = 'https://github.com/altaskur/OpenTimeTracker/releases'; - if (globalThis.window?.electronAPI?.openExternal) { - void globalThis.window.electronAPI.openExternal(url); - } else { - /* Fallback for web or if API not available. */ - window.open(url, '_blank', 'noopener,noreferrer'); - } - } -} diff --git a/src/app/services/update/index.ts b/src/app/services/update/index.ts deleted file mode 100644 index 74ed17b..0000000 --- a/src/app/services/update/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UpdateService } from './update.service'; diff --git a/src/app/services/update/update.service.spec.ts b/src/app/services/update/update.service.spec.ts deleted file mode 100644 index 2a1960e..0000000 --- a/src/app/services/update/update.service.spec.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { UpdateService } from './update.service'; -import { - UpdateInfo, - UpdateSettings, - DownloadProgress, - UpdateResult, -} from '../../../types/electron'; - -interface MockElectronAPI { - onUpdateChecking?: jasmine.Spy<(callback: () => void) => void>; - onUpdateAvailable?: jasmine.Spy< - (callback: (info: UpdateInfo) => void) => void - >; - onUpdateNotAvailable?: jasmine.Spy< - (callback: (info: { version: string }) => void) => void - >; - onDownloadProgress?: jasmine.Spy< - (callback: (progress: DownloadProgress) => void) => void - >; - onUpdateDownloaded?: jasmine.Spy< - (callback: (info: UpdateInfo) => void) => void - >; - onUpdateError?: jasmine.Spy< - (callback: (error: { message: string }) => void) => void - >; - checkForUpdates?: jasmine.Spy<() => Promise>; - downloadUpdate?: jasmine.Spy<() => Promise>; - installUpdate?: jasmine.Spy<() => Promise>; - getUpdateSettings?: jasmine.Spy<() => Promise>; - setUpdateSettings?: jasmine.Spy< - (settings: UpdateSettings) => Promise - >; - getUpdateStatus?: jasmine.Spy<() => Promise>; - getAppVersion?: jasmine.Spy<() => Promise>; -} - -describe('UpdateService', () => { - let service: UpdateService; - let mockElectronAPI: MockElectronAPI; - - const mockUpdateInfo: UpdateInfo = { - version: '2.0.0', - releaseName: 'Version 2.0.0', - releaseNotes: 'New features', - releaseDate: '2026-02-01', - }; - - const mockUpdateSettings: UpdateSettings = { - autoCheckEnabled: true, - }; - - beforeEach(() => { - /* - * Setup mock electronAPI - */ - mockElectronAPI = { - onUpdateChecking: jasmine.createSpy('onUpdateChecking'), - onUpdateAvailable: jasmine.createSpy('onUpdateAvailable'), - onUpdateNotAvailable: jasmine.createSpy('onUpdateNotAvailable'), - onDownloadProgress: jasmine.createSpy('onDownloadProgress'), - onUpdateDownloaded: jasmine.createSpy('onUpdateDownloaded'), - onUpdateError: jasmine.createSpy('onUpdateError'), - checkForUpdates: jasmine - .createSpy('checkForUpdates') - .and.returnValue(Promise.resolve({ success: true })), - downloadUpdate: jasmine - .createSpy('downloadUpdate') - .and.returnValue(Promise.resolve({ success: true })), - installUpdate: jasmine - .createSpy('installUpdate') - .and.returnValue(Promise.resolve({ success: true })), - getUpdateSettings: jasmine - .createSpy('getUpdateSettings') - .and.returnValue( - Promise.resolve({ success: true, settings: mockUpdateSettings }), - ), - setUpdateSettings: jasmine - .createSpy('setUpdateSettings') - .and.returnValue(Promise.resolve({ success: true })), - getUpdateStatus: jasmine - .createSpy('getUpdateStatus') - .and.returnValue(Promise.resolve({ success: true })), - } as MockElectronAPI; - - /* - * Define electronAPI as a configurable property so it can be deleted in tests - */ - Object.defineProperty(window, 'electronAPI', { - configurable: true, - writable: true, - value: mockElectronAPI, - }); - - TestBed.configureTestingModule({}); - service = TestBed.inject(UpdateService); - }); - - afterEach(() => { - delete (window as { electronAPI?: unknown }).electronAPI; - }); - - describe('initialization', () => { - it('should create', () => { - expect(service).toBeTruthy(); - }); - - it('should initialize with default signal values', () => { - expect(service.updateAvailable()).toBeNull(); - expect(service.downloadProgress()).toBe(0); - expect(service.isChecking()).toBe(false); - expect(service.isDownloading()).toBe(false); - expect(service.updateDownloaded()).toBe(false); - expect(service.settings()).toEqual({ autoCheckEnabled: true }); - expect(service.errorMessage()).toBeNull(); - }); - - it('should setup listeners on construction', () => { - expect(mockElectronAPI.onUpdateChecking).toHaveBeenCalled(); - expect(mockElectronAPI.onUpdateAvailable).toHaveBeenCalled(); - expect(mockElectronAPI.onUpdateNotAvailable).toHaveBeenCalled(); - expect(mockElectronAPI.onDownloadProgress).toHaveBeenCalled(); - expect(mockElectronAPI.onUpdateDownloaded).toHaveBeenCalled(); - expect(mockElectronAPI.onUpdateError).toHaveBeenCalled(); - }); - - it('should not setup listeners when electronAPI is not available', () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - const newService = new UpdateService(); - - expect(newService).toBeTruthy(); - }); - - it('should load settings on init', async () => { - await service.init(); - - expect(mockElectronAPI.getUpdateSettings).toHaveBeenCalled(); - expect(service.settings()).toEqual(mockUpdateSettings); - }); - - it('should handle init when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - const newService = new UpdateService(); - - await newService.init(); - - expect(newService.settings()).toEqual({ autoCheckEnabled: true }); - }); - - it('should handle errors during settings load', async () => { - mockElectronAPI.getUpdateSettings!.and.returnValue( - Promise.reject('Error'), - ); - spyOn(console, 'error'); - - await service.init(); - - expect(console.error).toHaveBeenCalledWith( - '[UpdateService] Failed to load settings:', - 'Error', - ); - }); - - it('should handle unsuccessful settings load', async () => { - mockElectronAPI.getUpdateSettings!.and.returnValue( - Promise.resolve({ success: false }), - ); - - await service.init(); - - expect(service.settings()).toEqual({ autoCheckEnabled: true }); - }); - }); - - describe('event listeners', () => { - let checkingCallback: () => void; - let availableCallback: (info: UpdateInfo) => void; - let notAvailableCallback: (info: { version: string }) => void; - let progressCallback: (progress: DownloadProgress) => void; - let downloadedCallback: (info: UpdateInfo) => void; - let errorCallback: (error: { message: string }) => void; - - beforeEach(() => { - checkingCallback = mockElectronAPI.onUpdateChecking!.calls.argsFor(0)[0]; - availableCallback = - mockElectronAPI.onUpdateAvailable!.calls.argsFor(0)[0]; - notAvailableCallback = - mockElectronAPI.onUpdateNotAvailable!.calls.argsFor(0)[0]; - progressCallback = - mockElectronAPI.onDownloadProgress!.calls.argsFor(0)[0]; - downloadedCallback = - mockElectronAPI.onUpdateDownloaded!.calls.argsFor(0)[0]; - errorCallback = mockElectronAPI.onUpdateError!.calls.argsFor(0)[0]; - }); - - it('should handle onUpdateChecking event', () => { - spyOn(console, 'log'); - - checkingCallback(); - - expect(service.isChecking()).toBe(true); - expect(service.errorMessage()).toBeNull(); - expect(console.log).toHaveBeenCalledWith( - '[UpdateService] Checking for updates...', - ); - }); - - it('should handle onUpdateAvailable event', () => { - spyOn(console, 'log'); - - availableCallback(mockUpdateInfo); - - expect(service.isChecking()).toBe(false); - expect(service.updateAvailable()).toEqual(mockUpdateInfo); - expect(console.log).toHaveBeenCalledWith( - '[UpdateService] Update available:', - '2.0.0', - ); - }); - - it('should handle onUpdateNotAvailable event', () => { - spyOn(console, 'log'); - - notAvailableCallback({ version: '1.0.0' }); - - expect(service.isChecking()).toBe(false); - expect(service.updateAvailable()).toBeNull(); - expect(console.log).toHaveBeenCalledWith( - '[UpdateService] No updates available. Current version:', - '1.0.0', - ); - }); - - it('should handle onDownloadProgress event', () => { - spyOn(console, 'log'); - const progress: DownloadProgress = { - percent: 45.67, - bytesPerSecond: 1024000, - transferred: 5000000, - total: 10000000, - }; - - progressCallback(progress); - - expect(service.isDownloading()).toBe(true); - expect(service.downloadProgress()).toBe(46); - expect(console.log).toHaveBeenCalledWith( - '[UpdateService] Download progress: 45.67%', - ); - }); - - it('should handle onUpdateDownloaded event', () => { - spyOn(console, 'log'); - - downloadedCallback(mockUpdateInfo); - - expect(service.isDownloading()).toBe(false); - expect(service.updateDownloaded()).toBe(true); - expect(service.downloadProgress()).toBe(100); - expect(console.log).toHaveBeenCalledWith( - '[UpdateService] Update downloaded:', - '2.0.0', - ); - }); - - it('should handle onUpdateError event', () => { - spyOn(console, 'error'); - - errorCallback({ message: 'Download failed' }); - - expect(service.isChecking()).toBe(false); - expect(service.isDownloading()).toBe(false); - expect(service.errorMessage()).toBe('Download failed'); - expect(console.error).toHaveBeenCalledWith( - '[UpdateService] Update error:', - 'Download failed', - ); - }); - }); - - describe('checkForUpdates', () => { - it('should check for updates successfully', async () => { - await service.checkForUpdates(); - - expect(mockElectronAPI.checkForUpdates).toHaveBeenCalled(); - }); - - it('should handle check for updates error', async () => { - mockElectronAPI.checkForUpdates!.and.returnValue( - Promise.resolve({ success: false, error: 'Network error' }), - ); - - await expectAsync(service.checkForUpdates()).toBeRejectedWithError( - 'Failed to check for updates', - ); - expect(service.errorMessage()).toBe('Network error'); - }); - - it('should throw error when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.checkForUpdates()).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should handle missing checkForUpdates method', async () => { - delete mockElectronAPI.checkForUpdates; - - await expectAsync(service.checkForUpdates()).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('downloadUpdate', () => { - it('should download update successfully', async () => { - await service.downloadUpdate(); - - expect(mockElectronAPI.downloadUpdate).toHaveBeenCalled(); - }); - - it('should handle download update error', async () => { - mockElectronAPI.downloadUpdate!.and.returnValue( - Promise.resolve({ success: false, error: 'Download failed' }), - ); - - await expectAsync(service.downloadUpdate()).toBeRejectedWithError( - 'Failed to download update', - ); - expect(service.errorMessage()).toBe('Download failed'); - }); - - it('should throw error when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.downloadUpdate()).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should handle missing downloadUpdate method', async () => { - delete mockElectronAPI.downloadUpdate; - - await expectAsync(service.downloadUpdate()).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('installUpdate', () => { - it('should install update successfully', async () => { - await service.installUpdate(); - - expect(mockElectronAPI.installUpdate).toHaveBeenCalled(); - }); - - it('should handle install update error', async () => { - mockElectronAPI.installUpdate!.and.returnValue( - Promise.resolve({ success: false, error: 'Install failed' }), - ); - - await expectAsync(service.installUpdate()).toBeRejectedWithError( - 'Failed to install update', - ); - expect(service.errorMessage()).toBe('Install failed'); - }); - - it('should throw error when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.installUpdate()).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should handle missing installUpdate method', async () => { - delete mockElectronAPI.installUpdate; - - await expectAsync(service.installUpdate()).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('getSettings', () => { - it('should get settings successfully', async () => { - const settings = await service.getSettings(); - - expect(mockElectronAPI.getUpdateSettings).toHaveBeenCalled(); - expect(settings).toEqual(mockUpdateSettings); - expect(service.settings()).toEqual(mockUpdateSettings); - }); - - it('should handle get settings error', async () => { - mockElectronAPI.getUpdateSettings!.and.returnValue( - Promise.resolve({ success: false, error: 'Settings error' }), - ); - - await expectAsync(service.getSettings()).toBeRejectedWithError( - 'Failed to get update settings', - ); - }); - - it('should handle get settings without error message', async () => { - mockElectronAPI.getUpdateSettings!.and.returnValue( - Promise.resolve({ success: false }), - ); - - await expectAsync(service.getSettings()).toBeRejectedWithError( - 'Failed to get update settings', - ); - }); - - it('should throw error when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.getSettings()).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should handle missing getUpdateSettings method', async () => { - delete mockElectronAPI.getUpdateSettings; - - await expectAsync(service.getSettings()).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('setAutoCheck', () => { - it('should enable auto-check successfully', async () => { - await service.setAutoCheck(true); - - expect(mockElectronAPI.setUpdateSettings!).toHaveBeenCalledWith({ - autoCheckEnabled: true, - }); - expect(service.settings().autoCheckEnabled).toBe(true); - }); - - it('should disable auto-check successfully', async () => { - await service.setAutoCheck(false); - - expect(mockElectronAPI.setUpdateSettings!).toHaveBeenCalledWith({ - autoCheckEnabled: false, - }); - expect(service.settings().autoCheckEnabled).toBe(false); - }); - - it('should handle set auto-check error', async () => { - mockElectronAPI.setUpdateSettings!.and.returnValue( - Promise.resolve({ success: false, error: 'Update failed' }), - ); - - await expectAsync(service.setAutoCheck(true)).toBeRejectedWithError( - 'Failed to set auto-check setting', - ); - }); - - it('should handle set auto-check error without message', async () => { - mockElectronAPI.setUpdateSettings!.and.returnValue( - Promise.resolve({ success: false }), - ); - - await expectAsync(service.setAutoCheck(true)).toBeRejectedWithError( - 'Failed to set auto-check setting', - ); - }); - - it('should throw error when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.setAutoCheck(true)).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should handle missing setUpdateSettings method', async () => { - delete mockElectronAPI.setUpdateSettings; - - await expectAsync(service.setAutoCheck(true)).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('getAppVersion', () => { - it('should return the app version when available', async () => { - mockElectronAPI.getAppVersion = jasmine - .createSpy('getAppVersion') - .and.returnValue(Promise.resolve({ success: true, version: '1.2.3' })); - - const version = await service.getAppVersion(); - - expect(mockElectronAPI.getAppVersion).toHaveBeenCalled(); - expect(version).toBe('1.2.3'); - }); - - it('should reject when app version retrieval fails', async () => { - mockElectronAPI.getAppVersion = jasmine - .createSpy('getAppVersion') - .and.returnValue(Promise.resolve({ success: false, error: 'boom' })); - - await expectAsync(service.getAppVersion()).toBeRejectedWithError( - 'Failed to get app version', - ); - }); - - it('should reject when version is missing in result', async () => { - mockElectronAPI.getAppVersion = jasmine - .createSpy('getAppVersion') - .and.returnValue(Promise.resolve({ success: true })); - - await expectAsync(service.getAppVersion()).toBeRejectedWithError( - 'Failed to get app version', - ); - }); - - it('should throw when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.getAppVersion()).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should throw when getAppVersion is missing', async () => { - delete (mockElectronAPI as { getAppVersion?: unknown }).getAppVersion; - - await expectAsync(service.getAppVersion()).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('getStatus', () => { - it('should get update status successfully', async () => { - const expectedStatus: UpdateResult = { - success: true, - }; - - const status = await service.getStatus(); - - expect(mockElectronAPI.getUpdateStatus!).toHaveBeenCalled(); - expect(status).toEqual(expectedStatus); - }); - - it('should throw error when electronAPI is not available', async () => { - delete (window as { electronAPI?: unknown }).electronAPI; - - await expectAsync(service.getStatus()).toBeRejectedWithError( - 'Update API not available', - ); - }); - - it('should handle missing getUpdateStatus method', async () => { - delete mockElectronAPI.getUpdateStatus; - - await expectAsync(service.getStatus()).toBeRejectedWithError( - 'Update API not available', - ); - }); - }); - - describe('resetState', () => { - it('should reset all state signals', () => { - /* - * Set some non-default values - */ - service.updateAvailable.set(mockUpdateInfo); - service.downloadProgress.set(50); - service.isChecking.set(true); - service.isDownloading.set(true); - service.updateDownloaded.set(true); - service.errorMessage.set('Some error'); - - service.resetState(); - - expect(service.updateAvailable()).toBeNull(); - expect(service.downloadProgress()).toBe(0); - expect(service.isChecking()).toBe(false); - expect(service.isDownloading()).toBe(false); - expect(service.updateDownloaded()).toBe(false); - expect(service.errorMessage()).toBeNull(); - }); - - it('should be safe to call resetState multiple times', () => { - service.resetState(); - service.resetState(); - - expect(service.updateAvailable()).toBeNull(); - expect(service.downloadProgress()).toBe(0); - }); - }); - - describe('signal updates', () => { - it('should allow manual signal updates', () => { - service.updateAvailable.set(mockUpdateInfo); - service.downloadProgress.set(75); - service.isChecking.set(true); - - expect(service.updateAvailable()).toEqual(mockUpdateInfo); - expect(service.downloadProgress()).toBe(75); - expect(service.isChecking()).toBe(true); - }); - - it('should handle settings signal update', () => { - const newSettings: UpdateSettings = { autoCheckEnabled: false }; - service.settings.set(newSettings); - - expect(service.settings()).toEqual(newSettings); - }); - - it('should handle settings signal partial update', () => { - service.settings.update((s) => ({ ...s, autoCheckEnabled: false })); - - expect(service.settings().autoCheckEnabled).toBe(false); - }); - }); - - describe('edge cases', () => { - it('should handle multiple simultaneous checks', async () => { - const promise1 = service.checkForUpdates(); - const promise2 = service.checkForUpdates(); - - await Promise.all([promise1, promise2]); - - expect(mockElectronAPI.checkForUpdates!).toHaveBeenCalledTimes(2); - }); - - it('should handle rapid state changes', () => { - service.isChecking.set(true); - service.isChecking.set(false); - service.isChecking.set(true); - - expect(service.isChecking()).toBe(true); - }); - - it('should handle progress rounding edge cases', () => { - const progressCallback = - mockElectronAPI.onDownloadProgress!.calls.argsFor(0)[0]; - - progressCallback({ percent: 99.99 } as DownloadProgress); - expect(service.downloadProgress()).toBe(100); - - progressCallback({ percent: 0.01 } as DownloadProgress); - expect(service.downloadProgress()).toBe(0); - - progressCallback({ percent: 50.5 } as DownloadProgress); - expect(service.downloadProgress()).toBe(51); - }); - }); -}); diff --git a/src/app/services/update/update.service.ts b/src/app/services/update/update.service.ts deleted file mode 100644 index d517959..0000000 --- a/src/app/services/update/update.service.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { Injectable, signal } from '@angular/core'; -import { - UpdateInfo, - UpdateSettings, - DownloadProgress, - UpdateResult, -} from '../../../types/electron'; -import { BaseDatabaseService } from '../database/base-database.service'; - -/** - * Service for managing application updates. - * Provides functionality for checking, downloading, and installing updates from GitHub Releases. - */ -@Injectable({ - providedIn: 'root', -}) -export class UpdateService extends BaseDatabaseService { - /** - * Signal indicating if an update is available. - */ - readonly updateAvailable = signal(null); - - /** - * Signal tracking download progress percentage (0-100). - */ - readonly downloadProgress = signal(0); - - /** - * Signal indicating if the app is currently checking for updates. - */ - readonly isChecking = signal(false); - - /** - * Signal indicating if an update is currently being downloaded. - */ - readonly isDownloading = signal(false); - - /** - * Signal indicating if an update has been downloaded and is ready to install. - */ - readonly updateDownloaded = signal(false); - - /** - * Signal holding current update settings. - */ - readonly settings = signal({ autoCheckEnabled: true }); - - /** - * Signal holding any update error message. - */ - readonly errorMessage = signal(null); - - constructor() { - super(); - this.setupListeners(); - } - - /** - * Initializes the service by loading update settings. - */ - async init(): Promise { - await this.loadSettings(); - } - - /** - * Sets up event listeners for update events from main process. - */ - private setupListeners(): void { - // Check if electronAPI exists (not available in tests) - if (!globalThis.window?.electronAPI) { - return; - } - - const api = globalThis.window.electronAPI; - - if (api.onUpdateChecking) { - api.onUpdateChecking(() => { - this.isChecking.set(true); - this.errorMessage.set(null); - console.log('[UpdateService] Checking for updates...'); - }); - } - - if (api.onUpdateAvailable) { - api.onUpdateAvailable((info: UpdateInfo) => { - this.isChecking.set(false); - this.updateAvailable.set(info); - console.log('[UpdateService] Update available:', info.version); - }); - } - - if (api.onUpdateNotAvailable) { - api.onUpdateNotAvailable((info: { version: string }) => { - this.isChecking.set(false); - this.updateAvailable.set(null); - console.log( - '[UpdateService] No updates available. Current version:', - info.version, - ); - }); - } - - if (api.onDownloadProgress) { - api.onDownloadProgress((progress: DownloadProgress) => { - this.isDownloading.set(true); - this.downloadProgress.set(Math.round(progress.percent)); - console.log( - `[UpdateService] Download progress: ${progress.percent.toFixed(2)}%`, - ); - }); - } - - if (api.onUpdateDownloaded) { - api.onUpdateDownloaded((info: UpdateInfo) => { - this.isDownloading.set(false); - this.updateDownloaded.set(true); - this.downloadProgress.set(100); - console.log('[UpdateService] Update downloaded:', info.version); - }); - } - - if (api.onUpdateError) { - api.onUpdateError((error: { message: string }) => { - this.isChecking.set(false); - this.isDownloading.set(false); - this.errorMessage.set(error.message); - console.error('[UpdateService] Update error:', error.message); - }); - } - } - - /** - * Loads current update settings from main process. - */ - private async loadSettings(): Promise { - if (!globalThis.window?.electronAPI?.getUpdateSettings) { - return; - } - - try { - const result = await globalThis.window.electronAPI.getUpdateSettings(); - if (result.success && result.settings) { - this.settings.set(result.settings); - } - } catch (error) { - console.error('[UpdateService] Failed to load settings:', error); - } - } - - /** - * Manually checks for available updates. - */ - async checkForUpdates(): Promise { - if (!globalThis.window?.electronAPI?.checkForUpdates) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('check for updates', async () => { - const result: UpdateResult = - await globalThis.window.electronAPI.checkForUpdates(); - if (!result.success && result.error) { - this.errorMessage.set(result.error); - throw new Error(result.error); - } - }); - } - - /** - * Downloads the available update. - */ - async downloadUpdate(): Promise { - if (!globalThis.window?.electronAPI?.downloadUpdate) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('download update', async () => { - const result: UpdateResult = - await globalThis.window.electronAPI.downloadUpdate(); - if (!result.success && result.error) { - this.errorMessage.set(result.error); - throw new Error(result.error); - } - }); - } - - /** - * Installs the downloaded update and restarts the application. - */ - async installUpdate(): Promise { - if (!globalThis.window?.electronAPI?.installUpdate) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('install update', async () => { - const result: UpdateResult = - await globalThis.window.electronAPI.installUpdate(); - if (!result.success && result.error) { - this.errorMessage.set(result.error); - throw new Error(result.error); - } - }); - } - - /** - * Gets current update settings. - */ - async getSettings(): Promise { - if (!globalThis.window?.electronAPI?.getUpdateSettings) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('get update settings', async () => { - const result: UpdateResult = - await globalThis.window.electronAPI.getUpdateSettings(); - if (result.success && result.settings) { - this.settings.set(result.settings); - return result.settings; - } - throw new Error(result.error ?? 'Failed to get settings'); - }); - } - - /** - * Updates the auto-check setting. - */ - async setAutoCheck(enabled: boolean): Promise { - if (!globalThis.window?.electronAPI?.setUpdateSettings) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('set auto-check setting', async () => { - const result: UpdateResult = - await globalThis.window.electronAPI.setUpdateSettings({ - autoCheckEnabled: enabled, - }); - if (result.success) { - this.settings.update((s) => ({ ...s, autoCheckEnabled: enabled })); - } else { - throw new Error(result.error ?? 'Failed to update settings'); - } - }); - } - - /** - * Gets the current application version. - */ - async getAppVersion(): Promise { - if (!globalThis.window?.electronAPI?.getAppVersion) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('get app version', async () => { - const result: UpdateResult = - await globalThis.window.electronAPI.getAppVersion(); - if (result.success && result.version) { - return result.version; - } - throw new Error(result.error ?? 'Failed to get app version'); - }); - } - - /** - * Gets current update status. - */ - async getStatus(): Promise { - if (!globalThis.window?.electronAPI?.getUpdateStatus) { - throw new Error('Update API not available'); - } - - return this.executeWithErrorHandling('get update status', async () => { - return globalThis.window.electronAPI.getUpdateStatus(); - }); - } - - /** - * Resets all update state signals. - */ - resetState(): void { - this.updateAvailable.set(null); - this.downloadProgress.set(0); - this.isChecking.set(false); - this.isDownloading.set(false); - this.updateDownloaded.set(false); - this.errorMessage.set(null); - } -} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index afdefc2..e789003 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -366,27 +366,5 @@ "cannotDeleteDefault": "Cannot delete default statuses", "cannotDeleteInUse": "Cannot delete: status is in use" } - }, - "update": { - "availableTitle": "Update Available", - "downloadingTitle": "Downloading Update", - "readyToInstallTitle": "Ready to Install", - "newVersion": "New version", - "releaseDate": "Release date", - "releaseNotes": "Release notes", - "downloading": "Downloading update...", - "readyToInstall": "Update ready to install", - "later": "Later", - "download": "Download", - "installRestart": "Install & Restart", - "downloadingInProgress": "Downloading...", - "checkForUpdates": "Check for Updates", - "autoCheckEnabled": "Check for updates on startup", - "currentVersion": "Current version", - "upToDate": "You're up to date", - "checking": "Checking for updates...", - "noUpdatesAvailable": "No updates available", - "errorChecking": "Error checking for updates", - "viewReleases": "View Releases on GitHub" } } diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 1208d28..5bf049b 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -366,27 +366,5 @@ "cannotDeleteDefault": "No se pueden eliminar los estados predeterminados", "cannotDeleteInUse": "No se puede eliminar: el estado está en uso" } - }, - "update": { - "availableTitle": "Actualización Disponible", - "downloadingTitle": "Descargando Actualización", - "readyToInstallTitle": "Lista para Instalar", - "newVersion": "Nueva versión", - "releaseDate": "Fecha de lanzamiento", - "releaseNotes": "Notas de la versión", - "downloading": "Descargando actualización...", - "readyToInstall": "Actualización lista para instalar", - "later": "Más tarde", - "download": "Descargar", - "installRestart": "Instalar y Reiniciar", - "downloadingInProgress": "Descargando...", - "checkForUpdates": "Buscar Actualizaciones", - "autoCheckEnabled": "Buscar actualizaciones al iniciar", - "currentVersion": "Versión actual", - "upToDate": "Estás actualizado", - "checking": "Buscando actualizaciones...", - "noUpdatesAvailable": "No hay actualizaciones disponibles", - "errorChecking": "Error al buscar actualizaciones", - "viewReleases": "Ver Lanzamientos en GitHub" } } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index d64642e..add2972 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -153,68 +153,6 @@ export interface BackupResult { error?: string; } -// ==================== UPDATE MODELS ==================== - -export interface UpdateInfo { - version: string; - releaseDate: string; - releaseName?: string; - releaseNotes?: string; - size?: number; -} - -export interface DownloadProgress { - bytesPerSecond: number; - percent: number; - transferred: number; - total: number; -} - -export interface UpdateSettings { - autoCheckEnabled: boolean; - lastCheckDate?: Date; -} - -export interface UpdateResult { - success: boolean; - error?: string; - settings?: UpdateSettings; - status?: string; - updateInfo?: UpdateInfo | null; - version?: string; -} - -// ==================== UPDATE MODELS ==================== - -export interface UpdateInfo { - version: string; - releaseDate: string; - releaseName?: string; - releaseNotes?: string; - size?: number; -} - -export interface DownloadProgress { - bytesPerSecond: number; - percent: number; - transferred: number; - total: number; -} - -export interface UpdateSettings { - autoCheckEnabled: boolean; - lastCheckDate?: Date; -} - -export interface UpdateResult { - success: boolean; - error?: string; - settings?: UpdateSettings; - status?: string; - updateInfo?: UpdateInfo | null; - version?: string; -} - // ==================== ELECTRON API ==================== export interface DeleteResult { @@ -407,27 +345,6 @@ declare global { // System openExternal: (url: string) => Promise; - - // Updates - checkForUpdates: () => Promise; - downloadUpdate: () => Promise; - installUpdate: () => Promise; - getAppVersion: () => Promise; - getUpdateSettings: () => Promise; - setUpdateSettings: ( - settings: Partial, - ) => Promise; - getUpdateStatus: () => Promise; - onUpdateChecking: (callback: () => void) => void; - onUpdateAvailable: (callback: (info: UpdateInfo) => void) => void; - onUpdateNotAvailable: ( - callback: (info: { version: string }) => void, - ) => void; - onDownloadProgress: ( - callback: (progress: DownloadProgress) => void, - ) => void; - onUpdateDownloaded: (callback: (info: UpdateInfo) => void) => void; - onUpdateError: (callback: (error: { message: string }) => void) => void; }; } }