diff --git a/.storybook/main.ts b/.storybook/main.ts index 33319382..7e623966 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -2,13 +2,13 @@ import type { StorybookConfig } from '@storybook/react-webpack5' const config: StorybookConfig = { stories: [ - '../src/**/*.mdx', - '../src/components/SchemaForm/**/*.stories.tsx', - '../src/components/Icons/BladeIcon/BladeIcon.stories.tsx', - '../src/stories/Doc/Api.stories.tsx', - '../src/components/Popover/Popover.stories.tsx', - '../src/pages/Devices/DeviceCard/DeviceCard.stories.tsx', - ], + '../src/**/*.mdx', + '../src/components/SchemaForm/**/*.stories.tsx', + '../src/components/Icons/BladeIcon/BladeIcon.stories.tsx', + '../src/stories/Doc/Api.stories.tsx', + '../src/components/Popover/Popover.stories.tsx', + '../src/pages/Devices/DeviceCard/DeviceCard.stories.tsx' + ], addons: [ '@storybook/preset-create-react-app', // '@storybook/addon-onboarding', diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 061dd2d8..807beade 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,24 +1,27 @@ import type { Preview } from '@storybook/react' -import { BladeDarkBlueTheme as darkTheme, BladeLightBlueTheme as lightTheme } from '../src/themes/AppThemes' +import { + BladeDarkBlueTheme as darkTheme, + BladeLightBlueTheme as lightTheme +} from '../src/themes/AppThemes' import './globals.css' import '../src/index.css' -import { ThemeProvider, CssBaseline } from '@mui/material'; -import { withThemeFromJSXProvider } from '@storybook/addon-themes'; - +import { ThemeProvider, CssBaseline } from '@mui/material' +import { withThemeFromJSXProvider } from '@storybook/addon-themes' const preview: Preview = { decorators: [ withThemeFromJSXProvider({ - GlobalStyles: CssBaseline, - Provider: ThemeProvider, - themes: { - // Provide your custom themes here - light: lightTheme, - dark: darkTheme, - }, - defaultTheme: 'light', - })], + GlobalStyles: CssBaseline, + Provider: ThemeProvider, + themes: { + // Provide your custom themes here + light: lightTheme, + dark: darkTheme + }, + defaultTheme: 'light' + }) + ], parameters: { options: { storySort: { @@ -27,7 +30,13 @@ const preview: Preview = { 'BladeBook', ['Introduction', 'Getting Started', 'App Structure', 'Guides'], 'UI Components', - ['Examples', 'Schema Components', 'Base Components', 'Default', ['*', 'Color']], + [ + 'Examples', + 'Schema Components', + 'Base Components', + 'Default', + ['*', 'Color'] + ], 'Api' ] } @@ -40,7 +49,7 @@ const preview: Preview = { } } }, - tags: ['autodocs'], + tags: ['autodocs'] } export default preview diff --git a/eslint.config.mjs b/eslint.config.mjs index 4599d94d..60d8af74 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,21 +17,22 @@ const compat = new FlatCompat({ export default [ { - ignores: ['**/build/*', '**/*.js', '**/*.jsx', 'extraResources/**'] + ignores: [ + '**/build/*', + '**/*.js', + '**/*.jsx', + '**/*.mjs', + '**/*.cjs', + 'extraResources/**' + ] }, ...compat.extends( 'standard', 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended' - // 'plugin:react-hooks/recommended' + 'plugin:react-hooks/recommended', + 'plugin:prettier/recommended', ), { - plugins: { - '@typescript-eslint': typescriptEslint, - 'prettier/prettier': prettier - // 'react-hooks': reactHooks - }, - languageOptions: { parser: tsParser, parserOptions: { @@ -94,7 +95,7 @@ export default [ 'no-nested-ternary': 0, 'import/extensions': 0, 'import/first': 0, - '@typescript-eslint/indent': [2, 2], + '@/indent': [2, 2, { SwitchCase: 1}], 'react/react-in-jsx-scope': 'off', 'react/function-component-definition': [0], 'react/jsx-props-no-spreading': 0, @@ -107,6 +108,11 @@ export default [ '@typescript-eslint/no-explicit-any': 0, '@prettier/trailing-comma': 0, 'jsx-a11y/label-has-associated-control': 0 - } + }, + plugins: { + '@typescript-eslint': typescriptEslint, + 'prettier/prettier': prettier + // 'react-hooks': reactHooks + }, } ] diff --git a/package.json b/package.json index 854f7d83..dc74b879 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,7 @@ "cross-env": "^7.0.3", "electron": "^33.2.1", "electron-builder": "^25.1.8", - "eslint": "8.57", + "eslint": "9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-flowtype": "^8.0.3", @@ -215,6 +215,7 @@ "eslint-plugin-n": "^17.14.0", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-storybook": "^0.11.1", "gh-pages": "^6.1.1", "prettier": "^3.4.1", diff --git a/src/App.tsx b/src/App.tsx index 5faa5ff4..6b0b4c43 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/indent */ +/* eslint-disable @/indent */ import { useEffect, useMemo } from 'react' import { createTheme, ThemeProvider } from '@mui/material/styles' import { SnackbarProvider } from 'notistack' @@ -38,11 +38,16 @@ export default function App() { const reloadTheme = useStore((state) => state.ui.reloadTheme) const theme = useMemo( - () => createTheme({ - ...ledfxThemes[window.localStorage.getItem('ledfx-theme') ?? ledfxTheme], + () => + createTheme({ + ...ledfxThemes[ + window.localStorage.getItem('ledfx-theme') ?? ledfxTheme + ], ...common, palette: { - ...ledfxThemes[window.localStorage.getItem('ledfx-theme') ?? ledfxTheme].palette + ...ledfxThemes[ + window.localStorage.getItem('ledfx-theme') ?? ledfxTheme + ].palette } }), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -59,7 +64,6 @@ export default function App() { initFrontendConfig() console.info( - // eslint-disable-next-line no-useless-concat '%c Ledfx ' + '%c\n ReactApp by Blade ', 'padding: 10px 40px; color: #ffffff; border-radius: 5px 5px 0 0; background-color: #800000;', 'background: #fff; color: #800000; border-radius: 0 0 5px 5px;padding: 5px 0;' @@ -159,41 +163,53 @@ export default function App() { } } else if (proto[1] === 'song') { const v = proto[2] - const virtual = (Object.keys(virtuals).find((virt) => virtuals[virt].id === v)) + const virtual = Object.keys(virtuals).find( + (virt) => virtuals[virt].id === v + ) if (virtual && proto[3].length > 3) { - setEffect(v, 'texter2d', { - "gradient": "linear-gradient(90deg, rgb(255, 0, 0) 0%, rgb(255, 120, 0) 14%, rgb(255, 200, 0) 28%, rgb(0, 255, 0) 42%, rgb(0, 199, 140) 56%, rgb(0, 0, 255) 70%, rgb(128, 0, 128) 84%, rgb(255, 0, 178) 98%)", - "option_2": false, - "flip": false, - "blur": 0, - "flip_horizontal": false, - "speed_option_1": 2, - "resize_method": "Fast", - "gradient_roll": 0, - "alpha": false, - "value_option_1": 0.5, - "font": "Blade-5x8", - "use_gradient": false, - "diag": false, - "test": false, - "impulse_decay": 0.1, - "mirror": false, - "flip_vertical": false, - "text_effect": "Side Scroll", - "multiplier": 1, - "brightness": 1, - "text_color": "#ff0000", - "background_brightness": 1, - "rotate": 0, - "dump": false, - "option_1": false, - "height_percent": 100, - "background_color": "#000000", - "text": proto[3] - }, true, true) + setEffect( + v, + 'texter2d', + { + gradient: + 'linear-gradient(90deg, rgb(255, 0, 0) 0%, rgb(255, 120, 0) 14%, rgb(255, 200, 0) 28%, rgb(0, 255, 0) 42%, rgb(0, 199, 140) 56%, rgb(0, 0, 255) 70%, rgb(128, 0, 128) 84%, rgb(255, 0, 178) 98%)', + option_2: false, + flip: false, + blur: 0, + flip_horizontal: false, + speed_option_1: 2, + resize_method: 'Fast', + gradient_roll: 0, + alpha: false, + value_option_1: 0.5, + font: 'Blade-5x8', + use_gradient: false, + diag: false, + test: false, + impulse_decay: 0.1, + mirror: false, + flip_vertical: false, + text_effect: 'Side Scroll', + multiplier: 1, + brightness: 1, + text_color: '#ff0000', + background_brightness: 1, + rotate: 0, + dump: false, + option_1: false, + height_percent: 100, + background_color: '#000000', + text: proto[3] + }, + true, + true + ) } } else { - showSnackbar('info', `External call: ${protoCall.replace('ledfx://', '')}`) + showSnackbar( + 'info', + `External call: ${protoCall.replace('ledfx://', '')}` + ) } setProtoCall('') } @@ -222,27 +238,27 @@ export default function App() { height={height} /> )} - {new Date().getFullYear() === 2024 && - new Date().getMonth() === 11 && + {new Date().getFullYear() === 2024 && + new Date().getMonth() === 11 && new Date().getDate() >= 24 && ( -
- )} + + )} {new Date().getFullYear() === 2025 && new Date().getMonth() === 0 && new Date().getDate() === 1 && ( diff --git a/src/app/app/media.ts b/src/app/app/media.ts index 55bf478f..65aa7a86 100644 --- a/src/app/app/media.ts +++ b/src/app/app/media.ts @@ -1,38 +1,38 @@ -const { execFile } = require('child_process'); -const path = require('path'); -import { fileURLToPath } from 'node:url'; +import { execFile } from 'child_process' +import path from 'path' +import { fileURLToPath } from 'node:url' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const exePath = path.join(__dirname, 'path_to_dist_folder', 'media.exe'); +const exePath = path.join(__dirname, 'path_to_dist_folder', 'media.exe') interface MediaInfo { - title: string; - artist: string; - album: string; - error?: string; + title: string + artist: string + album: string + error?: string } execFile(exePath, (error: Error | null, stdout: string, stderr: string) => { - if (error) { - console.error(`Error: ${error.message}`); - return; + if (error) { + console.error(`Error: ${error.message}`) + return + } + if (stderr) { + console.error(`Stderr: ${stderr}`) + return + } + try { + const mediaInfo: MediaInfo = JSON.parse(stdout) + if (mediaInfo.error) { + console.log(mediaInfo.error) + } else { + console.log(`Title: ${mediaInfo.title}`) + console.log(`Artist: ${mediaInfo.artist}`) + console.log(`Album: ${mediaInfo.album}`) } - if (stderr) { - console.error(`Stderr: ${stderr}`); - return; - } - try { - const mediaInfo: MediaInfo = JSON.parse(stdout); - if (mediaInfo.error) { - console.log(mediaInfo.error); - } else { - console.log(`Title: ${mediaInfo.title}`); - console.log(`Artist: ${mediaInfo.artist}`); - console.log(`Album: ${mediaInfo.album}`); - } - } catch (parseError) { - console.error(`JSON Parse Error: ${(parseError as Error).message}`); - } -}); \ No newline at end of file + } catch (parseError) { + console.error(`JSON Parse Error: ${(parseError as Error).message}`) + } +}) diff --git a/src/app/app/otp.ts b/src/app/app/otp.ts index cf0a2fc0..4b697303 100644 --- a/src/app/app/otp.ts +++ b/src/app/app/otp.ts @@ -5,46 +5,45 @@ import base32Encode from 'base32-encode' import store from './utils/store.mjs' import { BrowserWindow } from 'electron' - export function generateHOTP(secret: string, counter: number) { - const decodedSecret = base32Decode(secret, 'RFC4648'); + const decodedSecret = base32Decode(secret, 'RFC4648') - const buffer = Buffer.alloc(8); + const buffer = Buffer.alloc(8) for (let i = 0; i < 8; i++) { - buffer[7 - i] = counter & 0xff; - counter = counter >> 8; + buffer[7 - i] = counter & 0xff + counter = counter >> 8 } // Step 1: Generate an HMAC-SHA-1 value - const hmac = crypto.createHmac('sha1', Buffer.from(decodedSecret)); - hmac.update(buffer); - const hmacResult = hmac.digest(); + const hmac = crypto.createHmac('sha1', Buffer.from(decodedSecret)) + hmac.update(buffer) + const hmacResult = hmac.digest() // Step 2: Generate a 4-byte string (Dynamic Truncation) - const offset = hmacResult[hmacResult.length - 1] & 0xf; + const offset = hmacResult[hmacResult.length - 1] & 0xf const code = ((hmacResult[offset] & 0x7f) << 24) | ((hmacResult[offset + 1] & 0xff) << 16) | ((hmacResult[offset + 2] & 0xff) << 8) | - (hmacResult[offset + 3] & 0xff); + (hmacResult[offset + 3] & 0xff) // Step 3: Compute an HOTP value - return `${code % 10 ** 6}`.padStart(6, '0'); + return `${code % 10 ** 6}`.padStart(6, '0') } export function generateTOTP(secret: string, window = 0) { - const counter = Math.floor(Date.now() / 30000); - return generateHOTP(secret, counter + window); + const counter = Math.floor(Date.now() / 30000) + return generateHOTP(secret, counter + window) } export function verifyTOTP(token: string, secret: string, window = 1) { for (let errorWindow = -window; errorWindow <= +window; errorWindow++) { - const totp = generateTOTP(secret, errorWindow); + const totp = generateTOTP(secret, errorWindow) if (token === totp) { - return true; + return true } } - return false; + return false } export function generateMfaQr(wind: BrowserWindow) { @@ -52,31 +51,32 @@ export function generateMfaQr(wind: BrowserWindow) { username: 'FreeUser', mfaEnabled: false, mfaSecret: null - }; + } // console.log('generate-mfa-qr:', user) // For security, we no longer show the QR code after is verified - if (user.mfaEnabled) return; + if (user.mfaEnabled) return if (!user.mfaSecret) { // generate unique secret for user // this secret will be used to check the verification code sent by user - const buffer = crypto.randomBytes(14); - user.mfaSecret = base32Encode(buffer, 'RFC4648', { padding: false }); + const buffer = crypto.randomBytes(14) + user.mfaSecret = base32Encode(buffer, 'RFC4648', { padding: false }) // console.log('generated-mfa-qr', user); - store.set('user', user); + store.set('user', user) } - const issuer = 'Blade\'s LedFx'; - const algorithm = 'SHA1'; - const digits = '6'; - const period = '30'; - const otpType = 'totp'; - const configUri = `otpauth://${otpType}/${issuer}:${user.username}?algorithm=${algorithm}&digits=${digits}&period=${period}&issuer=${issuer}&secret=${user.mfaSecret}`; + // eslint-disable-next-line quotes + const issuer = "Blade's LedFx" + const algorithm = 'SHA1' + const digits = '6' + const period = '30' + const otpType = 'totp' + const configUri = `otpauth://${otpType}/${issuer}:${user.username}?algorithm=${algorithm}&digits=${digits}&period=${period}&issuer=${issuer}&secret=${user.mfaSecret}` - qrcode.toDataURL(configUri, { - color: { dark: '#333333FF', light: '#00000000' }, - }).then(((png: any) => wind.webContents.send('fromMain', ['mfa-qr-code', png]))); - - return; + qrcode + .toDataURL(configUri, { + color: { dark: '#333333FF', light: '#00000000' } + }) + .then((png: any) => wind.webContents.send('fromMain', ['mfa-qr-code', png])) } export function handleVerifyOTP(wind: BrowserWindow, parameters: any) { const user = store.get('user') || { @@ -84,17 +84,15 @@ export function handleVerifyOTP(wind: BrowserWindow, parameters: any) { mfaEnabled: false, mfaSecret: null } - const token = parameters.token; - const secret = user.mfaSecret; + const token = parameters.token + const secret = user.mfaSecret // console.log('verify_otp:', user) - const verified = verifyTOTP(token, secret); + const verified = verifyTOTP(token, secret) if (verified) { - user.mfaEnabled = true; - store.set('user', user); + user.mfaEnabled = true + store.set('user', user) } // console.log('verified_otp:', verified ,user) - wind.webContents.send('fromMain', ['mfa-verified', verified]); - return; + wind.webContents.send('fromMain', ['mfa-verified', verified]) } - diff --git a/src/app/app/toast.ts b/src/app/app/toast.ts index 1faae695..606f72bd 100644 --- a/src/app/app/toast.ts +++ b/src/app/app/toast.ts @@ -1,23 +1,32 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import fs from 'fs' import path from 'path' import { Notification } from 'electron' import { fileURLToPath } from 'node:url' -import isDev from 'electron-is-dev'; +import isDev from 'electron-is-dev' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -const NOTIFICATION_TITLE = 'LedFx Client - by Blade'; -const NOTIFICATION_BODY = 'Testing Notification from the Main process'; +const NOTIFICATION_TITLE = 'LedFx Client - by Blade' +const NOTIFICATION_BODY = 'Testing Notification from the Main process' function getConfig() { - const configPath = path.join(path.dirname(__dirname), isDev ? '../' : '../../', 'frontend_config.json') - const configData = fs.readFileSync(configPath); - return JSON.parse(configData.toString()); + const configPath = path.join( + path.dirname(__dirname), + isDev ? '../' : '../../', + 'frontend_config.json' + ) + const configData = fs.readFileSync(configPath) + return JSON.parse(configData.toString()) } -export function showNotification(title = NOTIFICATION_TITLE, body = NOTIFICATION_BODY) { - const config = getConfig(); - const updateUrl = config.updateUrl; +export function showNotification( + title = NOTIFICATION_TITLE, + body = NOTIFICATION_BODY +) { + const config = getConfig() + const updateUrl = config.updateUrl new Notification({ toastXml: `