diff --git a/package.json b/package.json index 520d8927..2eb29101 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260209.3", + "version": "0.20260209.4", "scripts": { "archive:web": "bun src/renderer/scripts/archive.web.js", "build": "electron-vite build", diff --git a/src/main/MainMenu.ts b/src/main/MainMenu.ts index 1708d1a9..22d0db0d 100644 --- a/src/main/MainMenu.ts +++ b/src/main/MainMenu.ts @@ -11,7 +11,7 @@ let disabledMenu: electron.Menu | null = null; let recentFilePaths: string[] = []; let hasFiles = false; -export function enableDisableMainMenu(enable: boolean): void { +export const enableDisableMainMenu = (enable: boolean): void => { // Build our menu, if needed. if (enable && enabledMenu) { @@ -290,9 +290,9 @@ export function enableDisableMainMenu(enable: boolean): void { electron.Menu.setApplicationMenu(enable ? enabledMenu : disabledMenu); } -} +}; -export function enableDisableFileCloseAndCloseAllMenuItems(enable: boolean): void { +export const enableDisableFileCloseAndCloseAllMenuItems = (enable: boolean): void => { if (enabledMenu) { hasFiles = enable; @@ -304,11 +304,11 @@ export function enableDisableFileCloseAndCloseAllMenuItems(enable: boolean): voi fileCloseAllMenu.enabled = hasFiles; } } -} +}; -export function updateReopenMenu(filePaths: string[]): void { +export const updateReopenMenu = (filePaths: string[]): void => { enabledMenu = null; recentFilePaths = filePaths; enableDisableMainMenu(true); -} +}; diff --git a/src/main/MainWindow.ts b/src/main/MainWindow.ts index 0a2559d5..eb8cc189 100644 --- a/src/main/MainWindow.ts +++ b/src/main/MainWindow.ts @@ -18,7 +18,7 @@ import type { SplashScreenWindow } from './SplashScreenWindow.ts'; autoUpdater.autoDownload = false; autoUpdater.logger = null; -export function checkForUpdates(atStartup: boolean): void { +export const checkForUpdates = (atStartup: boolean): void => { // Check for updates, if requested and if OpenCOR is packaged. if (isPackaged() && electronConf.get('settings.general.checkForUpdatesAtStartup')) { @@ -37,13 +37,13 @@ export function checkForUpdates(atStartup: boolean): void { MainWindow.instance?.webContents.send('update-check-error', formatError(error)); }); } -} +}; autoUpdater.on('download-progress', (info: ProgressInfo) => { MainWindow.instance?.webContents.send('update-download-progress', info.percent); }); -export function downloadAndInstallUpdate(): void { +export const downloadAndInstallUpdate = (): void => { autoUpdater .downloadUpdate() .then(() => { @@ -52,23 +52,23 @@ export function downloadAndInstallUpdate(): void { .catch((error: unknown) => { MainWindow.instance?.webContents.send('update-download-error', formatError(error)); }); -} +}; -export function installUpdateAndRestart(): void { +export const installUpdateAndRestart = (): void => { autoUpdater.quitAndInstall(true, true); -} +}; -export function loadSettings(): ISettings { +export const loadSettings = (): ISettings => { return electronConf.get('settings'); -} +}; -export function saveSettings(settings: ISettings): void { +export const saveSettings = (settings: ISettings): void => { electronConf.set('settings', settings); -} +}; let _resetAll = false; -export function resetAll(): void { +export const resetAll = (): void => { _resetAll = true; /* TODO: enable once our GitHub integration is fully ready. @@ -77,17 +77,17 @@ export function resetAll(): void { electron.app.relaunch(); electron.app.quit(); -} +}; let recentFilePaths: string[] = []; -export function clearRecentFiles(): void { +export const clearRecentFiles = (): void => { recentFilePaths = []; updateReopenMenu(recentFilePaths); -} +}; -export function fileClosed(filePath: string): void { +export const fileClosed = (filePath: string): void => { // Make sure that the file is not a COMBINE archive that was opened using a data URL. if (isDataUrlOmexFileName(filePath)) { @@ -98,15 +98,15 @@ export function fileClosed(filePath: string): void { recentFilePaths = recentFilePaths.slice(0, 10); updateReopenMenu(recentFilePaths); -} +}; -export function fileIssue(filePath: string): void { +export const fileIssue = (filePath: string): void => { recentFilePaths = recentFilePaths.filter((recentFilePath) => recentFilePath !== filePath); updateReopenMenu(recentFilePaths); -} +}; -export function fileOpened(filePath: string): void { +export const fileOpened = (filePath: string): void => { recentFilePaths = recentFilePaths.filter((recentFilePath) => recentFilePath !== filePath); updateReopenMenu(recentFilePaths); @@ -116,23 +116,23 @@ export function fileOpened(filePath: string): void { // are no more files to reopen. MainWindow.instance?.reopenFilePathsAndSelectFilePath(); -} +}; let openedFilePaths: string[] = []; -export function filesOpened(filePaths: string[]): void { +export const filesOpened = (filePaths: string[]): void => { openedFilePaths = filePaths; if (!filePaths.length) { selectedFilePath = null; } -} +}; let selectedFilePath: string | null = null; -export function fileSelected(filePath: string): void { +export const fileSelected = (filePath: string): void => { selectedFilePath = filePath; -} +}; export class MainWindow extends ApplicationWindow { // Properties. @@ -299,7 +299,7 @@ export class MainWindow extends ApplicationWindow { // so that the OAuth flow can proceed. this.webContents.setWindowOpenHandler((details) => { - function isFirebaseOauthPopup(url: string): boolean { + const isFirebaseOauthPopup = (url: string): boolean => { try { const parsedUrl = new URL(url); @@ -311,7 +311,7 @@ export class MainWindow extends ApplicationWindow { } catch { return false; } - } + }; if (isFirebaseOauthPopup(details.url)) { return { diff --git a/src/renderer/package.json b/src/renderer/package.json index 407fe9b2..37c85959 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -39,7 +39,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260209.3", + "version": "0.20260209.4", "scripts": { "build": "vite build && bun scripts/generate.version.js", "build:lib": "vite build --config vite.lib.config.ts && cp index.d.ts dist/index.d.ts", diff --git a/src/renderer/scripts/version.js b/src/renderer/scripts/version.js index bba1179a..46eb13a8 100644 --- a/src/renderer/scripts/version.js +++ b/src/renderer/scripts/version.js @@ -35,13 +35,13 @@ const version = `${majorVersion}.${minorVersion}.${patchVersion}`; // Update our package.json files. -function updatePackageJsonFile(filePath) { +const updatePackageJsonFile = (filePath) => { const contents = JSON.parse(fs.readFileSync(filePath)); contents.version = version; fs.writeFileSync(filePath, `${JSON.stringify(contents, null, 2)}\n`); -} +}; // Perform the updates. diff --git a/src/renderer/src/common/common.ts b/src/renderer/src/common/common.ts index 3cd67c59..c707610d 100644 --- a/src/renderer/src/common/common.ts +++ b/src/renderer/src/common/common.ts @@ -17,45 +17,45 @@ export interface ISettings { const uaParser = new UAParser(); -export function isWindows(): boolean { +export const isWindows = (): boolean => { return uaParser.getOS().name === 'Windows'; -} +}; -export function isLinux(): boolean { +export const isLinux = (): boolean => { return uaParser.getOS().name === 'Linux'; -} +}; -export function isMacOs(): boolean { +export const isMacOs = (): boolean => { return uaParser.getOS().name === 'macOS'; -} +}; -export function isDesktop(): boolean { +export const isDesktop = (): boolean => { return uaParser.getOS().name === 'Windows' || uaParser.getOS().name === 'Linux' || uaParser.getOS().name === 'macOS'; -} +}; // A method to determine whether the Ctrl or Cmd key is pressed, depending on the operating system. -export function isCtrlOrCmd(event: KeyboardEvent): boolean { +export const isCtrlOrCmd = (event: KeyboardEvent): boolean => { return isMacOs() ? event.metaKey : event.ctrlKey; -} +}; // A method to enable/disable the main menu. -export function enableDisableMainMenu(enable: boolean): void { +export const enableDisableMainMenu = (enable: boolean): void => { electronApi?.enableDisableMainMenu(enable); -} +}; // A method to determine whether a given file name indicates a data URL OMEX file. export const OMEX_PREFIX = 'OMEX #'; -export function isDataUrlOmexFileName(fileName: string): boolean { +export const isDataUrlOmexFileName = (fileName: string): boolean => { return fileName.startsWith(OMEX_PREFIX); -} +}; // A method to determine whether a given URL is an HTTP or HTTPS URL. -export function isHttpUrl(url: string): boolean { +export const isHttpUrl = (url: string): boolean => { try { const { protocol } = new URL(url); @@ -63,23 +63,23 @@ export function isHttpUrl(url: string): boolean { } catch { return false; } -} +}; // A method to get the CORS proxy URL for a given URL. -export function corsProxyUrl(url: string): string { +export const corsProxyUrl = (url: string): string => { return `https://cors-proxy.opencor.workers.dev/?url=${url}`; -} +}; // A method to return the SHA-256 hash of some data. -export function sha256(data: string | Uint8Array): string { +export const sha256 = (data: string | Uint8Array): string => { return SHA256(data).toString(); -} +}; // A method to format a given number of milliseconds into a string. -export function formatTime(time: number): string { +export const formatTime = (time: number): string => { const ms = Math.floor(time % 1000); const s = Math.floor((time / 1000) % 60); const m = Math.floor((time / (1000 * 60)) % 60); @@ -108,21 +108,21 @@ export function formatTime(time: number): string { } return res; -} +}; // A method to format an error into a string. -export function formatError(error: unknown): string { +export const formatError = (error: unknown): string => { if (error instanceof Error) { return error.message; } return String(error); -} +}; // A method to format a message, i.e. make sure that it starts with a capital letter and ends with a period, or not. -export function formatMessage(message: string, selfContained: boolean = true): string { +export const formatMessage = (message: string, selfContained: boolean = true): string => { message = selfContained ? message.charAt(0).toUpperCase() + message.slice(1) : message.charAt(0).toLowerCase() + message.slice(1); @@ -136,17 +136,17 @@ export function formatMessage(message: string, selfContained: boolean = true): s : selfContained ? `${message}.` : message; -} +}; // A method to determine whether a number is divisible by another one. -export function isDivisible(a: number, b: number): boolean { +export const isDivisible = (a: number, b: number): boolean => { return Number.isInteger(a / b); -} +}; // A method to trigger a browser download for a given file. -export function downloadFile(filename: string, content: string | Blob, type: string): void { +export const downloadFile = (filename: string, content: string | Blob, type: string): void => { const link = document.createElement('a'); const blob = content instanceof Blob ? content : new Blob([content], { type }); const url = URL.createObjectURL(blob); @@ -161,11 +161,11 @@ export function downloadFile(filename: string, content: string | Blob, type: str document.body.removeChild(link); URL.revokeObjectURL(url); -} +}; // A method to get the file name from a given file path. -export function fileName(filePath: string): string { +export const fileName = (filePath: string): string => { const res = filePath.split(/(\\|\/)/g).pop() || ''; try { @@ -175,10 +175,10 @@ export function fileName(filePath: string): string { return res; } -} +}; // A method to sleep for a given number of milliseconds. -export function sleep(ms: number): Promise { +export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); -} +}; diff --git a/src/renderer/src/common/electron.ts b/src/renderer/src/common/electron.ts index 149a2193..02da2937 100644 --- a/src/renderer/src/common/electron.ts +++ b/src/renderer/src/common/electron.ts @@ -1,17 +1,17 @@ import electron from 'electron'; -export function isPackaged(): boolean { +export const isPackaged = (): boolean => { return electron.app.isPackaged; -} +}; -export function isWindows(): boolean { +export const isWindows = (): boolean => { return process.platform === 'win32'; -} +}; -export function isLinux(): boolean { +export const isLinux = (): boolean => { return process.platform === 'linux'; -} +}; -export function isMacOs(): boolean { +export const isMacOs = (): boolean => { return process.platform === 'darwin'; -} +}; diff --git a/src/renderer/src/common/firebaseConfig.ts b/src/renderer/src/common/firebaseConfig.ts index 7aef4c98..e6028960 100644 --- a/src/renderer/src/common/firebaseConfig.ts +++ b/src/renderer/src/common/firebaseConfig.ts @@ -16,15 +16,15 @@ const firebaseEnvVarMap = { measurementId: 'VITE_FIREBASE_MEASUREMENT_ID' } as const; -export function missingFirebaseKeys(): string[] { - function missingFirebaseKey(firebaseValue: unknown): boolean { +export const missingFirebaseKeys = (): string[] => { + const missingFirebaseKey = (firebaseValue: unknown): boolean => { return !firebaseValue; - } + }; return (Object.entries(firebaseEnvVarMap) as Array<[keyof typeof firebaseEnvVarMap, string]>) .filter(([prop]) => missingFirebaseKey((firebaseConfig as Record)[prop as string])) .map(([, envName]) => envName); -} +}; interface IFirebaseConfig { apiKey: string; diff --git a/src/renderer/src/common/gitHubIntegration.ts b/src/renderer/src/common/gitHubIntegration.ts index 835d9286..be72daf6 100644 --- a/src/renderer/src/common/gitHubIntegration.ts +++ b/src/renderer/src/common/gitHubIntegration.ts @@ -2,7 +2,7 @@ import { AsyncEntry } from '@napi-rs/keyring'; import electron from 'electron'; -export async function clearGitHubCache(): Promise { +export const clearGitHubCache = async (): Promise => { await Promise.all( ['https://github.com', 'https://api.github.com', 'https://opencorapp.firebaseapp.com'].map(async (origin) => { try { @@ -12,11 +12,11 @@ export async function clearGitHubCache(): Promise { } }) ); -} +}; let storeAvailable = true; -function gitHubAccessTokenError(operation: string, error: unknown): void { +const gitHubAccessTokenError = (operation: string, error: unknown): void => { if (storeAvailable) { console.warn( `Failed to ${operation} the GitHub access token using the system credential store. Subsequent attempts will be skipped.`, @@ -25,9 +25,9 @@ function gitHubAccessTokenError(operation: string, error: unknown): void { } storeAvailable = false; -} +}; -function gitHubAccessTokenEntry(): AsyncEntry | null { +const gitHubAccessTokenEntry = (): AsyncEntry | null => { if (!storeAvailable) { return null; } @@ -39,9 +39,9 @@ function gitHubAccessTokenEntry(): AsyncEntry | null { return null; } -} +}; -export async function deleteGitHubAccessToken(): Promise { +export const deleteGitHubAccessToken = async (): Promise => { const entry = gitHubAccessTokenEntry(); if (!entry) { @@ -55,9 +55,9 @@ export async function deleteGitHubAccessToken(): Promise { return false; } -} +}; -export async function loadGitHubAccessToken(): Promise { +export const loadGitHubAccessToken = async (): Promise => { const entry = gitHubAccessTokenEntry(); if (!entry) { @@ -73,9 +73,9 @@ export async function loadGitHubAccessToken(): Promise { return null; } -} +}; -export async function saveGitHubAccessToken(token: string): Promise { +export const saveGitHubAccessToken = async (token: string): Promise => { if (!token.trim()) { console.warn('Ignoring request to store an empty GitHub access token.'); @@ -97,4 +97,4 @@ export async function saveGitHubAccessToken(token: string): Promise { return false; } -} +}; diff --git a/src/renderer/src/common/locCommon.ts b/src/renderer/src/common/locCommon.ts index bb8674ff..b774db71 100644 --- a/src/renderer/src/common/locCommon.ts +++ b/src/renderer/src/common/locCommon.ts @@ -14,7 +14,7 @@ export interface IDataUriInfo { error?: string; } -function zipDataFromDataUrl(dataUrl: string | Uint8Array | File, mimeType: string): IDataUriInfo { +const zipDataFromDataUrl = (dataUrl: string | Uint8Array | File, mimeType: string): IDataUriInfo => { // Make sure that we have a string data URL. if (dataUrl instanceof Uint8Array || dataUrl instanceof File) { @@ -64,9 +64,9 @@ function zipDataFromDataUrl(dataUrl: string | Uint8Array | File, mimeType: strin res: true, data }; -} +}; -export async function zipCellmlDataUrl(dataUrl: string | Uint8Array | File): Promise { +export const zipCellmlDataUrl = async (dataUrl: string | Uint8Array | File): Promise => { // Try to retrieve a CellML file from the given data URL. const mimeType = 'application/x.vnd.zip-cellml+zip'; @@ -126,9 +126,9 @@ export async function zipCellmlDataUrl(dataUrl: string | Uint8Array | File): Pro return { res: false }; -} +}; -export function combineArchiveDataUrl(dataUrl: string | Uint8Array | File): IDataUriInfo { +export const combineArchiveDataUrl = (dataUrl: string | Uint8Array | File): IDataUriInfo => { // Try to retrieve a COMBINE archive from the given data URL. const mimeType = 'application/zip'; @@ -150,17 +150,17 @@ export function combineArchiveDataUrl(dataUrl: string | Uint8Array | File): IDat return { res: false }; -} +}; -export function isRemoteFilePath(filePath: string): boolean { +export const isRemoteFilePath = (filePath: string): boolean => { return filePath.startsWith('http://') || filePath.startsWith('https://'); -} +}; -export function filePath( +export const filePath = ( fileFilePathOrFileContents: string | Uint8Array | File, dataUrlFileName: string, dataUrlCounter: number -): string { +): string => { return dataUrlFileName ? dataUrlFileName : dataUrlCounter @@ -172,13 +172,13 @@ export function filePath( : typeof fileFilePathOrFileContents === 'string' ? fileFilePathOrFileContents : sha256(fileFilePathOrFileContents); -} +}; -export function file( +export const file = ( fileFilePathOrFileContents: string | Uint8Array | File, dataUrlFileName: string, dataUrlCounter: number -): Promise { +): Promise => { if (typeof fileFilePathOrFileContents === 'string') { if (isRemoteFilePath(fileFilePathOrFileContents)) { return new Promise((resolve, reject) => { @@ -257,7 +257,7 @@ export function file( reject(new Error(formatError(error))); }); }); -} +}; // A method to retrieve the simulation data information for a given name from an instance task. @@ -276,7 +276,7 @@ export interface ISimulationDataInfo { index: number; } -export function simulationDataInfo(instanceTask: locApi.SedInstanceTask, name: string): ISimulationDataInfo { +export const simulationDataInfo = (instanceTask: locApi.SedInstanceTask, name: string): ISimulationDataInfo => { if (!name) { return { type: ESimulationDataInfoType.UNKNOWN, @@ -340,11 +340,11 @@ export function simulationDataInfo(instanceTask: locApi.SedInstanceTask, name: s type: ESimulationDataInfoType.UNKNOWN, index: -1 }; -} +}; // A method to retrieve the simulation data for a given name from an instance task. -export function simulationData(instanceTask: locApi.SedInstanceTask, info: ISimulationDataInfo): Float64Array { +export const simulationData = (instanceTask: locApi.SedInstanceTask, info: ISimulationDataInfo): Float64Array => { switch (info.type) { case ESimulationDataInfoType.VOI: return instanceTask.voi(); @@ -361,4 +361,4 @@ export function simulationData(instanceTask: locApi.SedInstanceTask, info: ISimu default: return []; } -} +}; diff --git a/src/renderer/src/common/rendererServer.ts b/src/renderer/src/common/rendererServer.ts index 6098e41d..a1125ce6 100644 --- a/src/renderer/src/common/rendererServer.ts +++ b/src/renderer/src/common/rendererServer.ts @@ -11,7 +11,7 @@ import path from 'node:path'; let rendererServer: http.Server | null = null; let rendererBaseUrl: string | null = null; -export async function startRendererServer(): Promise { +export const startRendererServer = async (): Promise => { // If we already have a base URL then return it. if (rendererBaseUrl) { @@ -89,9 +89,9 @@ export async function startRendererServer(): Promise { } return rendererBaseUrl; -} +}; -export async function stopRendererServer(): Promise { +export const stopRendererServer = async (): Promise => { // Make sure that we have a server to stop. if (!rendererServer) { @@ -116,4 +116,4 @@ export async function stopRendererServer(): Promise { rendererServer = null; rendererBaseUrl = null; -} +}; diff --git a/src/renderer/src/common/vueCommon.ts b/src/renderer/src/common/vueCommon.ts index 1d356770..e2ad109c 100644 --- a/src/renderer/src/common/vueCommon.ts +++ b/src/renderer/src/common/vueCommon.ts @@ -16,14 +16,14 @@ export const useTheme = vueusecore.createGlobalState(() => { const isDarkMode = vue.ref(!prefersColorScheme.matches); const _theme = vue.ref('system'); - function updateLightAndDarkModes(prefersColorScheme: MediaQueryList | MediaQueryListEvent) { + const updateLightAndDarkModes = (prefersColorScheme: MediaQueryList | MediaQueryListEvent) => { isLightMode.value = prefersColorScheme.matches; isDarkMode.value = !prefersColorScheme.matches; - } + }; - function updateDocumentClasses() { + const updateDocumentClasses = () => { document.documentElement.classList.toggle('opencor-dark-mode', isDarkMode.value); - } + }; prefersColorScheme.addEventListener('change', (event) => { if (_theme.value === 'system') { @@ -32,11 +32,11 @@ export const useTheme = vueusecore.createGlobalState(() => { } }); - function theme(): Theme { + const theme = (): Theme => { return _theme.value; - } + }; - function setTheme(newTheme: Theme | undefined) { + const setTheme = (newTheme: Theme | undefined) => { _theme.value = newTheme ?? 'system'; if (_theme.value === 'light') { @@ -50,15 +50,15 @@ export const useTheme = vueusecore.createGlobalState(() => { } updateDocumentClasses(); - } + }; - function useLightMode(): boolean { + const useLightMode = (): boolean => { return isLightMode.value; - } + }; - function useDarkMode(): boolean { + const useDarkMode = (): boolean => { return isDarkMode.value; - } + }; return { theme, diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index 9504064a..6c67e0e3 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -80,14 +80,6 @@ const props = defineProps<{ uiEnabled: boolean; }>(); defineEmits<(event: 'error', message: string) => void>(); -defineExpose({ - openFile, - closeCurrentFile, - closeAllFiles, - hasFile, - hasFiles, - selectFile -}); export interface IContentsComponent { openFile(file: locApi.File): void; @@ -112,18 +104,37 @@ const filePaths = vue.computed(() => { return res; }); -vue.watch(filePaths, (newFilePaths: string[]) => { - electronApi?.filesOpened(newFilePaths); -}); +// Some methods to handle files and file tabs. -vue.watch(activeFile, (newActiveFile: string) => { - // Note: activeFile can get updated by clicking on a tab or by calling selectFile(), hence we need to watch it to let - // people know that a file has been selected. +const hasFile = (filePath: string): boolean => { + return fileTabs.value.find((fileTab) => fileTab.file.path() === filePath) !== undefined; +}; - electronApi?.fileSelected(newActiveFile); -}); +const hasFiles = (): boolean => { + return fileTabs.value.length > 0; +}; + +const selectFile = (filePath: string): void => { + activeFile.value = filePath; +}; + +const selectNextFile = (): void => { + const fileTabIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === activeFile.value); + + if (fileTabIndex !== -1) { + selectFile(fileTabs.value[(fileTabIndex + 1) % fileTabs.value.length].file.path()); + } +}; + +const selectPreviousFile = (): void => { + const fileTabIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === activeFile.value); + + if (fileTabIndex !== -1) { + selectFile(fileTabs.value[(fileTabIndex - 1 + fileTabs.value.length) % fileTabs.value.length].file.path()); + } +}; -function openFile(file: locApi.File): void { +const openFile = (file: locApi.File): void => { const filePath = file.path(); const prevActiveFile = activeFile.value; @@ -135,41 +146,9 @@ function openFile(file: locApi.File): void { }); electronApi?.fileOpened(filePath); -} - -function hasFile(filePath: string): boolean { - return fileTabs.value.find((fileTab) => fileTab.file.path() === filePath) !== undefined; -} - -function hasFiles(): boolean { - return fileTabs.value.length > 0; -} - -function selectFile(filePath: string): void { - activeFile.value = filePath; -} - -function selectNextFile(): void { - const crtFileTabIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === activeFile.value); - const nextFileTabIndex = (crtFileTabIndex + 1) % fileTabs.value.length; - const nextFileTab = fileTabs.value[nextFileTabIndex]; +}; - if (nextFileTab) { - selectFile(nextFileTab.file.path()); - } -} - -function selectPreviousFile(): void { - const crtFileTabIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === activeFile.value); - const nextFileTabIndex = (crtFileTabIndex - 1 + fileTabs.value.length) % fileTabs.value.length; - const nextFileTab = fileTabs.value[nextFileTabIndex]; - - if (nextFileTab) { - selectFile(nextFileTab.file.path()); - } -} - -function closeFile(filePath: string): void { +const closeFile = (filePath: string): void => { locApi.fileManager.unmanage(filePath); const fileTabIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === filePath); @@ -185,17 +164,39 @@ function closeFile(filePath: string): void { } electronApi?.fileClosed(filePath); -} +}; -function closeCurrentFile(): void { +const closeCurrentFile = (): void => { closeFile(activeFile.value); -} +}; -function closeAllFiles(): void { +const closeAllFiles = (): void => { while (fileTabs.value.length) { closeCurrentFile(); } -} +}; + +defineExpose({ + openFile, + closeCurrentFile, + closeAllFiles, + hasFile, + hasFiles, + selectFile +}); + +// Some watchers to let people know about changes to the opened files and the selected file. + +vue.watch(filePaths, (newFilePaths: string[]) => { + electronApi?.filesOpened(newFilePaths); +}); + +vue.watch(activeFile, (newActiveFile: string) => { + // Note: activeFile can get updated by clicking on a tab or by calling selectFile(), hence we need to watch it to let + // people know that a file has been selected. + + electronApi?.fileSelected(newActiveFile); +}); // Various things that need to be done once we are mounted. diff --git a/src/renderer/src/components/MainMenu.vue b/src/renderer/src/components/MainMenu.vue index 89c03218..87989c42 100644 --- a/src/renderer/src/components/MainMenu.vue +++ b/src/renderer/src/components/MainMenu.vue @@ -191,7 +191,7 @@ vue.onMounted(() => { // Close the menu when clicking clicking on the menubar but outside of the main menu items. - function onClick(event: MouseEvent) { + const onClick = (event: MouseEvent) => { const target = event.target as Node; if ( @@ -201,7 +201,7 @@ vue.onMounted(() => { ) { menuBar.value?.hide(); } - } + }; document.addEventListener('click', onClick); diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 54cdc5ac..3567c5d8 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -150,9 +150,9 @@ const octokit = vue.ref(null); // Keep track of which instance of OpenCOR is currently active. -function activateInstance(): void { +const activateInstance = (): void => { activeInstanceUid.value = String(crtInstance?.uid); -} +}; const compIsActive = vue.computed(() => { return activeInstanceUid.value === String(crtInstance?.uid); @@ -312,10 +312,10 @@ electronApi?.onAction((action: string) => { handleAction(action); }); -function handleAction(action: string): void { - function isAction(actionName: string, expectedActionName: string): boolean { +const handleAction = (action: string): void => { + const isAction = (actionName: string, expectedActionName: string): boolean => { return actionName.localeCompare(expectedActionName, undefined, { sensitivity: 'base' }) === 0; - } + }; const index = action.indexOf('/'); const actionName = index !== -1 ? action.substring(0, index) : action; @@ -345,7 +345,7 @@ function handleAction(action: string): void { }); } } -} +}; // Enable/disable the UI from Electron. @@ -375,10 +375,10 @@ const updateErrorVisible = vue.ref(false); const updateErrorTitle = vue.ref(''); const updateErrorIssue = vue.ref(''); -function onUpdateErrorDialogClose(): void { +const onUpdateErrorDialogClose = (): void => { updateErrorVisible.value = false; updateDownloadProgressVisible.value = false; -} +}; const updateAvailableVisible = vue.ref(false); const updateDownloadProgressVisible = vue.ref(false); @@ -390,13 +390,13 @@ electronApi?.onUpdateAvailable((version: string) => { updateVersion.value = version; }); -function onDownloadAndInstall(): void { +const onDownloadAndInstall = (): void => { updateDownloadPercent.value = 0; // Just to be on the safe side. updateDownloadProgressVisible.value = true; updateAvailableVisible.value = false; electronApi?.downloadAndInstallUpdate(); -} +}; electronApi?.onUpdateDownloadError((issue: string) => { updateErrorTitle.value = 'Downloading Update...'; @@ -428,7 +428,7 @@ electronApi?.onUpdateCheckError((issue: string) => { // Handle errors. -function onError(message: string): void { +const onError = (message: string): void => { toast.add({ severity: 'error', group: toastId.value, @@ -436,7 +436,7 @@ function onError(message: string): void { detail: message, life: TOAST_LIFE }); -} +}; // About dialog. @@ -446,13 +446,13 @@ electronApi?.onAbout(() => { onAboutMenu(); }); -function onAboutMenu(): void { +const onAboutMenu = (): void => { if (props.omex) { return; } aboutVisible.value = true; -} +}; // Settings dialog. @@ -462,19 +462,19 @@ electronApi?.onSettings(() => { onSettingsMenu(); }); -function onSettingsMenu(): void { +const onSettingsMenu = (): void => { if (props.omex) { return; } settingsVisible.value = true; -} +}; // Open a file. let globalOmexDataUrlCounter = 0; -function openFile(fileFilePathOrFileContents: string | Uint8Array | File): void { +const openFile = (fileFilePathOrFileContents: string | Uint8Array | File): void => { // Check whether we were passed a ZIP-CellML data URL. let cellmlDataUrlFileName: string = ''; @@ -607,11 +607,11 @@ function openFile(fileFilePathOrFileContents: string | Uint8Array | File): void electronApi?.fileIssue(filePath); }); }); -} +}; // Open file(s) dialog. -function onChange(event: Event): void { +const onChange = (event: Event): void => { // Open the selected file(s). const input = event.target as HTMLInputElement; @@ -626,21 +626,21 @@ function onChange(event: Event): void { // Note: this is needed to ensure that selecting the same file(s) again will trigger the change event. input.value = ''; -} +}; // Drag and drop. const dragAndDropCounter = vue.ref(0); -function onDragEnter(): void { +const onDragEnter = (): void => { if (!compUiEnabled.value || props.omex) { return; } dragAndDropCounter.value += 1; -} +}; -function onDrop(event: DragEvent): void { +const onDrop = (event: DragEvent): void => { if (!dragAndDropCounter.value) { return; } @@ -654,15 +654,15 @@ function onDrop(event: DragEvent): void { openFile(file); } } -} +}; -function onDragLeave(): void { +const onDragLeave = (): void => { if (!dragAndDropCounter.value) { return; } dragAndDropCounter.value -= 1; -} +}; // Open. @@ -670,13 +670,13 @@ electronApi?.onOpen((filePath: string) => { openFile(filePath); }); -function onOpenMenu(): void { +const onOpenMenu = (): void => { if (props.omex) { return; } files.value?.click(); -} +}; // Open remote. @@ -686,22 +686,22 @@ electronApi?.onOpenRemote(() => { openRemoteVisible.value = true; }); -function onOpenRemoteMenu(): void { +const onOpenRemoteMenu = (): void => { if (props.omex) { return; } openRemoteVisible.value = true; -} +}; -function onOpenRemote(url: string): void { +const onOpenRemote = (url: string): void => { // Note: no matter whether this is OpenCOR or OpenCOR's Web app, we always retrieve the file contents of a remote // file. We could, in OpenCOR, rely on libOpenCOR to retrieve it for us, but this would block the UI. To // retrieve the file here means that it is done asynchronously, which in turn means that the UI is not blocked // and that we can show a spinning wheel to indicate that something is happening. openFile(url); -} +}; // Open sample Lorenz. @@ -709,13 +709,13 @@ electronApi?.onOpenSampleLorenz(() => { onOpenSampleLorenzMenu(); }); -function onOpenSampleLorenzMenu(): void { +const onOpenSampleLorenzMenu = (): void => { if (props.omex) { return; } openFile('https://github.com/opencor/webapp/raw/refs/heads/main/tests/models/ui/lorenz.omex'); -} +}; // Close. @@ -723,13 +723,13 @@ electronApi?.onClose(() => { onCloseMenu(); }); -function onCloseMenu(): void { +const onCloseMenu = (): void => { if (props.omex) { return; } contents.value?.closeCurrentFile(); -} +}; // Close all. @@ -737,13 +737,13 @@ electronApi?.onCloseAll(() => { onCloseAllMenu(); }); -function onCloseAllMenu(): void { +const onCloseAllMenu = (): void => { if (props.omex) { return; } contents.value?.closeAllFiles(); -} +}; // Reset all. @@ -753,9 +753,9 @@ electronApi?.onResetAll(() => { resetAllVisible.value = true; }); -function onResetAll(): void { +const onResetAll = (): void => { electronApi?.resetAll(); -} +}; // Select. @@ -886,7 +886,7 @@ vue.watch(compBlockUiEnabled, (newCompBlockUiEnabled: boolean) => { const disconnectFromGitHubVisible = vue.ref(false); -async function deleteGitHubAccessToken(silent: boolean = false): Promise { +const deleteGitHubAccessToken = async (silent: boolean = false): Promise => { if (!electronApi) { return; } @@ -906,10 +906,10 @@ async function deleteGitHubAccessToken(silent: boolean = false): Promise { }); } } -} +}; /* TODO: enable once our GitHub integration is fully ready. -async function loadGitHubAccessToken(): Promise { +const loadGitHubAccessToken = async (): Promise => { if (!electronApi || props.omex || !firebaseConfig) { return; } @@ -939,9 +939,9 @@ async function loadGitHubAccessToken(): Promise { } finally { connectingToGitHub.value = false; } -} +}; -async function saveGitHubAccessToken(accessToken: string): Promise { +const saveGitHubAccessToken = async (accessToken: string): Promise => { if (!electronApi) { return; } @@ -967,9 +967,9 @@ async function saveGitHubAccessToken(accessToken: string): Promise { life: TOAST_LIFE }); } -} +}; -async function checkGitHubAccessToken(accessToken: string): Promise { +const checkGitHubAccessToken = async (accessToken: string): Promise => { const client = new Octokit({ auth: accessToken }); const user = await client.rest.users.getAuthenticated(); @@ -990,9 +990,9 @@ async function checkGitHubAccessToken(accessToken: string): Promise { } catch (error: unknown) { console.warn(`Failed to retrieve repositories for user ${user.data.login}:`, error); } -} +}; -async function onDisconnectFromGitHub(): Promise { +const onDisconnectFromGitHub = async (): Promise => { try { await firebase.auth().signOut(); @@ -1014,9 +1014,9 @@ async function onDisconnectFromGitHub(): Promise { } finally { disconnectFromGitHubVisible.value = false; } -} +}; -async function onGitHubButtonClick(): Promise { +const onGitHubButtonClick = async (): Promise => { if (octokit.value) { disconnectFromGitHubVisible.value = true; diff --git a/src/renderer/src/components/dialogs/BaseDialog.vue b/src/renderer/src/components/dialogs/BaseDialog.vue index 25ef2c60..9604ab7b 100644 --- a/src/renderer/src/components/dialogs/BaseDialog.vue +++ b/src/renderer/src/components/dialogs/BaseDialog.vue @@ -32,7 +32,7 @@ let dialogElement: HTMLElement | null = null; let containerElement: HTMLElement | null | undefined = null; let mutationObserver: MutationObserver | null = null; -function checkDialogPosition() { +const checkDialogPosition = () => { if (dialogElement instanceof HTMLElement && containerElement instanceof HTMLElement) { const dialogRect = dialogElement.getBoundingClientRect(); const containerRect = containerElement.getBoundingClientRect(); @@ -40,9 +40,9 @@ function checkDialogPosition() { dialogElement.style.top = `${String(Math.max(Math.min(dialogRect.top, containerRect.bottom - dialogRect.height), containerRect.top))}px`; dialogElement.style.left = `${String(Math.max(Math.min(dialogRect.left, containerRect.right - dialogRect.width), containerRect.left))}px`; } -} +}; -function onShow() { +const onShow = () => { incrementDialogs(); enableDisableMainMenu(false); @@ -61,9 +61,9 @@ function onShow() { checkDialogPosition(); } }); -} +}; -function onHide() { +const onHide = () => { decrementDialogs(); enableDisableMainMenu(true); @@ -77,7 +77,7 @@ function onHide() { mutationObserver = null; } }); -} +}; diff --git a/src/renderer/src/components/dialogs/SettingsDialog.vue b/src/renderer/src/components/dialogs/SettingsDialog.vue index d6ba2365..0e33d46b 100644 --- a/src/renderer/src/components/dialogs/SettingsDialog.vue +++ b/src/renderer/src/components/dialogs/SettingsDialog.vue @@ -20,23 +20,23 @@ const emit = defineEmits<(event: 'close') => void>(); const checkForUpdatesAtStartup = vue.ref(settings.general.checkForUpdatesAtStartup); -function initialiseDialog() { +const initialiseDialog = () => { // Note: we can come here as a result of hiding the dialog and this in case the dialog gets opened multiple times. We // could do this when showing the dialog, but it might result in the UI flashing (e.g., a checkbox was checked // and then it gets unchecked), hence we do it when hiding the dialog. checkForUpdatesAtStartup.value = settings.general.checkForUpdatesAtStartup; -} +}; settings.onInitialised(() => { initialiseDialog(); }); -function onOk() { +const onOk = () => { settings.general.checkForUpdatesAtStartup = checkForUpdatesAtStartup.value; settings.save(); emit('close'); -} +}; diff --git a/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue b/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue index 520725db..f8bff58e 100644 --- a/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue +++ b/src/renderer/src/components/dialogs/SimulationExperimentViewSettingsDialog.vue @@ -886,19 +886,19 @@ const uiJsonIssues = vue.computed(() => { return validateUiJson(localSettings.value.interactive.uiJson); }); -function plotTraceCount(plot: locApi.IUiJsonOutputPlot): number { +const plotTraceCount = (plot: locApi.IUiJsonOutputPlot): number => { return 1 + (plot.additionalTraces?.length ?? 0); -} +}; -function traceNameTooltip(): string { +const traceNameTooltip = (): string => { return ` You can provide a name for the trace or leave it empty to have a name generated automatically as follows: <Y value> vs. <X value>.

If you provide a name, you can use HTML tags for formatting (e.g., <em>I<sub>Na</sub></em> will render as INa). `; -} +}; -function xyParameterValueTooltip(value: string, idType: string, idPrefix: string): string { +const xyParameterValueTooltip = (value: string, idType: string, idPrefix: string): string => { return ` You can provide the value of the ${value} using an algebraic expression that includes ${idType} IDs.

@@ -906,20 +906,24 @@ function xyParameterValueTooltip(value: string, idType: string, idPrefix: string
You can also use mathematical functions and operators in the expression (e.g., 2 * ${idPrefix}_1 + sin(${idPrefix}_2)). `; -} +}; -function xyValueTooltip(xAxis: boolean): string { +const xyValueTooltip = (xAxis: boolean): string => { return xyParameterValueTooltip(`${xAxis ? 'X' : 'Y'} axis`, 'simulation data', 'simulation_data'); -} +}; -function parameterValueTooltip(): string { +const parameterValueTooltip = (): string => { return xyParameterValueTooltip('model parameter', 'model input', 'model_input'); -} - -function traceName(plot: locApi.IUiJsonOutputPlot, traceIndex: number): string { - function actualTraceName(name: string | undefined, xValue: string | undefined, yValue: string | undefined): string { +}; + +const traceName = (plot: locApi.IUiJsonOutputPlot, traceIndex: number): string => { + const actualTraceName = ( + name: string | undefined, + xValue: string | undefined, + yValue: string | undefined + ): string => { return name || (xValue && yValue ? `${yValue} vs. ${xValue}` : '???'); - } + }; if (traceIndex === -1) { return actualTraceName(plot.name, plot.xValue, plot.yValue); @@ -928,9 +932,9 @@ function traceName(plot: locApi.IUiJsonOutputPlot, traceIndex: number): string { const additionalTrace = plot.additionalTraces?.[traceIndex]; return actualTraceName(additionalTrace?.name, additionalTrace?.xValue, additionalTrace?.yValue); -} +}; -function addInput() { +const addInput = () => { localSettings.value.interactive.uiJson.input.push({ id: `model_input_${localSettings.value.interactive.uiJson.input.length + 1}`, name: `Model input #${localSettings.value.interactive.uiJson.input.length + 1}`, @@ -938,13 +942,13 @@ function addInput() { minimumValue: 0, maximumValue: 10 }); -} +}; -function removeInput(index: number) { +const removeInput = (index: number) => { localSettings.value.interactive.uiJson.input.splice(index, 1); -} +}; -function toggleInputType(index: number, type: string) { +const toggleInputType = (index: number, type: string) => { const input = localSettings.value.interactive.uiJson.input[index]; if (!input) { @@ -973,9 +977,9 @@ function toggleInputType(index: number, type: string) { maximumValue: 10 }; } -} +}; -function addPossibleValue(inputIndex: number) { +const addPossibleValue = (inputIndex: number) => { const input = localSettings.value.interactive.uiJson.input[inputIndex]; if (input && locApi.isDiscreteInput(input)) { @@ -986,28 +990,28 @@ function addPossibleValue(inputIndex: number) { value: maxValue + 1 }); } -} +}; -function removePossibleValue(inputIndex: number, possibleValueIndex: number) { +const removePossibleValue = (inputIndex: number, possibleValueIndex: number) => { const input = localSettings.value.interactive.uiJson.input[inputIndex]; if (input && locApi.isDiscreteInput(input)) { input.possibleValues.splice(possibleValueIndex, 1); } -} +}; -function addSimulationData() { +const addSimulationData = () => { localSettings.value.interactive.uiJson.output.data.push({ id: `simulation_data`, name: 'component/variable' }); -} +}; -function removeSimulationData(index: number) { +const removeSimulationData = (index: number) => { localSettings.value.interactive.uiJson.output.data.splice(index, 1); -} +}; -function addPlot() { +const addPlot = () => { if (localSettings.value.interactive.uiJson.output.plots.length < 9) { localSettings.value.interactive.uiJson.output.plots.push({ name: '', @@ -1018,13 +1022,13 @@ function addPlot() { additionalTraces: [] }); } -} +}; -function removePlot(index: number) { +const removePlot = (index: number) => { localSettings.value.interactive.uiJson.output.plots.splice(index, 1); -} +}; -function addTrace(plotIndex: number) { +const addTrace = (plotIndex: number) => { const plot = localSettings.value.interactive.uiJson.output.plots[plotIndex]; if (plot) { @@ -1037,9 +1041,9 @@ function addTrace(plotIndex: number) { yValue: 'y_id' }); } -} +}; -function removeTrace(plotIndex: number, traceIndex: number) { +const removeTrace = (plotIndex: number, traceIndex: number) => { const plot = localSettings.value.interactive.uiJson.output.plots[plotIndex]; if (!plot) { @@ -1068,28 +1072,28 @@ function removeTrace(plotIndex: number, traceIndex: number) { if (traceIndex >= 0 && plot.additionalTraces && plot.additionalTraces.length > 0) { plot.additionalTraces.splice(traceIndex, 1); } -} +}; -function addParameter() { +const addParameter = () => { localSettings.value.interactive.uiJson.parameters.push({ name: 'component/variable', value: 'input_id' }); -} +}; -function removeParameter(index: number) { +const removeParameter = (index: number) => { localSettings.value.interactive.uiJson.parameters.splice(index, 1); -} +}; -function resetUxSettings() { +const resetUxSettings = () => { activeTab.value = DEFAULT_TAB; activeInteractiveTab.value = DEFAULT_INTERACTIVE_TAB; showSimulationSettingsIssuesPanel.value = false; showUiJsonIssuesPanel.value = false; -} +}; -function onOk() { +const onOk = () => { // Reset our UX settings. resetUxSettings(); @@ -1112,9 +1116,9 @@ function onOk() { liveUpdates: localSettings.value.miscellaneous.liveUpdates } }); -} +}; -function onCancel() { +const onCancel = () => { // Reset our local settings to the original settings and close the dialog. localSettings.value = JSON.parse(JSON.stringify(props.settings)); @@ -1126,25 +1130,25 @@ function onCancel() { // Close the dialog. emit('close'); -} +}; -function toggleSimulationSettingsIssues(event: Event) { +const toggleSimulationSettingsIssues = (event: Event) => { simulationSettingsIssuesPopup.value?.toggle(event); showSimulationSettingsIssuesPanel.value = !showSimulationSettingsIssuesPanel.value; -} +}; -function toggleSolversSettingsIssues(event: Event) { +const toggleSolversSettingsIssues = (event: Event) => { solversSettingsIssuesPopup.value?.toggle(event); showSolversSettingsIssuesPanel.value = !showSolversSettingsIssuesPanel.value; -} +}; -function toggleUiJsonIssues(event: Event) { +const toggleUiJsonIssues = (event: Event) => { uiJsonIssuesPopup.value?.toggle(event); showUiJsonIssuesPanel.value = !showUiJsonIssuesPanel.value; -} +};