From c1821f169d3da999804dd43fa7a0062beb92601e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 12:39:02 +0700 Subject: [PATCH 001/199] convert to ts part 1 --- electron-main/{expressServer.js => expressServer.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename electron-main/{expressServer.js => expressServer.ts} (95%) diff --git a/electron-main/expressServer.js b/electron-main/expressServer.ts similarity index 95% rename from electron-main/expressServer.js rename to electron-main/expressServer.ts index f27734d6..b28c6755 100644 --- a/electron-main/expressServer.js +++ b/electron-main/expressServer.ts @@ -14,11 +14,11 @@ const getRandomPort = () => { }; // Function to check if a port is available -const checkPortAvailability = (port) => { +const checkPortAvailability = (port: number) => { return new Promise((resolve, reject) => { const server = net.createServer(); - server.once("error", (err) => { + server.once("error", (err: any) => { if (err.code === "EADDRINUSE" || err.code === "ECONNREFUSED") { resolve(false); // Port is in use applog.log("Port is in use. Error:", err.code); From f7e3907f847efa51f319ff60d5cc6302fa4cb71d Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:19:33 +0700 Subject: [PATCH 002/199] fix eslint error --- electron-main/expressServer.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/electron-main/expressServer.ts b/electron-main/expressServer.ts index b28c6755..780da3ee 100644 --- a/electron-main/expressServer.ts +++ b/electron-main/expressServer.ts @@ -1,6 +1,7 @@ +import { ipcMain } from "electron"; import applog from "electron-log"; import express from "express"; -import net from "net"; +import net, { AddressInfo } from "net"; const DEFAULT_PORT = 8998; const MIN_PORT = 1024; // Minimum valid port number @@ -35,6 +36,11 @@ const checkPortAvailability = (port: number) => { }); server.listen(port); + + // IPC event to get the current server port + ipcMain.handle("get-port", () => { + return (server.address() as AddressInfo)?.port || DEFAULT_PORT; + }); }); }; From b595cbd286116605cf54e55277434091c71bedb3 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:23:05 +0700 Subject: [PATCH 003/199] convert to ts part 2 --- electron-main/{filePath.js => filePath.ts} | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) rename electron-main/{filePath.js => filePath.ts} (81%) diff --git a/electron-main/filePath.js b/electron-main/filePath.ts similarity index 81% rename from electron-main/filePath.js rename to electron-main/filePath.ts index c9e5f140..f1545cfa 100644 --- a/electron-main/filePath.js +++ b/electron-main/filePath.ts @@ -6,17 +6,17 @@ import path from "node:path"; // Singleton instance for settings const settingsConf = new Conf({ name: "ispeakerreact_config" }); -const readUserSettings = async () => { +const readUserSettings = async (): Promise> => { return settingsConf.store || {}; }; -const getSaveFolder = async () => { +const getSaveFolder = async (): Promise => { // Try to get custom folder from user settings const userSettings = await readUserSettings(); - let saveFolder; - if (userSettings.customSaveFolder) { + let saveFolder: string; + if (userSettings.customSaveFolder && typeof userSettings.customSaveFolder === "string") { // For custom folder, use a subfolder 'ispeakerreact_data' - const baseFolder = userSettings.customSaveFolder; + const baseFolder: string = userSettings.customSaveFolder; // Ensure the base directory exists try { await fsPromises.access(baseFolder); @@ -44,7 +44,7 @@ const getSaveFolder = async () => { return saveFolder; }; -const getLogFolder = async () => { +const getLogFolder = async (): Promise => { const saveFolder = path.join(await getSaveFolder(), "logs"); // Ensure the directory exists try { @@ -56,15 +56,15 @@ const getLogFolder = async () => { }; // Helper to get the data subfolder path -const getDataSubfolder = (baseFolder) => { +const getDataSubfolder = (baseFolder: string): string => { return path.join(baseFolder, "ispeakerreact_data"); }; // Synchronous version to get the log folder path -const getLogFolderSync = () => { - let saveFolder; +const getLogFolderSync = (): string => { + let saveFolder: string; const userSettings = settingsConf.store || {}; - if (userSettings.customSaveFolder) { + if (userSettings.customSaveFolder && typeof userSettings.customSaveFolder === "string") { // For custom folder, use the data subfolder saveFolder = path.join(userSettings.customSaveFolder, "ispeakerreact_data"); } else { @@ -75,7 +75,7 @@ const getLogFolderSync = () => { }; // Helper to delete the empty ispeakerreact_data subfolder -const deleteEmptyDataSubfolder = async (baseFolder) => { +const deleteEmptyDataSubfolder = async (baseFolder: string): Promise => { const dataFolder = path.join(baseFolder, "ispeakerreact_data"); try { const files = await fsPromises.readdir(dataFolder); From 06f898a1c842ad1816bef5b11df5639dc13cdb1c Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:28:01 +0700 Subject: [PATCH 004/199] convert to ts part 3 --- .../{createWindow.js => createWindow.ts} | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) rename electron-main/{createWindow.js => createWindow.ts} (83%) diff --git a/electron-main/createWindow.js b/electron-main/createWindow.ts similarity index 83% rename from electron-main/createWindow.js rename to electron-main/createWindow.ts index 9de5e006..968825b5 100644 --- a/electron-main/createWindow.js +++ b/electron-main/createWindow.ts @@ -6,10 +6,10 @@ import { startExpressServer } from "./expressServer.js"; const isDev = process.env.NODE_ENV === "development"; -let mainWindow; -let splashWindow; +let mainWindow: BrowserWindow | null; +let splashWindow: BrowserWindow | null; -const createSplashWindow = (rootDir, ipcMain, conf) => { +const createSplashWindow = (rootDir: string, ipcMain: any, conf: any) => { splashWindow = new BrowserWindow({ width: 854, height: 413, @@ -21,13 +21,12 @@ const createSplashWindow = (rootDir, ipcMain, conf) => { preload: path.join(rootDir, "preload.cjs"), nodeIntegration: false, contextIsolation: true, - enableRemoteModule: false, devTools: isDev ? true : false, }, }); // For splash screen - ipcMain.handle("get-conf", (event, key) => { + ipcMain.handle("get-conf", (key: string) => { return conf.get(key); }); @@ -42,7 +41,7 @@ const createSplashWindow = (rootDir, ipcMain, conf) => { }); }; -const createWindow = (rootDir, onServerReady) => { +const createWindow = (rootDir: string, onServerReady: (srv: any) => void) => { mainWindow = new BrowserWindow({ width: 1280, height: 720, @@ -51,7 +50,6 @@ const createWindow = (rootDir, onServerReady) => { preload: path.join(rootDir, "preload.cjs"), nodeIntegration: false, contextIsolation: true, - enableRemoteModule: false, devTools: isDev ? true : false, }, icon: path.join(rootDir, "dist", "appicon.png"), @@ -66,10 +64,13 @@ const createWindow = (rootDir, onServerReady) => { // Show the main window only when it's ready mainWindow.once("ready-to-show", () => { setTimeout(() => { - splashWindow.close(); - mainWindow.maximize(); - mainWindow.show(); - + if (splashWindow) { + splashWindow.close(); + } + if (mainWindow) { + mainWindow.maximize(); + mainWindow.show(); + } // Start Express server in the background after main window is shown startExpressServer().then((srv) => { if (onServerReady) onServerReady(srv); @@ -82,11 +83,15 @@ const createWindow = (rootDir, onServerReady) => { }); mainWindow.on("enter-full-screen", () => { - mainWindow.setMenuBarVisibility(false); + if (mainWindow) { + mainWindow.setMenuBarVisibility(false); + } }); mainWindow.on("leave-full-screen", () => { - mainWindow.setMenuBarVisibility(true); + if (mainWindow) { + mainWindow.setMenuBarVisibility(true); + } }); const menu = Menu.buildFromTemplate([ @@ -116,8 +121,8 @@ const createWindow = (rootDir, onServerReady) => { { role: "zoomOut" }, { type: "separator" }, { role: "togglefullscreen" }, - isDev ? { role: "toggleDevTools" } : null, - ].filter(Boolean), + ...(isDev ? [{ role: "toggleDevTools" as const }] : []), + ], }, { label: "Window", From 54642335cd5c88dcccc5fc2c4cb9939a4b5cb170 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:29:24 +0700 Subject: [PATCH 005/199] convert to ts part 4 --- .../{getFileAndFolder.js => getFileAndFolder.ts} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename electron-main/{getFileAndFolder.js => getFileAndFolder.ts} (91%) diff --git a/electron-main/getFileAndFolder.js b/electron-main/getFileAndFolder.ts similarity index 91% rename from electron-main/getFileAndFolder.js rename to electron-main/getFileAndFolder.ts index db95a350..4539cc90 100644 --- a/electron-main/getFileAndFolder.js +++ b/electron-main/getFileAndFolder.ts @@ -1,10 +1,10 @@ import { ipcMain, shell } from "electron"; +import fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; import { getSaveFolder, readUserSettings } from "./filePath.js"; -import fs from "node:fs"; -const getVideoFileDataIPC = (rootDir) => { +const getVideoFileDataIPC = (rootDir: string) => { ipcMain.handle("get-video-file-data", async () => { const jsonPath = path.join(rootDir, "dist", "json", "videoFilesInfo.json"); try { @@ -19,7 +19,7 @@ const getVideoFileDataIPC = (rootDir) => { const getVideoSaveFolderIPC = () => { ipcMain.handle("get-video-save-folder", async () => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const videoFolder = path.join(saveFolder, "video_files"); // Ensure the directory exists @@ -39,7 +39,7 @@ const getVideoSaveFolderIPC = () => { // IPC: Get current save folder (resolved) const getSaveFolderIPC = () => { ipcMain.handle("get-save-folder", async () => { - return await getSaveFolder(readUserSettings); + return await getSaveFolder(); }); }; @@ -52,7 +52,7 @@ const getCustomSaveFolderIPC = () => { }; // IPC: Get ffmpeg wasm absolute path -const getFfmpegWasmPathIPC = (rootDir) => { +const getFfmpegWasmPathIPC = (rootDir: string) => { ipcMain.handle("get-ffmpeg-wasm-path", async () => { // Adjust the path as needed if you move the file elsewhere return path.resolve(rootDir, "data", "ffmpeg"); From a9f4142caadae08c94c14fe036919d36fe245df0 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:36:15 +0700 Subject: [PATCH 006/199] convert to ts part 5 --- ...on.js => customFolderLocationOperation.ts} | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) rename electron-main/{customFolderLocationOperation.js => customFolderLocationOperation.ts} (90%) diff --git a/electron-main/customFolderLocationOperation.js b/electron-main/customFolderLocationOperation.ts similarity index 90% rename from electron-main/customFolderLocationOperation.js rename to electron-main/customFolderLocationOperation.ts index a53f1dfa..85aa8bec 100644 --- a/electron-main/customFolderLocationOperation.js +++ b/electron-main/customFolderLocationOperation.ts @@ -14,8 +14,8 @@ import isDeniedSystemFolder from "./isDeniedSystemFolder.js"; import { generateLogFileName } from "./logOperations.js"; // Helper: Recursively collect all files in a directory -const getAllFiles = async (dir, base = dir) => { - let files = []; +const getAllFiles = async (dir: string, base = dir): Promise<{ abs: string; rel: string }[]> => { + let files: { abs: string; rel: string }[] = []; const entries = await fsPromises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); @@ -32,10 +32,10 @@ const getAllFiles = async (dir, base = dir) => { }; // Helper: Determine if folder contents should be moved -const shouldMoveContents = (src, dest) => { +const shouldMoveContents = (src: string, dest: string): boolean => { return ( - src && - dest && + Boolean(src) && + Boolean(dest) && src !== dest && fs.existsSync(src) && fs.existsSync(dest) && @@ -45,7 +45,7 @@ const shouldMoveContents = (src, dest) => { }; // Helper: Delete pronunciation-venv in oldSaveFolder before moving -const deletePronunciationVenv = async (oldSaveFolder, event) => { +const deletePronunciationVenv = async (oldSaveFolder: string, event: any) => { const oldVenvPath = path.join(oldSaveFolder, "pronunciation-venv"); if (fs.existsSync(oldVenvPath)) { event.sender.send("venv-delete-status", { status: "deleting", path: oldVenvPath }); @@ -54,7 +54,7 @@ const deletePronunciationVenv = async (oldSaveFolder, event) => { console.log("Deleted old pronunciation-venv at:", oldVenvPath); applog.info("Deleted old pronunciation-venv at:", oldVenvPath); event.sender.send("venv-delete-status", { status: "deleted", path: oldVenvPath }); - } catch (venvErr) { + } catch (venvErr: any) { // Log but do not block move if venv doesn't exist or can't be deleted console.log("Could not delete old pronunciation-venv:", venvErr.message); applog.warn("Could not delete old pronunciation-venv:", venvErr.message); @@ -68,7 +68,7 @@ const deletePronunciationVenv = async (oldSaveFolder, event) => { }; // Helper: Move all contents from one folder to another (copy then delete, robust for cross-device) -const moveFolderContents = async (src, dest, event) => { +const moveFolderContents = async (src: string, dest: string, event: any) => { // Recursively collect all files for accurate progress const files = await getAllFiles(src); const total = files.length; @@ -104,7 +104,7 @@ const moveFolderContents = async (src, dest, event) => { } // 3. Remove empty directories in src (track progress for dirs) // We'll collect all directories and send progress for each - const collectDirs = async (dir) => { + const collectDirs = async (dir: string): Promise => { let dirs = [dir]; const entries = await fsPromises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { @@ -147,27 +147,27 @@ const moveFolderContents = async (src, dest, event) => { const setCustomSaveFolderIPC = () => { ipcMain.handle("set-custom-save-folder", async (event, folderPath) => { const oldSaveFolder = await getSaveFolder(); - let newSaveFolder; - let prevCustomFolder = null; + let newSaveFolder: string; + let prevCustomFolder: string | null = null; if (!folderPath) { // Reset to default const userSettings = settingsConf.store || {}; if (userSettings.customSaveFolder) { - prevCustomFolder = userSettings.customSaveFolder; + prevCustomFolder = userSettings.customSaveFolder as string; } settingsConf.delete("customSaveFolder"); // Use getSaveFolder to get the default save folder newSaveFolder = await getSaveFolder(); applog.info("Reset to default save folder:", newSaveFolder); // Move contents back from previous custom folder's data subfolder if it exists - let prevDataSubfolder = null; + let prevDataSubfolder: string | null = null; if (prevCustomFolder) { prevDataSubfolder = getDataSubfolder(prevCustomFolder); } // Ensure the destination exists before checking shouldMoveContents try { await fsPromises.mkdir(newSaveFolder, { recursive: true }); - } catch (e) { + } catch (e: any) { console.log("Failed to create default save folder:", e); applog.error("Failed to create default save folder:", e); return { @@ -178,7 +178,9 @@ const setCustomSaveFolderIPC = () => { } applog.info("[DEBUG] prevDataSubfolder:", prevDataSubfolder); applog.info("[DEBUG] newSaveFolder:", newSaveFolder); - const shouldMove = shouldMoveContents(prevDataSubfolder, newSaveFolder); + const shouldMove = prevDataSubfolder + ? shouldMoveContents(prevDataSubfolder, newSaveFolder) + : false; applog.info("[DEBUG] shouldMoveContents:", shouldMove); if (shouldMove) { applog.info( @@ -187,9 +189,11 @@ const setCustomSaveFolderIPC = () => { newSaveFolder ); try { - await deletePronunciationVenv(prevDataSubfolder, event); - await moveFolderContents(prevDataSubfolder, newSaveFolder, event); - } catch (moveBackErr) { + if (prevDataSubfolder) { + await deletePronunciationVenv(prevDataSubfolder, event); + await moveFolderContents(prevDataSubfolder, newSaveFolder, event); + } + } catch (moveBackErr: any) { console.log("Failed to move contents back to default folder:", moveBackErr); applog.error("Failed to move contents back to default folder:", moveBackErr); return { @@ -247,7 +251,7 @@ const setCustomSaveFolderIPC = () => { // Ensure the data subfolder exists try { await fsPromises.mkdir(newSaveFolder, { recursive: true }); - } catch (e) { + } catch (e: any) { console.log("Failed to create data subfolder:", e); applog.error("Failed to create data subfolder:", e); return { @@ -258,7 +262,7 @@ const setCustomSaveFolderIPC = () => { } console.log("New save folder:", newSaveFolder); applog.info("New save folder:", newSaveFolder); - } catch (err) { + } catch (err: any) { console.log("Error setting custom save folder:", err); applog.error("Error setting custom save folder:", err); return { @@ -278,7 +282,7 @@ const setCustomSaveFolderIPC = () => { const currentLogFolder = path.join(newSaveFolder, "logs"); try { await fsPromises.mkdir(currentLogFolder, { recursive: true }); - } catch (e) { + } catch (e: any) { console.log("Failed to create new log directory:", e); applog.warn("Failed to create new log directory:", e); } @@ -292,7 +296,7 @@ const setCustomSaveFolderIPC = () => { await deleteEmptyDataSubfolder(prevCustomFolder); } return { success: true, newPath: newSaveFolder }; - } catch (moveErr) { + } catch (moveErr: any) { console.log("Failed to move folder contents:", moveErr); applog.error("Failed to move folder contents:", moveErr); return { From 268fc49f8b5351378e146536cbb3393f794ff6c9 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:40:00 +0700 Subject: [PATCH 007/199] convert to ts part 6 --- ...dSystemFolder.js => isDeniedSystemFolder.ts} | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) rename electron-main/{isDeniedSystemFolder.js => isDeniedSystemFolder.ts} (96%) diff --git a/electron-main/isDeniedSystemFolder.js b/electron-main/isDeniedSystemFolder.ts similarity index 96% rename from electron-main/isDeniedSystemFolder.js rename to electron-main/isDeniedSystemFolder.ts index 9bf3973e..986592a5 100644 --- a/electron-main/isDeniedSystemFolder.js +++ b/electron-main/isDeniedSystemFolder.ts @@ -4,17 +4,20 @@ import path from "node:path"; import process from "node:process"; import fs from "node:fs"; -const isDeniedSystemFolder = (folderPath) => { +const isDeniedSystemFolder = (folderPath: string) => { // Normalize and resolve the path to prevent path traversal attacks // This converts to absolute path and resolves ".." and "." segments - let absoluteInput; + let absoluteInput: string | undefined; try { // Resolve symlinks for robust security absoluteInput = fs.realpathSync.native(path.resolve(folderPath)); - } catch (err) { + } catch (err: any) { console.warn("Error getting realpath:", err.message); applog.error("Error getting realpath:", err.message); + // If we can't resolve the path, deny access for safety + return true; } + const platform = process.platform; const isCaseSensitive = platform !== "win32"; // Handle potential trailing slashes inconsistencies @@ -83,7 +86,7 @@ const isDeniedSystemFolder = (folderPath) => { } } } - } catch (err) { + } catch (err: any) { console.warn("Error getting users directory:", err.message); applog.error("Error getting users directory:", err.message); } @@ -130,7 +133,7 @@ const isDeniedSystemFolder = (folderPath) => { } } } - } catch (err) { + } catch (err: any) { console.warn("Error getting home directory:", err.message); applog.error("Error getting home directory:", err.message); } @@ -147,7 +150,7 @@ const isDeniedSystemFolder = (folderPath) => { app.getPath("crashDumps"), ]; denyList = [...denyList, ...appPaths]; - } catch (err) { + } catch (err: any) { console.warn("Error getting app paths:", err.message); applog.error("Error getting app paths:", err.message); } @@ -164,7 +167,7 @@ const isDeniedSystemFolder = (folderPath) => { let formatted; try { formatted = fs.realpathSync.native(path.resolve(p)); - } catch { + } catch (err: any) { formatted = path.resolve(p); } if (!isCaseSensitive) formatted = formatted.toLowerCase(); From 78d07ed4edbc02da43fd21271e450d1ea9149bd9 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:43:22 +0700 Subject: [PATCH 008/199] convert to ts part 7 --- .../{logOperations.js => logOperations.ts} | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) rename electron-main/{logOperations.js => logOperations.ts} (85%) diff --git a/electron-main/logOperations.js b/electron-main/logOperations.ts similarity index 85% rename from electron-main/logOperations.js rename to electron-main/logOperations.ts index e3eab366..9fb76112 100644 --- a/electron-main/logOperations.js +++ b/electron-main/logOperations.ts @@ -1,8 +1,8 @@ import { ipcMain } from "electron"; -import applog from "electron-log"; +import applog, { LevelOption } from "electron-log"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; -import { getLogFolder, getLogFolderSync, readUserSettings, settingsConf } from "./filePath.js"; +import { getLogFolder, getLogFolderSync, settingsConf } from "./filePath.js"; const defaultLogSettings = { numOfLogs: 10, @@ -23,7 +23,7 @@ const getCurrentLogSettings = () => { return currentLogSettings; }; -const setCurrentLogSettings = (newSettings) => { +const setCurrentLogSettings = (newSettings: any) => { currentLogSettings = { ...currentLogSettings, ...newSettings }; }; @@ -46,10 +46,10 @@ applog.transports.file.resolvePathFn = () => { return path.join(logFolder, applog.transports.file.fileName); }; applog.transports.file.maxSize = currentLogSettings.maxLogSize; -applog.transports.console.level = currentLogSettings.logLevel; +applog.transports.console.level = currentLogSettings.logLevel as LevelOption; // Handle updated log settings from the renderer -ipcMain.on("update-log-settings", async (event, newSettings) => { +ipcMain.on("update-log-settings", async (event: any, newSettings: any) => { setCurrentLogSettings(newSettings); applog.info("Log settings updated:", currentLogSettings); @@ -67,7 +67,7 @@ const manageLogFiles = async () => { applog.info("Log settings:", currentLogSettings); // Get the current log folder dynamically - const logFolder = await getLogFolder(readUserSettings); + const logFolder = await getLogFolder(); // Get all log files const logFiles = await fsPromises.readdir(logFolder); @@ -80,16 +80,16 @@ const manageLogFiles = async () => { path: filePath, birthtime: stats.birthtime, }); - } catch (err) { + } catch (err: any) { + // If ENOENT, just skip this file if (err.code !== "ENOENT") { applog.error(`Error stating log file: ${filePath}`, err); } - // If ENOENT, just skip this file } } // Sort log files by creation time (oldest first) - logFilesResolved.sort((a, b) => a.birthtime - b.birthtime); + logFilesResolved.sort((a, b) => a.birthtime.getTime() - b.birthtime.getTime()); // Remove logs if they exceed the specified limit (excluding 0 for unlimited) if (numOfLogs > 0 && logFilesResolved.length > numOfLogs) { @@ -98,7 +98,7 @@ const manageLogFiles = async () => { try { await fsPromises.unlink(file.path); applog.info(`Deleted log file: ${file.path}`); - } catch (err) { + } catch (err: any) { if (err.code !== "ENOENT") { applog.error(`Error deleting log file: ${file.path}`, err); } @@ -110,12 +110,13 @@ const manageLogFiles = async () => { if (keepForDays > 0) { const now = new Date(); for (const file of logFilesResolved) { - const ageInDays = (now - new Date(file.birthtime)) / (1000 * 60 * 60 * 24); + const ageInDays = + (now.getTime() - new Date(file.birthtime).getTime()) / (1000 * 60 * 60 * 24); if (ageInDays > keepForDays) { try { await fsPromises.unlink(file.path); applog.info(`Deleted old log file: ${file.path}`); - } catch (err) { + } catch (err: any) { if (err.code !== "ENOENT") { applog.error(`Error deleting old log file: ${file.path}`, err); } @@ -123,7 +124,7 @@ const manageLogFiles = async () => { } } } - } catch (error) { + } catch (error: any) { applog.error("Error managing log files:", error); } }; From 47157231a810779a687c376a6704fa28852ed43d Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:45:24 +0700 Subject: [PATCH 009/199] convert to ts part 8 --- ...eckerIPC.js => pronunciationCheckerIPC.ts} | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) rename electron-main/{pronunciationCheckerIPC.js => pronunciationCheckerIPC.ts} (94%) diff --git a/electron-main/pronunciationCheckerIPC.js b/electron-main/pronunciationCheckerIPC.ts similarity index 94% rename from electron-main/pronunciationCheckerIPC.js rename to electron-main/pronunciationCheckerIPC.ts index e4dea825..b337ca08 100644 --- a/electron-main/pronunciationCheckerIPC.js +++ b/electron-main/pronunciationCheckerIPC.ts @@ -1,21 +1,22 @@ import { spawn } from "child_process"; import { ipcMain } from "electron"; -import applog from "electron-log"; +import applog, { LevelOption } from "electron-log"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; import { getSaveFolder, readUserSettings } from "./filePath.js"; import { getCurrentLogSettings } from "./logOperations.js"; import { getVenvPythonPath, ensureVenvExists } from "./pronunciationOperations.js"; -const startProcess = (cmd, args) => { +const startProcess = (cmd: string, args: string[], callback: (err: any) => void) => { const proc = spawn(cmd, args, { shell: true }); + proc.on("error", callback); return proc; }; // IPC handler to check pronunciation const setupPronunciationCheckerIPC = () => { ipcMain.handle("pronunciation-check", async (event, audioPath, modelName) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); // If modelName is not provided, read from user settings if (!modelName) { const userSettings = await readUserSettings(); @@ -34,7 +35,7 @@ const setupPronunciationCheckerIPC = () => { // Configure electron-log with current settings applog.transports.file.maxSize = logSettings.maxLogSize; - applog.transports.console.level = logSettings.logLevel; + applog.transports.console.level = logSettings.logLevel as LevelOption; applog.info(`[PronunciationChecker] audioPath: ${audioPath}`); applog.info(`[PronunciationChecker] modelDir: ${modelDir}`); @@ -137,13 +138,13 @@ if __name__ == "__main__": try { await ensureVenvExists(); venvPython = await getVenvPythonPath(); - } catch (err) { + } catch (err: any) { applog.error(`[PronunciationChecker] Failed to create or find venv: ${err.message}`); return { status: "error", message: `Failed to create or find venv: ${err.message}` }; } applog.info(`[PronunciationChecker] About to run: ${venvPython} -u ${tempPyPath}`); return new Promise((resolve) => { - const py = startProcess(venvPython, ["-u", tempPyPath], (err) => { + const py = startProcess(venvPython, ["-u", tempPyPath], (err: any) => { fsPromises.unlink(tempPyPath).catch(() => { applog.warn( "[PronunciationChecker] Failed to delete temp pronunciation checker file" @@ -155,7 +156,7 @@ if __name__ == "__main__": ); } }); - let lastJson = null; + let lastJson: any = null; py.stdout.on("data", (data) => { const lines = data.toString().split(/\r?\n/); for (const line of lines) { @@ -223,12 +224,12 @@ if __name__ == "__main__": // IPC handler to get the recording blob for a given key const setupGetRecordingBlobIPC = () => { ipcMain.handle("get-recording-blob", async (_event, key) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const filePath = path.join(saveFolder, "saved_recordings", `${key}.wav`); try { const data = await fsPromises.readFile(filePath); return data.buffer; // ArrayBuffer for renderer - } catch (err) { + } catch (err: any) { throw new Error(`Failed to read recording: ${err.message}`); } }); From 2f67a95e68cc518768b55a1299aaf62f4ba739f7 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:51:12 +0700 Subject: [PATCH 010/199] convert to ts part 9 --- ...erations.js => pronunciationOperations.ts} | 86 +++++++++++-------- 1 file changed, 49 insertions(+), 37 deletions(-) rename electron-main/{pronunciationOperations.js => pronunciationOperations.ts} (90%) diff --git a/electron-main/pronunciationOperations.js b/electron-main/pronunciationOperations.ts similarity index 90% rename from electron-main/pronunciationOperations.js rename to electron-main/pronunciationOperations.ts index 0dd4f177..d6b09fca 100644 --- a/electron-main/pronunciationOperations.js +++ b/electron-main/pronunciationOperations.ts @@ -5,9 +5,9 @@ import { spawn } from "node:child_process"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; import process from "node:process"; -import { getSaveFolder, readUserSettings, settingsConf } from "./filePath.js"; +import { getSaveFolder, settingsConf } from "./filePath.js"; -let currentPythonProcess = null; +let currentPythonProcess: any = null; let pendingCancel = false; let isGloballyCancelled = false; @@ -18,7 +18,7 @@ const checkPythonInstalled = async () => { let log = ""; return new Promise((resolve) => { // Try python3 first - const tryPython = (cmd, cb) => { + const tryPython = (cmd: string, cb: (err: any, stdout: string, stderr: string) => void) => { const proc = spawn(cmd, ["--version"], { shell: true }); let stdout = ""; let stderr = ""; @@ -65,24 +65,28 @@ const checkPythonInstalled = async () => { }); }; -const startProcess = (cmd, args, onExit) => { +const startProcess = (cmd: string, args: string[], onExit: (err: any) => void) => { const proc = spawn(cmd, args, { shell: true }); currentPythonProcess = proc; if (pendingCancel) { // Wait 0.5s, then kill the process setTimeout(() => { - fkill(proc.pid, { force: true, tree: true }) - .then(() => { - console.log("[Cancel] Process killed after short delay due to pending cancel."); - }) - .catch((err) => { - if (err.message && err.message.includes("Process doesn't exist")) { - // Already dead, ignore - } else { - console.error("[Cancel] Error killing process after delay:", err); - } - }); - proc._wasCancelledImmediately = true; // Mark for downstream logic + if (typeof proc.pid !== "undefined") { + fkill(proc.pid, { force: true, tree: true }) + .then(() => { + console.log( + "[Cancel] Process killed after short delay due to pending cancel." + ); + }) + .catch((err: any) => { + if (err.message && err.message.includes("Process doesn't exist")) { + // Already dead, ignore + } else { + console.error("[Cancel] Error killing process after delay:", err); + } + }); + } + (proc as any)._wasCancelledImmediately = true; // Mark for downstream logic }, 500); // 0.5 second delay pendingCancel = false; } @@ -108,7 +112,7 @@ const resetGlobalCancel = () => { // --- VENV HELPERS --- const getVenvDir = async () => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); return path.join(saveFolder, "pronunciation-venv"); }; @@ -167,18 +171,18 @@ const ensureVenvExists = async () => { let systemPython = "python"; // Try to use python3 if available try { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const proc = spawn("python3", ["--version"], { shell: true }); proc.on("close", (code) => { if (code === 0) resolve(); - else reject(); + else reject(void 0); }); }); systemPython = "python3"; } catch { // fallback to python } - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const proc = spawn(systemPython, ["-m", "venv", venvDir], { shell: true }); proc.on("close", (code) => { if (code === 0) resolve(); @@ -206,10 +210,10 @@ const installDependencies = () => { try { await upgradeVenvPip(); log += "Upgraded pip to latest version.\n"; - } catch (err) { + } catch (err: any) { log += `Failed to upgrade pip: ${err.message}\n`; } - } catch (err) { + } catch (err: any) { log += `Failed to create virtual environment: ${err.message}\n`; event.sender.send("pronunciation-dep-progress", { name: "all", @@ -220,7 +224,7 @@ const installDependencies = () => { } return new Promise((resolve) => { const pipArgs = ["install", ...dependencies, "-U"]; - const pipProcess = startProcess(venvPip, pipArgs, (err) => { + const pipProcess = startProcess(venvPip, pipArgs, (err: any) => { const status = err ? "error" : "success"; event.sender.send("pronunciation-dep-progress", { name: "all", @@ -250,7 +254,11 @@ const installDependencies = () => { }; // Extracted model download logic -const downloadModelToDir = async (modelDir, modelName, onProgress) => { +const downloadModelToDir = async ( + modelDir: string, + modelName: string, + onProgress: (msg: any) => void +) => { if (isGloballyCancelled) { if (onProgress) onProgress({ status: "cancelled", message: "Cancelled before start" }); return { status: "cancelled", message: "Cancelled before start" }; @@ -266,7 +274,7 @@ const downloadModelToDir = async (modelDir, modelName, onProgress) => { try { await ensureVenvExists(); venvPython = await getVenvPythonPath(); - } catch (err) { + } catch (err: any) { if (onProgress) onProgress({ status: "error", @@ -376,7 +384,7 @@ else: ) sys.exit(1) `; - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const tempPyPath = path.join(saveFolder, "download_model_temp.py"); await fsPromises.writeFile(tempPyPath, pyCode, "utf-8"); // Emit 'downloading' status before launching Python process @@ -405,20 +413,20 @@ else: } }); // If process was cancelled immediately, resolve and do not proceed - if (py._wasCancelledImmediately) { + if ((py as any)._wasCancelledImmediately) { if (onProgress) onProgress({ status: "cancelled", message: "Process cancelled before start" }); resolve({ status: "cancelled", message: "Process cancelled before start" }); return; } - let lastStatus = null; + let lastStatus: any = null; let hadError = false; py.stdout.on("data", (data) => { const str = data.toString(); // Always forward raw output to renderer for logging if (onProgress) onProgress({ status: "log", message: str }); // Try to parse JSON lines as before - str.split(/\r?\n/).forEach((line) => { + str.split(/\r?\n/).forEach((line: string) => { if (line.trim()) { try { const msg = JSON.parse(line); @@ -464,19 +472,23 @@ else: const downloadModel = () => { ipcMain.handle("pronunciation-download-model", async (event, modelName) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); // Replace / with _ for folder name const safeModelFolder = modelName.replace(/\//g, "_"); const modelDir = path.join(saveFolder, "phoneme-model", safeModelFolder); // Forward progress to renderer - const finalStatus = await downloadModelToDir(modelDir, modelName, (msg) => { + const finalStatus = await downloadModelToDir(modelDir, modelName, (msg: any) => { event.sender.send("pronunciation-model-progress", msg); }); // After successful download, update user settings with modelName console.log( `[PronunciationOperations] finalStatus: ${JSON.stringify(finalStatus, null, 2)}` ); - if (finalStatus && (finalStatus.status === "success" || finalStatus.status === "found")) { + if ( + finalStatus && + typeof (finalStatus as any).status === "string" && + ((finalStatus as any).status === "success" || (finalStatus as any).status === "found") + ) { settingsConf.set("modelName", modelName); console.log(`[PronunciationOperations] modelName updated to ${modelName}`); console.log(`[PronunciationOperations] settingsConf: ${settingsConf.get("modelName")}`); @@ -497,7 +509,7 @@ const cancelProcess = () => { try { await fkill(currentPythonProcess.pid, { force: true, tree: true }); console.log("[Cancel] Python process tree killed with fkill."); - } catch (err) { + } catch (err: any) { console.error("[Cancel] Error killing Python process tree:", err); } currentPythonProcess = null; @@ -519,7 +531,7 @@ const killCurrentPythonProcess = async () => { try { await fkill(currentPythonProcess.pid, { force: true, tree: true }); console.log("[Cleanup] Python process tree killed on app quit."); - } catch (err) { + } catch (err: any) { console.error("[Cleanup] Error killing Python process tree on app quit:", err); } currentPythonProcess = null; @@ -560,7 +572,7 @@ const setupPronunciationInstallStatusIPC = () => { }; // Helper to migrate old flat status to new structured format -const migrateOldStatusToStructured = (status) => { +const migrateOldStatusToStructured = (status: any) => { if (!status) return null; // Try to extract python info const python = { @@ -592,10 +604,10 @@ export { cancelProcess, checkPythonInstalled, downloadModel, + ensureVenvExists, + getVenvPythonPath, installDependencies, killCurrentPythonProcess, resetGlobalCancel, setupPronunciationInstallStatusIPC, - ensureVenvExists, - getVenvPythonPath, }; From 095a8349b47eb5132b0a6cce4499c878033d61c8 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 13:52:15 +0700 Subject: [PATCH 011/199] convert to ts part 10 --- .../{videoFileOperations.js => videoFileOperations.ts} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename electron-main/{videoFileOperations.js => videoFileOperations.ts} (84%) diff --git a/electron-main/videoFileOperations.js b/electron-main/videoFileOperations.ts similarity index 84% rename from electron-main/videoFileOperations.js rename to electron-main/videoFileOperations.ts index 794520cb..8f622bd6 100644 --- a/electron-main/videoFileOperations.js +++ b/electron-main/videoFileOperations.ts @@ -1,11 +1,11 @@ import { ipcMain } from "electron"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; -import { getSaveFolder, readUserSettings } from "./filePath.js"; +import { getSaveFolder } from "./filePath.js"; const checkDownloads = () => { ipcMain.handle("check-downloads", async () => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const videoFolder = path.join(saveFolder, "video_files"); // Ensure the directory exists try { @@ -16,7 +16,7 @@ const checkDownloads = () => { const files = await fsPromises.readdir(videoFolder); // Return the list of zip files in the download folder - const zipFiles = files.filter((file) => file.endsWith(".7z")); + const zipFiles = files.filter((file: string) => file.endsWith(".7z")); return zipFiles.length === 0 ? "no zip files downloaded" : zipFiles; }); }; @@ -24,7 +24,7 @@ const checkDownloads = () => { // Check video extracted folder const checkExtractedFolder = () => { ipcMain.handle("check-extracted-folder", async (event, folderName, zipContents) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const extractedFolder = path.join(saveFolder, "video_files", folderName); // Check if extracted folder exists @@ -33,7 +33,7 @@ const checkExtractedFolder = () => { const extractedFiles = await fsPromises.readdir(extractedFolder); // Check if all expected files are present in the extracted folder - const allFilesExtracted = zipContents[0].extractedFiles.every((file) => { + const allFilesExtracted = zipContents[0].extractedFiles.every((file: any) => { return extractedFiles.includes(file.name); }); From 8e5590e85c17e4daba071d630460337749223c7e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 14:02:34 +0700 Subject: [PATCH 012/199] convert to ts part 11 --- .../{zipOperation.js => zipOperation.ts} | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) rename electron-main/{zipOperation.js => zipOperation.ts} (80%) diff --git a/electron-main/zipOperation.js b/electron-main/zipOperation.ts similarity index 80% rename from electron-main/zipOperation.js rename to electron-main/zipOperation.ts index 2037ef52..8b29625f 100644 --- a/electron-main/zipOperation.js +++ b/electron-main/zipOperation.ts @@ -5,10 +5,10 @@ import crypto from "node:crypto"; import fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; -import { getSaveFolder, readUserSettings } from "./filePath.js"; +import { getSaveFolder } from "./filePath"; // Function to calculate the SHA-256 hash of a file -const calculateFileHash = (filePath) => { +const calculateFileHash = (filePath: string) => { return new Promise((resolve, reject) => { const hash = crypto.createHash("sha256"); const stream = fs.createReadStream(filePath); @@ -18,7 +18,7 @@ const calculateFileHash = (filePath) => { }); }; -const fileVerification = async (event, zipContents, extractedFolder) => { +const fileVerification = async (event: any, zipContents: any, extractedFolder: string) => { // Verify existing extracted files const totalFiles = zipContents[0].extractedFiles.length; let filesProcessed = 0; @@ -81,7 +81,7 @@ const fileVerification = async (event, zipContents, extractedFolder) => { const verifyAndExtractIPC = () => { ipcMain.on("verify-and-extract", async (event, zipFileData) => { const { zipFile, zipHash, zipContents } = zipFileData; - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const videoFolder = path.join(saveFolder, "video_files"); const zipFilePath = path.join(videoFolder, zipFile); const extractedFolder = path.join(videoFolder, zipFile.replace(".7z", "")); @@ -101,7 +101,7 @@ const verifyAndExtractIPC = () => { applog.log(`Starting verification for ${zipFile}`); try { const js7z = await JS7z({ - print: (text) => { + print: (text: string) => { console.log(`7-Zip output: ${text}`); applog.log(`7-Zip output: ${text}`); if (text.includes("%")) { @@ -112,17 +112,17 @@ const verifyAndExtractIPC = () => { } } }, - printErr: (errText) => { + printErr: (errText: string) => { console.error(`7-Zip error: ${errText}`); applog.error(`7-Zip error: ${errText}`); event.sender.send("verification-error", `7-Zip error: ${errText}`); }, - onAbort: (reason) => { + onAbort: (reason: string) => { console.error(`7-Zip aborted: ${reason}`); applog.error(`7-Zip aborted: ${reason}`); event.sender.send("verification-error", `7-Zip aborted: ${reason}`); }, - onExit: (exitCode) => { + onExit: (exitCode: number) => { if (exitCode === 0) { console.log(`7-Zip exited successfully with code ${exitCode}`); applog.log(`7-Zip exited successfully with code ${exitCode}`); @@ -168,47 +168,49 @@ const verifyAndExtractIPC = () => { "progress-text", "settingPage.videoDownloadSettings.electronVerifyMessage.zipExtractingMsg" ); - js7z.callMain(["x", emZipFilePath, `-o${emExtractedFolder}`]); - - js7z.onExit = async (exitCode) => { - if (exitCode !== 0) { - applog.error(`Error extracting ${zipFile}`); - event.sender.send("verification-error", { - messageKey: - "settingPage.videoDownloadSettings.electronVerifyMessage.zipErrorMsg", - param: zipFile, - }); - return; - } - - console.log(`Extraction successful for ${zipFile}`); - applog.log(`Extraction successful for ${zipFile}`); - - // Step 3: Verifying extracted files - event.sender.send( - "progress-text", - "settingPage.videoDownloadSettings.verifyinProgressMsg" - ); - await fileVerification(event, zipContents, extractedFolder); - - // Clean up the ZIP file after successful extraction and verification - try { - await fsPromises.unlink(zipFilePath); - console.log(`Deleted ZIP file: ${zipFilePath}`); - applog.log(`Extraction successful for ${zipFile}`); - } catch (err) { - console.error(`Failed to delete ZIP file: ${err.message}`); - applog.error(`Failed to delete ZIP file: ${err.message}`); - } - - event.sender.send("verification-success", { + const extractionResult = await js7z.callMain([ + "x", + emZipFilePath, + `-o${emExtractedFolder}`, + ]); + + if (extractionResult !== 0) { + applog.error(`Error extracting ${zipFile}`); + event.sender.send("verification-error", { messageKey: - "settingPage.videoDownloadSettings.electronVerifyMessage.zipSuccessMsg", + "settingPage.videoDownloadSettings.electronVerifyMessage.zipErrorMsg", param: zipFile, }); - applog.log(`Successfully verified and extracted ${zipFile}`); - }; - } catch (err) { + return; + } + + console.log(`Extraction successful for ${zipFile}`); + applog.log(`Extraction successful for ${zipFile}`); + + // Step 3: Verifying extracted files + event.sender.send( + "progress-text", + "settingPage.videoDownloadSettings.verifyinProgressMsg" + ); + await fileVerification(event, zipContents, extractedFolder); + + // Clean up the ZIP file after successful extraction and verification + try { + await fsPromises.unlink(zipFilePath); + console.log(`Deleted ZIP file: ${zipFilePath}`); + applog.log(`Extraction successful for ${zipFile}`); + } catch (err: any) { + console.error(`Failed to delete ZIP file: ${err.message}`); + applog.error(`Failed to delete ZIP file: ${err.message}`); + } + + event.sender.send("verification-success", { + messageKey: + "settingPage.videoDownloadSettings.electronVerifyMessage.zipSuccessMsg", + param: zipFile, + }); + applog.log(`Successfully verified and extracted ${zipFile}`); + } catch (err: any) { console.error(`Error processing ${zipFile}: ${err.message}`); event.sender.send("verification-error", { messageKey: From 0e72c318cadf6b5f1472f87a994ee4f8a4cb7803 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 14:02:43 +0700 Subject: [PATCH 013/199] Create tsconfig.json --- tsconfig.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b9c2ad86 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["DOM", "ESNext"], + "jsx": "react-jsx", + "moduleResolution": "Node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["main.ts", "src", "electron-main", "preload.ts"], + "exclude": ["node_modules", "dist", "out", "data/ffmpeg"] +} From 3447f50e419c8cd769db985d374c3aeaec54686e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 14:02:53 +0700 Subject: [PATCH 014/199] convert to ts part 12 --- vite.config.js => vite.config.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vite.config.js => vite.config.ts (100%) diff --git a/vite.config.js b/vite.config.ts similarity index 100% rename from vite.config.js rename to vite.config.ts From 7c511c259e648f06b9b1b1cf397f5aa34a7fc71a Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 14:10:05 +0700 Subject: [PATCH 015/199] convert to ts part 13 --- electron-main/createWindow.ts | 2 +- electron-main/expressServer.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/electron-main/createWindow.ts b/electron-main/createWindow.ts index 968825b5..5d392f31 100644 --- a/electron-main/createWindow.ts +++ b/electron-main/createWindow.ts @@ -72,7 +72,7 @@ const createWindow = (rootDir: string, onServerReady: (srv: any) => void) => { mainWindow.show(); } // Start Express server in the background after main window is shown - startExpressServer().then((srv) => { + startExpressServer().then((srv: any) => { if (onServerReady) onServerReady(srv); }); }, 500); diff --git a/electron-main/expressServer.ts b/electron-main/expressServer.ts index 780da3ee..3e38afd6 100644 --- a/electron-main/expressServer.ts +++ b/electron-main/expressServer.ts @@ -14,11 +14,11 @@ const getRandomPort = () => { return Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT; }; +const server = net.createServer(); + // Function to check if a port is available const checkPortAvailability = (port: number) => { return new Promise((resolve, reject) => { - const server = net.createServer(); - server.once("error", (err: any) => { if (err.code === "EADDRINUSE" || err.code === "ECONNREFUSED") { resolve(false); // Port is in use @@ -62,4 +62,4 @@ const startExpressServer = async () => { }); }; -export { expressApp, startExpressServer }; +export { expressApp, server, startExpressServer }; From da2ee65b8c126027c84a88e35b6ae8bc784f8aa3 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 14:17:18 +0700 Subject: [PATCH 016/199] convert to ts part 14 --- main.js => main.ts | 74 +++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 40 deletions(-) rename main.js => main.ts (86%) diff --git a/main.js b/main.ts similarity index 86% rename from main.js rename to main.ts index e85bd389..2d8289cd 100644 --- a/main.js +++ b/main.ts @@ -10,22 +10,22 @@ import process from "node:process"; import { fileURLToPath } from "node:url"; import { Conf } from "electron-conf/main"; -import { createSplashWindow, createWindow } from "./electron-main/createWindow.js"; -import { setCustomSaveFolderIPC } from "./electron-main/customFolderLocationOperation.js"; -import { expressApp } from "./electron-main/expressServer.js"; -import { getLogFolder, getSaveFolder, readUserSettings } from "./electron-main/filePath.js"; +import { createSplashWindow, createWindow } from "./electron-main/createWindow"; +import { setCustomSaveFolderIPC } from "./electron-main/customFolderLocationOperation"; +import { expressApp } from "./electron-main/expressServer"; +import { getLogFolder, getSaveFolder } from "./electron-main/filePath"; import { getCustomSaveFolderIPC, getFfmpegWasmPathIPC, getSaveFolderIPC, getVideoFileDataIPC, getVideoSaveFolderIPC, -} from "./electron-main/getFileAndFolder.js"; +} from "./electron-main/getFileAndFolder"; import { getCurrentLogSettings, manageLogFiles, setCurrentLogSettings, -} from "./electron-main/logOperations.js"; +} from "./electron-main/logOperations"; import { cancelProcess, checkPythonInstalled, @@ -34,17 +34,13 @@ import { killCurrentPythonProcess, resetGlobalCancel, setupPronunciationInstallStatusIPC, -} from "./electron-main/pronunciationOperations.js"; -import { checkDownloads, checkExtractedFolder } from "./electron-main/videoFileOperations.js"; -import { verifyAndExtractIPC } from "./electron-main/zipOperation.js"; +} from "./electron-main/pronunciationOperations"; +import { checkDownloads, checkExtractedFolder } from "./electron-main/videoFileOperations"; +import { verifyAndExtractIPC } from "./electron-main/zipOperation"; import { setupPronunciationCheckerIPC, setupGetRecordingBlobIPC, -} from "./electron-main/pronunciationCheckerIPC.js"; - -const DEFAULT_PORT = 8998; - -let server; // Declare server at the top so it's in scope for all uses +} from "./electron-main/pronunciationCheckerIPC"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -78,7 +74,7 @@ expressApp.use(cors({ origin: "http://localhost:5173" })); // Set up the express server to serve video files expressApp.get("/video/:folderName/:fileName", async (req, res) => { const { folderName, fileName } = req.params; - const documentsPath = await getSaveFolder(readUserSettings); + const documentsPath = await getSaveFolder(); const videoFolder = path.resolve(documentsPath, "video_files", folderName); const videoFilePath = path.resolve(videoFolder, fileName); @@ -130,7 +126,7 @@ ipcMain.handle("open-external-link", async (event, url) => { // Handle saving a recording ipcMain.handle("save-recording", async (event, key, arrayBuffer) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const recordingFolder = path.join(saveFolder, "saved_recordings"); const filePath = path.join(recordingFolder, `${key}.wav`); @@ -155,7 +151,7 @@ ipcMain.handle("save-recording", async (event, key, arrayBuffer) => { // Handle checking if a recording exists ipcMain.handle("check-recording-exists", async (event, key) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); const filePath = path.join(saveFolder, "saved_recordings", `${key}.wav`); try { @@ -168,11 +164,7 @@ ipcMain.handle("check-recording-exists", async (event, key) => { // Handle playing a recording (this can be improved for streaming) ipcMain.handle("play-recording", async (event, key) => { - const filePath = path.join( - await getSaveFolder(readUserSettings), - "saved_recordings", - `${key}.wav` - ); + const filePath = path.join(await getSaveFolder(), "saved_recordings", `${key}.wav`); // Check if the file exists try { @@ -200,21 +192,16 @@ checkExtractedFolder(); /* End video file operations */ -// IPC event to get the current server port -ipcMain.handle("get-port", () => { - return server?.address()?.port || DEFAULT_PORT; -}); - ipcMain.handle("open-log-folder", async () => { // Open the folder in the file manager - const logFolder = await getLogFolder(readUserSettings); + const logFolder = await getLogFolder(); await shell.openPath(logFolder); // Open the folder return logFolder; // Send the path back to the renderer }); ipcMain.handle("open-recording-folder", async () => { // Open the folder in the file manager - const recordingFolder = await getSaveFolder(readUserSettings); + const recordingFolder = await getSaveFolder(); const recordingFolderPath = path.join(recordingFolder, "saved_recordings"); try { await fsPromises.access(recordingFolderPath); @@ -231,8 +218,8 @@ verifyAndExtractIPC(); // Listen for logging messages from the renderer process ipcMain.on("renderer-log", (event, logMessage) => { const { level, message } = logMessage; - if (applog[level]) { - applog[level](`Renderer log: ${message}`); + if (typeof level === "string" && typeof applog[level as keyof typeof applog] === "function") { + (applog as any)[level](`Renderer log: ${message}`); } }); @@ -250,7 +237,7 @@ process.on("unhandledRejection", (reason, promise) => { app.quit(); // Quit the app on an unhandled promise rejection }); -app.on("renderer-process-crashed", (event, webContents, killed) => { +app.on("renderer-process-crashed", (event: any, killed: any) => { applog.error("Renderer process crashed", { event, killed }); app.quit(); }); @@ -265,8 +252,8 @@ app.on("window-all-closed", () => { // Recreate the window on macOS when the dock icon is clicked. app.on("activate", () => { if (mainWindow === null) { - createWindow(__dirname, (srv) => { - server = srv; + createWindow(__dirname, (srv: any) => { + srv; }); } }); @@ -285,8 +272,8 @@ if (!gotTheLock) { // 2. Start heavy work in parallel after splash is shown setImmediate(() => { // Create main window (can be shown after splash) - createWindow(__dirname, (srv) => { - server = srv; + createWindow(__dirname, (srv: any) => { + srv; }); // Wait for log settings and manage logs in background @@ -323,8 +310,11 @@ setCustomSaveFolderIPC(); // IPC: Show open dialog for folder selection ipcMain.handle("show-open-dialog", async (event, options) => { const win = BrowserWindow.getFocusedWindow(); - const result = await dialog.showOpenDialog(win, options); - return result.filePaths; + if (!win) { + throw new Error("No focused window found"); + } + const filePaths = dialog.showOpenDialog(win, options); + return filePaths; }); ipcMain.handle("get-log-settings", async () => { @@ -345,7 +335,11 @@ console.log = (...args) => { ipcMain.handle("check-python-installed", async () => { try { - const result = await checkPythonInstalled(); + const result = (await checkPythonInstalled()) as { + found: boolean; + version: string | null; + stderr: string | null; + }; if (result.found) { applog.info("Python found:", result.version); } else { @@ -375,7 +369,7 @@ ipcMain.handle("pronunciation-reset-cancel-flag", async () => { /* End pronunciation checker operations */ ipcMain.handle("get-recording-path", async (_event, wordKey) => { - const saveFolder = await getSaveFolder(readUserSettings); + const saveFolder = await getSaveFolder(); return path.join(saveFolder, "saved_recordings", `${wordKey}.wav`); }); From e1a22b1df3be6e1b59b56c6e01a78c5e9a3e2cbd Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 15:43:26 +0700 Subject: [PATCH 017/199] update express server code --- electron-main/createWindow.ts | 6 ++--- electron-main/expressServer.ts | 44 ++++++++++++++-------------------- main.ts | 16 +++++-------- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/electron-main/createWindow.ts b/electron-main/createWindow.ts index 5d392f31..f1e53355 100644 --- a/electron-main/createWindow.ts +++ b/electron-main/createWindow.ts @@ -41,7 +41,7 @@ const createSplashWindow = (rootDir: string, ipcMain: any, conf: any) => { }); }; -const createWindow = (rootDir: string, onServerReady: (srv: any) => void) => { +const createWindow = (rootDir: string) => { mainWindow = new BrowserWindow({ width: 1280, height: 720, @@ -72,9 +72,7 @@ const createWindow = (rootDir: string, onServerReady: (srv: any) => void) => { mainWindow.show(); } // Start Express server in the background after main window is shown - startExpressServer().then((srv: any) => { - if (onServerReady) onServerReady(srv); - }); + startExpressServer(); }, 500); }); diff --git a/electron-main/expressServer.ts b/electron-main/expressServer.ts index 3e38afd6..8dde0292 100644 --- a/electron-main/expressServer.ts +++ b/electron-main/expressServer.ts @@ -1,50 +1,35 @@ import { ipcMain } from "electron"; import applog from "electron-log"; import express from "express"; -import net, { AddressInfo } from "net"; +import net from "net"; const DEFAULT_PORT = 8998; const MIN_PORT = 1024; // Minimum valid port number const MAX_PORT = 65535; // Maximum valid port number -// Create Express server const expressApp = express(); -// Function to generate a random port number within the range -const getRandomPort = () => { - return Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT; -}; +let currentPort: number | null = null; +let httpServer: any = null; -const server = net.createServer(); +const getRandomPort = () => Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT; -// Function to check if a port is available const checkPortAvailability = (port: number) => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { + const server = net.createServer(); server.once("error", (err: any) => { if (err.code === "EADDRINUSE" || err.code === "ECONNREFUSED") { - resolve(false); // Port is in use - applog.log("Port is in use. Error:", err.code); + resolve(false); } else { - reject(err); // Some other error occurred - applog.log("Another error related to the Express server. Error:", err.code); + reject(err); } }); - server.once("listening", () => { - server.close(() => { - resolve(true); // Port is available - }); + server.close(() => resolve(true)); }); - server.listen(port); - - // IPC event to get the current server port - ipcMain.handle("get-port", () => { - return (server.address() as AddressInfo)?.port || DEFAULT_PORT; - }); }); }; -// Function to start the Express server with the default port first, then randomize if necessary const startExpressServer = async () => { let port = DEFAULT_PORT; let isPortAvailable = await checkPortAvailability(port); @@ -57,9 +42,16 @@ const startExpressServer = async () => { } while (!isPortAvailable); } - return expressApp.listen(port, () => { + httpServer = expressApp.listen(port, () => { + currentPort = port; applog.info(`Express server is running on http://localhost:${port}`); }); + return httpServer; }; -export { expressApp, server, startExpressServer }; +const getExpressPort = () => currentPort; + +// IPC handler for renderer to get port +ipcMain.handle("get-port", () => getExpressPort()); + +export { expressApp, getExpressPort, startExpressServer }; diff --git a/main.ts b/main.ts index 2d8289cd..9ca1b702 100644 --- a/main.ts +++ b/main.ts @@ -26,6 +26,10 @@ import { manageLogFiles, setCurrentLogSettings, } from "./electron-main/logOperations"; +import { + setupGetRecordingBlobIPC, + setupPronunciationCheckerIPC, +} from "./electron-main/pronunciationCheckerIPC"; import { cancelProcess, checkPythonInstalled, @@ -37,10 +41,6 @@ import { } from "./electron-main/pronunciationOperations"; import { checkDownloads, checkExtractedFolder } from "./electron-main/videoFileOperations"; import { verifyAndExtractIPC } from "./electron-main/zipOperation"; -import { - setupPronunciationCheckerIPC, - setupGetRecordingBlobIPC, -} from "./electron-main/pronunciationCheckerIPC"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -252,9 +252,7 @@ app.on("window-all-closed", () => { // Recreate the window on macOS when the dock icon is clicked. app.on("activate", () => { if (mainWindow === null) { - createWindow(__dirname, (srv: any) => { - srv; - }); + createWindow(__dirname); } }); @@ -272,9 +270,7 @@ if (!gotTheLock) { // 2. Start heavy work in parallel after splash is shown setImmediate(() => { // Create main window (can be shown after splash) - createWindow(__dirname, (srv: any) => { - srv; - }); + createWindow(__dirname); // Wait for log settings and manage logs in background ipcMain.once("update-log-settings", (event, settings) => { From be58b27c8afb7e2aef019b20987c472eb83ec019 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 15:52:05 +0700 Subject: [PATCH 018/199] convert forge config to ts --- forge.config.cjs => forge.config.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) rename forge.config.cjs => forge.config.ts (88%) diff --git a/forge.config.cjs b/forge.config.ts similarity index 88% rename from forge.config.cjs rename to forge.config.ts index 13dd9dd4..60d14b1a 100644 --- a/forge.config.cjs +++ b/forge.config.ts @@ -1,8 +1,9 @@ -const { FusesPlugin } = require("@electron-forge/plugin-fuses"); -const { FuseV1Options, FuseVersion } = require("@electron/fuses"); -const path = require("path"); +import { FusesPlugin } from "@electron-forge/plugin-fuses"; +import type { ForgeConfig } from "@electron-forge/shared-types"; +import { FuseV1Options, FuseVersion } from "@electron/fuses"; +import path from "node:path"; -module.exports = { +const config: ForgeConfig = { packagerConfig: { asar: true, name: "iSpeakerReact", @@ -30,6 +31,7 @@ module.exports = { "^/public$", "^/src$", "^/netlify.toml$", + "^/\\.ts*$", // Ignore typescript ], }, rebuildConfig: {}, @@ -37,15 +39,8 @@ module.exports = { { name: "@electron-forge/maker-zip", platforms: ["linux", "win32", "darwin"], + config: {}, }, - /*{ - name: "@electron-forge/maker-dmg", - config: { - options: { - icon: path.join(__dirname, "dist", "appicon.icns"), - }, - }, - },*/ { name: "@electron-forge/maker-deb", config: { @@ -56,8 +51,11 @@ module.exports = { }, { name: "@electron-forge/maker-rpm", - options: { - icon: path.join(__dirname, "dist", "appicon.png"), + platforms: ["linux"], + config: { + options: { + icon: path.join(__dirname, "dist", "appicon.png"), + }, }, }, /*{ @@ -87,3 +85,5 @@ module.exports = { }), ], }; + +export default config; From 51e84df9afaeda8162b640f00f1d39f30dff410f Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 16:10:18 +0700 Subject: [PATCH 019/199] fix lint error right in tsconfig lol --- tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index b9c2ad86..291ff274 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { - "target": "ES2020", - "module": "ESNext", + "target": "ESNext", + "module": "NodeNext", "lib": ["DOM", "ESNext"], "jsx": "react-jsx", - "moduleResolution": "Node", + "moduleResolution": "NodeNext", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, From 9f1ab04fdb486f9c24137bea1ace2e16409766ff Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 16:14:29 +0700 Subject: [PATCH 020/199] format eslint config file --- eslint.config.js | 67 +++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 238d2e4e..c6b0565b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,38 +1,35 @@ -import js from '@eslint/js' -import globals from 'globals' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' +import js from "@eslint/js"; +import globals from "globals"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; export default [ - { ignores: ['dist'] }, - { - files: ['**/*.{js,jsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, + { ignores: ["dist"] }, + { + files: ["**/*.{js,jsx,ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: "latest", + ecmaFeatures: { jsx: true }, + sourceType: "module", + }, + }, + settings: { react: { version: "detect" } }, + plugins: { + react, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + ...reactHooks.configs.recommended.rules, + "react/jsx-no-target-blank": "off", + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, }, - settings: { react: { version: '18.3' } }, - plugins: { - react, - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...js.configs.recommended.rules, - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - ...reactHooks.configs.recommended.rules, - 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - }, -] +]; From d529eca328b1164e4ef090b2c9e3fd2dc5450a38 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 16:14:42 +0700 Subject: [PATCH 021/199] fix lint errors --- vite.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 0b4c90cf..5f3519a1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,6 @@ import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react-swc"; +import process from "node:process"; import { visualizer } from "rollup-plugin-visualizer"; import { defineConfig } from "vite"; import { VitePWA } from "vite-plugin-pwa"; @@ -111,7 +112,7 @@ export default defineConfig(({ mode }) => { }, ], id: "io.yllsttestinglabs.ispeakerreact", - dir: "auto", + dir: "ltr", orientation: "any", categories: ["education"], prefer_related_applications: false, From a91d6f569d36e8650ae9f0047d185a821c053aa7 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 16:55:29 +0700 Subject: [PATCH 022/199] Update tsconfig.json --- tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 291ff274..587cbac4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", + "target": "ES2024", + "module": "ESNext", "lib": ["DOM", "ESNext"], "jsx": "react-jsx", - "moduleResolution": "NodeNext", + "moduleResolution": "Node16", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, From a1ade74ab5e1adc6a7d8202c27e2cf4d97c94995 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 16:55:34 +0700 Subject: [PATCH 023/199] Update eslint.config.js --- eslint.config.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index c6b0565b..31a0cab6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,11 +1,16 @@ -import js from "@eslint/js"; -import globals from "globals"; +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; import react from "eslint-plugin-react"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; -export default [ - { ignores: ["dist"] }, +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.strict, + tseslint.configs.stylistic, { files: ["**/*.{js,jsx,ts,tsx}"], languageOptions: { @@ -24,12 +29,11 @@ export default [ "react-refresh": reactRefresh, }, rules: { - ...js.configs.recommended.rules, ...react.configs.recommended.rules, ...react.configs["jsx-runtime"].rules, ...reactHooks.configs.recommended.rules, "react/jsx-no-target-blank": "off", "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, - }, -]; + } +); From 473bfff037cdd077f6c07736fb30a651545c1c08 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 16:55:55 +0700 Subject: [PATCH 024/199] update videoFileOperation to conform eslint rule --- electron-main/videoFileOperations.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/electron-main/videoFileOperations.ts b/electron-main/videoFileOperations.ts index 8f622bd6..5731c498 100644 --- a/electron-main/videoFileOperations.ts +++ b/electron-main/videoFileOperations.ts @@ -16,7 +16,7 @@ const checkDownloads = () => { const files = await fsPromises.readdir(videoFolder); // Return the list of zip files in the download folder - const zipFiles = files.filter((file: string) => file.endsWith(".7z")); + const zipFiles = files.filter((file) => file.endsWith(".7z")); return zipFiles.length === 0 ? "no zip files downloaded" : zipFiles; }); }; @@ -33,9 +33,11 @@ const checkExtractedFolder = () => { const extractedFiles = await fsPromises.readdir(extractedFolder); // Check if all expected files are present in the extracted folder - const allFilesExtracted = zipContents[0].extractedFiles.every((file: any) => { - return extractedFiles.includes(file.name); - }); + const allFilesExtracted = zipContents[0].extractedFiles.every( + (file: { name: string }) => { + return extractedFiles.includes(file.name); + } + ); event.sender.send("progress-update", 0); From 983fd18105d0e3fb61a40089d95b87229edc3611 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 17:08:10 +0700 Subject: [PATCH 025/199] update pronunciationOperations to conform eslint rule --- electron-main/pronunciationOperations.ts | 194 ++++++++++++++--------- 1 file changed, 116 insertions(+), 78 deletions(-) diff --git a/electron-main/pronunciationOperations.ts b/electron-main/pronunciationOperations.ts index d6b09fca..8b50d330 100644 --- a/electron-main/pronunciationOperations.ts +++ b/electron-main/pronunciationOperations.ts @@ -7,7 +7,7 @@ import path from "node:path"; import process from "node:process"; import { getSaveFolder, settingsConf } from "./filePath.js"; -let currentPythonProcess: any = null; +let currentPythonProcess: ReturnType | null = null; let pendingCancel = false; let isGloballyCancelled = false; @@ -18,7 +18,10 @@ const checkPythonInstalled = async () => { let log = ""; return new Promise((resolve) => { // Try python3 first - const tryPython = (cmd: string, cb: (err: any, stdout: string, stderr: string) => void) => { + const tryPython = ( + cmd: string, + cb: (err: number | null, stdout: string, stderr: string) => void + ) => { const proc = spawn(cmd, ["--version"], { shell: true }); let stdout = ""; let stderr = ""; @@ -65,7 +68,11 @@ const checkPythonInstalled = async () => { }); }; -const startProcess = (cmd: string, args: string[], onExit: (err: any) => void) => { +const startProcess = ( + cmd: string, + args: string[], + onExit: (err: { code: number } | null) => void +): ReturnType => { const proc = spawn(cmd, args, { shell: true }); currentPythonProcess = proc; if (pendingCancel) { @@ -78,20 +85,26 @@ const startProcess = (cmd: string, args: string[], onExit: (err: any) => void) = "[Cancel] Process killed after short delay due to pending cancel." ); }) - .catch((err: any) => { - if (err.message && err.message.includes("Process doesn't exist")) { + .catch((err: unknown) => { + if ( + err && + typeof err === "object" && + "message" in err && + typeof (err as { message: string }).message === "string" && + (err as { message: string }).message.includes("Process doesn't exist") + ) { // Already dead, ignore } else { console.error("[Cancel] Error killing process after delay:", err); } }); } - (proc as any)._wasCancelledImmediately = true; // Mark for downstream logic + (proc as { _wasCancelledImmediately?: boolean })._wasCancelledImmediately = true; // Mark for downstream logic }, 500); // 0.5 second delay pendingCancel = false; } proc.on("close", (code) => { - onExit && onExit(code !== 0 ? { code } : null); + if (onExit) onExit(code !== 0 ? { code: code ?? -1 } : null); }); return proc; }; @@ -160,7 +173,7 @@ const upgradeVenvPip = async () => { const ensureVenvExists = async () => { const venvDir = await getVenvDir(); - let venvPython = await getVenvPythonPath(); + const venvPython = await getVenvPythonPath(); try { await fsPromises.access(venvPython); // Already exists @@ -210,11 +223,11 @@ const installDependencies = () => { try { await upgradeVenvPip(); log += "Upgraded pip to latest version.\n"; - } catch (err: any) { - log += `Failed to upgrade pip: ${err.message}\n`; + } catch (err) { + log += `Failed to upgrade pip: ${(err as Error).message}\n`; } - } catch (err: any) { - log += `Failed to create virtual environment: ${err.message}\n`; + } catch (err) { + log += `Failed to create virtual environment: ${(err as Error).message}\n`; event.sender.send("pronunciation-dep-progress", { name: "all", status: "error", @@ -224,7 +237,7 @@ const installDependencies = () => { } return new Promise((resolve) => { const pipArgs = ["install", ...dependencies, "-U"]; - const pipProcess = startProcess(venvPip, pipArgs, (err: any) => { + const pipProcess = startProcess(venvPip, pipArgs, (err) => { const status = err ? "error" : "success"; event.sender.send("pronunciation-dep-progress", { name: "all", @@ -233,22 +246,26 @@ const installDependencies = () => { }); resolve({ deps: [{ name: "all", status }], log }); }); - pipProcess.stdout.on("data", (data) => { - log += data.toString(); - event.sender.send("pronunciation-dep-progress", { - name: "all", - status: "pending", - log: log.trim(), + if (pipProcess.stdout) { + pipProcess.stdout.on("data", (data) => { + log += data.toString(); + event.sender.send("pronunciation-dep-progress", { + name: "all", + status: "pending", + log: log.trim(), + }); }); - }); - pipProcess.stderr.on("data", (data) => { - log += data.toString(); - event.sender.send("pronunciation-dep-progress", { - name: "all", - status: "pending", - log: log.trim(), + } + if (pipProcess.stderr) { + pipProcess.stderr.on("data", (data) => { + log += data.toString(); + event.sender.send("pronunciation-dep-progress", { + name: "all", + status: "pending", + log: log.trim(), + }); }); - }); + } }); }); }; @@ -257,7 +274,7 @@ const installDependencies = () => { const downloadModelToDir = async ( modelDir: string, modelName: string, - onProgress: (msg: any) => void + onProgress: ((msg: Record) => void) | undefined ) => { if (isGloballyCancelled) { if (onProgress) onProgress({ status: "cancelled", message: "Cancelled before start" }); @@ -274,13 +291,16 @@ const downloadModelToDir = async ( try { await ensureVenvExists(); venvPython = await getVenvPythonPath(); - } catch (err: any) { + } catch (err) { if (onProgress) onProgress({ status: "error", - message: `Failed to create virtual environment: ${err.message}`, + message: `Failed to create virtual environment: ${(err as Error).message}`, }); - return { status: "error", message: `Failed to create virtual environment: ${err.message}` }; + return { + status: "error", + message: `Failed to create virtual environment: ${(err as Error).message}`, + }; } // Write Python code to temp file in save folder const pyCode = ` @@ -413,40 +433,44 @@ else: } }); // If process was cancelled immediately, resolve and do not proceed - if ((py as any)._wasCancelledImmediately) { + if ((py as { _wasCancelledImmediately?: boolean })._wasCancelledImmediately) { if (onProgress) onProgress({ status: "cancelled", message: "Process cancelled before start" }); resolve({ status: "cancelled", message: "Process cancelled before start" }); return; } - let lastStatus: any = null; + let lastStatus: Record | null = null; let hadError = false; - py.stdout.on("data", (data) => { - const str = data.toString(); - // Always forward raw output to renderer for logging - if (onProgress) onProgress({ status: "log", message: str }); - // Try to parse JSON lines as before - str.split(/\r?\n/).forEach((line: string) => { - if (line.trim()) { - try { - const msg = JSON.parse(line); - lastStatus = msg; - if (msg.status === "error") { - hadError = true; - if (onProgress) onProgress(msg); - } else { - if (onProgress) onProgress(msg); + if (py.stdout) { + py.stdout.on("data", (data) => { + const str = data.toString(); + // Always forward raw output to renderer for logging + if (onProgress) onProgress({ status: "log", message: str }); + // Try to parse JSON lines as before + str.split(/\r?\n/).forEach((line: string) => { + if (line.trim()) { + try { + const msg = JSON.parse(line); + lastStatus = msg; + if (msg.status === "error") { + hadError = true; + if (onProgress) onProgress(msg); + } else { + if (onProgress) onProgress(msg); + } + } catch { + // Ignore parse errors for non-JSON lines } - } catch { - // Ignore parse errors for non-JSON lines } - } + }); }); - }); - py.stderr.on("data", (data) => { - // Only log stderr, do not send error status unless process exits with error - if (onProgress) onProgress({ status: "log", message: data.toString() }); - }); + } + if (py.stderr) { + py.stderr.on("data", (data) => { + // Only log stderr, do not send error status unless process exits with error + if (onProgress) onProgress({ status: "log", message: data.toString() }); + }); + } py.on("exit", (code) => { currentPythonProcess = null; fsPromises.unlink(tempPyPath).catch(() => { @@ -471,23 +495,28 @@ else: }; const downloadModel = () => { - ipcMain.handle("pronunciation-download-model", async (event, modelName) => { + ipcMain.handle("pronunciation-download-model", async (event, modelName: string) => { const saveFolder = await getSaveFolder(); // Replace / with _ for folder name const safeModelFolder = modelName.replace(/\//g, "_"); const modelDir = path.join(saveFolder, "phoneme-model", safeModelFolder); // Forward progress to renderer - const finalStatus = await downloadModelToDir(modelDir, modelName, (msg: any) => { - event.sender.send("pronunciation-model-progress", msg); - }); + const finalStatus = await downloadModelToDir( + modelDir, + modelName, + (msg: Record) => { + event.sender.send("pronunciation-model-progress", msg); + } + ); // After successful download, update user settings with modelName console.log( `[PronunciationOperations] finalStatus: ${JSON.stringify(finalStatus, null, 2)}` ); if ( finalStatus && - typeof (finalStatus as any).status === "string" && - ((finalStatus as any).status === "success" || (finalStatus as any).status === "found") + typeof (finalStatus as { status?: string }).status === "string" && + ((finalStatus as { status?: string }).status === "success" || + (finalStatus as { status?: string }).status === "found") ) { settingsConf.set("modelName", modelName); console.log(`[PronunciationOperations] modelName updated to ${modelName}`); @@ -507,9 +536,13 @@ const cancelProcess = () => { ); if (currentPythonProcess) { try { - await fkill(currentPythonProcess.pid, { force: true, tree: true }); - console.log("[Cancel] Python process tree killed with fkill."); - } catch (err: any) { + if (typeof currentPythonProcess.pid === "number") { + await fkill(currentPythonProcess.pid, { force: true, tree: true }); + console.log("[Cancel] Python process tree killed with fkill."); + } else { + console.log("[Cancel] No valid pid to kill."); + } + } catch (err) { console.error("[Cancel] Error killing Python process tree:", err); } currentPythonProcess = null; @@ -529,9 +562,13 @@ const cancelProcess = () => { const killCurrentPythonProcess = async () => { if (currentPythonProcess) { try { - await fkill(currentPythonProcess.pid, { force: true, tree: true }); - console.log("[Cleanup] Python process tree killed on app quit."); - } catch (err: any) { + if (typeof currentPythonProcess.pid === "number") { + await fkill(currentPythonProcess.pid, { force: true, tree: true }); + console.log("[Cleanup] Python process tree killed on app quit."); + } else { + console.log("[Cleanup] No valid pid to kill."); + } + } catch (err) { console.error("[Cleanup] Error killing Python process tree on app quit:", err); } currentPythonProcess = null; @@ -572,30 +609,31 @@ const setupPronunciationInstallStatusIPC = () => { }; // Helper to migrate old flat status to new structured format -const migrateOldStatusToStructured = (status: any) => { - if (!status) return null; +const migrateOldStatusToStructured = (status: unknown) => { + if (!status || typeof status !== "object" || status === null) return null; + const s = status as Record; // Try to extract python info const python = { - found: status.found, - version: status.version, + found: s.found, + version: s.version, }; // Dependencies: if array, use as is; if single dep, wrap in array - let dependencies = status.deps; + let dependencies = s.deps; if (!Array.isArray(dependencies)) { dependencies = dependencies ? [dependencies] : []; } // Model info const model = { - status: status.modelStatus || status.status, - message: status.modelMessage || status.message, - log: status.modelLog || "", + status: s.modelStatus || s.status, + message: s.modelMessage || s.message, + log: s.modelLog || "", }; return { python, dependencies, model, - stderr: status.stderr || "", - log: status.log || "", + stderr: s.stderr || "", + log: s.log || "", timestamp: Date.now(), }; }; From 22aa33a72f99db97c95b9f6b9204b66db219b3a8 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 17:17:51 +0700 Subject: [PATCH 026/199] update main.ts to conform eslint rule --- main.ts | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/main.ts b/main.ts index 9ca1b702..d1b46f00 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,8 @@ /* global setImmediate */ // for eslint because setImmediate is node global import cors from "cors"; +import type { IpcMainEvent } from "electron"; import { app, BrowserWindow, dialog, ipcMain, shell } from "electron"; +import type { LogFunctions, LogLevel } from "electron-log"; import applog from "electron-log"; import { Buffer } from "node:buffer"; import fs from "node:fs"; @@ -10,26 +12,26 @@ import process from "node:process"; import { fileURLToPath } from "node:url"; import { Conf } from "electron-conf/main"; -import { createSplashWindow, createWindow } from "./electron-main/createWindow"; -import { setCustomSaveFolderIPC } from "./electron-main/customFolderLocationOperation"; -import { expressApp } from "./electron-main/expressServer"; -import { getLogFolder, getSaveFolder } from "./electron-main/filePath"; +import { createSplashWindow, createWindow } from "./electron-main/createWindow.js"; +import { setCustomSaveFolderIPC } from "./electron-main/customFolderLocationOperation.js"; +import { expressApp } from "./electron-main/expressServer.js"; +import { getLogFolder, getSaveFolder } from "./electron-main/filePath.js"; import { getCustomSaveFolderIPC, getFfmpegWasmPathIPC, getSaveFolderIPC, getVideoFileDataIPC, getVideoSaveFolderIPC, -} from "./electron-main/getFileAndFolder"; +} from "./electron-main/getFileAndFolder.js"; import { getCurrentLogSettings, manageLogFiles, setCurrentLogSettings, -} from "./electron-main/logOperations"; +} from "./electron-main/logOperations.js"; import { setupGetRecordingBlobIPC, setupPronunciationCheckerIPC, -} from "./electron-main/pronunciationCheckerIPC"; +} from "./electron-main/pronunciationCheckerIPC.js"; import { cancelProcess, checkPythonInstalled, @@ -38,9 +40,9 @@ import { killCurrentPythonProcess, resetGlobalCancel, setupPronunciationInstallStatusIPC, -} from "./electron-main/pronunciationOperations"; -import { checkDownloads, checkExtractedFolder } from "./electron-main/videoFileOperations"; -import { verifyAndExtractIPC } from "./electron-main/zipOperation"; +} from "./electron-main/pronunciationOperations.js"; +import { checkDownloads, checkExtractedFolder } from "./electron-main/videoFileOperations.js"; +import { verifyAndExtractIPC } from "./electron-main/zipOperation.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -216,12 +218,17 @@ ipcMain.handle("open-recording-folder", async () => { verifyAndExtractIPC(); // Listen for logging messages from the renderer process -ipcMain.on("renderer-log", (event, logMessage) => { - const { level, message } = logMessage; - if (typeof level === "string" && typeof applog[level as keyof typeof applog] === "function") { - (applog as any)[level](`Renderer log: ${message}`); +ipcMain.on( + "renderer-log", + (event: IpcMainEvent, logMessage: { level: LogLevel; message: string }) => { + const { level, message } = logMessage; + // Only allow valid log levels + const logFn = (applog as LogFunctions)[level]; + if (typeof logFn === "function") { + logFn(`Renderer log: ${message}`); + } } -}); +); // Handle uncaught exceptions globally and quit the app process.on("uncaughtException", (error) => { @@ -237,10 +244,13 @@ process.on("unhandledRejection", (reason, promise) => { app.quit(); // Quit the app on an unhandled promise rejection }); -app.on("renderer-process-crashed", (event: any, killed: any) => { - applog.error("Renderer process crashed", { event, killed }); - app.quit(); -}); +app.on( + "render-process-gone", + (event: Electron.Event, details: Electron.RenderProcessGoneDetails) => { + applog.error("Renderer process crashed", { event, details }); + app.quit(); + } +); // Quit when all windows are closed, except on macOS. app.on("window-all-closed", () => { From c732026b951eda6c9fd96ac967f8ae60bf16846e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 17:37:46 +0700 Subject: [PATCH 027/199] update zipOperation to conform eslint rule --- electron-main/zipOperation.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/electron-main/zipOperation.ts b/electron-main/zipOperation.ts index 8b29625f..72e4166f 100644 --- a/electron-main/zipOperation.ts +++ b/electron-main/zipOperation.ts @@ -1,11 +1,11 @@ -import { ipcMain } from "electron"; +import { ipcMain, IpcMainEvent } from "electron"; import applog from "electron-log"; import JS7z from "js7z-tools"; import crypto from "node:crypto"; import fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; -import { getSaveFolder } from "./filePath"; +import { getSaveFolder } from "./filePath.js"; // Function to calculate the SHA-256 hash of a file const calculateFileHash = (filePath: string) => { @@ -18,11 +18,15 @@ const calculateFileHash = (filePath: string) => { }); }; -const fileVerification = async (event: any, zipContents: any, extractedFolder: string) => { +const fileVerification = async ( + event: IpcMainEvent, + zipContents: { extractedFiles: { name: string; hash: string }[] }[], + extractedFolder: string +) => { // Verify existing extracted files const totalFiles = zipContents[0].extractedFiles.length; let filesProcessed = 0; - let fileErrors = []; + const fileErrors: { type: string; name: string; message: string }[] = []; for (const file of zipContents[0].extractedFiles) { const extractedFilePath = path.join(extractedFolder, file.name); @@ -199,9 +203,10 @@ const verifyAndExtractIPC = () => { await fsPromises.unlink(zipFilePath); console.log(`Deleted ZIP file: ${zipFilePath}`); applog.log(`Extraction successful for ${zipFile}`); - } catch (err: any) { - console.error(`Failed to delete ZIP file: ${err.message}`); - applog.error(`Failed to delete ZIP file: ${err.message}`); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.error(`Failed to delete ZIP file: ${errorMsg}`); + applog.error(`Failed to delete ZIP file: ${errorMsg}`); } event.sender.send("verification-success", { @@ -210,13 +215,14 @@ const verifyAndExtractIPC = () => { param: zipFile, }); applog.log(`Successfully verified and extracted ${zipFile}`); - } catch (err: any) { - console.error(`Error processing ${zipFile}: ${err.message}`); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.error(`Error processing ${zipFile}: ${errorMsg}`); event.sender.send("verification-error", { messageKey: "settingPage.videoDownloadSettings.electronVerifyMessage.zipErrorMsg", param: zipFile, - errorMessage: err.message, + errorMessage: errorMsg, }); } } else { From a5cf2c76caccc4a1c41188a26ff72d4dfe1e8b72 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 17:41:16 +0700 Subject: [PATCH 028/199] Update createWindow.ts --- electron-main/createWindow.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/electron-main/createWindow.ts b/electron-main/createWindow.ts index f1e53355..3aa9cf6b 100644 --- a/electron-main/createWindow.ts +++ b/electron-main/createWindow.ts @@ -1,4 +1,5 @@ -import { app, BrowserWindow, Menu, shell } from "electron"; +import { app, BrowserWindow, IpcMain, IpcMainInvokeEvent, Menu, shell } from "electron"; +import { Conf } from "electron-conf"; import applog from "electron-log"; import path from "node:path"; import process from "node:process"; @@ -9,7 +10,11 @@ const isDev = process.env.NODE_ENV === "development"; let mainWindow: BrowserWindow | null; let splashWindow: BrowserWindow | null; -const createSplashWindow = (rootDir: string, ipcMain: any, conf: any) => { +const createSplashWindow = ( + rootDir: string, + ipcMain: IpcMain, + conf: Conf> +) => { splashWindow = new BrowserWindow({ width: 854, height: 413, @@ -26,7 +31,7 @@ const createSplashWindow = (rootDir: string, ipcMain: any, conf: any) => { }); // For splash screen - ipcMain.handle("get-conf", (key: string) => { + ipcMain.handle("get-conf", (event: IpcMainInvokeEvent, key: string) => { return conf.get(key); }); From 4bb9fd876b67c89300331e12289df08784ad42fa Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 18:12:45 +0700 Subject: [PATCH 029/199] Update customFolderLocationOperation.ts --- .../customFolderLocationOperation.ts | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/electron-main/customFolderLocationOperation.ts b/electron-main/customFolderLocationOperation.ts index 85aa8bec..1dbd88f8 100644 --- a/electron-main/customFolderLocationOperation.ts +++ b/electron-main/customFolderLocationOperation.ts @@ -1,14 +1,14 @@ -import { ipcMain } from "electron"; +import { ipcMain, IpcMainInvokeEvent } from "electron"; import applog from "electron-log"; import fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { + deleteEmptyDataSubfolder, + getDataSubfolder, getSaveFolder, settingsConf, - getDataSubfolder, - deleteEmptyDataSubfolder, } from "./filePath.js"; import isDeniedSystemFolder from "./isDeniedSystemFolder.js"; import { generateLogFileName } from "./logOperations.js"; @@ -45,7 +45,7 @@ const shouldMoveContents = (src: string, dest: string): boolean => { }; // Helper: Delete pronunciation-venv in oldSaveFolder before moving -const deletePronunciationVenv = async (oldSaveFolder: string, event: any) => { +const deletePronunciationVenv = async (oldSaveFolder: string, event: IpcMainInvokeEvent) => { const oldVenvPath = path.join(oldSaveFolder, "pronunciation-venv"); if (fs.existsSync(oldVenvPath)) { event.sender.send("venv-delete-status", { status: "deleting", path: oldVenvPath }); @@ -54,21 +54,22 @@ const deletePronunciationVenv = async (oldSaveFolder: string, event: any) => { console.log("Deleted old pronunciation-venv at:", oldVenvPath); applog.info("Deleted old pronunciation-venv at:", oldVenvPath); event.sender.send("venv-delete-status", { status: "deleted", path: oldVenvPath }); - } catch (venvErr: any) { + } catch (venvErr) { + const errorMsg = venvErr instanceof Error ? venvErr.message : String(venvErr); // Log but do not block move if venv doesn't exist or can't be deleted - console.log("Could not delete old pronunciation-venv:", venvErr.message); - applog.warn("Could not delete old pronunciation-venv:", venvErr.message); + console.log("Could not delete old pronunciation-venv:", errorMsg); + applog.warn("Could not delete old pronunciation-venv:", errorMsg); event.sender.send("venv-delete-status", { status: "error", path: oldVenvPath, - error: venvErr.message, + error: errorMsg, }); } } }; // Helper: Move all contents from one folder to another (copy then delete, robust for cross-device) -const moveFolderContents = async (src: string, dest: string, event: any) => { +const moveFolderContents = async (src: string, dest: string, event: IpcMainInvokeEvent) => { // Recursively collect all files for accurate progress const files = await getAllFiles(src); const total = files.length; @@ -167,13 +168,14 @@ const setCustomSaveFolderIPC = () => { // Ensure the destination exists before checking shouldMoveContents try { await fsPromises.mkdir(newSaveFolder, { recursive: true }); - } catch (e: any) { - console.log("Failed to create default save folder:", e); - applog.error("Failed to create default save folder:", e); + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e); + console.log("Failed to create default save folder:", errorMsg); + applog.error("Failed to create default save folder:", errorMsg); return { success: false, error: "folderChangeError", - reason: e.message || "Unknown error", + reason: errorMsg, }; } applog.info("[DEBUG] prevDataSubfolder:", prevDataSubfolder); @@ -193,13 +195,15 @@ const setCustomSaveFolderIPC = () => { await deletePronunciationVenv(prevDataSubfolder, event); await moveFolderContents(prevDataSubfolder, newSaveFolder, event); } - } catch (moveBackErr: any) { - console.log("Failed to move contents back to default folder:", moveBackErr); - applog.error("Failed to move contents back to default folder:", moveBackErr); + } catch (moveBackErr) { + const errorMsg = + moveBackErr instanceof Error ? moveBackErr.message : String(moveBackErr); + console.log("Failed to move contents back to default folder:", errorMsg); + applog.error("Failed to move contents back to default folder:", errorMsg); return { success: false, error: "folderMoveError", - reason: moveBackErr.message || "Unknown move error", + reason: errorMsg, }; } } @@ -251,24 +255,26 @@ const setCustomSaveFolderIPC = () => { // Ensure the data subfolder exists try { await fsPromises.mkdir(newSaveFolder, { recursive: true }); - } catch (e: any) { - console.log("Failed to create data subfolder:", e); - applog.error("Failed to create data subfolder:", e); + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e); + console.log("Failed to create data subfolder:", errorMsg); + applog.error("Failed to create data subfolder:", errorMsg); return { success: false, error: "folderChangeError", - reason: e.message || "Unknown error", + reason: errorMsg, }; } console.log("New save folder:", newSaveFolder); applog.info("New save folder:", newSaveFolder); - } catch (err: any) { - console.log("Error setting custom save folder:", err); - applog.error("Error setting custom save folder:", err); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.log("Error setting custom save folder:", errorMsg); + applog.error("Error setting custom save folder:", errorMsg); return { success: false, error: "folderChangeError", - reason: err.message || "Unknown error", + reason: errorMsg, }; } } @@ -282,9 +288,10 @@ const setCustomSaveFolderIPC = () => { const currentLogFolder = path.join(newSaveFolder, "logs"); try { await fsPromises.mkdir(currentLogFolder, { recursive: true }); - } catch (e: any) { - console.log("Failed to create new log directory:", e); - applog.warn("Failed to create new log directory:", e); + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e); + console.log("Failed to create new log directory:", errorMsg); + applog.warn("Failed to create new log directory:", errorMsg); } applog.transports.file.fileName = generateLogFileName(); applog.transports.file.resolvePathFn = () => @@ -296,13 +303,14 @@ const setCustomSaveFolderIPC = () => { await deleteEmptyDataSubfolder(prevCustomFolder); } return { success: true, newPath: newSaveFolder }; - } catch (moveErr: any) { - console.log("Failed to move folder contents:", moveErr); - applog.error("Failed to move folder contents:", moveErr); + } catch (moveErr) { + const errorMsg = moveErr instanceof Error ? moveErr.message : String(moveErr); + console.log("Failed to move folder contents:", errorMsg); + applog.error("Failed to move folder contents:", errorMsg); return { success: false, error: "folderMoveError", - reason: moveErr.message || "Unknown move error", + reason: errorMsg, }; } }); From 155441e82ece235be0b9ad71be71ecbb2174626b Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 20:52:57 +0700 Subject: [PATCH 030/199] add ts dependencies --- package-lock.json | 575 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 9 + 2 files changed, 578 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 497a8363..3bced04a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,11 +34,17 @@ "@electron-forge/maker-zip": "^7.8.1", "@electron-forge/plugin-auto-unpack-natives": "^7.8.1", "@electron-forge/plugin-fuses": "^7.8.1", + "@electron-forge/shared-types": "^7.8.1", "@electron/fuses": "^1.8.0", "@eslint/js": "^9.27.0", "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.7", + "@types/cors": "^2.8.18", + "@types/electron": "^1.4.38", + "@types/electron-squirrel-startup": "^1.0.2", + "@types/express": "^5.0.2", + "@types/node": "^22.15.21", "@types/react": "^19.1.5", "@types/react-dom": "^19.1.5", "@vidstack/react": "^1.12.13", @@ -74,6 +80,9 @@ "rollup-plugin-visualizer": "^6.0.0", "sonner": "^2.0.3", "tailwindcss": "^4.1.7", + "ts-node": "^10.9.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1", "vidstack": "^1.12.12", "vite": "^6.3.5", "vite-plugin-pwa": "^1.0.0", @@ -1707,6 +1716,30 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -2995,9 +3028,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -4554,6 +4587,34 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/appdmg": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@types/appdmg/-/appdmg-0.5.5.tgz", @@ -4610,6 +4671,17 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -4622,6 +4694,43 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz", + "integrity": "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/electron": { + "version": "1.4.38", + "resolved": "https://registry.npmjs.org/@types/electron/-/electron-1.4.38.tgz", + "integrity": "sha512-Cu6laqBamT6VSPi0LLlF9vE9Os8EbTaI/5eJSsd7CPoLUG3Znjh04u9TxMhQYPF1wGFM14Z8TFQ2914JZ+rGLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/electron-squirrel-startup": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz", + "integrity": "sha512-AzxnvBzNh8K/0SmxMmZtpJf1/IWoGXLP+pQDuUaVkPyotI8ryvAtBSqgxR/qOSvxWHYWrxkeNsJ+Ca5xOuUxJQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -4629,6 +4738,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -4646,6 +4780,13 @@ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4662,15 +4803,36 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.5", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", @@ -4707,6 +4869,29 @@ "@types/node": "*" } }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4724,6 +4909,222 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vidstack/react": { "version": "1.12.13", "resolved": "https://registry.npmjs.org/@vidstack/react/-/react-1.12.13.tgz", @@ -4841,6 +5242,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4972,6 +5386,13 @@ "node": ">=8.5" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6095,6 +6516,13 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -6474,6 +6902,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -9058,6 +9496,13 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -10783,6 +11228,13 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -14523,6 +14975,63 @@ "node": ">=0.8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -14647,6 +15156,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -14871,6 +15417,13 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -15801,6 +16354,16 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 1c2d5993..bfe4731e 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,17 @@ "@electron-forge/maker-zip": "^7.8.1", "@electron-forge/plugin-auto-unpack-natives": "^7.8.1", "@electron-forge/plugin-fuses": "^7.8.1", + "@electron-forge/shared-types": "^7.8.1", "@electron/fuses": "^1.8.0", "@eslint/js": "^9.27.0", "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.7", + "@types/cors": "^2.8.18", + "@types/electron": "^1.4.38", + "@types/electron-squirrel-startup": "^1.0.2", + "@types/express": "^5.0.2", + "@types/node": "^22.15.21", "@types/react": "^19.1.5", "@types/react-dom": "^19.1.5", "@vidstack/react": "^1.12.13", @@ -86,6 +92,9 @@ "rollup-plugin-visualizer": "^6.0.0", "sonner": "^2.0.3", "tailwindcss": "^4.1.7", + "ts-node": "^10.9.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1", "vidstack": "^1.12.12", "vite": "^6.3.5", "vite-plugin-pwa": "^1.0.0", From 8777bdad9ce5a4f1b0297d03522ea6cb9fe57e69 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 20:53:12 +0700 Subject: [PATCH 031/199] use correct types for expressServer --- electron-main/expressServer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/electron-main/expressServer.ts b/electron-main/expressServer.ts index 8dde0292..dffbe698 100644 --- a/electron-main/expressServer.ts +++ b/electron-main/expressServer.ts @@ -1,7 +1,8 @@ import { ipcMain } from "electron"; import applog from "electron-log"; import express from "express"; -import net from "net"; +import { Server } from "node:http"; +import net from "node:net"; const DEFAULT_PORT = 8998; const MIN_PORT = 1024; // Minimum valid port number @@ -9,14 +10,14 @@ const MAX_PORT = 65535; // Maximum valid port number const expressApp = express(); let currentPort: number | null = null; -let httpServer: any = null; +let httpServer: Server | null = null; const getRandomPort = () => Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT; const checkPortAvailability = (port: number) => { return new Promise((resolve, reject) => { const server = net.createServer(); - server.once("error", (err: any) => { + server.once("error", (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE" || err.code === "ECONNREFUSED") { resolve(false); } else { @@ -55,3 +56,4 @@ const getExpressPort = () => currentPort; ipcMain.handle("get-port", () => getExpressPort()); export { expressApp, getExpressPort, startExpressServer }; + From be1be9899bbc9920f3061288226a0e1531487db8 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 20:54:08 +0700 Subject: [PATCH 032/199] Update filePath.ts --- electron-main/filePath.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-main/filePath.ts b/electron-main/filePath.ts index f1545cfa..37f10af6 100644 --- a/electron-main/filePath.ts +++ b/electron-main/filePath.ts @@ -6,7 +6,7 @@ import path from "node:path"; // Singleton instance for settings const settingsConf = new Conf({ name: "ispeakerreact_config" }); -const readUserSettings = async (): Promise> => { +const readUserSettings = async (): Promise> => { return settingsConf.store || {}; }; From fd47ec0be805bc28707f08758fd7059aa3f4ca88 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 21:06:47 +0700 Subject: [PATCH 033/199] Update isDeniedSystemFolder.ts --- electron-main/isDeniedSystemFolder.ts | 33 ++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/electron-main/isDeniedSystemFolder.ts b/electron-main/isDeniedSystemFolder.ts index 986592a5..1246b43e 100644 --- a/electron-main/isDeniedSystemFolder.ts +++ b/electron-main/isDeniedSystemFolder.ts @@ -11,9 +11,10 @@ const isDeniedSystemFolder = (folderPath: string) => { try { // Resolve symlinks for robust security absoluteInput = fs.realpathSync.native(path.resolve(folderPath)); - } catch (err: any) { - console.warn("Error getting realpath:", err.message); - applog.error("Error getting realpath:", err.message); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.warn("Error getting realpath:", errorMsg); + applog.error("Error getting realpath:", errorMsg); // If we can't resolve the path, deny access for safety return true; } @@ -86,9 +87,10 @@ const isDeniedSystemFolder = (folderPath: string) => { } } } - } catch (err: any) { - console.warn("Error getting users directory:", err.message); - applog.error("Error getting users directory:", err.message); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.warn("Error getting users directory:", errorMsg); + applog.error("Error getting users directory:", errorMsg); } } else { // Linux and other Unix-like - using path.sep for consistency @@ -133,9 +135,10 @@ const isDeniedSystemFolder = (folderPath: string) => { } } } - } catch (err: any) { - console.warn("Error getting home directory:", err.message); - applog.error("Error getting home directory:", err.message); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.warn("Error getting home directory:", errorMsg); + applog.error("Error getting home directory:", errorMsg); } } @@ -150,9 +153,10 @@ const isDeniedSystemFolder = (folderPath: string) => { app.getPath("crashDumps"), ]; denyList = [...denyList, ...appPaths]; - } catch (err: any) { - console.warn("Error getting app paths:", err.message); - applog.error("Error getting app paths:", err.message); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.warn("Error getting app paths:", errorMsg); + applog.error("Error getting app paths:", errorMsg); } // De-duplicate and filter out any undefined or empty values @@ -167,7 +171,10 @@ const isDeniedSystemFolder = (folderPath: string) => { let formatted; try { formatted = fs.realpathSync.native(path.resolve(p)); - } catch (err: any) { + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + console.warn("Error formatting path:", errorMsg); + applog.error("Error formatting path:", errorMsg); formatted = path.resolve(p); } if (!isCaseSensitive) formatted = formatted.toLowerCase(); From 81a43b309133199838a315e3ad7f24298ead0750 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 21:10:07 +0700 Subject: [PATCH 034/199] Update pronunciationCheckerIPC.ts --- electron-main/pronunciationCheckerIPC.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/electron-main/pronunciationCheckerIPC.ts b/electron-main/pronunciationCheckerIPC.ts index b337ca08..f1e769c3 100644 --- a/electron-main/pronunciationCheckerIPC.ts +++ b/electron-main/pronunciationCheckerIPC.ts @@ -5,9 +5,9 @@ import * as fsPromises from "node:fs/promises"; import path from "node:path"; import { getSaveFolder, readUserSettings } from "./filePath.js"; import { getCurrentLogSettings } from "./logOperations.js"; -import { getVenvPythonPath, ensureVenvExists } from "./pronunciationOperations.js"; +import { ensureVenvExists, getVenvPythonPath } from "./pronunciationOperations.js"; -const startProcess = (cmd: string, args: string[], callback: (err: any) => void) => { +const startProcess = (cmd: string, args: string[], callback: (err: Error) => void) => { const proc = spawn(cmd, args, { shell: true }); proc.on("error", callback); return proc; @@ -134,17 +134,18 @@ if __name__ == "__main__": const tempPyPath = path.join(saveFolder, "pronunciation_checker_temp.py"); await fsPromises.writeFile(tempPyPath, pyCode, "utf-8"); applog.info(`[PronunciationChecker] tempPyPath: ${tempPyPath}`); - let venvPython; + let venvPython: string; try { await ensureVenvExists(); venvPython = await getVenvPythonPath(); - } catch (err: any) { - applog.error(`[PronunciationChecker] Failed to create or find venv: ${err.message}`); - return { status: "error", message: `Failed to create or find venv: ${err.message}` }; + } catch (err: unknown) { + const errorMsg = err instanceof Error ? err.message : String(err); + applog.error(`[PronunciationChecker] Failed to create or find venv: ${errorMsg}`); + return { status: "error", message: `Failed to create or find venv: ${errorMsg}` }; } applog.info(`[PronunciationChecker] About to run: ${venvPython} -u ${tempPyPath}`); return new Promise((resolve) => { - const py = startProcess(venvPython, ["-u", tempPyPath], (err: any) => { + const py = startProcess(venvPython, ["-u", tempPyPath], (err: Error) => { fsPromises.unlink(tempPyPath).catch(() => { applog.warn( "[PronunciationChecker] Failed to delete temp pronunciation checker file" @@ -152,11 +153,11 @@ if __name__ == "__main__": }); if (err) { applog.error( - `[PronunciationChecker] Python process exited with code: ${err.code}` + `[PronunciationChecker] Python process exited with code: ${(err as Error).message}` ); } }); - let lastJson: any = null; + let lastJson: Record | null = null; py.stdout.on("data", (data) => { const lines = data.toString().split(/\r?\n/); for (const line of lines) { @@ -229,8 +230,9 @@ const setupGetRecordingBlobIPC = () => { try { const data = await fsPromises.readFile(filePath); return data.buffer; // ArrayBuffer for renderer - } catch (err: any) { - throw new Error(`Failed to read recording: ${err.message}`); + } catch (err: unknown) { + const errorMsg = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to read recording: ${errorMsg}`); } }); }; From fb1bdb4b77d86d81e9fbdedbde9e04cf7078d02c Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 21:11:16 +0700 Subject: [PATCH 035/199] Update logOperations.ts --- electron-main/logOperations.ts | 41 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/electron-main/logOperations.ts b/electron-main/logOperations.ts index 9fb76112..cbf473fd 100644 --- a/electron-main/logOperations.ts +++ b/electron-main/logOperations.ts @@ -1,4 +1,4 @@ -import { ipcMain } from "electron"; +import { ipcMain, IpcMainEvent } from "electron"; import applog, { LevelOption } from "electron-log"; import * as fsPromises from "node:fs/promises"; import path from "node:path"; @@ -23,7 +23,7 @@ const getCurrentLogSettings = () => { return currentLogSettings; }; -const setCurrentLogSettings = (newSettings: any) => { +const setCurrentLogSettings = (newSettings: Record) => { currentLogSettings = { ...currentLogSettings, ...newSettings }; }; @@ -49,15 +49,18 @@ applog.transports.file.maxSize = currentLogSettings.maxLogSize; applog.transports.console.level = currentLogSettings.logLevel as LevelOption; // Handle updated log settings from the renderer -ipcMain.on("update-log-settings", async (event: any, newSettings: any) => { - setCurrentLogSettings(newSettings); - applog.info("Log settings updated:", currentLogSettings); +ipcMain.on( + "update-log-settings", + async (event: IpcMainEvent, newSettings: Record) => { + setCurrentLogSettings(newSettings); + applog.info("Log settings updated:", currentLogSettings); - // Save to user settings file - settingsConf.set("logSettings", currentLogSettings); + // Save to user settings file + settingsConf.set("logSettings", currentLogSettings); - manageLogFiles(); -}); + manageLogFiles(); + } +); // Function to check and manage log files based on the currentLogSettings const manageLogFiles = async () => { @@ -80,9 +83,10 @@ const manageLogFiles = async () => { path: filePath, birthtime: stats.birthtime, }); - } catch (err: any) { + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); // If ENOENT, just skip this file - if (err.code !== "ENOENT") { + if (errorMsg !== "ENOENT") { applog.error(`Error stating log file: ${filePath}`, err); } } @@ -98,8 +102,9 @@ const manageLogFiles = async () => { try { await fsPromises.unlink(file.path); applog.info(`Deleted log file: ${file.path}`); - } catch (err: any) { - if (err.code !== "ENOENT") { + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + if (errorMsg !== "ENOENT") { applog.error(`Error deleting log file: ${file.path}`, err); } } @@ -116,16 +121,18 @@ const manageLogFiles = async () => { try { await fsPromises.unlink(file.path); applog.info(`Deleted old log file: ${file.path}`); - } catch (err: any) { - if (err.code !== "ENOENT") { + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + if (errorMsg !== "ENOENT") { applog.error(`Error deleting old log file: ${file.path}`, err); } } } } } - } catch (error: any) { - applog.error("Error managing log files:", error); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + applog.error("Error managing log files:", errorMsg); } }; From 675b304c8fad3494b0f5023a8e9f840b7e2c915b Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 21:41:38 +0700 Subject: [PATCH 036/199] update preload to use ts --- preload.cjs | 36 ------------------------------------ preload.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 36 deletions(-) delete mode 100644 preload.cjs create mode 100644 preload.ts diff --git a/preload.cjs b/preload.cjs deleted file mode 100644 index 7f465904..00000000 --- a/preload.cjs +++ /dev/null @@ -1,36 +0,0 @@ -const { contextBridge, ipcRenderer } = require("electron"); - -contextBridge.exposeInMainWorld("electron", { - openExternal: (url) => ipcRenderer.invoke("open-external-link", url), - saveRecording: (key, arrayBuffer) => ipcRenderer.invoke("save-recording", key, arrayBuffer), - checkRecordingExists: (key) => ipcRenderer.invoke("check-recording-exists", key), - playRecording: (key) => ipcRenderer.invoke("play-recording", key), - ipcRenderer: { - invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), - send: (channel, ...args) => ipcRenderer.send(channel, ...args), - on: (channel, func) => ipcRenderer.on(channel, func), - removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), - removeListener: (channel, func) => ipcRenderer.removeListener(channel, func), - }, - getDirName: () => __dirname, - isUwp: () => process.windowsStore, - send: (channel, data) => { - ipcRenderer.send(channel, data); - }, - log: (level, message) => { - // Send log message to the main process - ipcRenderer.send("renderer-log", { level, message }); - }, - getRecordingBlob: async (key) => { - // Use IPC to ask the main process for the blob - return await ipcRenderer.invoke("get-recording-blob", key); - }, - getFfmpegWasmPath: async () => { - return await ipcRenderer.invoke("get-ffmpeg-wasm-path"); - }, - getFileAsBlobUrl: async (filePath, mimeType) => { - const arrayBuffer = await ipcRenderer.invoke("read-file-buffer", filePath); - const blob = new Blob([arrayBuffer], { type: mimeType }); - return URL.createObjectURL(blob); - }, -}); diff --git a/preload.ts b/preload.ts new file mode 100644 index 00000000..a9d0f9b2 --- /dev/null +++ b/preload.ts @@ -0,0 +1,38 @@ +import { contextBridge, ipcRenderer } from "electron"; + +contextBridge.exposeInMainWorld("electron", { + openExternal: (url: string) => ipcRenderer.invoke("open-external-link", url), + saveRecording: (key: string, arrayBuffer: ArrayBuffer) => + ipcRenderer.invoke("save-recording", key, arrayBuffer), + checkRecordingExists: (key: string) => ipcRenderer.invoke("check-recording-exists", key), + playRecording: (key: string) => ipcRenderer.invoke("play-recording", key), + ipcRenderer: { + invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args), + send: (channel: string, ...args: unknown[]) => ipcRenderer.send(channel, ...args), + on: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.on(channel, func), + removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel), + removeListener: (channel: string, func: (...args: unknown[]) => void) => + ipcRenderer.removeListener(channel, func), + }, + getDirName: () => __dirname, + isUwp: () => process.windowsStore, + send: (channel: string, data: unknown) => { + ipcRenderer.send(channel, data); + }, + log: (level: string, message: string) => { + // Send log message to the main process + ipcRenderer.send("renderer-log", { level, message }); + }, + getRecordingBlob: async (key: string) => { + // Use IPC to ask the main process for the blob + return await ipcRenderer.invoke("get-recording-blob", key); + }, + getFfmpegWasmPath: async () => { + return await ipcRenderer.invoke("get-ffmpeg-wasm-path"); + }, + getFileAsBlobUrl: async (filePath: string, mimeType: string) => { + const arrayBuffer = await ipcRenderer.invoke("read-file-buffer", filePath); + const blob = new Blob([arrayBuffer], { type: mimeType }); + return URL.createObjectURL(blob); + }, +}); From d9c51cc0f13ba05089708516d85f4ed29f45d037 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:37:54 +0700 Subject: [PATCH 037/199] Update tsconfig.json --- tsconfig.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 587cbac4..f9594894 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,17 @@ { + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "target": "ES2024", - "module": "ESNext", + "target": "ESNext", + "module": "NodeNext", "lib": ["DOM", "ESNext"], "jsx": "react-jsx", - "moduleResolution": "Node16", + "moduleResolution": "NodeNext", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "outDir": "dist", + "outDir": "production/dist", "baseUrl": ".", "paths": { "@/*": ["src/*"] From 43e34d4f7c8247728552caec68359d0e35741f2b Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:38:29 +0700 Subject: [PATCH 038/199] update useScrollTo --- src/utils/{useScrollTo.jsx => useScrollTo.ts} | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) rename src/utils/{useScrollTo.jsx => useScrollTo.ts} (52%) diff --git a/src/utils/useScrollTo.jsx b/src/utils/useScrollTo.ts similarity index 52% rename from src/utils/useScrollTo.jsx rename to src/utils/useScrollTo.ts index 537a33e7..bdf3f84f 100644 --- a/src/utils/useScrollTo.jsx +++ b/src/utils/useScrollTo.ts @@ -1,17 +1,20 @@ import { useRef } from "react"; -export const useScrollTo = () => { +const useScrollTo = () => { const ref = useRef(null); const padding = 300; // extra padding const scrollTo = (options = { behavior: "smooth" }) => { if (ref.current) { const element = ref.current; - const top = element.getBoundingClientRect().top + window.scrollY - padding; + const top = + (element as HTMLElement).getBoundingClientRect().top + window.scrollY - padding; - window.scrollTo({ top, behavior: options.behavior }); + window.scrollTo({ top, behavior: options.behavior as ScrollBehavior }); } }; return { ref, scrollTo }; }; + +export default useScrollTo; From 40833d90e9a64373f79d548a6ed81899e9539536 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:39:02 +0700 Subject: [PATCH 039/199] update ThemeContext --- .../{ThemeProvider.jsx => ThemeProvider.tsx} | 21 ++++++++----------- .../ThemeContext/ThemeProviderContext.jsx | 8 ------- .../ThemeContext/ThemeProviderContext.tsx | 15 +++++++++++++ ...oDetectTheme.jsx => useAutoDetectTheme.ts} | 2 +- .../{useTheme.jsx => useTheme.ts} | 6 ++++-- 5 files changed, 29 insertions(+), 23 deletions(-) rename src/utils/ThemeContext/{ThemeProvider.jsx => ThemeProvider.tsx} (84%) delete mode 100644 src/utils/ThemeContext/ThemeProviderContext.jsx create mode 100644 src/utils/ThemeContext/ThemeProviderContext.tsx rename src/utils/ThemeContext/{useAutoDetectTheme.jsx => useAutoDetectTheme.ts} (96%) rename src/utils/ThemeContext/{useTheme.jsx => useTheme.ts} (65%) diff --git a/src/utils/ThemeContext/ThemeProvider.jsx b/src/utils/ThemeContext/ThemeProvider.tsx similarity index 84% rename from src/utils/ThemeContext/ThemeProvider.jsx rename to src/utils/ThemeContext/ThemeProvider.tsx index 8e3b9c15..83014ecb 100644 --- a/src/utils/ThemeContext/ThemeProvider.jsx +++ b/src/utils/ThemeContext/ThemeProvider.tsx @@ -1,12 +1,15 @@ -import PropTypes from "prop-types"; import { useEffect, useState } from "react"; -import isElectron from "../isElectron"; -import ThemeProviderContext from "./ThemeProviderContext"; +import isElectron from "../isElectron.js"; +import ThemeProviderContext from "./ThemeProviderContext.jsx"; const ThemeProvider = ({ children, defaultTheme = "auto", storageKey = "ispeakerreact-ui-theme", +}: { + children: React.ReactNode; + defaultTheme: string; + storageKey: string; }) => { const [theme, setTheme] = useState(defaultTheme); const [loaded, setLoaded] = useState(false); @@ -14,7 +17,7 @@ const ThemeProvider = ({ // Load theme from storage on mount useEffect(() => { if (isElectron()) { - window.electron.ipcRenderer.invoke("get-theme", storageKey).then((storedTheme) => { + window.Electron.ipcRenderer.invoke("get-theme", storageKey).then((storedTheme) => { setTheme(storedTheme || defaultTheme); setLoaded(true); }); @@ -69,9 +72,9 @@ const ThemeProvider = ({ const value = { theme, - setTheme: async (newTheme) => { + setTheme: async (newTheme: string) => { if (isElectron()) { - await window.electron.ipcRenderer.invoke("set-theme", newTheme); + await window.Electron.ipcRenderer.invoke("set-theme", newTheme); } else { localStorage.setItem(storageKey, newTheme); } @@ -85,10 +88,4 @@ const ThemeProvider = ({ return {children}; }; -ThemeProvider.propTypes = { - children: PropTypes.node, - defaultTheme: PropTypes.string, - storageKey: PropTypes.string, -}; - export default ThemeProvider; diff --git a/src/utils/ThemeContext/ThemeProviderContext.jsx b/src/utils/ThemeContext/ThemeProviderContext.jsx deleted file mode 100644 index 1b8d9271..00000000 --- a/src/utils/ThemeContext/ThemeProviderContext.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import { createContext } from "react"; - -const ThemeProviderContext = createContext({ - theme: "auto", - setTheme: () => null, -}); - -export default ThemeProviderContext; diff --git a/src/utils/ThemeContext/ThemeProviderContext.tsx b/src/utils/ThemeContext/ThemeProviderContext.tsx new file mode 100644 index 00000000..4958da0e --- /dev/null +++ b/src/utils/ThemeContext/ThemeProviderContext.tsx @@ -0,0 +1,15 @@ +import { createContext } from "react"; + +interface ThemeProviderContextType { + theme: string; + setTheme: (newTheme: string) => Promise; +} + +const ThemeProviderContext = createContext({ + theme: "auto", + setTheme: async () => { + // Intentionally empty default implementation + }, +}); + +export default ThemeProviderContext; diff --git a/src/utils/ThemeContext/useAutoDetectTheme.jsx b/src/utils/ThemeContext/useAutoDetectTheme.ts similarity index 96% rename from src/utils/ThemeContext/useAutoDetectTheme.jsx rename to src/utils/ThemeContext/useAutoDetectTheme.ts index 2c6f8d2f..f9e6ea09 100644 --- a/src/utils/ThemeContext/useAutoDetectTheme.jsx +++ b/src/utils/ThemeContext/useAutoDetectTheme.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useTheme } from "./useTheme"; +import useTheme from "./useTheme.js"; const useAutoDetectTheme = () => { const { theme } = useTheme(); diff --git a/src/utils/ThemeContext/useTheme.jsx b/src/utils/ThemeContext/useTheme.ts similarity index 65% rename from src/utils/ThemeContext/useTheme.jsx rename to src/utils/ThemeContext/useTheme.ts index f2e17290..a8ea5132 100644 --- a/src/utils/ThemeContext/useTheme.jsx +++ b/src/utils/ThemeContext/useTheme.ts @@ -1,7 +1,7 @@ import { useContext } from "react"; -import ThemeProviderContext from "./ThemeProviderContext"; +import ThemeProviderContext from "./ThemeProviderContext.jsx"; -export const useTheme = () => { +const useTheme = () => { const context = useContext(ThemeProviderContext); if (!context) { @@ -10,3 +10,5 @@ export const useTheme = () => { return context; }; + +export default useTheme; From 5bb44ad3b60860fa0ab3a7d74cdfd33ca8d8f0a7 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:39:18 +0700 Subject: [PATCH 040/199] update sonnerCustomToast --- .../{sonnerCustomToast.jsx => sonnerCustomToast.tsx} | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) rename src/utils/{sonnerCustomToast.jsx => sonnerCustomToast.tsx} (77%) diff --git a/src/utils/sonnerCustomToast.jsx b/src/utils/sonnerCustomToast.tsx similarity index 77% rename from src/utils/sonnerCustomToast.jsx rename to src/utils/sonnerCustomToast.tsx index 464b897d..c32d4fd0 100644 --- a/src/utils/sonnerCustomToast.jsx +++ b/src/utils/sonnerCustomToast.tsx @@ -1,7 +1,7 @@ import { IoAlertCircleOutline, IoCheckmarkCircleOutline, IoWarningOutline } from "react-icons/io5"; import { toast } from "sonner"; -export const sonnerSuccessToast = (message) => { +const sonnerSuccessToast = (message: string) => { toast.custom(() => (
@@ -10,7 +10,7 @@ export const sonnerSuccessToast = (message) => { )); }; -export const sonnerWarningToast = (message) => { +const sonnerWarningToast = (message: string) => { toast.custom(() => (
@@ -19,7 +19,7 @@ export const sonnerWarningToast = (message) => { )); }; -export const sonnerErrorToast = (message) => { +const sonnerErrorToast = (message: string) => { toast.custom(() => (
@@ -27,3 +27,5 @@ export const sonnerErrorToast = (message) => {
)); }; + +export { sonnerErrorToast, sonnerSuccessToast, sonnerWarningToast }; From 11104ef5f41ead48ac81b476ca6fdf99a8820404 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:39:35 +0700 Subject: [PATCH 041/199] update ShuffleArray --- src/utils/{ShuffleArray.jsx => ShuffleArray.ts} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename src/utils/{ShuffleArray.jsx => ShuffleArray.ts} (70%) diff --git a/src/utils/ShuffleArray.jsx b/src/utils/ShuffleArray.ts similarity index 70% rename from src/utils/ShuffleArray.jsx rename to src/utils/ShuffleArray.ts index bdfc97ba..e98f136e 100644 --- a/src/utils/ShuffleArray.jsx +++ b/src/utils/ShuffleArray.ts @@ -1,7 +1,9 @@ -export const ShuffleArray = (array) => { +const ShuffleArray = (array: T[]): T[] => { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; }; + +export default ShuffleArray; From 14a497817335a2460f67dcffb267bb07154fd18f Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:40:32 +0700 Subject: [PATCH 042/199] Delete phonemeUtils.jsx --- src/utils/phonemeUtils.jsx | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/utils/phonemeUtils.jsx diff --git a/src/utils/phonemeUtils.jsx b/src/utils/phonemeUtils.jsx deleted file mode 100644 index a61a2786..00000000 --- a/src/utils/phonemeUtils.jsx +++ /dev/null @@ -1,9 +0,0 @@ -export const findPhonemeDetails = (phoneme, soundsData) => { - let phonemeIndex = soundsData.consonants.findIndex((p) => p.phoneme === phoneme); - if (phonemeIndex !== -1) return { index: phonemeIndex, type: "consonant" }; - - phonemeIndex = soundsData.vowels_n_diphthongs.findIndex((p) => p.phoneme === phoneme); - if (phonemeIndex !== -1) return { index: phonemeIndex, type: "vowel" }; - - return { index: -1, type: null }; // Not found -}; From 21267c682aa730f98b3845e5372959152049a382 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:40:57 +0700 Subject: [PATCH 043/199] update levenshtein --- src/utils/{levenshtein.js => levenshtein.ts} | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) rename src/utils/{levenshtein.js => levenshtein.ts} (83%) diff --git a/src/utils/levenshtein.js b/src/utils/levenshtein.ts similarity index 83% rename from src/utils/levenshtein.js rename to src/utils/levenshtein.ts index 91680ea7..2a235bca 100644 --- a/src/utils/levenshtein.js +++ b/src/utils/levenshtein.ts @@ -1,4 +1,4 @@ -const levenshtein = (a, b) => { +const levenshtein = (a: string, b: string) => { const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0)); for (let i = 0; i <= a.length; i++) matrix[i][0] = i; for (let j = 0; j <= b.length; j++) matrix[0][j] = j; @@ -18,7 +18,7 @@ const levenshtein = (a, b) => { return matrix[a.length][b.length]; }; -const getCharacterDiff = (a, b) => { +const getCharacterDiff = (a: string, b: string) => { // Simple character diff using dynamic programming const m = a.length, n = b.length; @@ -33,8 +33,8 @@ const getCharacterDiff = (a, b) => { } // Backtrack to get the diff let i = m, - j = n, - res = []; + j = n; + const res: { char: string; type: string }[] = []; while (i > 0 || j > 0) { if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) { res.unshift({ char: a[i - 1], type: "same" }); @@ -55,32 +55,31 @@ const getCharacterDiff = (a, b) => { return res; }; -const alignPhonemes = (modelPhoneme, officialPhoneme) => { +const alignPhonemes = (modelPhoneme: string, officialPhoneme: string) => { // Tokenize the official phoneme string const officialTokens = officialPhoneme.trim().split(/\s+/); // Remove all spaces from model output for easier matching - let modelStr = modelPhoneme.replace(/\s+/g, ""); + const modelStr = modelPhoneme.replace(/\s+/g, ""); let idx = 0; - const aligned = []; + const aligned: string[] = []; for (const token of officialTokens) { // Try to match the next segment of modelStr to the current official token - if (modelStr.substr(idx, token.length) === token) { + if (modelStr.substring(idx, idx + token.length) === token) { aligned.push(token); idx += token.length; } else { // If not matching, try to find the best match (fallback: take the next N chars) - aligned.push(modelStr.substr(idx, token.length)); + aligned.push(modelStr.substring(idx, idx + token.length)); idx += token.length; } } // If there are leftovers in modelStr, add them as extra tokens if (idx < modelStr.length) { - aligned.push(modelStr.substr(idx)); + aligned.push(modelStr.substring(idx)); } return aligned.join(" "); }; export { alignPhonemes, getCharacterDiff, levenshtein }; - From 69d430dccec73aa7ed679a5405ad706b18ec00d3 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:41:29 +0700 Subject: [PATCH 044/199] update isTouchDevice --- src/utils/isTouchDevice.jsx | 3 --- src/utils/isTouchDevice.ts | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 src/utils/isTouchDevice.jsx create mode 100644 src/utils/isTouchDevice.ts diff --git a/src/utils/isTouchDevice.jsx b/src/utils/isTouchDevice.jsx deleted file mode 100644 index f51658b5..00000000 --- a/src/utils/isTouchDevice.jsx +++ /dev/null @@ -1,3 +0,0 @@ -export const isTouchDevice = () => { - return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; -}; diff --git a/src/utils/isTouchDevice.ts b/src/utils/isTouchDevice.ts new file mode 100644 index 00000000..c26657b5 --- /dev/null +++ b/src/utils/isTouchDevice.ts @@ -0,0 +1,5 @@ +const isTouchDevice = () => { + return "ontouchstart" in window || navigator.maxTouchPoints > 0; +}; + +export default isTouchDevice; From 2557ba273b1733d1ae373ae63813dda85f0cf750 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:41:40 +0700 Subject: [PATCH 045/199] update isElectron --- src/utils/{isElectron.jsx => isElectron.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/utils/{isElectron.jsx => isElectron.ts} (100%) diff --git a/src/utils/isElectron.jsx b/src/utils/isElectron.ts similarity index 100% rename from src/utils/isElectron.jsx rename to src/utils/isElectron.ts From 3f667cf6eeb5f422ab632d840e714e646b0220d6 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:42:20 +0700 Subject: [PATCH 046/199] update AccentLocalStorage --- src/utils/{AccentLocalStorage.jsx => AccentLocalStorage.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/utils/{AccentLocalStorage.jsx => AccentLocalStorage.ts} (94%) diff --git a/src/utils/AccentLocalStorage.jsx b/src/utils/AccentLocalStorage.ts similarity index 94% rename from src/utils/AccentLocalStorage.jsx rename to src/utils/AccentLocalStorage.ts index 95a69b3d..236de5b2 100644 --- a/src/utils/AccentLocalStorage.jsx +++ b/src/utils/AccentLocalStorage.ts @@ -2,7 +2,7 @@ import { useState } from "react"; const AccentLocalStorage = () => { const [selectedAccent, setSelectedAccent] = useState(() => { - const savedSettings = JSON.parse(localStorage.getItem("ispeaker")); + const savedSettings = JSON.parse(localStorage.getItem("ispeaker") || "{}"); return savedSettings?.selectedAccent || "american"; }); From d17db5d508013649f48f2881c3d2e8d16f48a04e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:45:59 +0700 Subject: [PATCH 047/199] Update eslint.config.js --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 31a0cab6..984f7518 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,7 +14,7 @@ export default tseslint.config( { files: ["**/*.{js,jsx,ts,tsx}"], languageOptions: { - ecmaVersion: 2020, + ecmaVersion: "latest", globals: globals.browser, parserOptions: { ecmaVersion: "latest", From abd212337bc17cc5459dae846740fc888c90e23a Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:46:12 +0700 Subject: [PATCH 048/199] update @types/react version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bced04a..283fb546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "@types/electron-squirrel-startup": "^1.0.2", "@types/express": "^5.0.2", "@types/node": "^22.15.21", - "@types/react": "^19.1.5", + "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "@vidstack/react": "^1.12.13", "@vitejs/plugin-react": "^4.5.0", @@ -4834,9 +4834,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", - "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", + "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index bfe4731e..10292849 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@types/electron-squirrel-startup": "^1.0.2", "@types/express": "^5.0.2", "@types/node": "^22.15.21", - "@types/react": "^19.1.5", + "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "@vidstack/react": "^1.12.13", "@vitejs/plugin-react": "^4.5.0", From 413b52e56a9d7e92f6cd06c3be602f546f3a6bc5 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:47:45 +0700 Subject: [PATCH 049/199] update useCountdownTimer --- .../{useCountdownTimer.jsx => useCountdownTimer.ts} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename src/utils/{useCountdownTimer.jsx => useCountdownTimer.ts} (84%) diff --git a/src/utils/useCountdownTimer.jsx b/src/utils/useCountdownTimer.ts similarity index 84% rename from src/utils/useCountdownTimer.jsx rename to src/utils/useCountdownTimer.ts index 54f96636..b4bbc2cd 100644 --- a/src/utils/useCountdownTimer.jsx +++ b/src/utils/useCountdownTimer.ts @@ -1,8 +1,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; -const useCountdownTimer = (initialTime, onTimerEnd) => { +const useCountdownTimer = (initialTime: number, onTimerEnd: () => void) => { const [remainingTime, setRemainingTime] = useState(initialTime * 60); // Track remaining time in state (seconds) - const intervalIdRef = useRef(null); // Ref to store the interval ID + const intervalIdRef = useRef | null>(null); // Ref to store the interval ID const [isActive, setIsActive] = useState(false); // Control timer activation // Clear the timer when needed @@ -28,8 +28,10 @@ const useCountdownTimer = (initialTime, onTimerEnd) => { intervalIdRef.current = setInterval(() => { setRemainingTime((prevTime) => { if (prevTime <= 1) { - clearInterval(intervalIdRef.current); - intervalIdRef.current = null; + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + intervalIdRef.current = null; + } return 0; } return prevTime - 1; From 564aa9c9d50b438f83dfd24cc3864229a3af9573 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:51:03 +0700 Subject: [PATCH 050/199] Create global.d.ts --- src/types/global.d.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/types/global.d.ts diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 00000000..09ff3b75 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,26 @@ +declare global { + interface Window { + electron: { + openExternal: (url: string) => Promise; + saveRecording: (key: string, arrayBuffer: ArrayBuffer) => Promise; + checkRecordingExists: (key: string) => Promise; + playRecording: (key: string) => Promise; + ipcRenderer: { + invoke: (channel: string, ...args: unknown[]) => Promise; + send: (channel: string, ...args: unknown[]) => void; + on: (channel: string, func: (...args: unknown[]) => void) => void; + removeAllListeners: (channel: string) => void; + removeListener: (channel: string, func: (...args: unknown[]) => void) => void; + }; + getDirName: () => string; + isUwp: () => boolean | undefined; + send: (channel: string, data: unknown) => void; + log: (level: string, message: string) => void; + getRecordingBlob: (key: string) => Promise; + getFfmpegWasmPath: () => Promise; + getFileAsBlobUrl: (filePath: string, mimeType: string) => Promise; + }; + } +} + +export {}; From 9de4844234b03afba07097f64b68a986b507d4fd Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 22:51:12 +0700 Subject: [PATCH 051/199] update openExternal --- src/utils/{openExternal.jsx => openExternal.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/utils/{openExternal.jsx => openExternal.ts} (68%) diff --git a/src/utils/openExternal.jsx b/src/utils/openExternal.ts similarity index 68% rename from src/utils/openExternal.jsx rename to src/utils/openExternal.ts index bb309170..521b7d58 100644 --- a/src/utils/openExternal.jsx +++ b/src/utils/openExternal.ts @@ -1,6 +1,6 @@ -import isElectron from "./isElectron"; +import isElectron from "./isElectron.js"; -const openExternal = (url) => { +const openExternal = (url: string) => { if (isElectron()) { window.electron.openExternal(url); } else { From 48e0b5930ae4bd8a8ac328e35bf5a140c389aeac Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:11:50 +0700 Subject: [PATCH 052/199] update databaseOperations --- ...seOperations.jsx => databaseOperations.ts} | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) rename src/utils/{databaseOperations.jsx => databaseOperations.ts} (70%) diff --git a/src/utils/databaseOperations.jsx b/src/utils/databaseOperations.ts similarity index 70% rename from src/utils/databaseOperations.jsx rename to src/utils/databaseOperations.ts index 97ff326c..7c65144d 100644 --- a/src/utils/databaseOperations.jsx +++ b/src/utils/databaseOperations.ts @@ -1,20 +1,20 @@ import { fixWebmDuration } from "@fix-webm-duration/fix"; -import isElectron from "./isElectron"; +import isElectron from "./isElectron.js"; -let db; +let db: IDBDatabase | null = null; // Helper function to convert Blob to ArrayBuffer -const blobToArrayBuffer = (blob) => { +const blobToArrayBuffer = (blob: Blob): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); + reader.onloadend = () => resolve(reader.result as ArrayBuffer); reader.onerror = (error) => reject(error); reader.readAsArrayBuffer(blob); }); }; // Open IndexedDB database -const openDatabase = () => { +const openDatabase = (): Promise => { return new Promise((resolve, reject) => { if (db) { resolve(db); @@ -23,18 +23,18 @@ const openDatabase = () => { const request = window.indexedDB.open("iSpeaker_data", 1); - request.onerror = (event) => { - console.error("Database error: ", event.target.error); - reject(event.target.error); + request.onerror = (event: Event) => { + console.error("Database error: ", (event.target as IDBRequest).error); + reject((event.target as IDBRequest).error); }; - request.onsuccess = (event) => { - db = event.target.result; + request.onsuccess = (event: Event) => { + db = (event.target as IDBRequest).result as IDBDatabase; resolve(db); }; - request.onupgradeneeded = (event) => { - const db = event.target.result; + request.onupgradeneeded = (event: Event) => { + const db = (event.target as IDBRequest).result as IDBDatabase; // Create required object stores if they don't exist const storeNames = ["recording_data", "conversation_data", "exam_data"]; @@ -48,12 +48,17 @@ const openDatabase = () => { }; // Save recording to either Electron or IndexedDB -const saveRecording = async (blob, key, mimeType, duration) => { +const saveRecording = async ( + blob: Blob, + key: string, + mimeType: string, + duration?: number +): Promise => { // If duration is not provided, calculate it from the blob if (!duration) { const audioContext = new AudioContext(); const arrayBuffer = await blobToArrayBuffer(blob); - const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer as ArrayBuffer); duration = audioBuffer.duration * 1000; // Convert to milliseconds } @@ -63,7 +68,7 @@ const saveRecording = async (blob, key, mimeType, duration) => { if (isElectron()) { // Electron environment try { - await window.electron.saveRecording(key, arrayBuffer); + await window.electron.saveRecording(key, arrayBuffer as ArrayBuffer); console.log("Recording saved successfully via Electron API"); } catch (error) { console.error("Error saving recording in Electron:", error); @@ -79,12 +84,12 @@ const saveRecording = async (blob, key, mimeType, duration) => { const request = store.put({ id: key, recording: arrayBuffer, mimeType: mimeType }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { request.onsuccess = () => { console.log("Recording saved successfully to IndexedDB"); resolve(); }; - request.onerror = (error) => { + request.onerror = (error: Event) => { console.error("Error saving recording:", error); reject(error); }; @@ -96,41 +101,46 @@ const saveRecording = async (blob, key, mimeType, duration) => { }; // Check if recording exists -const checkRecordingExists = async (key) => { +const checkRecordingExists = async (key: string): Promise => { if (isElectron()) { - return window.electron.checkRecordingExists(key); + return window.electron.checkRecordingExists(key) as Promise; } else { - try { - const db = await openDatabase(); - if (!db) return false; + const db = await openDatabase(); + if (!db) return false; - const transaction = db.transaction(["recording_data"]); - const store = transaction.objectStore("recording_data"); - const request = store.get(key); + return new Promise((resolve) => { + try { + const transaction = db.transaction(["recording_data"]); + const store = transaction.objectStore("recording_data"); + const request = store.get(key); - return new Promise((resolve, reject) => { request.onsuccess = () => { if (request.result) resolve(true); else resolve(false); }; - request.onerror = (error) => reject(error); - }); - } catch (error) { - console.error("Error checking recording existence in IndexedDB:", error); - return false; - } + request.onerror = () => resolve(false); + } catch { + console.error("Error checking recording existence in IndexedDB."); + resolve(false); + } + }); } }; // Play recording from either Electron or IndexedDB -const playRecording = async (key, onSuccess, onError, onEnded) => { +const playRecording = async ( + key: string, + onSuccess?: (audio: HTMLAudioElement | null, source: AudioBufferSourceNode | null) => void, + onError?: (error: unknown) => void, + onEnded?: () => void +): Promise => { if (isElectron()) { try { // Get the audio data as an ArrayBuffer from the main process const arrayBuffer = await window.electron.playRecording(key); // Create a Blob from the ArrayBuffer - const audioBlob = new Blob([arrayBuffer], { type: "audio/wav" }); + const audioBlob = new Blob([arrayBuffer as ArrayBuffer], { type: "audio/wav" }); // Create a Blob URL const blobUrl = URL.createObjectURL(audioBlob); @@ -172,17 +182,22 @@ const playRecording = async (key, onSuccess, onError, onEnded) => { const request = store.get(key); request.onsuccess = async () => { - const { recording, mimeType } = request.result; + const result = request.result; + if (!result) { + if (onError) onError(new Error("Recording not found")); + return; + } + const { recording, mimeType } = result; try { // Use AudioContext for playback const audioContext = new AudioContext(); - const buffer = await audioContext.decodeAudioData(recording); + const buffer = await audioContext.decodeAudioData(recording as ArrayBuffer); const source = audioContext.createBufferSource(); source.buffer = buffer; source.connect(audioContext.destination); - source.onended = onEnded; + source.onended = onEnded || null; source.start(); if (onSuccess) onSuccess(null, source); @@ -190,12 +205,12 @@ const playRecording = async (key, onSuccess, onError, onEnded) => { console.error("Error decoding audio data:", decodeError); // Fallback to Blob URL - const audioBlob = new Blob([recording], { type: mimeType }); + const audioBlob = new Blob([recording as ArrayBuffer], { type: mimeType }); const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); - audio.onended = onEnded; - audio.onerror = onError; + audio.onended = onEnded || null; + audio.onerror = onError || null; audio .play() @@ -209,7 +224,7 @@ const playRecording = async (key, onSuccess, onError, onEnded) => { } }; - request.onerror = (error) => { + request.onerror = (error: Event) => { console.error("Error retrieving recording from IndexedDB:", error); if (onError) onError(error); }; From 8a4aea191608814ef4dcf691fffe4fad54018b71 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:11:58 +0700 Subject: [PATCH 053/199] update ffmpegWavConverter --- ...ffmpegWavConverter.js => ffmpegWavConverter.ts} | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) rename src/utils/{ffmpegWavConverter.js => ffmpegWavConverter.ts} (78%) diff --git a/src/utils/ffmpegWavConverter.js b/src/utils/ffmpegWavConverter.ts similarity index 78% rename from src/utils/ffmpegWavConverter.js rename to src/utils/ffmpegWavConverter.ts index 51fa1ff1..bc666c1e 100644 --- a/src/utils/ffmpegWavConverter.js +++ b/src/utils/ffmpegWavConverter.ts @@ -1,11 +1,11 @@ import { FFmpeg } from "@ffmpeg/ffmpeg"; import { fetchFile } from "@ffmpeg/util"; -import isElectron from "./isElectron"; +import isElectron from "./isElectron.js"; -let ffmpeg = null; -let ffmpegLoading = null; +let ffmpeg: FFmpeg | null = null; +let ffmpegLoading: Promise | null = null; -const getFFmpeg = async () => { +const getFFmpeg = async (): Promise => { if (!ffmpeg) { ffmpeg = new FFmpeg(); ffmpegLoading = (async () => { @@ -27,7 +27,7 @@ const getFFmpeg = async () => { return ffmpeg; }; -const convertToWav = async (inputBlob) => { +const convertToWav = async (inputBlob: Blob): Promise => { const ffmpeg = await getFFmpeg(); await ffmpeg.writeFile("input", await fetchFile(inputBlob)); await ffmpeg.exec([ @@ -44,7 +44,9 @@ const convertToWav = async (inputBlob) => { "output.wav", ]); const data = await ffmpeg.readFile("output.wav"); - const wavBlob = new Blob([data.buffer], { type: "audio/wav" }); + const wavBlob = new Blob([data instanceof Uint8Array ? data : new Uint8Array([])], { + type: "audio/wav", + }); await ffmpeg.deleteFile("input"); await ffmpeg.deleteFile("output.wav"); return wavBlob; From fa951279e559325d31d0f76179bc380c9dca07de Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:21:56 +0700 Subject: [PATCH 054/199] update Container (UI) --- src/ui/Container.jsx | 16 ---------------- src/ui/Container.tsx | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 src/ui/Container.jsx create mode 100644 src/ui/Container.tsx diff --git a/src/ui/Container.jsx b/src/ui/Container.jsx deleted file mode 100644 index e3a8b3bb..00000000 --- a/src/ui/Container.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import PropTypes from "prop-types"; - -const Container = ({ children, className = "", ...props }) => { - return ( -
- {children} -
- ); -}; - -Container.propTypes = { - children: PropTypes.node.isRequired, - className: PropTypes.string, -}; - -export default Container; diff --git a/src/ui/Container.tsx b/src/ui/Container.tsx new file mode 100644 index 00000000..4b7ec4af --- /dev/null +++ b/src/ui/Container.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +interface ContainerProps extends React.HTMLAttributes { + children: React.ReactNode; + className?: string; +} + +const Container = ({ children, className = "", ...props }: ContainerProps) => { + return ( +
+ {children} +
+ ); +}; + +export default Container; From 45712384fdb4316873f5260b8639e43bf4fbbc52 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:31:32 +0700 Subject: [PATCH 055/199] ignore `__APP_VERSION__` lint error --- eslint.config.js | 5 ++++- src/vite-env.d.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/vite-env.d.ts diff --git a/eslint.config.js b/eslint.config.js index 984f7518..e1f3a0f8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,10 @@ export default tseslint.config( files: ["**/*.{js,jsx,ts,tsx}"], languageOptions: { ecmaVersion: "latest", - globals: globals.browser, + globals: { + ...globals.browser, + __APP_VERSION__: "readonly", + }, parserOptions: { ecmaVersion: "latest", ecmaFeatures: { jsx: true }, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 00000000..dbb4c627 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const __APP_VERSION__: string; From 318cfe9b190d2d4accf30bbb656ba7b33deaa23e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:33:20 +0700 Subject: [PATCH 056/199] update ErrorBoundary to use ts --- src/ErrorBoundary.jsx | 10 ----- src/ErrorBoundary.tsx | 11 ++++++ ...undaryInner.jsx => ErrorBoundaryInner.tsx} | 37 +++++++++++-------- 3 files changed, 33 insertions(+), 25 deletions(-) delete mode 100644 src/ErrorBoundary.jsx create mode 100644 src/ErrorBoundary.tsx rename src/{ErrorBoundaryInner.jsx => ErrorBoundaryInner.tsx} (80%) diff --git a/src/ErrorBoundary.jsx b/src/ErrorBoundary.jsx deleted file mode 100644 index 2e560ce2..00000000 --- a/src/ErrorBoundary.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useTranslation } from "react-i18next"; -import ErrorBoundaryInner from "./ErrorBoundaryInner"; - -const ErrorBoundary = (props) => { - const { t } = useTranslation(); - - return ; -}; - -export default ErrorBoundary; diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx new file mode 100644 index 00000000..fcd73a30 --- /dev/null +++ b/src/ErrorBoundary.tsx @@ -0,0 +1,11 @@ +import { useTranslation } from "react-i18next"; +import ErrorBoundaryInner from "./ErrorBoundaryInner.js"; +import type { ErrorBoundaryProps } from "./ErrorBoundaryInner.jsx"; + +const ErrorBoundary = (props: Omit) => { + const { t } = useTranslation(); + + return {props.children}; +}; + +export default ErrorBoundary; diff --git a/src/ErrorBoundaryInner.jsx b/src/ErrorBoundaryInner.tsx similarity index 80% rename from src/ErrorBoundaryInner.jsx rename to src/ErrorBoundaryInner.tsx index fbbfa794..3f9d24cb 100644 --- a/src/ErrorBoundaryInner.jsx +++ b/src/ErrorBoundaryInner.tsx @@ -1,24 +1,35 @@ -import PropTypes from "prop-types"; import React from "react"; import { FaGithub } from "react-icons/fa"; import { FiRefreshCw } from "react-icons/fi"; import { HiOutlineClipboardCopy } from "react-icons/hi"; import { Toaster } from "sonner"; -import Container from "./ui/Container"; -import openExternal from "./utils/openExternal"; -import { sonnerSuccessToast } from "./utils/sonnerCustomToast"; +import Container from "./ui/Container.js"; +import openExternal from "./utils/openExternal.js"; +import { sonnerSuccessToast } from "./utils/sonnerCustomToast.js"; -export default class ErrorBoundary extends React.Component { - constructor(props) { +// Define the props and state types +export interface ErrorBoundaryProps { + t: (key: string) => string; + children: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +export default class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null, errorInfo: null }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } - componentDidCatch(error, errorInfo) { + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error("React Error Boundary caught an error:", error, errorInfo); this.setState({ errorInfo }); } @@ -68,6 +79,7 @@ App version: v${__APP_VERSION__}\n${error?.toString()}\n\nStack Trace:\n${errorI {t("appCrash.copyBtn")} - @@ -88,7 +100,7 @@ App version: v${__APP_VERSION__}\n${error?.toString()}\n\nStack Trace:\n${errorI ); @@ -97,8 +109,3 @@ App version: v${__APP_VERSION__}\n${error?.toString()}\n\nStack Trace:\n${errorI return this.props.children; } } - -ErrorBoundary.propTypes = { - t: PropTypes.func.isRequired, - children: PropTypes.node.isRequired, -}; From 8f9cc4ae3ed88f66a792e01ca700f20854f74a3a Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:36:49 +0700 Subject: [PATCH 057/199] update LogoLightOrDark --- .../{LogoLightOrDark.jsx => LogoLightOrDark.tsx} | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) rename src/components/general/{LogoLightOrDark.jsx => LogoLightOrDark.tsx} (69%) diff --git a/src/components/general/LogoLightOrDark.jsx b/src/components/general/LogoLightOrDark.tsx similarity index 69% rename from src/components/general/LogoLightOrDark.jsx rename to src/components/general/LogoLightOrDark.tsx index 8ab8e3cd..c0f7944c 100644 --- a/src/components/general/LogoLightOrDark.jsx +++ b/src/components/general/LogoLightOrDark.tsx @@ -1,7 +1,6 @@ -import PropTypes from "prop-types"; -import useAutoDetectTheme from "../../utils/ThemeContext/useAutoDetectTheme"; +import useAutoDetectTheme from "../../utils/ThemeContext/useAutoDetectTheme.js"; -const LogoLightOrDark = ({ width, height }) => { +const LogoLightOrDark = ({ width, height }: { width: number; height: number }) => { const { autoDetectedTheme } = useAutoDetectTheme(); const logoSrc = @@ -12,9 +11,4 @@ const LogoLightOrDark = ({ width, height }) => { return iSpeakerReact logo; }; -LogoLightOrDark.propTypes = { - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, -}; - export default LogoLightOrDark; From 6ec92cb2e24b48a7d0ae9eebdd69f5cb28c80c02 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:36:56 +0700 Subject: [PATCH 058/199] update Footer --- src/components/general/{Footer.jsx => Footer.tsx} | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) rename src/components/general/{Footer.jsx => Footer.tsx} (87%) diff --git a/src/components/general/Footer.jsx b/src/components/general/Footer.tsx similarity index 87% rename from src/components/general/Footer.jsx rename to src/components/general/Footer.tsx index a0e752da..264178ce 100644 --- a/src/components/general/Footer.jsx +++ b/src/components/general/Footer.tsx @@ -1,5 +1,5 @@ -import openExternal from "../../utils/openExternal"; -import LogoLightOrDark from "./LogoLightOrDark"; +import openExternal from "../../utils/openExternal.js"; +import LogoLightOrDark from "./LogoLightOrDark.js"; const Footer = () => { return ( @@ -8,14 +8,13 @@ const Footer = () => { className="footer sm:footer-horizontal bg-base-200 text-base-content p-6 pb-20 md:p-10" >
From 715de0e2e66b1122d1d5d71f4757739cc4298fc7 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Tue, 27 May 2025 23:40:33 +0700 Subject: [PATCH 061/199] update TopNavBar component --- .../general/{TopNavBar.jsx => TopNavBar.tsx} | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) rename src/components/general/{TopNavBar.jsx => TopNavBar.tsx} (97%) diff --git a/src/components/general/TopNavBar.jsx b/src/components/general/TopNavBar.tsx similarity index 97% rename from src/components/general/TopNavBar.jsx rename to src/components/general/TopNavBar.tsx index cf486f01..11831fd4 100644 --- a/src/components/general/TopNavBar.jsx +++ b/src/components/general/TopNavBar.tsx @@ -8,8 +8,8 @@ import { PiExam } from "react-icons/pi"; import { useTranslation } from "react-i18next"; import { NavLink, useLocation } from "react-router-dom"; -import useAutoDetectTheme from "../../utils/ThemeContext/useAutoDetectTheme"; -import openExternal from "../../utils/openExternal"; +import useAutoDetectTheme from "../../utils/ThemeContext/useAutoDetectTheme.js"; +import openExternal from "../../utils/openExternal.js"; const TopNavBar = () => { const { t } = useTranslation(); @@ -83,7 +83,7 @@ const TopNavBar = () => { - +
))} diff --git a/src/components/exam_page/PracticeTab.tsx b/src/components/exam_page/PracticeTab.tsx index 3b0a8024..b3dc7363 100644 --- a/src/components/exam_page/PracticeTab.tsx +++ b/src/components/exam_page/PracticeTab.tsx @@ -354,8 +354,8 @@ const PracticeTab = ({ accent, examId, taskData, tips }: PracticeTabProps) => { }) } onInput={autoExpand} - placeholder={t("tabConversationExam.textareaPlaceholder")} - title={t("tabConversationExam.textareaTitle")} + placeholder={t("tabConversationExam.practiceExamPlaceholder")} + title={t("tabConversationExam.practiceExamTextbox")} > From 8324f1cbf92a8d461136da03e153750c326622cd Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 10:27:10 +0700 Subject: [PATCH 111/199] setting page: update Appearance --- .../setting_page/{Appearance.jsx => Appearance.tsx} | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) rename src/components/setting_page/{Appearance.jsx => Appearance.tsx} (91%) diff --git a/src/components/setting_page/Appearance.jsx b/src/components/setting_page/Appearance.tsx similarity index 91% rename from src/components/setting_page/Appearance.jsx rename to src/components/setting_page/Appearance.tsx index 0264b34c..438bec12 100644 --- a/src/components/setting_page/Appearance.jsx +++ b/src/components/setting_page/Appearance.tsx @@ -1,10 +1,10 @@ import { useTranslation } from "react-i18next"; -import { useTheme } from "../../utils/ThemeContext/useTheme"; -import { sonnerSuccessToast } from "../../utils/sonnerCustomToast"; +import useTheme from "../../utils/ThemeContext/useTheme.js"; +import { sonnerSuccessToast } from "../../utils/sonnerCustomToast.js"; import { MdOutlineLightMode, MdOutlineDarkMode } from "react-icons/md"; import { LuSunMoon } from "react-icons/lu"; -const themeOptions = { +const themeOptions: Record = { auto: { labelKey: "settingPage.appearanceSettings.themeAuto", icon: , @@ -23,7 +23,7 @@ const AppearanceSettings = () => { const { t } = useTranslation(); const { theme, setTheme } = useTheme(); - const handleThemeSelect = (selectedTheme) => { + const handleThemeSelect = (selectedTheme: string) => { setTheme(selectedTheme); sonnerSuccessToast(t("settingPage.changeSaved")); }; @@ -50,7 +50,6 @@ const AppearanceSettings = () => { type="button" onClick={() => handleThemeSelect(key)} className={`${theme === key ? "menu-active" : ""} flex items-center justify-start gap-2`} - aria-pressed={theme === key} > {icon} {t(labelKey)} From 2ca1ec05810e200c6c6ec611a9f536c63d351e3f Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 10:33:47 +0700 Subject: [PATCH 112/199] setting page: update AppInfo --- src/components/setting_page/{AppInfo.jsx => AppInfo.tsx} | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename src/components/setting_page/{AppInfo.jsx => AppInfo.tsx} (96%) diff --git a/src/components/setting_page/AppInfo.jsx b/src/components/setting_page/AppInfo.tsx similarity index 96% rename from src/components/setting_page/AppInfo.jsx rename to src/components/setting_page/AppInfo.tsx index c79f118a..dd1f0047 100644 --- a/src/components/setting_page/AppInfo.jsx +++ b/src/components/setting_page/AppInfo.tsx @@ -36,7 +36,7 @@ const AppInfo = () => { try { const now = Math.floor(Date.now() / 1000); // Current timestamp in seconds const savedResetTime = localStorage.getItem(RATE_LIMIT_KEY); - const resetTime = new Date(parseInt(savedResetTime, 10) * 1000).toLocaleString(); + const resetTime = new Date(parseInt(savedResetTime || "0", 10) * 1000).toLocaleString(); // If a reset time is stored and it's in the future, skip API request if (savedResetTime && now < parseInt(savedResetTime, 10)) { @@ -87,7 +87,7 @@ const AppInfo = () => { `Rate limit is low (${rateLimitRemaining} remaining). Skipping update check.` ); const resetTimeFirst = new Date( - parseInt(rateLimitReset + 5 * 3600, 10) * 1000 + parseInt((rateLimitReset + 5 * 3600).toString(), 10) * 1000 ).toLocaleString(); localStorage.setItem(RATE_LIMIT_KEY, (rateLimitReset + 5 * 3600).toString()); setAlertMessage(t("alert.rateLimited", { time: resetTimeFirst })); @@ -147,6 +147,8 @@ const AppInfo = () => { )}
- -
- - ); -}; - -export default ExerciseTimer; diff --git a/src/components/setting_page/ExerciseTimer.tsx b/src/components/setting_page/ExerciseTimer.tsx new file mode 100644 index 00000000..b9dc7fca --- /dev/null +++ b/src/components/setting_page/ExerciseTimer.tsx @@ -0,0 +1,236 @@ +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sonnerSuccessToast } from "../../utils/sonnerCustomToast.js"; + +export interface TimerSettings { + enabled: boolean; + dictation: number; + matchup: number; + reordering: number; + sound_n_spelling: number; + sorting: number; + odd_one_out: number; +} + +// For temp input, allow numbers or empty string (for input fields) +interface TimerSettingsInput { + enabled: boolean; + dictation: number | ""; + matchup: number | ""; + reordering: number | ""; + sound_n_spelling: number | ""; + sorting: number | ""; + odd_one_out: number | ""; +} + +const defaultTimerSettings: TimerSettings = { + enabled: false, + dictation: 5, + matchup: 5, + reordering: 5, + sound_n_spelling: 5, + sorting: 5, + odd_one_out: 5, +}; + +const ExerciseTimer = () => { + const { t } = useTranslation(); + + const savedSettings = JSON.parse(localStorage.getItem("ispeaker") || "{}"); + + const [timerSettings, setTimerSettings] = useState(() => { + if (savedSettings && savedSettings.timerSettings) { + return savedSettings.timerSettings; + } + return defaultTimerSettings; + }); + + // tempSettings allows "" for input fields + const [tempSettings, setTempSettings] = useState(timerSettings); + const [inputEnabled, setInputEnabled] = useState(timerSettings.enabled); + const [isValid, setIsValid] = useState(true); + const [isModified, setIsModified] = useState(false); + + // Automatically save settings to localStorage whenever timerSettings change + useEffect(() => { + savedSettings.timerSettings = timerSettings; + localStorage.setItem("ispeaker", JSON.stringify(savedSettings)); + }, [savedSettings, timerSettings]); + + const handleTimerToggle = (enabled: boolean) => { + setTimerSettings((prev) => ({ + ...prev, + enabled, + })); + setInputEnabled(enabled); + sonnerSuccessToast(t("settingPage.changeSaved")); + }; + + // Validation function to check if the inputs are valid (0-10 numbers only) + const validateInputs = (settings: TimerSettingsInput) => { + return Object.entries(settings).every(([key, value]) => { + if (key === "enabled") return true; + return ( + value !== "" && !isNaN(Number(value)) && Number(value) >= 0 && Number(value) <= 10 + ); + }); + }; + + const checkIfModified = useCallback( + (settings: TimerSettingsInput) => { + const storedSettings: TimerSettings = + savedSettings.timerSettings || defaultTimerSettings; + // Only compare numeric fields + const keys: (keyof TimerSettings)[] = [ + "dictation", + "matchup", + "reordering", + "sound_n_spelling", + "sorting", + "odd_one_out", + "enabled", + ]; + for (const key of keys) { + if (settings[key] !== storedSettings[key]) return true; + } + return false; + }, + [savedSettings] + ); + + const handleInputChange = ( + e: React.ChangeEvent, + settingKey: keyof Omit + ) => { + const { value } = e.target; + if (/^\d*$/.test(value) && value.length <= 2) { + setTempSettings((prev) => ({ + ...prev, + [settingKey]: value === "" ? "" : parseInt(value, 10), + })); + } + }; + + const handleApply = () => { + if (validateInputs(tempSettings)) { + setTimerSettings((prev) => ({ + ...prev, + ...Object.fromEntries( + Object.entries(tempSettings).map(([k, v]) => [k, v === "" ? 0 : v]) + ), + enabled: prev.enabled, // Ensure the `enabled` flag is preserved + })); + setIsModified(false); + sonnerSuccessToast(t("settingPage.changeSaved")); + } + }; + + const handleCancel = () => { + setTempSettings(timerSettings); // revert to original settings + setIsModified(false); // Reset modified state + }; + + // Update validity and modified state when temporary settings change + useEffect(() => { + setIsValid(validateInputs(tempSettings)); + setIsModified(checkIfModified(tempSettings)); // Check if values differ from localStorage or defaults + }, [checkIfModified, tempSettings]); + + const exerciseNames: Record, string> = { + dictation: t("exercise_page.dictationHeading"), + matchup: t("exercise_page.matchUpHeading"), + reordering: t("exercise_page.reorderingHeading"), + sound_n_spelling: t("exercise_page.soundSpellingHeading"), + sorting: t("exercise_page.sortingHeading"), + odd_one_out: t("exercise_page.oddOneOutHeading"), + }; + + return ( +
+
+
+ +

+ {t("settingPage.exerciseSettings.timerDescription")} +

+
+
+ handleTimerToggle(e.target.checked)} + /> +
+
+ +
+ {Object.keys(exerciseNames).map((exercise) => { + const value = tempSettings[exercise as keyof typeof exerciseNames]; + const isInvalid = + value === "" || (typeof value === "number" && (value < 0 || value > 10)); + return ( +
+
+ + + {exerciseNames[exercise as keyof typeof exerciseNames]} + + + + + handleInputChange( + e, + exercise as keyof Omit + ) + } + className={`input input-bordered w-full max-w-xs ${ + isInvalid ? "input-error" : "" + }`} + disabled={!inputEnabled} + /> + + {isInvalid ? ( +

+ {t("settingPage.exerciseSettings.textboxError")} +

+ ) : null} +
+
+ ); + })} +
+ +

{t("settingPage.exerciseSettings.hint")}

+ +
+ + +
+
+ ); +}; + +export default ExerciseTimer; From 237c5047d5ec9699aa10b6d1a84ba7bb110ee0c8 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 10:48:17 +0700 Subject: [PATCH 114/199] setting page: update LogSettings --- .../{LogSettings.jsx => LogSettings.tsx} | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) rename src/components/setting_page/{LogSettings.jsx => LogSettings.tsx} (92%) diff --git a/src/components/setting_page/LogSettings.jsx b/src/components/setting_page/LogSettings.tsx similarity index 92% rename from src/components/setting_page/LogSettings.jsx rename to src/components/setting_page/LogSettings.tsx index ef3c85c8..c6605f5d 100644 --- a/src/components/setting_page/LogSettings.jsx +++ b/src/components/setting_page/LogSettings.tsx @@ -1,11 +1,19 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuExternalLink } from "react-icons/lu"; -import { sonnerSuccessToast } from "../../utils/sonnerCustomToast"; +import { sonnerSuccessToast } from "../../utils/sonnerCustomToast.js"; + +interface LogSettings { + numOfLogs: number; + keepForDays: number; + logLevel: string; + logFormat: string; + maxLogSize: number; +} const LogSettings = () => { const { t } = useTranslation(); - const [, setFolderPath] = useState(null); + const [, setFolderPath] = useState(null); const maxLogOptions = useMemo( () => [ @@ -73,7 +81,9 @@ const LogSettings = () => { let isMounted = true; async function fetchLogSettings() { try { - const settings = await window.electron.ipcRenderer.invoke("get-log-settings"); + const settings = (await window.electron.ipcRenderer.invoke( + "get-log-settings" + )) as LogSettings; if (!isMounted) return; // Find the corresponding options based on stored values const initialMaxLog = @@ -100,7 +110,7 @@ const LogSettings = () => { // Memoize the function so that it doesn't change on every render const handleApplySettings = useCallback( - (maxLogWrittenValue, deleteLogsOlderThanValue) => { + (maxLogWrittenValue: string, deleteLogsOlderThanValue: string) => { const selectedMaxLogOption = maxLogOptions.find( (option) => option.value === maxLogWrittenValue ); @@ -108,6 +118,10 @@ const LogSettings = () => { (option) => option.value === deleteLogsOlderThanValue ); + if (!selectedMaxLogOption || !selectedDeleteLogOption) { + return; + } + const electronSettings = { numOfLogs: selectedMaxLogOption.numOfLogs, keepForDays: selectedDeleteLogOption.keepForDays, @@ -121,7 +135,7 @@ const LogSettings = () => { ); // Helper function to get the label based on the current value - const getLabel = (options, value) => { + const getLabel = (options: { value: string; label: string }[], value: string) => { const selectedOption = options.find((option) => option.value === value); return selectedOption ? selectedOption.label : value; }; @@ -129,7 +143,7 @@ const LogSettings = () => { const handleOpenLogFolder = async () => { // Send an IPC message to open the folder and get the folder path const logFolder = await window.electron.ipcRenderer.invoke("open-log-folder"); - setFolderPath(logFolder); // Save the folder path in state + setFolderPath(logFolder as string); // Save the folder path in state }; if (loading) { From 9dae5d533193bb57e6bb498a4328aa96d9a258c5 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 10:58:53 +0700 Subject: [PATCH 115/199] setting page: update SaveFolderSettings --- ...derSettings.jsx => SaveFolderSettings.tsx} | 114 +++++++++++++----- 1 file changed, 87 insertions(+), 27 deletions(-) rename src/components/setting_page/{SaveFolderSettings.jsx => SaveFolderSettings.tsx} (76%) diff --git a/src/components/setting_page/SaveFolderSettings.jsx b/src/components/setting_page/SaveFolderSettings.tsx similarity index 76% rename from src/components/setting_page/SaveFolderSettings.jsx rename to src/components/setting_page/SaveFolderSettings.tsx index c3ba3c5f..72c78b7b 100644 --- a/src/components/setting_page/SaveFolderSettings.jsx +++ b/src/components/setting_page/SaveFolderSettings.tsx @@ -1,33 +1,85 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { IoWarningOutline } from "react-icons/io5"; -import isElectron from "../../utils/isElectron"; -import { sonnerErrorToast, sonnerSuccessToast } from "../../utils/sonnerCustomToast"; +import isElectron from "../../utils/isElectron.js"; +import { sonnerErrorToast, sonnerSuccessToast } from "../../utils/sonnerCustomToast.js"; + +// Types for IPC events +type MoveFolderProgressPhase = "copy" | "delete" | "delete-dir" | "delete-done"; + +interface MoveFolderProgress { + moved: number; + total: number; + phase: MoveFolderProgressPhase; + name: string | null; +} + +interface VenvDeleteStatusDeleting { + status: "deleting"; + path: string; +} +interface VenvDeleteStatusDeleted { + status: "deleted"; + path: string; +} +interface VenvDeleteStatusError { + status: "error"; + path: string; + error: string; +} +type VenvDeleteStatus = VenvDeleteStatusDeleting | VenvDeleteStatusDeleted | VenvDeleteStatusError; + +interface SetCustomSaveFolderSuccess { + success: true; + newPath: string; +} +interface SetCustomSaveFolderError { + success: false; + error: string; + reason?: string; +} +type SetCustomSaveFolderResult = SetCustomSaveFolderSuccess | SetCustomSaveFolderError; + +interface ShowOpenDialogResult { + canceled: boolean; + filePaths: string[]; + bookmarks?: string[]; +} + +// For translation return type +const isStringArray = (val: unknown): val is string[] => { + return Array.isArray(val) && val.every((v) => typeof v === "string"); +}; const SaveFolderSettings = () => { const { t } = useTranslation(); const [currentFolder, setCurrentFolder] = useState(""); - const [customFolder, setCustomFolder] = useState(null); + const [customFolder, setCustomFolder] = useState(null); const [loading, setLoading] = useState(false); const [moveDialogOpen, setMoveDialogOpen] = useState(false); - const [moveProgress, setMoveProgress] = useState(null); - const [venvDeleteStatus, setVenvDeleteStatus] = useState(null); - const moveDialogRef = useRef(null); - const confirmRef = useRef(null); - const [confirmAction, setConfirmAction] = useState(null); + const [moveProgress, setMoveProgress] = useState(null); + const [venvDeleteStatus, setVenvDeleteStatus] = useState(null); + const moveDialogRef = useRef(null); + const confirmRef = useRef(null); + const [confirmAction, setConfirmAction] = useState<"choose" | "reset" | null>(null); const [processStarting, setProcessStarting] = useState(false); useEffect(() => { if (!isElectron()) return; // Get the resolved save folder - window.electron.ipcRenderer.invoke("get-save-folder").then(setCurrentFolder); + window.electron.ipcRenderer.invoke("get-save-folder").then((folder) => { + setCurrentFolder(folder as string); + }); // Get the custom folder (if any) - window.electron.ipcRenderer.invoke("get-custom-save-folder").then(setCustomFolder); + window.electron.ipcRenderer.invoke("get-custom-save-folder").then((folder) => { + setCustomFolder(folder as string); + }); }, []); useEffect(() => { if (!isElectron()) return; - const handler = (_event, data) => { + const handler = (...args: unknown[]) => { + const data = args[0] as MoveFolderProgress; setProcessStarting(false); setMoveProgress(data); setMoveDialogOpen(true); @@ -44,7 +96,8 @@ const SaveFolderSettings = () => { useEffect(() => { if (!isElectron()) return; - const handler = (_event, data) => { + const handler = (...args: unknown[]) => { + const data = args[0] as VenvDeleteStatus; setProcessStarting(false); setVenvDeleteStatus(data); }; @@ -60,18 +113,18 @@ const SaveFolderSettings = () => { setProcessStarting(true); try { // Open folder dialog via Electron - const folderPaths = await window.electron.ipcRenderer.invoke("show-open-dialog", { + const dialogResult = (await window.electron.ipcRenderer.invoke("show-open-dialog", { properties: ["openDirectory"], title: t("settingPage.saveFolderSettings.saveFolderChooseBtn"), - }); - if (folderPaths && folderPaths.length > 0) { + })) as ShowOpenDialogResult; + if (!dialogResult.canceled && dialogResult.filePaths.length > 0) { setMoveDialogOpen(true); setMoveProgress({ moved: 0, total: 1, phase: "copy", name: "" }); - const selected = folderPaths[0]; - const result = await window.electron.ipcRenderer.invoke( + const selected = dialogResult.filePaths[0]; + const result = (await window.electron.ipcRenderer.invoke( "set-custom-save-folder", selected - ); + )) as SetCustomSaveFolderResult; setMoveDialogOpen(false); setMoveProgress(null); if (result.success) { @@ -99,11 +152,14 @@ const SaveFolderSettings = () => { try { setMoveDialogOpen(true); setMoveProgress({ moved: 0, total: 1, phase: "copy", name: "" }); - const result = await window.electron.ipcRenderer.invoke("set-custom-save-folder", null); + const result = (await window.electron.ipcRenderer.invoke( + "set-custom-save-folder", + null + )) as SetCustomSaveFolderResult; setMoveDialogOpen(false); setMoveProgress(null); setCustomFolder(null); - setCurrentFolder(result.newPath || ""); + setCurrentFolder(result.success ? result.newPath : ""); } finally { setLoading(false); } @@ -111,12 +167,12 @@ const SaveFolderSettings = () => { const openChooseDialog = () => { setConfirmAction("choose"); - confirmRef.current.showModal(); + confirmRef.current?.showModal(); }; const openResetDialog = () => { setConfirmAction("reset"); - confirmRef.current.showModal(); + confirmRef.current?.showModal(); }; const handleConfirm = () => { @@ -125,17 +181,23 @@ const SaveFolderSettings = () => { } else if (confirmAction === "reset") { handleResetDefault(); } - confirmRef.current.close(); + confirmRef.current?.close(); setConfirmAction(null); }; const handleCancel = () => { - confirmRef.current.close(); + confirmRef.current?.close(); setConfirmAction(null); }; if (!isElectron()) return null; + // For translation array + const confirmDescription = t("settingPage.saveFolderSettings.saveFolderConfirmDescription", { + returnObjects: true, + }); + const confirmDescriptionArr = isStringArray(confirmDescription) ? confirmDescription : []; + return ( <>
@@ -187,9 +249,7 @@ const SaveFolderSettings = () => { {t("settingPage.saveFolderSettings.saveFolderConfirmTitle")}
- {t("settingPage.saveFolderSettings.saveFolderConfirmDescription", { - returnObjects: true, - }).map((desc, index) => ( + {confirmDescriptionArr.map((desc, index) => (

{desc}

From 1562003c03f856a16d0c07058285219528a969c5 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 10:59:41 +0700 Subject: [PATCH 116/199] setting page: update ResetSettings --- .../{ResetSettings.jsx => ResetSettings.tsx} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename src/components/setting_page/{ResetSettings.jsx => ResetSettings.tsx} (96%) diff --git a/src/components/setting_page/ResetSettings.jsx b/src/components/setting_page/ResetSettings.tsx similarity index 96% rename from src/components/setting_page/ResetSettings.jsx rename to src/components/setting_page/ResetSettings.tsx index 1a0634a2..a60ab9ac 100644 --- a/src/components/setting_page/ResetSettings.jsx +++ b/src/components/setting_page/ResetSettings.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import isElectron from "../../utils/isElectron"; -import { sonnerSuccessToast } from "../../utils/sonnerCustomToast"; +import isElectron from "../../utils/isElectron.js"; +import { sonnerSuccessToast } from "../../utils/sonnerCustomToast.js"; const ResetSettings = () => { const { t } = useTranslation(); @@ -9,10 +9,10 @@ const ResetSettings = () => { const [isResettingLocalStorage, setIsResettingLocalStorage] = useState(false); const [isResettingIndexedDb, setIsResettingIndexedDb] = useState(false); - const localStorageModal = useRef(null); - const indexedDbModal = useRef(null); + const localStorageModal = useRef(null); + const indexedDbModal = useRef(null); - const checkAndCloseDatabase = async (dbName) => { + const checkAndCloseDatabase = async (dbName: string) => { // Check if the database exists const databases = await window.indexedDB.databases(); const exists = databases.some((db) => db.name === dbName); @@ -25,7 +25,7 @@ const ResetSettings = () => { return new Promise((resolve) => { const dbRequest = window.indexedDB.open(dbName); dbRequest.onsuccess = (event) => { - const db = event.target.result; + const db = (event.target as IDBOpenDBRequest).result; db.close(); resolve(true); // Database exists and is closed }; From b520ffba220aa5f4c82f96c9dffeeaeedd7e9c2e Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 11:00:34 +0700 Subject: [PATCH 117/199] setting page: update VideoDownloadMenu --- .../{VideoDownloadMenu.jsx => VideoDownloadMenu.tsx} | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) rename src/components/setting_page/{VideoDownloadMenu.jsx => VideoDownloadMenu.tsx} (83%) diff --git a/src/components/setting_page/VideoDownloadMenu.jsx b/src/components/setting_page/VideoDownloadMenu.tsx similarity index 83% rename from src/components/setting_page/VideoDownloadMenu.jsx rename to src/components/setting_page/VideoDownloadMenu.tsx index 4ea9b7c0..3b364244 100644 --- a/src/components/setting_page/VideoDownloadMenu.jsx +++ b/src/components/setting_page/VideoDownloadMenu.tsx @@ -1,7 +1,6 @@ -import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -const VideoDownloadMenu = ({ onClick }) => { +const VideoDownloadMenu = ({ onClick }: { onClick: () => void }) => { const { t } = useTranslation(); return ( @@ -22,8 +21,4 @@ const VideoDownloadMenu = ({ onClick }) => { ); }; -VideoDownloadMenu.propTypes = { - onClick: PropTypes.func.isRequired, -}; - export default VideoDownloadMenu; From d655787fceb823b03a90beba44f6217ed7db35cc Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 11:12:49 +0700 Subject: [PATCH 118/199] setting page: update VideoDownloadTable --- ...wnloadTable.jsx => VideoDownloadTable.tsx} | 131 ++++++++++-------- 1 file changed, 76 insertions(+), 55 deletions(-) rename src/components/setting_page/{VideoDownloadTable.jsx => VideoDownloadTable.tsx} (82%) diff --git a/src/components/setting_page/VideoDownloadTable.jsx b/src/components/setting_page/VideoDownloadTable.tsx similarity index 82% rename from src/components/setting_page/VideoDownloadTable.jsx rename to src/components/setting_page/VideoDownloadTable.tsx index 7fea6aeb..8a633f05 100644 --- a/src/components/setting_page/VideoDownloadTable.jsx +++ b/src/components/setting_page/VideoDownloadTable.tsx @@ -1,68 +1,96 @@ -import PropTypes from "prop-types"; import { useEffect, useRef, useState } from "react"; -import { Trans } from "react-i18next"; import { BsCheckCircleFill, BsXCircleFill } from "react-icons/bs"; import { LuExternalLink } from "react-icons/lu"; -const VideoDownloadTable = ({ t, data, isDownloaded, onStatusChange }) => { - const [, setShowVerifyModal] = useState(false); - const [showProgressModal, setShowProgressModal] = useState(false); - const [selectedZip, setSelectedZip] = useState(null); - const [verifyFiles, setVerifyFiles] = useState([]); - const [progress, setProgress] = useState(0); - const [modalMessage, setModalMessage] = useState(""); - const [isSuccess, setIsSuccess] = useState(null); - const [progressText, setProgressText] = useState(""); - const [isPercentage, setIsPercentage] = useState(false); - const [progressError, setProgressError] = useState(false); - const [verificationErrors, setVerificationErrors] = useState([]); +interface ExtractedFile { + name: string; + hash: string; +} - const verifyModal = useRef(null); - const progressModal = useRef(null); +interface ZipContent { + extractedFiles: ExtractedFile[]; +} + +export interface VideoFileData { + zipFile: string; + name: string; + fileSize: number; + link: string; + zipHash: string; + zipContents: ZipContent[]; +} + +export interface DownloadStatus { + zipFile: string; + isDownloaded: boolean; + hasExtractedFolder: boolean; +} + +interface VerificationError { + type: string; + name: string; + message: string; +} + +interface VideoDownloadTableProps { + t: (key: string) => string; + data: VideoFileData[]; + isDownloaded: DownloadStatus[]; + onStatusChange?: () => void | Promise; +} + +const VideoDownloadTable = ({ t, data, isDownloaded, onStatusChange }: VideoDownloadTableProps) => { + const [, setShowVerifyModal] = useState(false); + const [showProgressModal, setShowProgressModal] = useState(false); + const [selectedZip, setSelectedZip] = useState(null); + const [verifyFiles, setVerifyFiles] = useState([]); + const [progress, setProgress] = useState(0); + const [modalMessage, setModalMessage] = useState(""); + const [isSuccess, setIsSuccess] = useState(null); + const [progressText, setProgressText] = useState(""); + const [isPercentage, setIsPercentage] = useState(false); + const [progressError, setProgressError] = useState(false); + const [verificationErrors, setVerificationErrors] = useState([]); + + const verifyModal = useRef(null); + const progressModal = useRef(null); useEffect(() => { window.scrollTo(0, 0); - const handleProgressUpdate = (event, percentage) => { + const handleProgressUpdate = (...args: unknown[]) => { + const percentage = args[1] as number; setProgress(percentage); - setIsPercentage(true); // Use percentage-based progress + setIsPercentage(true); }; - const handleProgressText = (event, text) => { - setProgressText(t(text)); // Update progress text - setIsPercentage(false); // Not percentage-based, show full progress bar + const handleProgressText = (...args: unknown[]) => { + const text = args[1] as string; + setProgressText(t(text)); + setIsPercentage(false); }; - const handleVerificationSuccess = (event, data) => { - setModalMessage( - <> - {t(data.messageKey)}{" "} - - {data.param} - - - ); + const handleVerificationSuccess = (...args: unknown[]) => { + const data = args[1] as { messageKey: string; param: string }; + setModalMessage(`${t(data.messageKey)} ${data.param}`); setIsSuccess(true); - setShowProgressModal(false); // Hide modal after success + setShowProgressModal(false); setVerificationErrors([]); - if (onStatusChange) onStatusChange(); // Trigger parent to refresh status + if (onStatusChange) onStatusChange(); }; - const handleVerificationError = (event, data) => { + const handleVerificationError = (...args: unknown[]) => { + const data = args[1] as { messageKey: string; param: string; errorMessage?: string }; setModalMessage( - - - {data.param} - - {data.errorMessage ? `Error message: ${data.errorMessage}` : ""} - + `${t(data.messageKey)} ${data.param} ${data.errorMessage ? `Error message: ${data.errorMessage}` : ""}` ); setIsSuccess(false); - setShowProgressModal(false); // Hide modal on error + setShowProgressModal(false); setVerificationErrors([]); - if (onStatusChange) onStatusChange(); // Trigger parent to refresh status + if (onStatusChange) onStatusChange(); }; - const handleVerificationErrors = (event, errors) => { + const handleVerificationErrors = (...args: unknown[]) => { + const errors = args[1] as VerificationError[]; setVerificationErrors(errors); setIsSuccess(false); setShowProgressModal(false); @@ -82,9 +110,9 @@ const VideoDownloadTable = ({ t, data, isDownloaded, onStatusChange }) => { window.electron.ipcRenderer.removeAllListeners("verification-error"); window.electron.ipcRenderer.removeAllListeners("verification-errors"); }; - }, [t, data.messageKey, data.param, onStatusChange]); + }, [t, onStatusChange]); - const handleVerify = async (zip) => { + const handleVerify = async (zip: VideoFileData) => { if (onStatusChange) { // Await refresh if onStatusChange returns a promise await onStatusChange(); @@ -94,9 +122,9 @@ const VideoDownloadTable = ({ t, data, isDownloaded, onStatusChange }) => { // Allow verify if extracted folder exists, even if not downloaded const fileToVerify = fileStatus && zip.name && (fileStatus.isDownloaded || fileStatus.hasExtractedFolder) - ? [{ name: zip.name }] + ? [{ name: zip.name, hash: "" }] : fileStatus && fileStatus.hasExtractedFolder && zip.name - ? [{ name: zip.name }] + ? [{ name: zip.name, hash: "" }] : []; setSelectedZip(zip); setVerifyFiles(fileToVerify); @@ -114,9 +142,9 @@ const VideoDownloadTable = ({ t, data, isDownloaded, onStatusChange }) => { fileStatus && selectedZip?.name && (fileStatus.isDownloaded || fileStatus.hasExtractedFolder) - ? [{ name: selectedZip.name }] + ? [{ name: selectedZip.name, hash: "" }] : fileStatus && fileStatus.hasExtractedFolder && selectedZip?.name - ? [{ name: selectedZip.name }] + ? [{ name: selectedZip.name, hash: "" }] : []; if (fileToVerify.length === 0) { setShowVerifyModal(false); @@ -343,11 +371,4 @@ const VideoDownloadTable = ({ t, data, isDownloaded, onStatusChange }) => { ); }; -VideoDownloadTable.propTypes = { - t: PropTypes.func.isRequired, - data: PropTypes.array.isRequired, - isDownloaded: PropTypes.array.isRequired, - onStatusChange: PropTypes.func, -}; - export default VideoDownloadTable; From c93b1ecd379b0fba4c7e37de6c6b5c2bca5eb835 Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 11:13:02 +0700 Subject: [PATCH 119/199] setting page: update VideoDownloadSubPage --- ...adSubPage.jsx => VideoDownloadSubPage.tsx} | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) rename src/components/setting_page/{VideoDownloadSubPage.jsx => VideoDownloadSubPage.tsx} (78%) diff --git a/src/components/setting_page/VideoDownloadSubPage.jsx b/src/components/setting_page/VideoDownloadSubPage.tsx similarity index 78% rename from src/components/setting_page/VideoDownloadSubPage.jsx rename to src/components/setting_page/VideoDownloadSubPage.tsx index 62dc20f7..bdc7cb8e 100644 --- a/src/components/setting_page/VideoDownloadSubPage.jsx +++ b/src/components/setting_page/VideoDownloadSubPage.tsx @@ -1,24 +1,23 @@ -import PropTypes from "prop-types"; import { useCallback, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { BsArrowLeft } from "react-icons/bs"; import { IoWarningOutline } from "react-icons/io5"; import { LuExternalLink } from "react-icons/lu"; -import isElectron from "../../utils/isElectron"; -import VideoDownloadTable from "./VideoDownloadTable"; +import isElectron from "../../utils/isElectron.js"; +import VideoDownloadTable, { VideoFileData, DownloadStatus } from "./VideoDownloadTable.js"; -const VideoDownloadSubPage = ({ onGoBack }) => { +const VideoDownloadSubPage = ({ onGoBack }: { onGoBack: () => void }) => { const { t } = useTranslation(); - const [, setFolderPath] = useState(null); - const [zipFileData, setZipFileData] = useState([]); - const [isDownloaded, setIsDownloaded] = useState([]); + const [, setFolderPath] = useState(null); + const [zipFileData, setZipFileData] = useState([]); + const [isDownloaded, setIsDownloaded] = useState([]); const [tableLoading, setTableLoading] = useState(true); const handleOpenFolder = async () => { // Send an IPC message to open the folder and get the folder path const videoFolder = await window.electron.ipcRenderer.invoke("get-video-save-folder"); - setFolderPath(videoFolder); // Save the folder path in state + setFolderPath(videoFolder as string); // Save the folder path in state }; // Fetch JSON data from Electron's main process via IPC when component mounts @@ -26,10 +25,12 @@ const VideoDownloadSubPage = ({ onGoBack }) => { const fetchData = async () => { try { const data = await window.electron.ipcRenderer.invoke("get-video-file-data"); - setZipFileData(data); // Set the JSON data into the state + setZipFileData(data as VideoFileData[]); // Set the JSON data into the state } catch (error) { console.error("Error reading JSON file:", error); // Handle any error - isElectron() && window.electron.log("error", `Error reading JSON file: ${error}`); + if (isElectron()) { + window.electron.log("error", `Error reading JSON file: ${error}`); + } } }; @@ -39,32 +40,41 @@ const VideoDownloadSubPage = ({ onGoBack }) => { const checkDownloadedFiles = useCallback(async () => { try { const downloadedFiles = await window.electron.ipcRenderer.invoke("check-downloads"); - console.log("Downloaded files in folder:", downloadedFiles); - isElectron() && - window.electron.log("log", `Downloaded files in folder: ${downloadedFiles}`); + let downloadedList: string[] = []; + if (Array.isArray(downloadedFiles)) { + downloadedList = downloadedFiles as string[]; + } else if (typeof downloadedFiles === "string") { + // If the string is "no zip files downloaded", treat as empty + downloadedList = []; + } + if (isElectron()) { + window.electron.log("log", `Downloaded files in folder: ${downloadedList}`); + } // Initialize fileStatus as an array to hold individual statuses - const newFileStatus = []; + const newFileStatus: DownloadStatus[] = []; for (const item of zipFileData) { - let extractedFolderExists; + let extractedFolderExists = false; try { - extractedFolderExists = await window.electron.ipcRenderer.invoke( + const result = await window.electron.ipcRenderer.invoke( "check-extracted-folder", item.zipFile.replace(".7z", ""), item.zipContents ); + extractedFolderExists = Boolean(result); } catch (error) { console.error(`Error checking extracted folder for ${item.zipFile}:`, error); - isElectron() && + if (isElectron()) { window.electron.log( "error", `Error checking extracted folder for ${item.zipFile}: ${error}` ); + } extractedFolderExists = false; // Default to false if there's an error } - const isDownloadedFile = downloadedFiles.includes(item.zipFile); + const isDownloadedFile = downloadedList.includes(item.zipFile); newFileStatus.push({ zipFile: item.zipFile, isDownloaded: isDownloadedFile, @@ -77,11 +87,12 @@ const VideoDownloadSubPage = ({ onGoBack }) => { console.log(newFileStatus); } catch (error) { console.error("Error checking downloaded or extracted files:", error); - isElectron() && + if (isElectron()) { window.electron.log( "error", `Error checking downloaded or extracted files: ${error}` ); + } } }, [zipFileData]); @@ -92,11 +103,11 @@ const VideoDownloadSubPage = ({ onGoBack }) => { } }, [zipFileData, checkDownloadedFiles]); + // i18next returns $SpecialObject for returnObjects: true, so cast to string[] const localizedInstructionStep = t("settingPage.videoDownloadSettings.steps", { returnObjects: true, - }); - - const stepCount = localizedInstructionStep.length; + }) as string[]; + const stepCount = Array.isArray(localizedInstructionStep) ? localizedInstructionStep.length : 0; const stepKeys = Array.from( { length: stepCount }, (_, i) => `settingPage.videoDownloadSettings.steps.${i}` @@ -176,8 +187,4 @@ const VideoDownloadSubPage = ({ onGoBack }) => { ); }; -VideoDownloadSubPage.propTypes = { - onGoBack: PropTypes.func.isRequired, -}; - export default VideoDownloadSubPage; From 219ea6b0d3216ec6a1a2453653e33b3510af690d Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 11:28:34 +0700 Subject: [PATCH 120/199] setting page: update pronunciationStepUtils --- ...StepUtils.js => pronunciationStepUtils.ts} | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) rename src/components/setting_page/{pronunciationStepUtils.js => pronunciationStepUtils.ts} (53%) diff --git a/src/components/setting_page/pronunciationStepUtils.js b/src/components/setting_page/pronunciationStepUtils.ts similarity index 53% rename from src/components/setting_page/pronunciationStepUtils.js rename to src/components/setting_page/pronunciationStepUtils.ts index f8e07078..9cb621f2 100644 --- a/src/components/setting_page/pronunciationStepUtils.js +++ b/src/components/setting_page/pronunciationStepUtils.ts @@ -1,7 +1,57 @@ +// Types for pronunciation checker install process + +export type PronunciationStepStatus = "pending" | "success" | "error"; + +export interface PronunciationDependency { + name: string; + status: "pending" | "success" | "error" | "cancelled"; + log?: string; +} + +export interface PronunciationPythonStatus { + found: boolean; + version?: string | null; + stderr?: string; + log?: string; +} + +export interface PronunciationModelStatus { + status: "pending" | "success" | "error" | "found" | "downloading" | string; + message?: string; + log?: string; +} + +export interface PronunciationInstallStatus { + python?: PronunciationPythonStatus; + dependencies?: PronunciationDependency[]; + model?: PronunciationModelStatus; + stderr?: string; + log?: string; + timestamp?: number; + // For legacy/IPC compatibility + found?: boolean; + version?: string | null; + deps?: PronunciationDependency[]; + modelStatus?: string; + modelMessage?: string; + modelLog?: string; + pythonLog?: string; + dependencyLog?: string; +} + // Utility to calculate step statuses for pronunciation checker -const getPronunciationStepStatuses = (pythonCheckResult, checking, error) => { +export const getPronunciationStepStatuses = ( + pythonCheckResult: PronunciationInstallStatus | null, + checking: boolean, + error: string | null +): { + step1Status: PronunciationStepStatus; + step2Status: PronunciationStepStatus; + step3Status: PronunciationStepStatus; + deps: PronunciationDependency[] | undefined; +} => { // Step 1: Checking Python installation - let step1Status = checking + const step1Status: PronunciationStepStatus = checking ? "pending" : error ? "error" @@ -12,11 +62,14 @@ const getPronunciationStepStatuses = (pythonCheckResult, checking, error) => { : "pending"; // Step 2: Installing dependencies - let step2Status = "pending"; - let deps = pythonCheckResult && pythonCheckResult.deps; + let step2Status: PronunciationStepStatus = "pending"; + const deps = + pythonCheckResult && Array.isArray(pythonCheckResult.deps) + ? pythonCheckResult.deps + : undefined; if (step1Status === "error") { step2Status = "error"; - } else if (deps && Array.isArray(deps)) { + } else if (deps) { if (deps.some((dep) => dep.status === "error")) step2Status = "error"; else if (deps.every((dep) => dep.status === "success")) step2Status = "success"; else if (deps.some((dep) => dep.status === "pending")) step2Status = "pending"; @@ -25,7 +78,7 @@ const getPronunciationStepStatuses = (pythonCheckResult, checking, error) => { } // Step 3: Downloading phoneme model - let step3Status = "pending"; + let step3Status: PronunciationStepStatus = "pending"; if (step1Status === "error" || step2Status === "error") { step3Status = "error"; } else if (pythonCheckResult && pythonCheckResult.modelStatus) { @@ -47,23 +100,26 @@ const getPronunciationStepStatuses = (pythonCheckResult, checking, error) => { }; // Utility to determine overall install state: 'not_installed', 'failed', or 'complete' -const getPronunciationInstallState = (statusObj) => { +export const getPronunciationInstallState = ( + statusObj: PronunciationInstallStatus | null +): "not_installed" | "failed" | "complete" => { if (!statusObj) return "not_installed"; // Recursively check for any error/failed status - const hasErrorStatus = (obj) => { + const hasErrorStatus = (obj: unknown): boolean => { if (!obj || typeof obj !== "object") return false; if (Array.isArray(obj)) { return obj.some(hasErrorStatus); } for (const key in obj) { + const value = (obj as Record)[key]; if ( (key === "status" && - (obj[key] === "error" || obj[key] === "failed" || obj[key] === "cancelled")) || - (key === "found" && obj[key] === false) + (value === "error" || value === "failed" || value === "cancelled")) || + (key === "found" && value === false) ) { return true; } - if (typeof obj[key] === "object" && hasErrorStatus(obj[key])) { + if (typeof value === "object" && value !== null && hasErrorStatus(value)) { return true; } } @@ -82,5 +138,3 @@ const getPronunciationInstallState = (statusObj) => { } return "not_installed"; }; - -export { getPronunciationInstallState, getPronunciationStepStatuses }; From 72059fa6b33d53a53404d7da1d3ad394f21a5e8d Mon Sep 17 00:00:00 2001 From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Date: Sat, 31 May 2025 11:28:54 +0700 Subject: [PATCH 121/199] setting page: update PronunciationCheckerDialogContent --- ... => PronunciationCheckerDialogContent.tsx} | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) rename src/components/setting_page/{PronunciationCheckerDialogContent.jsx => PronunciationCheckerDialogContent.tsx} (87%) diff --git a/src/components/setting_page/PronunciationCheckerDialogContent.jsx b/src/components/setting_page/PronunciationCheckerDialogContent.tsx similarity index 87% rename from src/components/setting_page/PronunciationCheckerDialogContent.jsx rename to src/components/setting_page/PronunciationCheckerDialogContent.tsx index 842cca9e..1559415e 100644 --- a/src/components/setting_page/PronunciationCheckerDialogContent.jsx +++ b/src/components/setting_page/PronunciationCheckerDialogContent.tsx @@ -1,8 +1,17 @@ -import PropTypes from "prop-types"; import { Trans } from "react-i18next"; import { IoInformationCircleOutline } from "react-icons/io5"; -import openExternal from "../../utils/openExternal"; -import modelOptions from "./modelOptions"; +import openExternal from "../../utils/openExternal.js"; +import modelOptions from "./modelOptions.js"; + +interface PronunciationCheckerDialogContentProps { + t: (key: string, options?: Record) => string; + checking: boolean; + closeConfirmDialog: () => void; + handleProceed: () => void; + installState: "not_installed" | "failed" | "complete"; + modelValue: string; + onModelChange: (value: string) => void; +} const PronunciationCheckerDialogContent = ({ t, @@ -12,7 +21,8 @@ const PronunciationCheckerDialogContent = ({ installState, modelValue, onModelChange, -}) => { +}: PronunciationCheckerDialogContentProps) => { + const selectedModel = modelOptions.find((opt) => opt.value === modelValue); return (

@@ -24,13 +34,15 @@ const PronunciationCheckerDialogContent = ({