├── module ├── README.md ├── .gitignore ├── src │ ├── Constants.ts │ ├── main.ts │ └── WebUI │ │ └── ComfyUI (comfyanonymous) │ │ └── MainMethods.ts ├── package.json ├── tsconfig.json ├── rollup.config.mjs └── lynxModule.json ├── extension ├── README.md ├── .gitignore ├── src │ ├── cross │ │ └── CrossUtils.ts │ ├── main │ │ ├── Methods │ │ │ └── IpcChannels.ts │ │ └── lynxExtension.ts │ └── renderer │ │ ├── index.html │ │ ├── reducer.ts │ │ ├── Components │ │ ├── Cards_AddMenu.tsx │ │ └── Settings.tsx │ │ └── index.css ├── package.json ├── HeroUiConfig.ts ├── lynxExtension.json └── electron.vite.config.ts ├── .eslintignore ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── resources ├── 16x16.png ├── icon.ico ├── icon.png ├── 256x256.png └── icon-darwin.png ├── .prettierignore ├── readme └── lynxhub_screenshot.png ├── src ├── renderer │ ├── public │ │ └── LynxHub.png │ ├── src │ │ ├── App │ │ │ ├── Utils │ │ │ │ ├── CrossStyle.tsx │ │ │ │ ├── Types.ts │ │ │ │ ├── SetHtmlAttributes.tsx │ │ │ │ └── LocalStorage.tsx │ │ │ ├── Components │ │ │ │ ├── Modals │ │ │ │ │ ├── CardExtensions │ │ │ │ │ │ └── Types.ts │ │ │ │ │ ├── LaunchConfig │ │ │ │ │ │ ├── CustomRun │ │ │ │ │ │ │ └── CustomRun.tsx │ │ │ │ │ │ ├── PreLaunch │ │ │ │ │ │ │ ├── CardPreLaunch.tsx │ │ │ │ │ │ │ └── PreOpenPath │ │ │ │ │ │ │ │ └── PreOpenPath-Item.tsx │ │ │ │ │ │ ├── Arguments │ │ │ │ │ │ │ └── ManageArguments │ │ │ │ │ │ │ │ └── Items │ │ │ │ │ │ │ │ ├── CheckBoxArg-Item.tsx │ │ │ │ │ │ │ │ ├── FileArg-Item.tsx │ │ │ │ │ │ │ │ ├── DirectoryArg-Item.tsx │ │ │ │ │ │ │ │ └── InputArg-Item.tsx │ │ │ │ │ │ └── LaunchConfig-Section.tsx │ │ │ │ │ ├── InstallUI │ │ │ │ │ │ ├── types.d.ts │ │ │ │ │ │ ├── Install-Header.tsx │ │ │ │ │ │ └── Utils │ │ │ │ │ │ │ └── LocateWarning.tsx │ │ │ │ │ ├── UpdateApp │ │ │ │ │ │ └── Downloading.tsx │ │ │ │ │ ├── CardGitManager │ │ │ │ │ │ └── CommitInfo.tsx │ │ │ │ │ ├── Modals.tsx │ │ │ │ │ ├── CustomNotification │ │ │ │ │ │ └── CustomNotification.tsx │ │ │ │ │ └── CardInfo │ │ │ │ │ │ └── UseCardInfoApi.tsx │ │ │ │ ├── Initializer │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── steps │ │ │ │ │ │ └── StepWelcome.tsx │ │ │ │ │ └── components │ │ │ │ │ │ └── CheckRow.tsx │ │ │ │ ├── Pages │ │ │ │ │ ├── SettingsPages │ │ │ │ │ │ ├── Settings │ │ │ │ │ │ │ ├── SettingsPage-Contents.tsx │ │ │ │ │ │ │ ├── SettingsPage.tsx │ │ │ │ │ │ │ ├── Content │ │ │ │ │ │ │ │ ├── Card │ │ │ │ │ │ │ │ │ ├── Settings-Card.tsx │ │ │ │ │ │ │ │ │ └── CheckUpdateInterval.tsx │ │ │ │ │ │ │ │ ├── Terminal │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-FontSize.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-ScrollBack.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-ResizeDelay.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-BlinkCursor.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-Reset.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-CloseOnExit.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-OutputColor.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-Conpty.tsx │ │ │ │ │ │ │ │ │ ├── SettingsTerminal-CursorStyle.tsx │ │ │ │ │ │ │ │ │ ├── Settings-Terminal.tsx │ │ │ │ │ │ │ │ │ └── SettingsTerminal-CursorInactiveStyle.tsx │ │ │ │ │ │ │ │ ├── Browser │ │ │ │ │ │ │ │ │ ├── SettingsBrowser.tsx │ │ │ │ │ │ │ │ │ └── SettingsBrowser_Links.tsx │ │ │ │ │ │ │ │ ├── Startup │ │ │ │ │ │ │ │ │ ├── SettingsStartup-StartMaximized.tsx │ │ │ │ │ │ │ │ │ ├── SettingsStartup-StartMinimized.tsx │ │ │ │ │ │ │ │ │ ├── SettingsStartup-System.tsx │ │ │ │ │ │ │ │ │ ├── SettingsStartup-StartPage.tsx │ │ │ │ │ │ │ │ │ ├── SettingsStartup-LastSize.tsx │ │ │ │ │ │ │ │ │ ├── Settings-Startup.tsx │ │ │ │ │ │ │ │ │ └── SettingsStartup-DisableLoadingAnim.tsx │ │ │ │ │ │ │ │ ├── General │ │ │ │ │ │ │ │ │ ├── SettingsGeneral-TitleName.tsx │ │ │ │ │ │ │ │ │ ├── Settings-General.tsx │ │ │ │ │ │ │ │ │ └── SettingsGeneral-HWAcc.tsx │ │ │ │ │ │ │ │ └── Settings-Data.tsx │ │ │ │ │ │ │ ├── SettingsPage-ContentSection.tsx │ │ │ │ │ │ │ └── SettingsContainer.tsx │ │ │ │ │ │ ├── Dashboard │ │ │ │ │ │ │ ├── DashboardPage-Contents.tsx │ │ │ │ │ │ │ ├── DashboardPage.tsx │ │ │ │ │ │ │ ├── Content │ │ │ │ │ │ │ │ ├── Profile │ │ │ │ │ │ │ │ │ ├── Dashboard-Profile.tsx │ │ │ │ │ │ │ │ │ └── Profile_Google.tsx │ │ │ │ │ │ │ │ └── Dashboard-ReportIssue.tsx │ │ │ │ │ │ │ └── DashboardContainer.tsx │ │ │ │ │ │ └── Plugins │ │ │ │ │ │ │ ├── Page.tsx │ │ │ │ │ │ │ └── Preview │ │ │ │ │ │ │ └── Preview.tsx │ │ │ │ │ ├── ContentPages │ │ │ │ │ │ ├── Home │ │ │ │ │ │ │ ├── Notification │ │ │ │ │ │ │ │ └── StaticNotifications.tsx │ │ │ │ │ │ │ ├── HomeCategory.tsx │ │ │ │ │ │ │ └── HomeSearchBox.tsx │ │ │ │ │ │ ├── GamesPage.tsx │ │ │ │ │ │ ├── ToolsPage.tsx │ │ │ │ │ │ ├── AgentsPage.tsx │ │ │ │ │ │ ├── OthersPage.tsx │ │ │ │ │ │ ├── TextGenerationPage.tsx │ │ │ │ │ │ ├── ImageGenerationPage.tsx │ │ │ │ │ │ └── AudioGenerationPage.tsx │ │ │ │ │ ├── Page.tsx │ │ │ │ │ └── CardContainer.tsx │ │ │ │ ├── Browser_Terminal │ │ │ │ │ ├── Browser │ │ │ │ │ │ ├── Browser_Zoom.tsx │ │ │ │ │ │ ├── Browser_TopBar.tsx │ │ │ │ │ │ └── Browser_Search.tsx │ │ │ │ │ ├── TopBar │ │ │ │ │ │ ├── SharedTopBar.tsx │ │ │ │ │ │ ├── Switch.tsx │ │ │ │ │ │ ├── Terminate_AI.tsx │ │ │ │ │ │ ├── DownloadManager.tsx │ │ │ │ │ │ └── TopBar.tsx │ │ │ │ │ └── Terminal │ │ │ │ │ │ └── Terminal_Timer.tsx │ │ │ │ ├── Tabs │ │ │ │ │ ├── TabTitle.tsx │ │ │ │ │ ├── NewTab.tsx │ │ │ │ │ └── Tab_Utils.tsx │ │ │ │ ├── Reusable │ │ │ │ │ ├── LynxTooltip.tsx │ │ │ │ │ ├── LynxScroll.tsx │ │ │ │ │ ├── ShinyText.tsx │ │ │ │ │ ├── CopyClipboard.tsx │ │ │ │ │ └── SmallButton.tsx │ │ │ │ ├── Background.tsx │ │ │ │ ├── Cards │ │ │ │ │ ├── NavigatePluginsPage.tsx │ │ │ │ │ ├── Card │ │ │ │ │ │ ├── PulsingLine.tsx │ │ │ │ │ │ ├── Menu │ │ │ │ │ │ │ └── UninstalledMenu.tsx │ │ │ │ │ │ ├── RenderCardList.tsx │ │ │ │ │ │ └── Wrapper.tsx │ │ │ │ │ └── CardStore.ts │ │ │ │ ├── MainContents │ │ │ │ │ └── MainContents.tsx │ │ │ │ ├── NavBar │ │ │ │ │ └── NavBar.tsx │ │ │ │ └── TitleBar │ │ │ │ │ └── WindowButtons_Close.tsx │ │ │ ├── ExtensionHooks.tsx │ │ │ ├── MainHooks.tsx │ │ │ ├── PluginSentry.ts │ │ │ ├── App.tsx │ │ │ ├── Extensions │ │ │ │ └── Extension_Utils.tsx │ │ │ ├── AppEvents │ │ │ │ ├── AppEvents.ts │ │ │ │ └── AppStates_Hooks.ts │ │ │ ├── Redux │ │ │ │ ├── Reducer │ │ │ │ │ ├── TerminalReducer.ts │ │ │ │ │ └── UserReducer.ts │ │ │ │ └── Store.ts │ │ │ └── UIProviders.tsx │ │ ├── assets │ │ │ └── icons │ │ │ │ └── SvgIconsContainer.tsx │ │ └── main.tsx │ ├── assets │ │ ├── fonts │ │ │ ├── JetBrainsMono │ │ │ │ └── JetBrainsMono.ttf │ │ │ └── Nunito │ │ │ │ ├── Nunito-VariableFont_wght.ttf │ │ │ │ └── Nunito-Italic-VariableFont_wght.ttf │ │ └── fonts.css │ ├── federation.d.ts │ ├── toast__window │ │ ├── types.d.ts │ │ └── main.tsx │ ├── dialog │ │ ├── main.tsx │ │ └── Dialog.tsx │ ├── global.d.ts │ ├── loading │ │ ├── Loadings │ │ │ ├── SimpleLoading.tsx │ │ │ ├── ThreadsLoading.tsx │ │ │ ├── Container.tsx │ │ │ ├── RippleLoading.tsx │ │ │ ├── AppName.tsx │ │ │ └── LiquidChromeLoading.tsx │ │ ├── main.tsx │ │ └── Backgrounds │ │ │ └── RippleBG.tsx │ ├── dialog.html │ ├── context_menu.html │ ├── toast_window.html │ ├── context_menu │ │ ├── main.tsx │ │ └── ContextMenu.tsx │ ├── downloads_menu.html │ ├── loading.html │ ├── share_screen.html │ ├── downloads_menu │ │ └── main.tsx │ ├── index.html │ ├── share_screen │ │ └── main.tsx │ ├── HeroUiConfig.ts │ ├── Breadcrumbs.tsx │ └── cross_styles.css ├── cross │ ├── GitTypes.ts │ ├── ScreenShareConsts.ts │ ├── ScreenShareTypes.ts │ ├── DownloadManagerTypes.ts │ ├── plugin │ │ └── PluginTypes.ts │ └── AccentColorGenerator.ts ├── preload │ ├── utils.js │ ├── dialog.js │ ├── only_ipc.js │ ├── webview.js │ └── index.js └── main │ └── Managements │ ├── Breadcrumbs.ts │ ├── Plugin │ ├── PluginConstants.ts │ ├── PluginSentry.ts │ └── Extensions │ │ ├── ExtensionApi.ts │ │ └── ExtensionUtils.ts │ ├── Ipc │ ├── IpcMainSender.ts │ └── Methods │ │ ├── IpcMethods-Platform.ts │ │ ├── IpcMethods-Downloader.ts │ │ └── IpcMethods-Repository.ts │ ├── Git │ └── GitHelper.ts │ ├── HotkeysManager.ts │ ├── ToastWindowManager.ts │ └── DialogManager.ts ├── .gitignore ├── tsconfig.json ├── prettier.config.ts ├── tsconfig.web.json ├── tsconfig.node.json ├── removeDotExtensions.js └── notifications.json /module/README.md: -------------------------------------------------------------------------------- 1 | # Module entry -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # Extension entry -------------------------------------------------------------------------------- /module/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | .gitignore 4 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: LynxHub 2 | custom: patreon.com/LynxHub/Shop -------------------------------------------------------------------------------- /extension/src/cross/CrossUtils.ts: -------------------------------------------------------------------------------- 1 | // Shared between renderer and main process 2 | -------------------------------------------------------------------------------- /resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/resources/16x16.png -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/resources/256x256.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /resources/icon-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/resources/icon-darwin.png -------------------------------------------------------------------------------- /readme/lynxhub_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/readme/lynxhub_screenshot.png -------------------------------------------------------------------------------- /module/src/Constants.ts: -------------------------------------------------------------------------------- 1 | export const COMFYUI_ID = 'ComfyUI_SD'; 2 | export const OPEN_WEBUI_ID = 'OpenWebUI_TG'; 3 | -------------------------------------------------------------------------------- /src/renderer/public/LynxHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/src/renderer/public/LynxHub.png -------------------------------------------------------------------------------- /src/renderer/src/App/Utils/CrossStyle.tsx: -------------------------------------------------------------------------------- 1 | export const ContainersBg = ' bg-white/10 dark:bg-LynxRaisinBlack/20 '; 2 | -------------------------------------------------------------------------------- /extension/src/main/Methods/IpcChannels.ts: -------------------------------------------------------------------------------- 1 | export function listenForChannels() { 2 | // ipcMain.handle('channel-name', () => {}); 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | extOut 5 | extension_out 6 | module_out 7 | 8 | .DS_Store 9 | *.log* 10 | package-lock.json 11 | .idea -------------------------------------------------------------------------------- /src/renderer/assets/fonts/JetBrainsMono/JetBrainsMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/src/renderer/assets/fonts/JetBrainsMono/JetBrainsMono.ttf -------------------------------------------------------------------------------- /src/renderer/assets/fonts/Nunito/Nunito-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/src/renderer/assets/fonts/Nunito/Nunito-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/renderer/federation.d.ts: -------------------------------------------------------------------------------- 1 | declare module '__federation__' { 2 | export const __federation_method_setRemote: any; 3 | export const __federation_method_getRemote: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/cross/GitTypes.ts: -------------------------------------------------------------------------------- 1 | export type ShallowCloneOptions = { 2 | url: string; 3 | directory: string; 4 | singleBranch?: boolean; 5 | depth?: number; 6 | branch?: string; 7 | }; 8 | -------------------------------------------------------------------------------- /src/renderer/assets/fonts/Nunito/Nunito-Italic-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KindaBrazy/LynxHub/HEAD/src/renderer/assets/fonts/Nunito/Nunito-Italic-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/renderer/toast__window/types.d.ts: -------------------------------------------------------------------------------- 1 | import {ElectronAPI} from '@electron-toolkit/preload'; 2 | 3 | declare global { 4 | interface Window { 5 | ipc: ElectronAPI['ipcRenderer']; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.web.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/preload/utils.js: -------------------------------------------------------------------------------- 1 | import '@sentry/electron/preload'; 2 | 3 | export function isPortable() { 4 | if (process.env.PORTABLE_EXECUTABLE_FILE) return 'win'; 5 | if (process.env.APPIMAGE) return 'linux'; 6 | return null; 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/dialog/main.tsx: -------------------------------------------------------------------------------- 1 | import '../cross_styles.css'; 2 | 3 | import {createRoot} from 'react-dom/client'; 4 | 5 | import App from './Dialog'; 6 | 7 | createRoot(document.getElementById('root') as HTMLElement).render(); 8 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "dependencies": { 6 | "which": "^5.0.0" 7 | }, 8 | "devDependencies": { 9 | "@types/which": "^3.0.4" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/cross/ScreenShareConsts.ts: -------------------------------------------------------------------------------- 1 | export const screenShareChannels = { 2 | getScreenSources: 'screenShare-getScreenSources', 3 | getWindowSources: 'screenShare-getWindowSources', 4 | startShare: 'screenShare-startShare', 5 | cancel: 'screenShare-cancel', 6 | }; 7 | -------------------------------------------------------------------------------- /src/renderer/toast__window/main.tsx: -------------------------------------------------------------------------------- 1 | import '../cross_styles.css'; 2 | 3 | import {createRoot} from 'react-dom/client'; 4 | 5 | import ToastContent from './ToastContent'; 6 | 7 | createRoot(document.getElementById('root') as HTMLElement).render(); 8 | -------------------------------------------------------------------------------- /prettier.config.ts: -------------------------------------------------------------------------------- 1 | import {type Config} from 'prettier'; 2 | 3 | const config: Config = { 4 | singleQuote: true, 5 | printWidth: 120, 6 | bracketSpacing: false, 7 | bracketSameLine: true, 8 | arrowParens: 'avoid', 9 | endOfLine: 'auto', 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /extension/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LynxHub Extension 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/cross/ScreenShareTypes.ts: -------------------------------------------------------------------------------- 1 | export type ScreenShareSources = { 2 | icon?: string; 3 | id?: string; 4 | name: string; 5 | thumbnail?: string; 6 | display_id?: string; 7 | }; 8 | 9 | export type ScreenShareStart = { 10 | id: string; 11 | shareAudio: boolean; 12 | type: 'windows' | 'screens'; 13 | }; 14 | -------------------------------------------------------------------------------- /src/renderer/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'vite/client'; 2 | 3 | import {ElectronAPI} from '@electron-toolkit/preload'; 4 | 5 | declare global { 6 | interface Window { 7 | electron: ElectronAPI; 8 | osPlatform: NodeJS.Platform; 9 | isPortable: 'win' | 'linux' | null; 10 | appStartTime: number; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/loading/Loadings/SimpleLoading.tsx: -------------------------------------------------------------------------------- 1 | import {AppName} from './AppName'; 2 | import Container from './Container'; 3 | 4 | export default function SimpleLoading() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/Managements/Breadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import {addBreadcrumb} from '@sentry/electron/main'; 2 | 3 | import {storageManager} from '../index'; 4 | 5 | export default function AddBreadcrumb_Main(message: string) { 6 | if (storageManager.getData('app').addBreadcrumbs) addBreadcrumb({category: 'main-actions', message, level: 'info'}); 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/CardExtensions/Types.ts: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | export type InstalledExtensionsTable = { 4 | key: string; 5 | name: ReactNode; 6 | stars: ReactNode; 7 | size: ReactNode | string; 8 | update: ReactNode; 9 | remove: ReactNode; 10 | disable: ReactNode; 11 | }; 12 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Initializer/types.ts: -------------------------------------------------------------------------------- 1 | export type CheckResult = 'unknown' | 'checking' | 'ok' | 'failed' | 'installing'; 2 | 3 | export type RowData = { 4 | result: CheckResult; 5 | label?: string; 6 | }; 7 | 8 | export type RequirementStatus = { 9 | git: string; 10 | pwsh: string; 11 | appModule: string; 12 | }; 13 | -------------------------------------------------------------------------------- /src/renderer/loading/Loadings/ThreadsLoading.tsx: -------------------------------------------------------------------------------- 1 | import ThreadsBG from '../Backgrounds/ThreadsBG'; 2 | import {AppName} from './AppName'; 3 | import Container from './Container'; 4 | 5 | export default function ThreadsLoading() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/preload/dialog.js: -------------------------------------------------------------------------------- 1 | import '@sentry/electron/preload'; 2 | 3 | import {contextBridge, ipcRenderer} from 'electron'; 4 | 5 | contextBridge.exposeInMainWorld('electron', { 6 | ipcRenderer: { 7 | send: (channel, data) => { 8 | ipcRenderer.send(channel, data); 9 | }, 10 | on: (channel, data) => { 11 | ipcRenderer.on(channel, data); 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/renderer/loading/Loadings/Container.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | export default function Container({children}: {children: ReactNode}) { 4 | return ( 5 |
9 | {children} 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LynxHub Prompt 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/preload/only_ipc.js: -------------------------------------------------------------------------------- 1 | import '@sentry/electron/preload'; 2 | 3 | import {electronAPI} from '@electron-toolkit/preload'; 4 | import {contextBridge} from 'electron'; 5 | 6 | if (process.contextIsolated) { 7 | try { 8 | contextBridge.exposeInMainWorld('ipc', electronAPI.ipcRenderer); 9 | } catch (error) { 10 | console.error(error); 11 | } 12 | } else { 13 | window.ipc = electronAPI.ipcRenderer; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/context_menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LynxHub Menu 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/loading/Loadings/RippleLoading.tsx: -------------------------------------------------------------------------------- 1 | import {Ripple} from '../Backgrounds/RippleBG'; 2 | import {AppName} from './AppName'; 3 | import Container from './Container'; 4 | 5 | export default function ThreadsLoading() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/toast_window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LynxHub Toast 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/context_menu/main.tsx: -------------------------------------------------------------------------------- 1 | import '../cross_styles.css'; 2 | 3 | import {createRoot} from 'react-dom/client'; 4 | 5 | import rendererIpc from '../src/App/RendererIpc'; 6 | import App from './ContextMenu'; 7 | 8 | const {darkMode} = await rendererIpc.storage.get('app'); 9 | document.documentElement.className = darkMode ? 'dark' : 'light'; 10 | 11 | createRoot(document.getElementById('root') as HTMLElement).render(); 12 | -------------------------------------------------------------------------------- /src/renderer/downloads_menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LynxHub Downloads 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icons/SvgIconsContainer.tsx: -------------------------------------------------------------------------------- 1 | import {SVGProps} from 'react'; 2 | 3 | /* 4 | Svg icons from: 5 | 6 | "Solar" -> https://www.figma.com/community/file/1166831539721848736/solar-icons-set 7 | "MingCute" -> https://github.com/Richard9394/MingCute 8 | "Fluent UI System" -> https://github.com/microsoft/fluentui-system-icons 9 | */ 10 | 11 | export type SvgProps = SVGProps; 12 | -------------------------------------------------------------------------------- /src/main/Managements/Plugin/PluginConstants.ts: -------------------------------------------------------------------------------- 1 | export const hostName: string = 'localhost'; 2 | 3 | export const pluginFolders = { 4 | module: { 5 | oldFolder: 'Modules', 6 | mainScript: 'scripts/main.mjs', 7 | rendererScript: 'scripts/renderer.mjs', 8 | }, 9 | extension: { 10 | oldFolder: 'Extensions', 11 | mainScript: 'scripts/main/mainEntry.cjs', 12 | rendererScript: 'scripts/renderer/rendererEntry.mjs', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/renderer/src/App/ExtensionHooks.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | 3 | import {extensionsData} from './Extensions/ExtensionLoader'; 4 | 5 | const ExtensionHooks = () => { 6 | const customHooks = useMemo(() => extensionsData.addCustomHook, []); 7 | 8 | return ( 9 | <> 10 | {customHooks.map((Hook, index) => ( 11 | 12 | ))} 13 | 14 | ); 15 | }; 16 | 17 | export default ExtensionHooks; 18 | -------------------------------------------------------------------------------- /src/renderer/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LynxHub 6 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lynxhub-module", 3 | "type": "module", 4 | "dependencies": { 5 | "semver": "^7.7.2", 6 | "which": "^5.0.0" 7 | }, 8 | "devDependencies": { 9 | "@rollup/plugin-commonjs": "^28.0.6", 10 | "@rollup/plugin-json": "^6.1.0", 11 | "@rollup/plugin-node-resolve": "^16.0.1", 12 | "@rollup/plugin-typescript": "^12.1.3", 13 | "@types/semver": "^7.7.0", 14 | "@types/which": "^3.0.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/share_screen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Share Screen 6 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/preload/webview.js: -------------------------------------------------------------------------------- 1 | import '@sentry/electron/preload'; 2 | 3 | import {contextBridge, ipcRenderer, webFrame} from 'electron'; 4 | 5 | contextBridge.exposeInMainWorld('electron', { 6 | ipcRenderer: { 7 | promptDialog: message => { 8 | return ipcRenderer.sendSync('show-prompt', message); 9 | }, 10 | }, 11 | }); 12 | 13 | window.addEventListener('DOMContentLoaded', () => { 14 | webFrame.executeJavaScript(`window.prompt = window.electron.ipcRenderer.promptDialog`); 15 | }); 16 | -------------------------------------------------------------------------------- /src/renderer/downloads_menu/main.tsx: -------------------------------------------------------------------------------- 1 | import '../cross_styles.css'; 2 | import 'overlayscrollbars/overlayscrollbars.css'; 3 | 4 | import {createRoot} from 'react-dom/client'; 5 | 6 | import rendererIpc from '../src/App/RendererIpc'; 7 | import DownloadMenu from './DownloadMenu'; 8 | 9 | const {darkMode} = await rendererIpc.storage.get('app'); 10 | document.documentElement.className = darkMode ? 'dark' : 'light'; 11 | 12 | createRoot(document.getElementById('root') as HTMLElement).render(); 13 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/SettingsPage-Contents.tsx: -------------------------------------------------------------------------------- 1 | import LynxScroll from '../../../Reusable/LynxScroll'; 2 | import {SettingsSections} from './SettingsContainer'; 3 | 4 | /** Settings content */ 5 | const SettingsPageContents = () => { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default SettingsPageContents; 16 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/Home/Notification/StaticNotifications.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode, useEffect, useState} from 'react'; 2 | 3 | export default function useStaticNotifications() { 4 | const [staticNotifs, setStaticNotifs] = useState([]); 5 | const [haveWarn, setHaveWarn] = useState(false); 6 | 7 | useEffect(() => { 8 | setStaticNotifs([]); 9 | setHaveWarn(false); 10 | }, []); 11 | 12 | return {staticNotifs, staticNotifCount: staticNotifs.length, haveWarn}; 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Dashboard/DashboardPage-Contents.tsx: -------------------------------------------------------------------------------- 1 | import LynxScroll from '../../../Reusable/LynxScroll'; 2 | import {DashboardSections} from './DashboardContainer'; 3 | 4 | /** Settings content */ 5 | const DashboardPageContents = () => { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default DashboardPageContents; 16 | -------------------------------------------------------------------------------- /src/renderer/src/App/MainHooks.tsx: -------------------------------------------------------------------------------- 1 | import {memo} from 'react'; 2 | 3 | import useAppEvents from './AppEvents/AppEvents'; 4 | import {useFilterPinnedCards} from './AppEvents/AppStates_Hooks'; 5 | import {useRegisterHotkeys} from './Utils/RegisterHotkeys'; 6 | import useHtmlAttributes from './Utils/SetHtmlAttributes'; 7 | 8 | const AppHooks = memo(() => { 9 | useHtmlAttributes(); 10 | useRegisterHotkeys(); 11 | useAppEvents(); 12 | useFilterPinnedCards(); 13 | 14 | return null; 15 | }); 16 | 17 | export default AppHooks; 18 | -------------------------------------------------------------------------------- /module/src/main.ts: -------------------------------------------------------------------------------- 1 | import {MainModules, MainModuleUtils} from '../../src/cross/plugin/ModuleTypes'; 2 | import {COMFYUI_ID, OPEN_WEBUI_ID} from './Constants'; 3 | import Comfy_MM from './WebUI/ComfyUI (comfyanonymous)/MainMethods'; 4 | import OpenWebUI_MM from './WebUI/OpenWebUI/MainMethods'; 5 | 6 | export default async function initialModule(utils: MainModuleUtils): Promise { 7 | return [ 8 | {id: COMFYUI_ID, methods: () => Comfy_MM(utils)}, 9 | {id: OPEN_WEBUI_ID, methods: () => OpenWebUI_MM(utils)}, 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "moduleResolution": "Node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "lib": [ 11 | "ESNext", 12 | "DOM" 13 | ], 14 | "paths": { 15 | "@lynx_module/*": [ 16 | "./src/*" 17 | ], 18 | "@lynx_extension/*": [ 19 | "../extension/src" 20 | ] 21 | } 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/renderer/loading/Loadings/AppName.tsx: -------------------------------------------------------------------------------- 1 | import {APP_NAME} from '../../../cross/CrossConstants'; 2 | import ShinyText from '../../src/App/Components/Reusable/ShinyText'; 3 | 4 | export function AppName({className, textClassName}: {className?: string; textClassName?: string}) { 5 | return ( 6 |
7 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/renderer/share_screen/main.tsx: -------------------------------------------------------------------------------- 1 | import '../cross_styles.css'; 2 | 3 | import {createRoot} from 'react-dom/client'; 4 | 5 | import rendererIpc from '../src/App/RendererIpc'; 6 | import App from './ScreenShare'; 7 | 8 | const {darkMode} = await rendererIpc.storage.get('app'); 9 | const systemDarkMode = await rendererIpc.win.getSystemDarkMode(); 10 | 11 | let isDarkMode; 12 | 13 | if (darkMode === 'system') { 14 | isDarkMode = systemDarkMode === 'dark'; 15 | } else { 16 | isDarkMode = darkMode === 'dark'; 17 | } 18 | 19 | createRoot(document.getElementById('root') as HTMLElement).render(); 20 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/Browser/Browser_Zoom.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | 3 | import {Magnifier_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 4 | import rendererIpc from '../../../RendererIpc'; 5 | 6 | type Props = {id: string}; 7 | export default function Browser_Zoom({id}: Props) { 8 | const openZoomMenu = () => { 9 | rendererIpc.browser.openZoom(id); 10 | }; 11 | 12 | return ( 13 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/CustomRun/CustomRun.tsx: -------------------------------------------------------------------------------- 1 | import {motion} from 'framer-motion'; 2 | 3 | import {tabContentVariants} from '../../CardExtensions/Constants'; 4 | import CustomRunBehavior from './CustomRunBehavior'; 5 | import CustomRunCommands from './CustomRunCommands'; 6 | 7 | type Props = {id: string}; 8 | export default function CustomRun({id}: Props) { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/SettingsPage.tsx: -------------------------------------------------------------------------------- 1 | import {memo} from 'react'; 2 | 3 | import Page from '../../Page'; 4 | import SettingsPageContents from './SettingsPage-Contents'; 5 | import SettingsPageNav from './SettingsPage-Nav'; 6 | 7 | type Props = {show: boolean}; 8 | 9 | const SettingsPage = memo(({show}: Props) => { 10 | return ( 11 | 12 |
13 | 14 | 15 |
16 |
17 | ); 18 | }); 19 | 20 | export default SettingsPage; 21 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/InstallUI/types.d.ts: -------------------------------------------------------------------------------- 1 | import {FC} from 'react'; 2 | 3 | export type BodyState = 4 | | 'starter' 5 | | 'clone' 6 | | 'terminal' 7 | | 'progress' 8 | | 'progress-bar' 9 | | 'user-input' 10 | | 'install-extensions' 11 | | 'done' 12 | | 'extension-custom' 13 | | ''; 14 | 15 | export type InstallState = { 16 | body: BodyState; 17 | cloneUrl: string; 18 | doneAll: {type: 'success' | 'error'; title: string; description?: string}; 19 | startClone: boolean; 20 | disableSelectDir: boolean; 21 | extensionCustomContent: FC | undefined; 22 | extensionUserInput: FC[] | undefined; 23 | }; 24 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Dashboard/DashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import {memo} from 'react'; 2 | 3 | import Page from '../../Page'; 4 | import DashboardPageContents from './DashboardPage-Contents'; 5 | import DashboardPageNav from './DashboardPage-Nav'; 6 | 7 | type Props = {show: boolean}; 8 | 9 | const DashboardPage = memo(({show}: Props) => { 10 | return ( 11 | 12 |
13 | 14 | 15 |
16 |
17 | ); 18 | }); 19 | 20 | export default DashboardPage; 21 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/PreLaunch/CardPreLaunch.tsx: -------------------------------------------------------------------------------- 1 | import {motion} from 'framer-motion'; 2 | 3 | import {tabContentVariants} from '../../CardExtensions/Constants'; 4 | import PreOpenPath from './PreOpenPath/PreOpenPath'; 5 | import PreTerminalCommands from './PreTerminalCommands/PreTerminalCommands'; 6 | 7 | type Props = {id: string}; 8 | export default function CardPreLaunch({id}: Props) { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Dashboard/Content/Profile/Dashboard-Profile.tsx: -------------------------------------------------------------------------------- 1 | import {User_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 2 | import SettingsSection from '../../../Settings/SettingsPage-ContentSection'; 3 | import Profile_Patreon from './Profile_Patreon'; 4 | 5 | export const DashboardProfileId = 'settings_profile_elem'; 6 | 7 | export default function DashboardProfile() { 8 | return ( 9 | }> 10 | 11 | {/**/} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Card/Settings-Card.tsx: -------------------------------------------------------------------------------- 1 | import {EditCard_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 2 | import SettingsSection from '../../SettingsPage-ContentSection'; 3 | import CheckUpdateInterval from './CheckUpdateInterval'; 4 | 5 | export const SettingsCardId = 'settings_card_elem'; 6 | 7 | /** Reporting app issues on GitHub */ 8 | export default function SettingsCard() { 9 | return ( 10 | } itemsCenter> 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Tabs/TabTitle.tsx: -------------------------------------------------------------------------------- 1 | import {Dispatch, memo, SetStateAction, useEffect, useRef} from 'react'; 2 | 3 | type Props = {title: string; setIsTruncated: Dispatch>}; 4 | 5 | const TabTitle = memo(({title, setIsTruncated}: Props) => { 6 | const spanRef = useRef(null); 7 | 8 | useEffect(() => { 9 | if (spanRef.current) { 10 | setIsTruncated(spanRef.current.offsetWidth < spanRef.current.scrollWidth); 11 | } 12 | }, [title]); 13 | 14 | return ( 15 | 16 | {title} 17 | 18 | ); 19 | }); 20 | 21 | export default TabTitle; 22 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/**/*", 5 | "src/cross/**/*", 6 | "extension/src/renderer/**/*", 7 | "extension/src/cross/**/*", 8 | "module/src/**/*" 9 | ], 10 | "compilerOptions": { 11 | "moduleResolution": "bundler", 12 | "module": "ESNext", 13 | "allowSyntheticDefaultImports": true, 14 | "composite": true, 15 | "jsx": "react-jsx", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@lynx_module/*": [ 19 | "module/src/*" 20 | ], 21 | "@lynx_extension/*": [ 22 | "extension/src/*" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/HeroUiConfig.ts: -------------------------------------------------------------------------------- 1 | import {heroui} from '@heroui/react'; 2 | export default heroui({ 3 | themes: { 4 | dark: { 5 | colors: { 6 | danger: { 7 | DEFAULT: '#dc2626', 8 | }, 9 | primary: { 10 | DEFAULT: '#0050EF', 11 | }, 12 | secondary: { 13 | DEFAULT: '#AA00FF', 14 | }, 15 | }, 16 | }, 17 | light: { 18 | colors: { 19 | danger: { 20 | DEFAULT: '#dc2626', 21 | }, 22 | primary: { 23 | DEFAULT: '#00A9FF', 24 | }, 25 | secondary: { 26 | DEFAULT: '#d500f9', 27 | }, 28 | }, 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/renderer/HeroUiConfig.ts: -------------------------------------------------------------------------------- 1 | import {heroui} from '@heroui/react'; 2 | 3 | export default heroui({ 4 | themes: { 5 | dark: { 6 | colors: { 7 | danger: { 8 | DEFAULT: '#dc2626', 9 | }, 10 | primary: { 11 | DEFAULT: '#0050EF', 12 | }, 13 | secondary: { 14 | DEFAULT: '#AA00FF', 15 | }, 16 | }, 17 | }, 18 | light: { 19 | colors: { 20 | danger: { 21 | DEFAULT: '#dc2626', 22 | }, 23 | primary: { 24 | DEFAULT: '#00A9FF', 25 | }, 26 | secondary: { 27 | DEFAULT: '#d500f9', 28 | }, 29 | }, 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /module/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import json from '@rollup/plugin-json'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | 6 | /** @type {import('rollup').RollupOptions} */ 7 | const config = { 8 | input: ['module/src/main.ts', 'module/src/renderer.ts'], 9 | output: { 10 | dir: '../module_out/scripts', 11 | format: 'es', 12 | entryFileNames: '[name].mjs', 13 | chunkFileNames: '[name]_[hash:6].mjs', 14 | }, 15 | external: ['electron'], 16 | plugins: [json(), typescript({tsconfig: 'module/tsconfig.json'}), nodeResolve(), commonjs()], 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /module/lynxModule.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./lynxModule.schema.json", 3 | "id": "lynxhub_module", 4 | "title": "LynxHub Module", 5 | "description": "Example module.", 6 | "publishDate": "2024-08-21", 7 | "requireAppBuild": 16, 8 | "repoUrl": "???", 9 | "version": "0.0.1", 10 | "updateDate": "2025-06-18", 11 | "changes": [ 12 | { 13 | "title": "V0.0.1", 14 | "items": [ 15 | { 16 | "label": "✨ New", 17 | "subitems": [ 18 | { 19 | "label": "Nothing!" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | ], 26 | "logoUrl": "https://avatars.githubusercontent.com/u/143181745?v=4", 27 | "owner": false 28 | } -------------------------------------------------------------------------------- /src/main/Managements/Plugin/PluginSentry.ts: -------------------------------------------------------------------------------- 1 | import {defaultStackParser, getDefaultIntegrations, makeNodeTransport, NodeClient, Scope} from '@sentry/node'; 2 | 3 | export function initPluginNodeSentry(dsn: string) { 4 | const integrations = getDefaultIntegrations({}).filter(defaultIntegration => { 5 | return !['BrowserApiErrors', 'Breadcrumbs', 'GlobalHandlers'].includes(defaultIntegration.name); 6 | }); 7 | 8 | const client = new NodeClient({ 9 | dsn, 10 | transport: makeNodeTransport, 11 | stackParser: defaultStackParser, 12 | integrations: integrations, 13 | }); 14 | 15 | const scope = new Scope(); 16 | scope.setClient(client); 17 | client.init(); 18 | 19 | return scope; 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/App/PluginSentry.ts: -------------------------------------------------------------------------------- 1 | import {BrowserClient, defaultStackParser, getDefaultIntegrations, makeFetchTransport, Scope} from '@sentry/browser'; 2 | 3 | export function initPluginBrowserSentry(dsn: string) { 4 | const integrations = getDefaultIntegrations({}).filter(defaultIntegration => { 5 | return !['BrowserApiErrors', 'Breadcrumbs', 'GlobalHandlers'].includes(defaultIntegration.name); 6 | }); 7 | 8 | const client = new BrowserClient({ 9 | dsn, 10 | transport: makeFetchTransport, 11 | stackParser: defaultStackParser, 12 | integrations: integrations, 13 | }); 14 | 15 | const scope = new Scope(); 16 | scope.setClient(client); 17 | client.init(); 18 | 19 | return scope; 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/Arguments/ManageArguments/Items/CheckBoxArg-Item.tsx: -------------------------------------------------------------------------------- 1 | import {ChosenArgument} from '../../../../../../../../../cross/CrossTypes'; 2 | import {CheckSquareDuo_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 3 | import ArgumentItemBase from './Argument-Item-Base'; 4 | 5 | type Props = {argument: ChosenArgument; removeArg: () => void; changeValue: (value: any) => void; id: string}; 6 | 7 | export default function CheckBoxArgItem({argument, removeArg, id}: Props) { 8 | return ( 9 | } 14 | /> 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Reusable/LynxTooltip.tsx: -------------------------------------------------------------------------------- 1 | import {Tooltip, TooltipProps} from '@heroui/react'; 2 | 3 | import {useDisableTooltip} from '../../Utils/UtilHooks'; 4 | 5 | type Props = TooltipProps & { 6 | isEssential?: boolean; 7 | }; 8 | 9 | export default function LynxTooltip({isEssential, delay = 300, radius = 'sm', children, ...props}: Props) { 10 | const isDisabled = useDisableTooltip(isEssential); 11 | 12 | return ( 13 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/assets/fonts.css: -------------------------------------------------------------------------------- 1 | /* -----------------------------------------------> Nunito */ 2 | @font-face { 3 | font-family: 'Nunito'; 4 | src: url('fonts/Nunito/Nunito-VariableFont_wght.ttf'); 5 | font-weight: 100 1000; 6 | font-display: block; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Nunito'; 11 | src: url('fonts/Nunito/Nunito-Italic-VariableFont_wght.ttf'); 12 | font-weight: 100 1000; 13 | font-style: italic; 14 | font-display: block; 15 | } 16 | 17 | /* -----------------------------------------------> JetBrains Mono */ 18 | 19 | @font-face { 20 | font-family: 'JetBrainsMono'; 21 | src: url('fonts/JetBrainsMono/JetBrainsMono.ttf') format('truetype'); 22 | font-weight: 100 800; 23 | font-display: swap; 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/App/App.tsx: -------------------------------------------------------------------------------- 1 | import Background from './Components/Background'; 2 | import Initializer from './Components/Initializer/Initializer'; 3 | import MainContents from './Components/MainContents/MainContents'; 4 | import Modals from './Components/Modals/Modals'; 5 | import TitleBar from './Components/TitleBar/TitleBar'; 6 | import ExtensionHooks from './ExtensionHooks'; 7 | import AppHooks from './MainHooks'; 8 | import UIProviders from './UIProviders'; 9 | 10 | export default function App() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/preload/index.js: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | 3 | import {electronAPI} from '@electron-toolkit/preload'; 4 | import {contextBridge} from 'electron'; 5 | 6 | import {isPortable} from './utils.js'; 7 | 8 | if (process.contextIsolated) { 9 | try { 10 | contextBridge.exposeInMainWorld('electron', electronAPI); 11 | contextBridge.exposeInMainWorld('osPlatform', os.platform()); 12 | contextBridge.exposeInMainWorld('isPortable', isPortable()); 13 | contextBridge.exposeInMainWorld('appStartTime', Date.now()); 14 | } catch (error) { 15 | console.error(error); 16 | } 17 | } else { 18 | window.electron = electronAPI; 19 | window.osPlatform = os.platform(); 20 | window.isPortable = isPortable(); 21 | window.appStartTime = Date.now(); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/TopBar/SharedTopBar.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | 3 | import {RunningCard} from '../../../Utils/Types'; 4 | import DownloadManager from './DownloadManager'; 5 | import Switch from './Switch'; 6 | import Terminate_AI from './Terminate_AI'; 7 | 8 | type Props = {runningCard: RunningCard}; 9 | 10 | export default function SharedTopBar({runningCard}: Props) { 11 | const {type, currentView} = useMemo(() => runningCard, [runningCard]); 12 | 13 | return ( 14 |
15 | {type === 'both' && } 16 | {(type === 'both' || type === 'terminal') && } 17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Please complete the following information:** 27 | - OS: [e.g. Windows] 28 | - LynxHub Version [e.g. 1.0.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/Arguments/ManageArguments/Items/FileArg-Item.tsx: -------------------------------------------------------------------------------- 1 | import {ChosenArgument} from '../../../../../../../../../cross/CrossTypes'; 2 | import {FileDuo_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 3 | import PathArgItem from './PathArgItem'; 4 | 5 | type Props = {argument: ChosenArgument; removeArg: () => void; changeValue: (value: any) => void; id: string}; 6 | 7 | export default function FileArgItem({argument, changeValue, removeArg, id}: Props) { 8 | return ( 9 | } 15 | changeValue={changeValue} 16 | placeholder="Click to choose file..." 17 | /> 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/src/App/Extensions/Extension_Utils.tsx: -------------------------------------------------------------------------------- 1 | import {FC} from 'react'; 2 | 3 | import {extensionRendererApi} from './ExtensionLoader'; 4 | 5 | export const eventUtil_CollectUserInputs = (id: string, job: (elements: FC[]) => void) => { 6 | const elements: FC[] = []; 7 | let doneAdd: number = 0; 8 | 9 | const listenerCount = extensionRendererApi.events.getListenerCount('card_collect_user_input'); 10 | if (listenerCount > 0) { 11 | extensionRendererApi.events.emit('card_collect_user_input', { 12 | id, 13 | addElements: element => { 14 | elements.push(...element); 15 | doneAdd += 1; 16 | 17 | if (doneAdd === listenerCount) { 18 | job(elements); 19 | } 20 | }, 21 | }); 22 | } else { 23 | job(elements); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/renderer/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import {addBreadcrumb} from '@sentry/electron/renderer'; 2 | import {isEmpty} from 'lodash'; 3 | import {useEffect} from 'react'; 4 | 5 | let isEnabled: boolean = true; 6 | 7 | export function onBreadcrumbStateChange(enabled: boolean) { 8 | isEnabled = enabled; 9 | } 10 | 11 | export default function AddBreadcrumb_Renderer(message: string) { 12 | if (isEnabled) addBreadcrumb({category: 'renderer-actions', message, level: 'info'}); 13 | } 14 | 15 | export function useDebounceBreadcrumb(message: string, ...deps: any[]) { 16 | useEffect(() => { 17 | const timeoutId = setTimeout(() => { 18 | if (!deps.every(isEmpty)) AddBreadcrumb_Renderer(`${message}: ${JSON.stringify(deps)}`); 19 | }, 2000); 20 | 21 | return () => clearTimeout(timeoutId); 22 | }, deps); 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/Arguments/ManageArguments/Items/DirectoryArg-Item.tsx: -------------------------------------------------------------------------------- 1 | import {ChosenArgument} from '../../../../../../../../../cross/CrossTypes'; 2 | import {FolderDuo_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 3 | import PathArgItem from './PathArgItem'; 4 | 5 | type Props = {argument: ChosenArgument; removeArg: () => void; changeValue: (value: any) => void; id: string}; 6 | 7 | export default function DirectoryArgItem({argument, changeValue, removeArg, id}: Props) { 8 | return ( 9 | } 15 | changeValue={changeValue} 16 | placeholder="Click to choose folder..." 17 | /> 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-FontSize.tsx: -------------------------------------------------------------------------------- 1 | import {NumberInput} from '@heroui/react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | 7 | export default function SettingsTerminalFontSize() { 8 | const fontSize = useTerminalState('fontSize'); 9 | const dispatch = useDispatch(); 10 | 11 | const onChange = (value: number) => { 12 | dispatch(terminalActions.setTerminalState({key: 'fontSize', value})); 13 | }; 14 | 15 | return ( 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "electron.vite.config.*", 5 | "extension/electron.vite.config.*", 6 | "src/main/**/*", 7 | "src/preload/**/*", 8 | "src/cross/**/*", 9 | "extension/src/main/**/*", 10 | "extension/src/cross/**/*", 11 | "module/src/**/*" 12 | ], 13 | "compilerOptions": { 14 | "moduleResolution": "bundler", 15 | "module": "ESNext", 16 | "allowSyntheticDefaultImports": true, 17 | "useUnknownInCatchVariables": false, 18 | "composite": true, 19 | "types": [ 20 | "electron-vite/node" 21 | ], 22 | "paths": { 23 | "@lynx_module/*": [ 24 | "./module/src/*" 25 | ], 26 | "@lynx_extension/*": [ 27 | "./extension/src/*" 28 | ] 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Browser/SettingsBrowser.tsx: -------------------------------------------------------------------------------- 1 | import {Web_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 2 | import SettingsSection from '../../SettingsPage-ContentSection'; 3 | import SettingsBrowser_ClearData from './SettingsBrowser_ClearData'; 4 | import SettingsBrowser_Links from './SettingsBrowser_Links'; 5 | import SettingsBrowser_UserAgent from './SettingsBrowser_UserAgent'; 6 | 7 | export const SettingsBrowserId = 'settings_browser_elem'; 8 | 9 | export default function SettingsBrowser() { 10 | return ( 11 | }> 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/loading/Loadings/LiquidChromeLoading.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | 3 | import LiquidChromeBG from '../Backgrounds/LiquidChromeBG'; 4 | import {AppName} from './AppName'; 5 | import Container from './Container'; 6 | 7 | export default function LiquidChromeLoading() { 8 | const [amplitude, setAmplitude] = useState(0.1); 9 | 10 | useEffect(() => { 11 | setAmplitude(Math.random() * (0.4 - 0.1) + 0.1); 12 | }, []); 13 | 14 | return ( 15 | 16 | 17 |
20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/Home/HomeCategory.tsx: -------------------------------------------------------------------------------- 1 | import {motion} from 'framer-motion'; 2 | import {ReactNode} from 'react'; 3 | 4 | import CardContainer from '../../CardContainer'; 5 | 6 | type Props = { 7 | children: ReactNode; 8 | icon: ReactNode; 9 | title: string; 10 | subTitle?: string; 11 | }; 12 | 13 | export default function HomeCategory({children, subTitle, icon, title}: Props) { 14 | return ( 15 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/App/Utils/Types.ts: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | export type UpdatingCard = {id: string; devName: string; title: string}; 4 | export type UpdatingCards = UpdatingCard[]; 5 | 6 | export type RunningCard = { 7 | id: string; 8 | tabId: string; 9 | webUIAddress: string; 10 | customAddress: string; 11 | currentAddress: string; 12 | startTime: string; 13 | currentView: 'browser' | 'terminal'; 14 | type: 'browser' | 'terminal' | 'both'; 15 | isEmptyRunning: boolean; 16 | browserTitle: string; 17 | }; 18 | 19 | export type HeroToastPlacement = 20 | | 'bottom-right' 21 | | 'bottom-left' 22 | | 'bottom-center' 23 | | 'top-right' 24 | | 'top-left' 25 | | 'top-center'; 26 | 27 | export type NavItem = { 28 | title: string; 29 | icon: ReactNode; 30 | badge?: ReactNode | boolean; 31 | path: string; 32 | }; 33 | -------------------------------------------------------------------------------- /src/renderer/src/App/AppEvents/AppEvents.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useAppTitleEvents, 3 | useBrowserEvents, 4 | useCheckCardsUpdate, 5 | useCheckPluginsUpdate, 6 | useContextEvents, 7 | useHotkeyEvents, 8 | useIpcEvents, 9 | useListenForUpdateError, 10 | useNewTabEvents, 11 | useOnlineEvents, 12 | usePatreon, 13 | useShowToast, 14 | useStorageData, 15 | } from './AppEvents_Hooks'; 16 | 17 | /** Listening for various app events and modify redux states */ 18 | export default function useAppEvents() { 19 | useCheckCardsUpdate(); 20 | useCheckPluginsUpdate(); 21 | useOnlineEvents(); 22 | useStorageData(); 23 | usePatreon(); 24 | useIpcEvents(); 25 | useAppTitleEvents(); 26 | useHotkeyEvents(); 27 | useNewTabEvents(); 28 | useBrowserEvents(); 29 | useContextEvents(); 30 | useShowToast(); 31 | useListenForUpdateError(); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Background.tsx: -------------------------------------------------------------------------------- 1 | import {isNil} from 'lodash'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {extensionsData} from '../Extensions/ExtensionLoader'; 5 | 6 | const Background = memo(() => { 7 | const BG = useMemo(() => extensionsData.replaceBackground, []); 8 | 9 | return !isNil(BG) ? ( 10 | 11 | ) : ( 12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 | ); 20 | }); 21 | 22 | export default Background; 23 | -------------------------------------------------------------------------------- /src/renderer/loading/main.tsx: -------------------------------------------------------------------------------- 1 | import '../cross_styles.css'; 2 | 3 | import {createRoot} from 'react-dom/client'; 4 | 5 | import rendererIpc from '../src/App/RendererIpc'; 6 | import LiquidChromeLoading from './Loadings/LiquidChromeLoading'; 7 | import RippleLoading from './Loadings/RippleLoading'; 8 | import SimpleLoading from './Loadings/SimpleLoading'; 9 | import ThreadsLoading from './Loadings/ThreadsLoading'; 10 | 11 | rendererIpc.storage.get('app').then(({disableLoadingAnimations}) => { 12 | let TargetComponent = SimpleLoading; 13 | 14 | if (!disableLoadingAnimations) { 15 | const components = [ThreadsLoading, LiquidChromeLoading, RippleLoading]; 16 | const randomIndex = Math.floor(Math.random() * components.length); 17 | TargetComponent = components[randomIndex]; 18 | } 19 | 20 | createRoot(document.getElementById('root') as HTMLElement).render(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/renderer/context_menu/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode, useRef, useState} from 'react'; 2 | 3 | import {useContextMenuSetup, useResize} from './Components/ContextHooks'; 4 | import UIProviders from './UIProviders'; 5 | 6 | export default function ContextMenu() { 7 | const [elements, setElements] = useState([]); 8 | const [widthSize, setWidthSize] = useState<'sm' | 'md' | 'lg'>('sm'); 9 | 10 | const divRef = useRef(null); 11 | 12 | useResize(divRef); 13 | 14 | useContextMenuSetup(setElements, setWidthSize); 15 | 16 | return ( 17 | 18 |
24 | {...elements} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-ScrollBack.tsx: -------------------------------------------------------------------------------- 1 | import {NumberInput} from '@heroui/react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | 7 | export default function SettingsTerminalScrollBack() { 8 | const scrollBack = useTerminalState('scrollBack'); 9 | const dispatch = useDispatch(); 10 | 11 | const onChange = (value: number) => { 12 | dispatch(terminalActions.setTerminalState({key: 'scrollBack', value})); 13 | }; 14 | 15 | return ( 16 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-ResizeDelay.tsx: -------------------------------------------------------------------------------- 1 | import {NumberInput} from '@heroui/react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | 7 | export default function SettingsTerminalResizeDelay() { 8 | const resizeDelay = useTerminalState('resizeDelay'); 9 | const dispatch = useDispatch(); 10 | 11 | const onChange = (value: number) => { 12 | dispatch(terminalActions.setTerminalState({key: 'resizeDelay', value})); 13 | }; 14 | 15 | return ( 16 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/Managements/Ipc/IpcMainSender.ts: -------------------------------------------------------------------------------- 1 | import {ipcMain, IpcMainEvent} from 'electron'; 2 | 3 | import {CustomNotificationInfo} from '../../../cross/CrossTypes'; 4 | import {customNotifChannels} from '../../../cross/DownloadManagerTypes'; 5 | import {appManager} from '../../index'; 6 | 7 | const sendIt = (channel: string, data: any) => { 8 | const webContent = appManager?.getWebContent(); 9 | if (webContent) { 10 | webContent.send(channel, data); 11 | } else { 12 | console.error('Failed to send IPC message: ', 'webContent is undefined!'); 13 | } 14 | }; 15 | 16 | export const ipcMainSender = { 17 | customNotification: { 18 | open: (data: CustomNotificationInfo) => sendIt(customNotifChannels.onOpen, data), 19 | close: (key: string) => sendIt(customNotifChannels.onClose, key), 20 | onBtnPress: (result: (event: IpcMainEvent, btnId: string, notifKey: string) => void) => 21 | ipcMain.on(customNotifChannels.onBtnPress, result), 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/main/Managements/Git/GitHelper.ts: -------------------------------------------------------------------------------- 1 | import {SimpleGitProgressEvent} from 'simple-git'; 2 | 3 | import {gitChannels} from '../../../cross/IpcChannelAndTypes'; 4 | import {appManager} from '../../index'; 5 | import GitManager from './GitManager'; 6 | 7 | /** 8 | * Sets up listeners for GitManager instance. 9 | * @param manager - The GitManager instance. 10 | * @param id - Optional identifier for pull operations. 11 | */ 12 | export function setupGitManagerListeners(manager: GitManager, id?: string): void { 13 | manager.onProgress = (progress: SimpleGitProgressEvent) => { 14 | appManager?.getWebContent()?.send(gitChannels.onProgress, id, 'Progress', progress); 15 | }; 16 | 17 | manager.onComplete = (result?: any) => { 18 | appManager?.getWebContent()?.send(gitChannels.onProgress, id, 'Completed', result); 19 | }; 20 | 21 | manager.onError = (reason: string) => { 22 | appManager?.getWebContent()?.send(gitChannels.onProgress, id, 'Failed', reason); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/Managements/Ipc/Methods/IpcMethods-Platform.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | 3 | import {SystemInfo} from '../../../../cross/IpcChannelAndTypes'; 4 | 5 | /** 6 | * Parses the Windows build number from the release string. 7 | * @param releaseString - The Windows release string. 8 | * @returns The parsed build number. 9 | */ 10 | function parseWindowsBuildNumber(releaseString: string): number { 11 | const buildNumber = releaseString.split('.')[2]; 12 | return parseInt(buildNumber, 10); 13 | } 14 | 15 | /** 16 | * Retrieves the system information including the OS platform and build number. 17 | * @returns An object containing the OS platform and build number. 18 | */ 19 | export function getSystemInfo(): SystemInfo { 20 | const platform = process.platform; 21 | let buildNumber: string | number = os.release(); 22 | 23 | if (platform === 'win32') { 24 | buildNumber = parseWindowsBuildNumber(buildNumber); 25 | } 26 | 27 | return {os: platform, buildNumber}; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Reusable/LynxScroll.tsx: -------------------------------------------------------------------------------- 1 | import {OverflowBehavior} from 'overlayscrollbars'; 2 | import {OverlayScrollbarsComponent} from 'overlayscrollbars-react'; 3 | import {ReactNode} from 'react'; 4 | 5 | import {useAppState} from '../../Redux/Reducer/AppReducer'; 6 | 7 | type Props = { 8 | children?: ReactNode; 9 | className?: string; 10 | overflow?: Partial<{ 11 | x: OverflowBehavior; 12 | y: OverflowBehavior; 13 | }>; 14 | }; 15 | export default function LynxScroll({children, className, overflow = {x: 'hidden', y: 'scroll'}}: Props) { 16 | const isDarkMode = useAppState('darkMode'); 17 | 18 | return ( 19 | 28 | {children} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Tabs/NewTab.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {motion} from 'framer-motion'; 3 | import {memo} from 'react'; 4 | import {useDispatch} from 'react-redux'; 5 | 6 | import {Add_Icon} from '../../../assets/icons/SvgIcons/SvgIcons'; 7 | import {tabsActions} from '../../Redux/Reducer/TabsReducer'; 8 | import {AppDispatch} from '../../Redux/Store'; 9 | import {defaultTabItem} from '../../Utils/Constants'; 10 | 11 | const NewTab = memo(() => { 12 | const dispatch = useDispatch(); 13 | const addTab = () => { 14 | dispatch(tabsActions.addTab(defaultTabItem)); 15 | }; 16 | 17 | return ( 18 | 19 | 22 | 23 | ); 24 | }); 25 | 26 | export default NewTab; 27 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Cards/NavigatePluginsPage.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {useCallback} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | 5 | import {PageID, PageTitles} from '../../../../../cross/CrossConstants'; 6 | import {Plugins_Icon} from '../../../assets/icons/SvgIcons/SvgIcons'; 7 | import {tabsActions} from '../../Redux/Reducer/TabsReducer'; 8 | import {AppDispatch} from '../../Redux/Store'; 9 | 10 | export default function NavigatePluginsPage({size}: {size?: 'sm' | 'md'}) { 11 | const dispatch = useDispatch(); 12 | 13 | const handleGoModules = useCallback(() => { 14 | dispatch( 15 | tabsActions.setActivePage({ 16 | pageID: PageID.plugins, 17 | title: PageTitles.plugins, 18 | }), 19 | ); 20 | }, [dispatch]); 21 | 22 | return ( 23 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /extension/src/renderer/reducer.ts: -------------------------------------------------------------------------------- 1 | import {createSlice} from '@reduxjs/toolkit'; 2 | import {useSelector} from 'react-redux'; 3 | 4 | type ExtensionsState = {someString: string; someBoolean: boolean; someNumber: number; someArray: []}; 5 | 6 | type ExtensionsStateTypes = { 7 | [K in keyof ExtensionsState]: ExtensionsState[K]; 8 | }; 9 | 10 | const initialState: ExtensionsState = { 11 | someString: 'WoW', 12 | someBoolean: true, 13 | someNumber: 77, 14 | someArray: [], 15 | }; 16 | 17 | const extensionReducer = createSlice({ 18 | initialState, 19 | name: 'extension', 20 | reducers: { 21 | increaseNumber: (state: ExtensionsState) => { 22 | state.someNumber = state.someNumber + 1; 23 | }, 24 | }, 25 | }); 26 | 27 | export const useExtensionState = (name: T): ExtensionsStateTypes[T] => 28 | useSelector((state: any) => state.extension[name]); 29 | 30 | export const extensionActions = extensionReducer.actions; 31 | 32 | export default extensionReducer.reducer; 33 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/TopBar/Switch.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {Terminal_Icon, Web_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 5 | import {cardsActions} from '../../../Redux/Reducer/CardsReducer'; 6 | import {useTabsState} from '../../../Redux/Reducer/TabsReducer'; 7 | import {AppDispatch} from '../../../Redux/Store'; 8 | 9 | type Props = {currentView: 'browser' | 'terminal'}; 10 | export default function Switch({currentView}: Props) { 11 | const activeTab = useTabsState('activeTab'); 12 | const dispatch = useDispatch(); 13 | 14 | const onPress = () => { 15 | dispatch(cardsActions.toggleRunningCardView({tabId: activeTab})); 16 | }; 17 | 18 | return ( 19 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Cards/Card/PulsingLine.tsx: -------------------------------------------------------------------------------- 1 | import {motion, type Variants} from 'framer-motion'; 2 | import {memo} from 'react'; 3 | 4 | const variants: Variants = { 5 | hover: {opacity: 0.5, width: '70%', transition: {duration: 0.3}}, 6 | initial: {opacity: 0.3, width: '50%', transition: {duration: 0.7}}, 7 | }; 8 | 9 | type Props = {isInstalled: boolean; accentColor: string}; 10 | const PulsingLine = memo(({isInstalled, accentColor}: Props) => { 11 | if (!isInstalled) { 12 | return null; 13 | } 14 | 15 | const gradientStyle = { 16 | background: `linear-gradient(to right, transparent, ${accentColor}, transparent)`, 17 | }; 18 | 19 | return ( 20 | 30 | ); 31 | }); 32 | 33 | export default PulsingLine; 34 | -------------------------------------------------------------------------------- /extension/src/renderer/Components/Cards_AddMenu.tsx: -------------------------------------------------------------------------------- 1 | import {DropdownItem, DropdownSection} from '@heroui/react'; 2 | import {useEffect} from 'react'; 3 | 4 | import {UseCardStoreType} from '../../../../src/cross/plugin/ExtensionTypes_Renderer'; 5 | import {Bug_Icon, Trash_Icon} from '../../../../src/renderer/src/assets/icons/SvgIcons/SvgIcons'; 6 | 7 | type Props = {useCardStore: UseCardStoreType}; 8 | 9 | export default function CardsAddMenu({useCardStore}: Props) { 10 | const title = useCardStore(state => state.title); 11 | 12 | useEffect(() => { 13 | console.log(title); 14 | }, [title]); 15 | 16 | return ( 17 | 18 | }> 19 | }> 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/PreLaunch/PreOpenPath/PreOpenPath-Item.tsx: -------------------------------------------------------------------------------- 1 | import {Card} from '@heroui/card'; 2 | import {Button} from '@heroui/react'; 3 | import {ReactNode, useCallback} from 'react'; 4 | 5 | import {TrashDuo_Icon} from '../../../../../../assets/icons/SvgIcons/SvgIcons'; 6 | 7 | type Props = {icon: ReactNode; index: number; defaultText: string; onRemove?: (index: number) => void}; 8 | 9 | /** Manage pre-opened items, display paths, and remove */ 10 | export default function PreOpenPathItem({defaultText, icon, index, onRemove}: Props) { 11 | const remove = useCallback(() => onRemove?.(index), [onRemove, index]); 12 | 13 | return ( 14 | 15 | {icon} 16 | {defaultText} 17 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/App/Utils/SetHtmlAttributes.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | import {APP_NAME} from '../../../../cross/CrossConstants'; 4 | import {useAppState} from '../Redux/Reducer/AppReducer'; 5 | import {useSettingsState} from '../Redux/Reducer/SettingsReducer'; 6 | 7 | /** HTML attributes and document title */ 8 | export default function useHtmlAttributes() { 9 | const darkMode = useAppState('darkMode'); 10 | const appTitle = useAppState('appTitle'); 11 | const dynamicAppTitle = useSettingsState('dynamicAppTitle'); 12 | 13 | useEffect(() => { 14 | const title = 15 | dynamicAppTitle && appTitle ? `${appTitle}${!appTitle?.endsWith('- ') ? ' -' : ''} ${APP_NAME}` : APP_NAME; 16 | if (document.title !== title) { 17 | document.title = title; 18 | } 19 | }, [appTitle, dynamicAppTitle]); 20 | 21 | useEffect(() => { 22 | document.documentElement.className = 23 | `select-none text-foreground bg-background ` + `overflow-hidden ${darkMode ? 'dark' : 'light'}`; 24 | }, [darkMode]); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/InstallUI/Install-Header.tsx: -------------------------------------------------------------------------------- 1 | import {ModalHeader} from '@heroui/react'; 2 | import {Steps} from 'antd'; 3 | import {memo} from 'react'; 4 | 5 | type Props = { 6 | steps: string[]; 7 | currentStep: number; 8 | }; 9 | 10 | const InstallHeader = ({steps, currentStep}: Props) => { 11 | return ( 12 | 13 | { 15 | return { 16 | title: ( 17 | 18 | {step} 19 | 20 | ), 21 | }; 22 | })} 23 | type="inline" 24 | current={currentStep} 25 | className="!w-full scale-125 items-center justify-center bg-foreground-200 dark:bg-foreground-100" 26 | /> 27 | 28 | ); 29 | }; 30 | export default memo(InstallHeader); 31 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/SettingsPage-ContentSection.tsx: -------------------------------------------------------------------------------- 1 | import {Card, CardBody, CardHeader} from '@heroui/react'; 2 | import {ReactNode} from 'react'; 3 | 4 | import {ContainersBg} from '../../../../Utils/CrossStyle'; 5 | 6 | type Props = { 7 | title: string; 8 | id?: string; 9 | icon?: ReactNode; 10 | children?: ReactNode; 11 | titleColor?: 'text-warning' | 'text-danger' | string; 12 | itemsCenter?: boolean; 13 | }; 14 | 15 | /** Render card for a specif settings */ 16 | export default function SettingsSection({children, id, title, titleColor = '', itemsCenter = false, icon}: Props) { 17 | return ( 18 | 19 | 20 | {icon} 21 | {title} 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/SettingsStartup-StartMaximized.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from 'react'; 2 | 3 | import rendererIpc from '../../../../../../RendererIpc'; 4 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 5 | 6 | /** Manage app start minimized */ 7 | export default function SettingsStartupStartMaximized() { 8 | const [isSelected, setIsSelected] = useState(false); 9 | 10 | useEffect(() => { 11 | rendererIpc.storage.get('app').then(app => { 12 | setIsSelected(app.startMaximized); 13 | }); 14 | }, []); 15 | 16 | const onEnabledChange = useCallback((selected: boolean) => { 17 | rendererIpc.storage.update('app', {startMaximized: selected}); 18 | setIsSelected(selected); 19 | }, []); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/SettingsStartup-StartMinimized.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from 'react'; 2 | 3 | import rendererIpc from '../../../../../../RendererIpc'; 4 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 5 | 6 | /** Manage app start minimized */ 7 | export default function SettingsStartupStartMinimized() { 8 | const [isSelected, setIsSelected] = useState(false); 9 | 10 | useEffect(() => { 11 | rendererIpc.storage.get('app').then(app => { 12 | setIsSelected(app.startMinimized); 13 | }); 14 | }, []); 15 | 16 | const onEnabledChange = useCallback((selected: boolean) => { 17 | rendererIpc.storage.update('app', {startMinimized: selected}); 18 | setIsSelected(selected); 19 | }, []); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-BlinkCursor.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 7 | 8 | export default function SettingsTerminalBlinkCursor() { 9 | const blinkCursor = useTerminalState('blinkCursor'); 10 | const dispatch = useDispatch(); 11 | 12 | const onEnabledChange = useCallback( 13 | (value: boolean) => { 14 | dispatch(terminalActions.setTerminalState({key: 'blinkCursor', value})); 15 | }, 16 | [dispatch], 17 | ); 18 | 19 | return ( 20 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/SettingsStartup-System.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from 'react'; 2 | 3 | import rendererIpc from '../../../../../../RendererIpc'; 4 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 5 | 6 | /** Manage launch app on system startup */ 7 | export default function SettingsStartupSystem() { 8 | const [isSelected, setIsSelected] = useState(false); 9 | 10 | useEffect(() => { 11 | rendererIpc.storage.get('app').then(app => { 12 | setIsSelected(app.systemStartup); 13 | }); 14 | }, []); 15 | 16 | const onEnabledChange = useCallback((selected: boolean) => { 17 | rendererIpc.storageUtils.setSystemStartup(selected); 18 | setIsSelected(selected); 19 | }, []); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/AppEvents/AppStates_Hooks.ts: -------------------------------------------------------------------------------- 1 | import {isEmpty, isEqual} from 'lodash'; 2 | import {useEffect} from 'react'; 3 | 4 | import {useCardsState} from '../Redux/Reducer/CardsReducer'; 5 | import rendererIpc from '../RendererIpc'; 6 | 7 | // Remove not installed cards from pinned cards 8 | export function useFilterPinnedCards() { 9 | const installedCards = useCardsState('installedCards'); 10 | const pinnedCards = useCardsState('pinnedCards'); 11 | 12 | useEffect(() => { 13 | if (isEmpty(installedCards) || isEmpty(pinnedCards)) { 14 | return; // Exit early if either array is empty 15 | } 16 | 17 | const installedCardIds = new Set(installedCards.map(card => card.id)); 18 | 19 | // Filter pinned cards based on whether their ID exists in the installed cards 20 | const filteredPins = pinnedCards.filter(pCard => installedCardIds.has(pCard)); 21 | 22 | if (!isEqual(filteredPins, pinnedCards)) { 23 | rendererIpc.storageUtils.pinnedCards('set', '', filteredPins); 24 | } 25 | }, [installedCards, pinnedCards]); 26 | } 27 | -------------------------------------------------------------------------------- /extension/src/renderer/Components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import SettingsSection from '../../../../src/renderer/src/App/Components/Pages/SettingsPages/Settings/SettingsPage-ContentSection'; 2 | import {GroupSection} from '../../../../src/renderer/src/App/Components/Pages/SettingsPages/Settings/SettingsPage-Nav'; 3 | import {Bug_Icon} from '../../../../src/renderer/src/assets/icons/SvgIcons/SvgIcons'; 4 | 5 | const sectionId = 'my_extension_settings'; 6 | const sectionTitle = 'Config'; 7 | 8 | export function SettingsNavButton() { 9 | return ( 10 | , 15 | elementId: sectionId, 16 | }, 17 | ]} 18 | title="My Extension" 19 | /> 20 | ); 21 | } 22 | 23 | export function SettingsContent() { 24 | return ( 25 | } itemsCenter> 26 | This is extension settings 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/TopBar/Terminate_AI.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | 3 | import {Stop_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 4 | import {useHotkeysState} from '../../../Redux/Reducer/HotkeysReducer'; 5 | import {useSettingsState} from '../../../Redux/Reducer/SettingsReducer'; 6 | import rendererIpc from '../../../RendererIpc'; 7 | 8 | type Props = {id: string}; 9 | 10 | export default function Terminate_AI({id}: Props) { 11 | const isCtrlPressed = useHotkeysState('isCtrlPressed'); 12 | const showTerminateConfirm = useSettingsState('terminateAIConfirm'); 13 | 14 | const stopAi = () => { 15 | if (id) { 16 | if (isCtrlPressed || !showTerminateConfirm) { 17 | rendererIpc.contextMenu.stopAI(id); 18 | } else { 19 | rendererIpc.contextMenu.openTerminateAI(id); 20 | } 21 | } 22 | }; 23 | 24 | return ( 25 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/Page.tsx: -------------------------------------------------------------------------------- 1 | import {motion, Variants} from 'framer-motion'; 2 | import {ReactNode, useEffect, useState} from 'react'; 3 | 4 | const pageTransitionVariants: Variants = { 5 | enter: {opacity: 1}, 6 | exit: {opacity: 0}, 7 | }; 8 | 9 | type Props = { 10 | children: ReactNode; 11 | className?: string; 12 | show?: boolean; 13 | }; 14 | 15 | export default function Page({children, className, show}: Props) { 16 | const [showClassName, setShowClassName] = useState(''); 17 | 18 | useEffect(() => { 19 | if (show) { 20 | setShowClassName('block'); 21 | } else { 22 | setTimeout(() => { 23 | setShowClassName('hidden'); 24 | }, 200); 25 | } 26 | }, [show]); 27 | 28 | return ( 29 | 35 | {children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-Reset.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {useCallback, useState} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | 5 | import {RefreshDuo_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {terminalActions} from '../../../../../../Redux/Reducer/TerminalReducer'; 7 | import {AppDispatch} from '../../../../../../Redux/Store'; 8 | 9 | export default function SettingsTerminalReset() { 10 | const [isSaving, setIsSaving] = useState(false); 11 | 12 | const dispatch = useDispatch(); 13 | 14 | const onApply = useCallback(() => { 15 | const fakeDelay = 300; 16 | setIsSaving(true); 17 | 18 | dispatch(terminalActions.resetToDefaults()); 19 | setTimeout(() => { 20 | setIsSaving(false); 21 | }, fakeDelay); 22 | }, []); 23 | 24 | return ( 25 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /removeDotExtensions.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | // Check if a file path was provided as a command-line argument. 4 | if (process.argv.length < 3) { 5 | console.error('Usage: node modifyFile.js '); 6 | process.exit(1); 7 | } 8 | 9 | // Get the file path from the command-line arguments. 10 | const filePath = process.argv[2]; 11 | 12 | try { 13 | // Read the content of the file synchronously. 14 | let content = fs.readFileSync(filePath, 'utf8'); 15 | 16 | // Define the string to find and the string to replace it with. 17 | const findString = `const base = './';`; 18 | const replaceString = `const base = '/';`; 19 | 20 | // Use the replace method to update the content. 21 | const updatedContent = content.replace(findString, replaceString); 22 | 23 | // Write the updated content back to the file. 24 | fs.writeFileSync(filePath, updatedContent, 'utf8'); 25 | 26 | console.log(`Successfully modified: ${filePath}`); 27 | } catch (error) { 28 | // Handle any errors that occur during file operations. 29 | console.error(`An error occurred: ${error.message}`); 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/Home/HomeSearchBox.tsx: -------------------------------------------------------------------------------- 1 | import {Input} from '@heroui/react'; 2 | import {Dispatch, memo, SetStateAction} from 'react'; 3 | 4 | import {Circle_Icon} from '../../../../../assets/icons/SvgIcons/SvgIcons'; 5 | 6 | type Props = {searchValue: string; setSearchValue: Dispatch>}; 7 | 8 | const HomeSearchBox = memo(({searchValue, setSearchValue}: Props) => { 9 | return ( 10 | } 24 | fullWidth 25 | /> 26 | ); 27 | }); 28 | 29 | export default HomeSearchBox; 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/SettingsStartup-StartPage.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from 'react'; 2 | 3 | import rendererIpc from '../../../../../../RendererIpc'; 4 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 5 | 6 | /** Manage app reopen page */ 7 | export default function SettingsStartupStartPage() { 8 | const [isSelected, setIsSelected] = useState(true); 9 | 10 | useEffect(() => { 11 | rendererIpc.storage.get('app').then(app => { 12 | setIsSelected(app.startupLastActivePage); 13 | }); 14 | }, []); 15 | 16 | const onEnabledChange = useCallback((selected: boolean) => { 17 | rendererIpc.storage.update('app', {startupLastActivePage: selected}); 18 | setIsSelected(selected); 19 | }, []); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-CloseOnExit.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 7 | 8 | export default function SettingsTerminalCloseOnExit() { 9 | const closeTabOnExit = useTerminalState('closeTabOnExit'); 10 | const dispatch = useDispatch(); 11 | 12 | const onEnabledChange = useCallback( 13 | (value: boolean) => { 14 | dispatch(terminalActions.setTerminalState({key: 'closeTabOnExit', value})); 15 | }, 16 | [dispatch], 17 | ); 18 | 19 | return ( 20 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Reusable/ShinyText.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | 3 | type ShinyTextProps = { 4 | text: string; 5 | disabled?: boolean; 6 | speed?: number; 7 | className?: string; 8 | darkMode?: boolean; 9 | }; 10 | 11 | export default function ShinyText({ 12 | text, 13 | disabled = false, 14 | speed = 5, 15 | className = '', 16 | darkMode = true, 17 | }: ShinyTextProps) { 18 | const animationDuration = `${speed}s`; 19 | 20 | const backgroundImage = useMemo(() => { 21 | const color = darkMode ? '255' : '70'; 22 | const rgb = `${color}, ${color}, ${color}`; 23 | 24 | return `linear-gradient(120deg, rgba(${rgb}, 0) 40%, rgba(${rgb}, 0.8) 50%, rgba(${rgb}, 0) 60%)`; 25 | }, [darkMode]); 26 | 27 | return ( 28 |
36 | {text} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/MainContents/MainContents.tsx: -------------------------------------------------------------------------------- 1 | import {memo} from 'react'; 2 | 3 | import {PageID} from '../../../../../cross/CrossConstants'; 4 | import {useAppState} from '../../Redux/Reducer/AppReducer'; 5 | import {useTabsState} from '../../Redux/Reducer/TabsReducer'; 6 | import NavBar from '../NavBar/NavBar'; 7 | import AppPages from './AppPages'; 8 | import StatusBar from './StatusBar'; 9 | 10 | /** Main app contents */ 11 | const MainContents = memo(() => { 12 | const navBar = useAppState('navBar'); 13 | const activePage = useTabsState('activePage'); 14 | 15 | return ( 16 |
17 |
18 | 19 |
23 | 24 |
25 |
26 | 27 |
28 | ); 29 | }); 30 | 31 | export default MainContents; 32 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/CardContainer.tsx: -------------------------------------------------------------------------------- 1 | import {Card, CardBody, CardHeader} from '@heroui/react'; 2 | import {ReactNode} from 'react'; 3 | 4 | import {ContainersBg} from '../../Utils/CrossStyle'; 5 | 6 | type Props = { 7 | children?: ReactNode; 8 | icon: ReactNode; 9 | title: string; 10 | subTitle?: string; 11 | extraClassNames?: string; 12 | }; 13 | 14 | export const CardContainerClasses = 'size-6 mr-2 hover:transition hover:duration-500 hover:opacity-80'; 15 | 16 | export default function CardContainer({children, icon, title, subTitle, extraClassNames}: Props) { 17 | return ( 18 | 19 | 20 |
21 | {icon} 22 | {title} 23 |
24 | {subTitle} 25 |
26 | {children} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/UpdateApp/Downloading.tsx: -------------------------------------------------------------------------------- 1 | import {Progress} from '@heroui/react'; 2 | import {Descriptions} from 'antd'; 3 | import DescriptionsItem from 'antd/es/descriptions/Item'; 4 | 5 | import {UpdateDownloadProgress} from '../../../../../../cross/CrossTypes'; 6 | import {formatSize} from '../../../../../../cross/CrossUtils'; 7 | 8 | type Props = {progress: UpdateDownloadProgress | undefined}; 9 | 10 | /** Downloading update progress */ 11 | export default function Downloading({progress}: Props) { 12 | return ( 13 |
14 | 15 | 16 | 17 | {`${Math.floor(progress?.percent || 0)}%`} 18 | 19 | {`${formatSize(progress?.transferred)}` + `/${formatSize(progress?.total)}`} 20 | 21 | {formatSize(progress?.bytesPerSecond)} 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/CardGitManager/CommitInfo.tsx: -------------------------------------------------------------------------------- 1 | import {RepositoryInfo} from '../../../../../../cross/CrossTypes'; 2 | 3 | type Props = {repoInfo: RepositoryInfo}; 4 | export default function CommitInfo({repoInfo}: Props) { 5 | return ( 6 |
7 |
8 | Current Branch: 9 | {repoInfo.currentBranch} 10 |
11 |
12 | Last Commit Hash: 13 | {repoInfo.lastCommitHash} 14 |
15 |
16 | Last Commit Message: 17 | {repoInfo.lastCommitMessage} 18 |
19 |
20 | Last Commit Time: 21 | {repoInfo.lastCommitTime} 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/TopBar/DownloadManager.tsx: -------------------------------------------------------------------------------- 1 | import {Badge, Button} from '@heroui/react'; 2 | import {useEffect, useState} from 'react'; 3 | 4 | import {DownloadDuo_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 5 | import rendererIpc from '../../../RendererIpc'; 6 | 7 | export default function DownloadManager() { 8 | const [itemCount, setItemCount] = useState(0); 9 | 10 | useEffect(() => { 11 | const offDownloadCount = rendererIpc.downloadManager.onDownloadCount((_, count) => { 12 | setItemCount(count); 13 | }); 14 | 15 | return () => offDownloadCount(); 16 | }, []); 17 | 18 | const openDownloadsWindow = () => rendererIpc.downloadManager.openMenu(); 19 | 20 | if (itemCount === 0) return null; 21 | 22 | return ( 23 | 30 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/GamesPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | 3 | import {GamePad_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 4 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 5 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 6 | import Page from '../Page'; 7 | 8 | type Props = {show: boolean}; 9 | 10 | const GamesPage = ({show}: Props) => { 11 | const {addComponent} = extensionsData.customizePages.games; 12 | 13 | return ( 14 | 15 | 16 | }> 21 |
22 | {addComponent.map((Comp, index) => ( 23 | 24 | ))} 25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default GamesPage; 33 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/General/SettingsGeneral-TitleName.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {settingsActions, useSettingsState} from '../../../../../../Redux/Reducer/SettingsReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import rendererIpc from '../../../../../../RendererIpc'; 7 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 8 | 9 | export default function SettingsGeneralTitleName() { 10 | const dispatch = useDispatch(); 11 | const dynamicAppTitle = useSettingsState('dynamicAppTitle'); 12 | 13 | const onAppTitleChange = useCallback( 14 | (selected: boolean) => { 15 | rendererIpc.storage.update('app', {dynamicAppTitle: selected}); 16 | dispatch(settingsActions.setSettingsState({key: 'dynamicAppTitle', value: selected})); 17 | }, 18 | [dispatch], 19 | ); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Browser/SettingsBrowser_Links.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {settingsActions, useSettingsState} from '../../../../../../Redux/Reducer/SettingsReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import rendererIpc from '../../../../../../RendererIpc'; 7 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 8 | 9 | export default function SettingsBrowser_Links() { 10 | const dispatch = useDispatch(); 11 | const openLinkExternal = useSettingsState('openLinkExternal'); 12 | 13 | const onEnabledChange = useCallback( 14 | (selected: boolean) => { 15 | rendererIpc.storage.update('app', {openLinkExternal: selected}); 16 | dispatch(settingsActions.setSettingsState({key: 'openLinkExternal', value: selected})); 17 | }, 18 | [dispatch], 19 | ); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Card/CheckUpdateInterval.tsx: -------------------------------------------------------------------------------- 1 | import {NumberInput} from '@heroui/react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {cardsActions, useCardsState} from '../../../../../../Redux/Reducer/CardsReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import rendererIpc from '../../../../../../RendererIpc'; 7 | 8 | export default function CheckUpdateInterval() { 9 | const updateInterval = useCardsState('checkUpdateInterval'); 10 | const dispatch = useDispatch(); 11 | 12 | const onChange = (value: number) => { 13 | if (value) { 14 | dispatch(cardsActions.setUpdateInterval(value)); 15 | rendererIpc.storage.update('cards', {checkUpdateInterval: value}); 16 | } 17 | }; 18 | 19 | return ( 20 |
21 | Minutes} 29 | /> 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/SettingsStartup-LastSize.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {settingsActions, useSettingsState} from '../../../../../../Redux/Reducer/SettingsReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import rendererIpc from '../../../../../../RendererIpc'; 7 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 8 | 9 | export default function SettingsStartupLastSize() { 10 | const dispatch = useDispatch(); 11 | const openLastSize = useSettingsState('openLastSize'); 12 | 13 | const onOpenLastSizeChange = useCallback( 14 | (selected: boolean) => { 15 | rendererIpc.storage.update('app', {openLastSize: selected}); 16 | dispatch(settingsActions.setSettingsState({key: 'openLastSize', value: selected})); 17 | }, 18 | [dispatch], 19 | ); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /extension/src/main/lynxExtension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EMenuItem, 3 | ExtensionMainApi, 4 | MainExtensionUtils, 5 | } from '../../../src/main/Managements/Plugin/Extensions/ExtensionTypes_Main'; 6 | import StorageManager from '../../../src/main/Managements/Storage/StorageManager'; 7 | import {listenForChannels} from './Methods/IpcChannels'; 8 | 9 | let storeManager: StorageManager | undefined = undefined; 10 | 11 | async function onAppReady() { 12 | console.log('Extension onAppReady storeManager: ', storeManager?.getData('cards').installedCards); 13 | } 14 | 15 | function trayMenu_AddItem(): {item: EMenuItem; index: number} { 16 | return {item: {label: 'Extension Test', type: 'checkbox'}, index: 3}; 17 | } 18 | 19 | export async function initialExtension(lynxApi: ExtensionMainApi, utils: MainExtensionUtils) { 20 | return; 21 | 22 | utils.getStorageManager().then(storageManager => { 23 | storeManager = storageManager; 24 | }); 25 | utils.getModuleManager().then(moduleManager => { 26 | console.log('Extension moduleManager: ', moduleManager.getInstalledPluginInfo()); 27 | }); 28 | 29 | lynxApi.trayMenu_AddItem(trayMenu_AddItem); 30 | lynxApi.listenForChannels(listenForChannels); 31 | 32 | lynxApi.onAppReady(onAppReady); 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Cards/Card/Menu/UninstalledMenu.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Dropdown, DropdownMenu, DropdownTrigger} from '@heroui/react'; 2 | import {memo} from 'react'; 3 | 4 | import {MenuDots_Icon} from '../../../../../assets/icons/SvgIcons/SvgIcons'; 5 | import {useCardStore} from '../Wrapper'; 6 | import {MenuDuplicate, MenuHomePage} from './Items/About'; 7 | 8 | const UninstalledMenu = memo(() => { 9 | const menuIsOpen = useCardStore(state => state.menuIsOpen); 10 | const setMenuIsOpen = useCardStore(state => state.setMenuIsOpen); 11 | 12 | return ( 13 | 20 | 21 | 24 | 25 | 26 | {MenuHomePage()} 27 | {MenuDuplicate()} 28 | 29 | 30 | ); 31 | }); 32 | 33 | export default UninstalledMenu; 34 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/General/Settings-General.tsx: -------------------------------------------------------------------------------- 1 | import {Tuning_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 2 | import SettingsSection from '../../SettingsPage-ContentSection'; 3 | import SettingsGeneralCollectErrors from './SettingsGeneral-CollectErrors'; 4 | import SettingsGeneralConfirm from './SettingsGeneral-Confirm'; 5 | import SettingsGeneralHwAcc from './SettingsGeneral-HWAcc'; 6 | import SettingsGeneralTaskbar from './SettingsGeneral-Taskbar'; 7 | import SettingsGeneralTheme from './SettingsGeneral-Theme'; 8 | import SettingsGeneralTitleName from './SettingsGeneral-TitleName'; 9 | import SettingsGeneralTooltip from './SettingsGeneral-Tooltip'; 10 | 11 | export const SettingsGeneralId = 'settings_app_elem'; 12 | 13 | export default function SettingsGeneral() { 14 | return ( 15 | }> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/Settings-Startup.tsx: -------------------------------------------------------------------------------- 1 | import {Rocket_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 2 | import SettingsSection from '../../SettingsPage-ContentSection'; 3 | import SettingsStartupDisableLoadingAnim from './SettingsStartup-DisableLoadingAnim'; 4 | import SettingsStartupLastSize from './SettingsStartup-LastSize'; 5 | import SettingsStartupStartMaximized from './SettingsStartup-StartMaximized'; 6 | import SettingsStartupStartMinimized from './SettingsStartup-StartMinimized'; 7 | import SettingsStartupStartPage from './SettingsStartup-StartPage'; 8 | import SettingsStartupSystem from './SettingsStartup-System'; 9 | 10 | export const SettingsStartupId = 'settings_startup_elem'; 11 | 12 | export default function SettingsStartup() { 13 | return ( 14 | }> 15 | {window.osPlatform === 'win32' && } 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/ToolsPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | import {memo} from 'react'; 3 | 4 | import {Rocket_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 5 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 6 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 7 | import Page from '../Page'; 8 | 9 | type Props = {show: boolean}; 10 | 11 | const ToolsPage = memo(({show}: Props) => { 12 | const {addComponent} = extensionsData.customizePages.tools; 13 | 14 | return ( 15 | 16 | 17 | }> 22 |
23 | {addComponent.map((Comp, index) => ( 24 | 25 | ))} 26 |
27 |
28 |
29 |
30 | ); 31 | }); 32 | 33 | export default ToolsPage; 34 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Tabs/Tab_Utils.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {cardsActions, useCardsState} from '../../Redux/Reducer/CardsReducer'; 5 | import {modalActions} from '../../Redux/Reducer/ModalsReducer'; 6 | import {tabsActions} from '../../Redux/Reducer/TabsReducer'; 7 | import {AppDispatch} from '../../Redux/Store'; 8 | import rendererIpc from '../../RendererIpc'; 9 | 10 | export function useRemoveTab() { 11 | const runningCards = useCardsState('runningCard'); 12 | const dispatch = useDispatch(); 13 | 14 | return useCallback( 15 | ({tabId, id}: {tabId?: string; id?: string}) => { 16 | const running = runningCards.find(card => card.tabId === tabId || card.id === id); 17 | 18 | if (running && running.type !== 'browser') rendererIpc.pty.stop(running.id); 19 | 20 | const tId = tabId || running?.tabId; 21 | 22 | if (tId) { 23 | dispatch(tabsActions.removeTab(tId)); 24 | dispatch(modalActions.removeAllModalsForTabId({tabId: tId})); 25 | dispatch(cardsActions.stopRunningCard({tabId: tId})); 26 | rendererIpc.win.setDiscordRpAiRunning({running: false}); 27 | } 28 | }, 29 | [runningCards, dispatch], 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/cross/DownloadManagerTypes.ts: -------------------------------------------------------------------------------- 1 | export type DownloadManagerProgress = { 2 | name: string; 3 | totalBytes: number; 4 | receivedBytes: number; 5 | percent: number; 6 | bytesPerSecond: number; 7 | etaSecond: number; 8 | }; 9 | 10 | export type DownloadStartInfo = {name: string; startTime: number; url: string; totalBytes: number}; 11 | export type DownloadDoneInfo = {name: string; state: 'completed' | 'cancelled' | 'interrupted'}; 12 | 13 | export type DownloadItemInfo = DownloadManagerProgress & { 14 | status: 'downloading' | 'paused' | 'completed' | 'cancelled'; 15 | } & Omit; 16 | 17 | export const browserDownloadChannels = { 18 | onDone: 'browserDL:on-done', 19 | onProgress: 'browserDL:on-progress', 20 | onDlStart: 'browserDL:on-dl-start', 21 | 22 | cancel: 'browserDL:cancel', 23 | pause: 'browserDL:pause', 24 | resume: 'browserDL:resume', 25 | clear: 'browserDL:clear', 26 | openDownloadsMenu: 'browserDL:open-downloads-menu', 27 | 28 | openItem: 'browserDL:open-item', 29 | 30 | mainDownloadCount: 'browserDL:main-download-count', 31 | }; 32 | 33 | export const customNotifChannels = { 34 | onOpen: 'customNotif-onOpen', 35 | onClose: 'customNotif-onClose', 36 | onBtnPress: 'customNotif-onBtnPress', 37 | }; 38 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Cards/CardStore.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'zustand'; 2 | 3 | import {CardState} from '../../../../../cross/CrossTypes'; 4 | import {validateGitRepoUrl} from '../../../../../cross/CrossUtils'; 5 | import {LoadedCardData} from '../../../../../cross/plugin/ModuleTypes'; 6 | 7 | // Create a reusable function to generate a store 8 | export const createCardStore = (initialData: LoadedCardData & {isInstalled: boolean; hasArguments: boolean}) => { 9 | return create(set => ({ 10 | // Static Info from props 11 | title: initialData.title, 12 | id: initialData.id, 13 | description: initialData.description, 14 | repoUrl: validateGitRepoUrl(initialData.repoUrl), 15 | extensionsDir: initialData.extensionsDir, 16 | haveArguments: initialData.hasArguments, 17 | type: initialData.type, 18 | 19 | // Component State 20 | menuIsOpen: false, 21 | checkingForUpdate: false, 22 | installed: initialData.isInstalled, 23 | 24 | // Actions that modify the state 25 | setMenuIsOpen: isOpen => set({menuIsOpen: isOpen}), 26 | setCheckingForUpdate: isChecking => set({checkingForUpdate: isChecking}), 27 | })); 28 | }; 29 | 30 | // We will use this type for our React Context 31 | export type CardStore = ReturnType; 32 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Startup/SettingsStartup-DisableLoadingAnim.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {settingsActions, useSettingsState} from '../../../../../../Redux/Reducer/SettingsReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import rendererIpc from '../../../../../../RendererIpc'; 7 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 8 | 9 | export default function SettingsStartupDisableLoadingAnim() { 10 | const dispatch = useDispatch(); 11 | const disableLoadingAnimations = useSettingsState('disableLoadingAnimations'); 12 | 13 | const onEnabledChange = useCallback( 14 | (selected: boolean) => { 15 | rendererIpc.storage.update('app', {disableLoadingAnimations: selected}); 16 | dispatch(settingsActions.setSettingsState({key: 'disableLoadingAnimations', value: selected})); 17 | }, 18 | [dispatch], 19 | ); 20 | 21 | return ( 22 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Cards/Card/RenderCardList.tsx: -------------------------------------------------------------------------------- 1 | import {motion, Variants} from 'framer-motion'; 2 | 3 | import {LoadedCardData} from '../../../../../../cross/plugin/ModuleTypes'; 4 | import {InstalledCards} from '../../../../../../cross/StorageTypes'; 5 | import Wrapper from './Wrapper'; 6 | 7 | const variants: Variants = { 8 | initial: {opacity: 0, translateY: 20}, 9 | animate: (index: number) => ({ 10 | opacity: 1, 11 | translateY: 0, 12 | transition: {delay: index * 0.05}, 13 | }), 14 | }; 15 | 16 | type Props = {sortedCards: LoadedCardData[]; installedCards: InstalledCards; hasArguments: Set}; 17 | export default function RenderCardList({sortedCards, installedCards, hasArguments}: Props) { 18 | return ( 19 | <> 20 | {sortedCards.map((card, index) => { 21 | const isInstalled = installedCards.some(c => c.id === card.id); 22 | 23 | return ( 24 | 31 | 32 | 33 | ); 34 | })} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Plugins/Page.tsx: -------------------------------------------------------------------------------- 1 | import {memo, useEffect} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {pluginsActions} from '../../../../Redux/Reducer/PluginsReducer'; 5 | import {useUserState} from '../../../../Redux/Reducer/UserReducer'; 6 | import {AppDispatch} from '../../../../Redux/Store'; 7 | import rendererIpc from '../../../../RendererIpc'; 8 | import PageView from '../../Page'; 9 | import List from './List/List'; 10 | import Preview from './Preview/Preview'; 11 | 12 | type Props = {show: boolean}; 13 | const Page = memo(({show}: Props) => { 14 | const dispatch = useDispatch(); 15 | const updateChannel = useUserState('updateChannel'); 16 | 17 | useEffect(() => { 18 | rendererIpc.plugins.getInstalledList().then(items => { 19 | dispatch(pluginsActions.setPluginsState({key: 'installedList', value: items})); 20 | }); 21 | rendererIpc.plugins.checkForSync(updateChannel); 22 | rendererIpc.plugins.getUnloadedList().then(items => { 23 | dispatch(pluginsActions.setPluginsState({key: 'unloadedList', value: items})); 24 | }); 25 | }, [updateChannel]); 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | ); 33 | }); 34 | 35 | export default Page; 36 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Dashboard/Content/Profile/Profile_Google.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, Checkbox, User} from '@heroui/react'; 2 | 3 | import {Google_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 4 | import {useAppState} from '../../../../../../Redux/Reducer/AppReducer'; 5 | 6 | export default function Profile_Google() { 7 | const darkMode = useAppState('darkMode'); 8 | 9 | return ( 10 | 11 |
12 | 13 | Google 14 |
15 |
16 | 17 | 20 |
21 |
22 | 23 | Sync Data with Cloud 24 | 25 |
26 |
30 | Coming Soon... 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/Managements/HotkeysManager.ts: -------------------------------------------------------------------------------- 1 | import {Event, Input, WebContents} from 'electron'; 2 | import {isEqual} from 'lodash'; 3 | 4 | import {appWindowChannels, LynxInput} from '../../cross/IpcChannelAndTypes'; 5 | import {appManager} from '../index'; 6 | 7 | const initialKeys: LynxInput = { 8 | control: false, 9 | alt: false, 10 | shift: false, 11 | key: '', 12 | meta: false, 13 | type: '', 14 | }; 15 | 16 | let prevKeys: LynxInput = initialKeys; 17 | let currentKeys: LynxInput = initialKeys; 18 | 19 | const registeredHotkeys: number[] = []; 20 | 21 | function sendToRenderer() { 22 | if (isEqual(prevKeys, currentKeys)) return; 23 | prevKeys = currentKeys; 24 | appManager?.getWebContent()?.send(appWindowChannels.hotkeysChange, currentKeys); 25 | } 26 | 27 | function onBlur() { 28 | currentKeys = initialKeys; 29 | sendToRenderer(); 30 | } 31 | 32 | function onInput(_event: Event, input: Input) { 33 | const {control, key, shift, alt, meta, type} = input; 34 | currentKeys = {control, key: key.toLowerCase(), shift, alt, meta, type}; 35 | 36 | sendToRenderer(); 37 | } 38 | 39 | export default function RegisterHotkeys(contents: WebContents) { 40 | if (registeredHotkeys.includes(contents.id)) return; 41 | registeredHotkeys.push(contents.id); 42 | 43 | contents.on('blur', onBlur); 44 | contents.on('before-input-event', onInput); 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/Browser/Browser_TopBar.tsx: -------------------------------------------------------------------------------- 1 | import {memo, useMemo} from 'react'; 2 | 3 | import {useCardsState} from '../../../Redux/Reducer/CardsReducer'; 4 | import {RunningCard} from '../../../Utils/Types'; 5 | import Browser_ActionButtons from './Browser_ActionButtons'; 6 | import Browser_AddressBar from './Browser_AddressBar'; 7 | import Browser_Search from './Browser_Search'; 8 | import Browser_Zoom from './Browser_Zoom'; 9 | 10 | type Props = { 11 | runningCard: RunningCard; 12 | setCustomAddress?: (address: string) => void; 13 | tabID: string; 14 | }; 15 | 16 | const Browser_TopBar = memo(({runningCard, setCustomAddress, tabID}: Props) => { 17 | const domReadyIds = useCardsState('browserDomReadyIds'); 18 | 19 | const isDomReady = useMemo(() => domReadyIds.includes(runningCard.id), [domReadyIds, runningCard]); 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | {isDomReady && ( 27 | <> 28 | 29 | 30 | 31 | )} 32 | 33 | ); 34 | }); 35 | 36 | export default Browser_TopBar; 37 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Reusable/CopyClipboard.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Tooltip} from '@heroui/react'; 2 | import {memo, useCallback, useState} from 'react'; 3 | 4 | import {CheckDuo_Icon, CopyDuo_Icon} from '../../../assets/icons/SvgIcons/SvgIcons'; 5 | 6 | type Props = {tooltipTitle?: string; showTooltip?: boolean; contentToCopy: string; className?: string}; 7 | 8 | const CopyClipboard = memo(({showTooltip = true, tooltipTitle, contentToCopy, className}: Props) => { 9 | const [copied, setCopied] = useState(false); 10 | 11 | const handleCopy = useCallback(() => { 12 | navigator.clipboard.writeText(contentToCopy); 13 | setCopied(true); 14 | setTimeout(() => { 15 | setCopied(false); 16 | }, 1000); 17 | }, [contentToCopy]); 18 | 19 | return ( 20 | 26 | 33 | 34 | ); 35 | }); 36 | 37 | export default CopyClipboard; 38 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/Browser/Browser_Search.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {memo, useRef} from 'react'; 3 | 4 | import {Hotkey_Names} from '../../../../../../cross/HotkeyConstants'; 5 | import {Circle_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {useTabsState} from '../../../Redux/Reducer/TabsReducer'; 7 | import rendererIpc from '../../../RendererIpc'; 8 | import useHotkeyPress from '../../../Utils/RegisterHotkeys'; 9 | 10 | type Props = {id: string; tabID: string}; 11 | 12 | const Browser_Search = memo(({id, tabID}: Props) => { 13 | const activeTab = useTabsState('activeTab'); 14 | const btnRef = useRef(null); 15 | const openSearchMenu = () => { 16 | const bounds = btnRef.current?.getBoundingClientRect(); 17 | if (bounds) { 18 | const {x, y} = bounds; 19 | rendererIpc.browser.openFindInPage(id, {x: x - 125, y: y + 17}); 20 | } else { 21 | rendererIpc.browser.openFindInPage(id); 22 | } 23 | }; 24 | 25 | useHotkeyPress([{name: Hotkey_Names.findInPage, method: tabID === activeTab ? openSearchMenu : null}]); 26 | 27 | return ( 28 | 31 | ); 32 | }); 33 | 34 | export default Browser_Search; 35 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import {isEmpty} from 'lodash'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {extensionsData} from '../../Extensions/ExtensionLoader'; 5 | import {useAppState} from '../../Redux/Reducer/AppReducer'; 6 | import {ContentsNav, SettingsNav} from './NavButtons'; 7 | 8 | const CONTAINER_WIDTH = 'w-[5.5rem]'; 9 | 10 | /** Navigation bar containing two sections: Contents and Settings */ 11 | const NavBar = memo(() => { 12 | const navBar = useAppState('navBar'); 13 | const { 14 | container: Container, 15 | contentBar: ContentBar, 16 | settingsBar: SettingsBar, 17 | } = useMemo(() => extensionsData.navBar.replace, []); 18 | 19 | if (!navBar) return null; 20 | 21 | if (Container) { 22 | return ; 23 | } 24 | 25 | return ( 26 |
27 | {isEmpty(ContentBar) ? ( 28 |
29 | 30 |
31 | ) : ( 32 | 33 | )} 34 | {isEmpty(SettingsBar) ? ( 35 |
36 | 37 |
38 | ) : ( 39 | 40 | )} 41 |
42 | ); 43 | }); 44 | 45 | export default NavBar; 46 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/General/SettingsGeneral-HWAcc.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {settingsActions, useSettingsState} from '../../../../../../Redux/Reducer/SettingsReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import rendererIpc from '../../../../../../RendererIpc'; 7 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 8 | 9 | export default function SettingsGeneralHwAcc() { 10 | const dispatch = useDispatch(); 11 | const hardwareAcceleration = useSettingsState('hardwareAcceleration'); 12 | 13 | const onEnabledChange = useCallback( 14 | (selected: boolean) => { 15 | rendererIpc.storage.update('app', {hardwareAcceleration: selected}); 16 | dispatch(settingsActions.setSettingsState({key: 'hardwareAcceleration', value: selected})); 17 | }, 18 | [dispatch], 19 | ); 20 | 21 | return ( 22 | 25 | Enables hardware acceleration to potentially improve performance. 26 |
27 | If you experience lagging or freezing, try disabling this option. 28 | 29 | } 30 | enabled={hardwareAcceleration} 31 | onEnabledChange={onEnabledChange} 32 | title="Use Hardware Acceleration" 33 | /> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Initializer/steps/StepWelcome.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {motion, Variants} from 'framer-motion'; 3 | 4 | import {APP_NAME} from '../../../../../../cross/CrossConstants'; 5 | 6 | const containerVariants: Variants = { 7 | hidden: {opacity: 0, y: 20}, 8 | enter: {opacity: 1, y: 0, transition: {duration: 0.5, ease: 'easeOut'}}, 9 | exit: {opacity: 0, y: -20, transition: {duration: 0.3, ease: 'easeIn'}}, 10 | }; 11 | 12 | type Props = {onNext: () => void}; 13 | 14 | export default function StepWelcome({onNext}: Props) { 15 | return ( 16 | 23 | 24 | Welcome to {APP_NAME} 25 | 26 | 27 | Thanks for installing {APP_NAME}! I'll walk you through a quick setup to check for necessary tools and 28 | extensions. 29 | 30 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-OutputColor.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 5 | import {AppDispatch} from '../../../../../../Redux/Store'; 6 | import LynxSwitch from '../../../../../Reusable/LynxSwitch'; 7 | 8 | export default function SettingsTerminalOutputColor() { 9 | const outputColor = useTerminalState('outputColor'); 10 | const dispatch = useDispatch(); 11 | 12 | const onEnabledChange = useCallback( 13 | (value: boolean) => { 14 | dispatch(terminalActions.setTerminalState({key: 'outputColor', value})); 15 | }, 16 | [dispatch], 17 | ); 18 | 19 | return ( 20 | 23 | Whether colorize output of terminal text based on: Errors 24 | Warnings Success 25 | Info Debug . 26 | 27 | } 28 | size="default" 29 | enabled={outputColor} 30 | title="Colorize Output Text" 31 | onEnabledChange={onEnabledChange} 32 | /> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /module/src/WebUI/ComfyUI (comfyanonymous)/MainMethods.ts: -------------------------------------------------------------------------------- 1 | import {CardMainMethodsInitial, ChosenArgument} from '../../../../src/cross/plugin/ModuleTypes'; 2 | import {COMFYUI_ID} from '../../Constants'; 3 | import {isWin} from '../../utils/CrossUtils'; 4 | import {utilReadArgs, utilRunCommands, utilSaveArgs} from '../../utils/MainUtils'; 5 | import {parseArgsToString, parseStringToArgs} from './RendererMethods'; 6 | 7 | const BAT_FILE_NAME = isWin ? 'lynx-user.bat' : 'lynx-user.sh'; 8 | const DEFAULT_BATCH_DATA: string = isWin ? '@echo off\n\npython main.py' : '#!/bin/bash\n\npython main.py'; 9 | 10 | export async function getRunCommands(dir?: string): Promise { 11 | return await utilRunCommands(BAT_FILE_NAME, dir, DEFAULT_BATCH_DATA); 12 | } 13 | 14 | async function saveArgs(args: ChosenArgument[], dir?: string) { 15 | return await utilSaveArgs(args, BAT_FILE_NAME, parseArgsToString, dir); 16 | } 17 | 18 | export async function readArgs(dir?: string) { 19 | return await utilReadArgs(BAT_FILE_NAME, DEFAULT_BATCH_DATA, parseStringToArgs, dir); 20 | } 21 | 22 | const Comfy_MM: CardMainMethodsInitial = utils => { 23 | const installDir = utils.getInstallDir(COMFYUI_ID); 24 | 25 | return { 26 | getRunCommands: () => getRunCommands(installDir), 27 | readArgs: () => readArgs(installDir), 28 | saveArgs: args => saveArgs(args, installDir), 29 | }; 30 | }; 31 | 32 | export default Comfy_MM; 33 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/TitleBar/WindowButtons_Close.tsx: -------------------------------------------------------------------------------- 1 | import {motion} from 'framer-motion'; 2 | import {useCallback} from 'react'; 3 | 4 | import {Power_Icon} from '../../../assets/icons/SvgIcons/SvgIcons'; 5 | import {useHotkeysState} from '../../Redux/Reducer/HotkeysReducer'; 6 | import {useSettingsState} from '../../Redux/Reducer/SettingsReducer'; 7 | import {useTabsState} from '../../Redux/Reducer/TabsReducer'; 8 | import rendererIpc from '../../RendererIpc'; 9 | 10 | type Props = { 11 | buttonProps: any; 12 | commonStyles: string; 13 | }; 14 | 15 | export default function WindowButtons_Close({buttonProps, commonStyles}: Props) { 16 | const isCtrlPressed = useHotkeysState('isCtrlPressed'); 17 | const activePage = useTabsState('activePage'); 18 | const showCloseConfirm = useSettingsState('closeConfirm'); 19 | const close = useCallback(() => rendererIpc.win.changeWinState('close'), []); 20 | 21 | const onClick = () => { 22 | rendererIpc.storage.update('app', {lastPage: activePage}); 23 | if (isCtrlPressed || !showCloseConfirm) { 24 | close(); 25 | } else { 26 | rendererIpc.contextMenu.openCloseApp(); 27 | } 28 | }; 29 | 30 | return ( 31 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/TopBar/TopBar.tsx: -------------------------------------------------------------------------------- 1 | import {SerializeAddon} from '@xterm/addon-serialize'; 2 | import {memo, RefObject} from 'react'; 3 | 4 | import {RunningCard} from '../../../Utils/Types'; 5 | import Browser_TopBar from '../Browser/Browser_TopBar'; 6 | import Terminal_TopBar from '../Terminal/Terminal_TopBar'; 7 | import SharedTopBar from './SharedTopBar'; 8 | 9 | type Props = { 10 | runningCard: RunningCard; 11 | serializeAddon?: SerializeAddon; 12 | tabID: string; 13 | clearTerminal: RefObject<(() => void) | undefined>; 14 | selectedTerminalText: string; 15 | }; 16 | 17 | const TopBar = memo(({runningCard, serializeAddon, tabID, clearTerminal, selectedTerminalText}: Props) => { 18 | return ( 19 |
24 | {runningCard.currentView === 'terminal' ? ( 25 | 31 | ) : ( 32 | 33 | )} 34 | 35 | 36 |
37 | ); 38 | }); 39 | 40 | export default TopBar; 41 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Reusable/SmallButton.tsx: -------------------------------------------------------------------------------- 1 | import {motion, Variants} from 'framer-motion'; 2 | import {ReactNode, useMemo} from 'react'; 3 | 4 | import {useAppState} from '../../Redux/Reducer/AppReducer'; 5 | import {getColor} from '../../Utils/Constants'; 6 | 7 | type Props = {icon: ReactNode; onClick?: () => void}; 8 | 9 | export default function SmallButton({icon, onClick}: Props) { 10 | const darkMode = useAppState('darkMode'); 11 | 12 | const variants: Variants = useMemo(() => { 13 | return { 14 | init: {scale: 0.9, opacity: 0}, 15 | animate: { 16 | scale: 1, 17 | opacity: 1, 18 | backgroundColor: getColor('black', 0), 19 | transition: {duration: 0.5}, 20 | }, 21 | whileHover: { 22 | backgroundColor: darkMode ? getColor('white', 0.1) : getColor('black', 0.2), 23 | transition: {duration: 0.5}, 24 | }, 25 | whileTap: { 26 | backgroundColor: darkMode ? getColor('white', 0.3) : getColor('black', 0.4), 27 | borderRadius: '12px', 28 | scale: 0.7, 29 | transition: {duration: 0.1}, 30 | }, 31 | }; 32 | }, [darkMode]); 33 | 34 | return ( 35 | 43 | {icon} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/InstallUI/Utils/LocateWarning.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from '@heroui/react'; 2 | import {Dispatch, SetStateAction} from 'react'; 3 | 4 | import {ShieldWarning_Icon} from '../../../../../assets/icons/SvgIcons/SvgIcons'; 5 | 6 | type Props = {isOpen: boolean; setIsOpen: Dispatch>}; 7 | export default function LocateWarning({isOpen, setIsOpen}: Props) { 8 | const onClose = () => setIsOpen(false); 9 | return ( 10 | 16 | 17 | 18 | 19 | Installation Directory 20 | 21 | 22 | 23 | Saving data to an application's installation directory can result in{' '} 24 | data loss. Please move your data to a different folder. 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /extension/src/renderer/index.css: -------------------------------------------------------------------------------- 1 | @layer theme, base, antd, components, utilities; 2 | 3 | @import 'tailwindcss'; 4 | 5 | @plugin '@tailwindcss/typography'; 6 | @plugin '../../HeroUiConfig.ts'; 7 | 8 | @source '../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; 9 | @custom-variant dark (&:is(.dark *)); 10 | 11 | @theme { 12 | --font-JetBrainsMono: JetBrainsMono, sans-serif; 13 | --font-Nunito: Nunito, sans-serif; 14 | 15 | --color-LynxOrange: #ff5f00; 16 | --color-LynxRaisinBlack: #212121; 17 | 18 | --color-LynxWhiteSecond: #f6f6f6; 19 | --color-LynxWhiteThird: #ececec; 20 | --color-LynxWhiteFourth: #e1e1e1; 21 | --color-LynxWhiteFifth: #d0d0d0; 22 | 23 | --color-DarkGray: #424242; 24 | 25 | --animate-shine: shine 5s linear infinite; 26 | 27 | @keyframes shine { 28 | 0% { 29 | background-position: 100%; 30 | } 31 | 100% { 32 | background-position: -100%; 33 | } 34 | } 35 | 36 | --backgroundImage-GradientDark: linear-gradient( 37 | to right top, 38 | #212121, 39 | #202020, 40 | #1f1f1f, 41 | #1e1e1e, 42 | #1d1d1d, 43 | #1c1c1c, 44 | #1b1b1b, 45 | #1a1a1a, 46 | #181818, 47 | #161616, 48 | #141414, 49 | #121212 50 | ); 51 | 52 | --backgroundImage-GradientLight: linear-gradient( 53 | to right top, 54 | #f3f3f3, 55 | #f3f3f3, 56 | #f3f3f3, 57 | #f3f3f3, 58 | #f3f3f3, 59 | #f4f4f5, 60 | #f4f4f6, 61 | #f4f5f8, 62 | #f3f7fc, 63 | #f0fafe, 64 | #eefdfe, 65 | #eefffc 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Cards/Card/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, memo, useContext, useMemo} from 'react'; 2 | 3 | import {CardState} from '../../../../../../cross/CrossTypes'; 4 | import {LoadedCardData} from '../../../../../../cross/plugin/ModuleTypes'; 5 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 6 | import {CardStore, createCardStore} from '../CardStore'; 7 | import LynxCard from './Card'; 8 | 9 | const CardStoreContext = createContext(null); 10 | 11 | type Props = {cardData: LoadedCardData; isInstalled: boolean; hasArguments: boolean}; 12 | 13 | const Wrapper = memo(({cardData, isInstalled, hasArguments}: Props) => { 14 | const ReplaceComponent = useMemo(() => extensionsData.cards.replaceComponent, []); 15 | 16 | const storeValue = useMemo( 17 | () => createCardStore({...cardData, isInstalled, hasArguments}), 18 | [cardData, isInstalled, hasArguments], 19 | ); 20 | 21 | return ( 22 | 23 | {ReplaceComponent ? ( 24 | 25 | ) : ( 26 | 27 | )} 28 | 29 | ); 30 | }); 31 | 32 | export default Wrapper; 33 | 34 | export const useCardStore = (selector: (state: CardState) => T): T => { 35 | const store = useContext(CardStoreContext); 36 | if (!store) throw new Error('Missing CardStoreContext.Provider'); 37 | return store(selector); 38 | }; 39 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/LaunchConfig-Section.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, CardBody, CardHeader} from '@heroui/react'; 2 | import {isEmpty, isString} from 'lodash'; 3 | import {ReactNode} from 'react'; 4 | 5 | import {Add_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import LynxTooltip from '../../Reusable/LynxTooltip'; 7 | 8 | type Props = { 9 | children: ReactNode; 10 | title: ReactNode; 11 | description?: string; 12 | onAddPress?: () => void; 13 | addTooltipTitle?: string; 14 | customButton?: ReactNode; 15 | }; 16 | 17 | /** Display card containing configurations */ 18 | export default function LaunchConfigSection({ 19 | children, 20 | title, 21 | description, 22 | onAddPress, 23 | addTooltipTitle, 24 | customButton, 25 | }: Props) { 26 | return ( 27 | 28 | 29 |
30 | {isString(title) ? {title} : title} 31 | {customButton || ( 32 | 33 | 36 | 37 | )} 38 |
39 | {!isEmpty(description) && {description}} 40 |
41 | {children} 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Dashboard/Content/Dashboard-ReportIssue.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | 3 | import {ISSUE_PAGE} from '../../../../../../../../cross/CrossConstants'; 4 | import {ExternalDuo_Icon} from '../../../../../../../context_menu/Components/SvgIcons'; 5 | import {GitHub_Icon, SmileCircleDuo_Icon} from '../../../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import SettingsSection from '../../Settings/SettingsPage-ContentSection'; 7 | 8 | export const DashboardReportIssueId = 'settings_report_issue_elem'; 9 | 10 | /** Reporting app issues on GitHub */ 11 | export default function DashboardReportIssue() { 12 | return ( 13 | } 17 | itemsCenter> 18 |
19 | {"Found a bug or have feedback? I'd love to hear from you!"} 20 | Share your experience on GitHub to help me improve the app. 21 |
22 | 23 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/Managements/Ipc/Methods/IpcMethods-Downloader.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import {app, DownloadItem} from 'electron'; 4 | import {download} from 'electron-dl'; 5 | 6 | import {utilsChannels} from '../../../../cross/IpcChannelAndTypes'; 7 | import {appManager} from '../../../index'; 8 | 9 | let downloadingItem: DownloadItem | undefined; 10 | 11 | export function downloadFile(url: string) { 12 | const window = appManager?.getMainWindow(); 13 | 14 | if (!window) { 15 | console.error('Failed to download file: ', 'window object is undefined!'); 16 | return; 17 | } 18 | 19 | download(window, url, { 20 | showBadge: false, 21 | directory: path.join(app.getPath('downloads'), 'LynxHub'), 22 | onStarted: item => { 23 | downloadingItem = item; 24 | }, 25 | onProgress: progress => { 26 | window.webContents.send(utilsChannels.onDownloadFile, { 27 | stage: 'progress', 28 | percentage: progress.percent, 29 | downloaded: progress.transferredBytes, 30 | total: progress.totalBytes, 31 | fileName: downloadingItem?.getFilename(), 32 | }); 33 | }, 34 | }) 35 | .then(item => { 36 | window.webContents.send(utilsChannels.onDownloadFile, { 37 | stage: 'done', 38 | finalPath: item.savePath, 39 | }); 40 | }) 41 | .catch(e => { 42 | window.webContents.send(utilsChannels.onDownloadFile, { 43 | stage: 'failed', 44 | }); 45 | console.error('Failed to download file: ', e); 46 | }); 47 | } 48 | 49 | export function cancelDownload() { 50 | downloadingItem?.cancel(); 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/cross_styles.css: -------------------------------------------------------------------------------- 1 | @import './assets/fonts.css'; 2 | @import 'tailwindcss'; 3 | 4 | @plugin '@tailwindcss/typography'; 5 | @plugin './HeroUiConfig.ts'; 6 | 7 | @source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; 8 | @custom-variant dark (&:is(.dark *)); 9 | 10 | @theme { 11 | --font-JetBrainsMono: JetBrainsMono, sans-serif; 12 | --font-Nunito: Nunito, sans-serif; 13 | 14 | --color-LynxOrange: #ff5f00; 15 | --color-LynxRaisinBlack: #212121; 16 | 17 | --color-LynxWhiteSecond: #f6f6f6; 18 | --color-LynxWhiteThird: #ececec; 19 | --color-LynxWhiteFourth: #e1e1e1; 20 | --color-LynxWhiteFifth: #d0d0d0; 21 | 22 | --color-DarkGray: #424242; 23 | 24 | --animate-shine: shine 5s linear infinite; 25 | 26 | @keyframes shine { 27 | 0% { 28 | background-position: 100%; 29 | } 30 | 100% { 31 | background-position: -100%; 32 | } 33 | } 34 | 35 | @keyframes ripple { 36 | 0%, 37 | 100% { 38 | transform: translate(-50%, -50%) scale(1); 39 | } 40 | 50% { 41 | transform: translate(-50%, -50%) scale(0.9); 42 | } 43 | } 44 | } 45 | 46 | .animate-ripple { 47 | animation: ripple var(--duration, 2s) ease calc(var(--i, 0) * 0.2s) infinite; 48 | } 49 | 50 | :root { 51 | color-scheme: light dark; 52 | font-family: Nunito, sans-serif; 53 | 54 | --duration: 2s; 55 | --i: 0; 56 | } 57 | 58 | * { 59 | -webkit-user-select: none; 60 | -webkit-user-drag: none; 61 | } 62 | 63 | .draggable { 64 | -webkit-user-select: none; 65 | -webkit-app-region: drag; 66 | } 67 | 68 | .notDraggable { 69 | -webkit-app-region: no-drag; 70 | } 71 | -------------------------------------------------------------------------------- /extension/lynxExtension.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./lynxExtension.schema.json", 3 | "id": "git_flow_helper", 4 | "title": "Git Flow Helper", 5 | "description": "Simplifies Git workflow with visual branch management and common Git operations automation.", 6 | "publishDate": "2024-11-27", 7 | "repoUrl": "https://github.com/KindaBrazy/LynxHub", 8 | "version": "1.2.0", 9 | "updateDate": "2024-11-27 14:05", 10 | "requireAppBuild": 15, 11 | "tag": "feature", 12 | "platforms": [ 13 | "linux", 14 | "win32", 15 | "darwin" 16 | ], 17 | "changeLog": [ 18 | { 19 | "title": "Version 1.2.0 Release Notes", 20 | "items": [ 21 | { 22 | "label": "New Features", 23 | "subitems": [ 24 | { 25 | "label": "Interactive rebase helper" 26 | }, 27 | { 28 | "label": "Branch visualization improvements" 29 | }, 30 | { 31 | "label": "Commit message templates" 32 | } 33 | ] 34 | }, 35 | { 36 | "label": "Performance Improvements", 37 | "subitems": [ 38 | { 39 | "label": "Faster branch switching" 40 | }, 41 | { 42 | "label": "Optimized large repository handling" 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "title": "Version 1.0.0", 50 | "items": [ 51 | { 52 | "label": "Releasing V1" 53 | } 54 | ] 55 | } 56 | ], 57 | "avatarUrl": "https://avatars.githubusercontent.com/u/143181745?v=4" 58 | } -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Dashboard/DashboardContainer.tsx: -------------------------------------------------------------------------------- 1 | import {isEmpty} from 'lodash'; 2 | import {useEffect, useMemo, useState} from 'react'; 3 | 4 | import {extensionsData} from '../../../../Extensions/ExtensionLoader'; 5 | import rendererIpc from '../../../../RendererIpc'; 6 | import DashboardAbout, {DashboardAboutId} from './Content/Dashboard-About'; 7 | import DashboardCredits, {DashboardCreditsId} from './Content/Dashboard-Credits'; 8 | import DashboardReportIssue, {DashboardReportIssueId} from './Content/Dashboard-ReportIssue'; 9 | import DashboardUpdate, {DashboardUpdateId} from './Content/Dashboard-Update'; 10 | import DashboardProfile, {DashboardProfileId} from './Content/Profile/Dashboard-Profile'; 11 | 12 | export const dashboardSectionId = { 13 | DashboardProfileId, 14 | DashboardUpdateId, 15 | DashboardCreditsId, 16 | DashboardReportIssueId, 17 | DashboardAboutId, 18 | }; 19 | 20 | export const DashboardSections = () => { 21 | const content = useMemo(() => extensionsData.customizePages.dashboard.add.content, []); 22 | 23 | const [creditsAvailable, setCreditsAvailable] = useState(false); 24 | useEffect(() => { 25 | rendererIpc.statics.getPatrons().then(cr => { 26 | setCreditsAvailable(!isEmpty(cr)); 27 | }); 28 | }, []); 29 | 30 | return ( 31 | <> 32 | 33 | 34 | {creditsAvailable && } 35 | 36 | 37 | 38 | {content.map((Content, index) => ( 39 | 40 | ))} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-Conpty.tsx: -------------------------------------------------------------------------------- 1 | import {Select, Selection, SelectItem} from '@heroui/react'; 2 | import {useCallback} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | 5 | import {TerminalUseConpty} from '../../../../../../../../../cross/IpcChannelAndTypes'; 6 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 7 | import {AppDispatch} from '../../../../../../Redux/Store'; 8 | 9 | export default function SettingsTerminalConpty() { 10 | const useConpty = useTerminalState('useConpty'); 11 | const dispatch = useDispatch(); 12 | 13 | const onChange = useCallback( 14 | (keys: Selection) => { 15 | if (keys !== 'all') { 16 | const value = keys.values().next().value as TerminalUseConpty; 17 | dispatch(terminalActions.setTerminalState({key: 'useConpty', value})); 18 | } 19 | }, 20 | [dispatch], 21 | ); 22 | 23 | return ( 24 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/LaunchConfig/Arguments/ManageArguments/Items/InputArg-Item.tsx: -------------------------------------------------------------------------------- 1 | import {Input} from '@heroui/react'; 2 | import {useCallback, useState} from 'react'; 3 | 4 | import {ChosenArgument} from '../../../../../../../../../cross/CrossTypes'; 5 | import {getArgumentDefaultValue} from '../../../../../../../../../cross/GetArgumentsData'; 6 | import {TextDuo_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 7 | import {useGetArgumentsByID} from '../../../../../../Modules/ModuleLoader'; 8 | import ArgumentItemBase from './Argument-Item-Base'; 9 | 10 | type Props = {argument: ChosenArgument; removeArg: () => void; changeValue: (value: any) => void; id: string}; 11 | 12 | export default function InputArgItem({argument, changeValue, removeArg, id}: Props) { 13 | const cardArgument = useGetArgumentsByID(id); 14 | 15 | const [inputValue, setInputValue] = useState( 16 | argument.value || getArgumentDefaultValue(argument.name, cardArgument) || '', 17 | ); 18 | 19 | const onBlur = useCallback(() => { 20 | changeValue(inputValue); 21 | }, [inputValue]); 22 | 23 | const onChange = useCallback((value: string) => { 24 | setInputValue(value); 25 | }, []); 26 | 27 | return ( 28 | }> 29 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/AgentsPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {PageID} from '../../../../../../cross/CrossConstants'; 5 | import {Robot_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 7 | import {GetComponentsByPath} from '../../Cards/Cards'; 8 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 9 | import Page from '../Page'; 10 | 11 | type Props = {show: boolean}; 12 | 13 | const AgentsPage = memo(({show}: Props) => { 14 | const {top, scrollTop, scrollBottom, bottom, cardsContainer} = useMemo( 15 | () => extensionsData.customizePages.agents.add, 16 | [], 17 | ); 18 | 19 | return ( 20 | 21 | {top && top.map((Top, index) => )} 22 | 23 | 24 | {scrollTop && scrollTop.map((ScrollTop, index) => )} 25 | 26 | }> 31 | 32 | 33 | 34 | {scrollBottom && scrollBottom.map((ScrollBottom, index) => )} 35 | 36 | 37 | {bottom && bottom.map((Bottom, index) => )} 38 | 39 | ); 40 | }); 41 | 42 | export default AgentsPage; 43 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-CursorStyle.tsx: -------------------------------------------------------------------------------- 1 | import {Select, Selection, SelectItem} from '@heroui/react'; 2 | import {useCallback} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | 5 | import {TerminalCursorStyle} from '../../../../../../../../../cross/IpcChannelAndTypes'; 6 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 7 | import {AppDispatch} from '../../../../../../Redux/Store'; 8 | 9 | export default function SettingsTerminalCursorStyle() { 10 | const cursorStyle = useTerminalState('cursorStyle'); 11 | const dispatch = useDispatch(); 12 | 13 | const onChange = useCallback( 14 | (keys: Selection) => { 15 | if (keys !== 'all') { 16 | const value = keys.values().next().value as TerminalCursorStyle; 17 | dispatch(terminalActions.setTerminalState({key: 'cursorStyle', value})); 18 | } 19 | }, 20 | [dispatch], 21 | ); 22 | 23 | return ( 24 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/Modals.tsx: -------------------------------------------------------------------------------- 1 | import {memo, useMemo} from 'react'; 2 | 3 | import {extensionsData} from '../../Extensions/ExtensionLoader'; 4 | import CardExtensionsModal from './CardExtensions/CardExtensions'; 5 | import GitManagerModal from './CardGitManager/CardGitManager_Modal'; 6 | import CardInfoModal from './CardInfo/CardInfo-Modal'; 7 | import ReadMeModal from './CardReadme_Modal'; 8 | import CustomNotification from './CustomNotification/CustomNotification'; 9 | import InstallCardModal from './InstallUI/Install-Modal'; 10 | import LaunchConfigModal from './LaunchConfig/LaunchConfig'; 11 | import UnassignCardComp from './UnassignCard/UnassignCard'; 12 | import UninstallCardComp from './UninstallCard/UninstallCard'; 13 | import UpdateApp from './UpdateApp/UpdateApp'; 14 | import UpdatingNotification from './UpdatingCard/UpdatingNotification'; 15 | import WarningModal from './Warning/WarningModal'; 16 | 17 | const Modals = memo(() => { 18 | const {warning: Warning, updateApp: UApp} = useMemo(() => extensionsData.replaceModals, []); 19 | 20 | const addModal = useMemo(() => extensionsData.addModal, []); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {Warning ? : } 37 | {UApp ? : } 38 | 39 | {addModal.map((Modal, index) => ( 40 | 41 | ))} 42 | 43 | ); 44 | }); 45 | 46 | export default Modals; 47 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/OthersPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {PageID} from '../../../../../../cross/CrossConstants'; 5 | import {MagicStickDuo_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 7 | import {GetComponentsByPath} from '../../Cards/Cards'; 8 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 9 | import Page from '../Page'; 10 | 11 | type Props = {show: boolean}; 12 | 13 | const OthersPage = memo(({show}: Props) => { 14 | const {top, scrollTop, scrollBottom, bottom, cardsContainer} = useMemo( 15 | () => extensionsData.customizePages.others.add, 16 | [], 17 | ); 18 | 19 | return ( 20 | 21 | {top && top.map((Top, index) => )} 22 | 23 | 24 | {scrollTop && scrollTop.map((ScrollTop, index) => )} 25 | 26 | }> 31 | 32 | 33 | 34 | {scrollBottom && scrollBottom.map((ScrollBottom, index) => )} 35 | 36 | 37 | {bottom && bottom.map((Bottom, index) => )} 38 | 39 | ); 40 | }); 41 | 42 | export default OthersPage; 43 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/Settings-Terminal.tsx: -------------------------------------------------------------------------------- 1 | import {Spacer} from '@heroui/react'; 2 | 3 | import {Terminal_Icon} from '../../../../../../../assets/icons/SvgIcons/SvgIcons'; 4 | import SettingsSection from '../../SettingsPage-ContentSection'; 5 | import SettingsTerminalBlinkCursor from './SettingsTerminal-BlinkCursor'; 6 | import SettingsTerminalCloseOnExit from './SettingsTerminal-CloseOnExit'; 7 | import SettingsTerminalConpty from './SettingsTerminal-Conpty'; 8 | import SettingsTerminalCursorInactiveStyle from './SettingsTerminal-CursorInactiveStyle'; 9 | import SettingsTerminalCursorStyle from './SettingsTerminal-CursorStyle'; 10 | import SettingsTerminalFontSize from './SettingsTerminal-FontSize'; 11 | import SettingsTerminalOutputColor from './SettingsTerminal-OutputColor'; 12 | import SettingsTerminalReset from './SettingsTerminal-Reset'; 13 | import SettingsTerminalResizeDelay from './SettingsTerminal-ResizeDelay'; 14 | import SettingsTerminalScrollBack from './SettingsTerminal-ScrollBack'; 15 | 16 | export const SettingsTerminalId = 'settings_terminal_elem'; 17 | 18 | export default function SettingsTerminal() { 19 | return ( 20 | }> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/TextGenerationPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {PageID} from '../../../../../../cross/CrossConstants'; 5 | import {TextGeneration_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 7 | import {GetComponentsByPath} from '../../Cards/Cards'; 8 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 9 | import Page from '../Page'; 10 | 11 | type Props = {show: boolean}; 12 | 13 | const TextGenerationPage = memo(({show}: Props) => { 14 | const {top, scrollTop, scrollBottom, bottom, cardsContainer} = useMemo( 15 | () => extensionsData.customizePages.text.add, 16 | [], 17 | ); 18 | 19 | return ( 20 | 21 | {top && top.map((Top, index) => )} 22 | 23 | 24 | {scrollTop && scrollTop.map((ScrollTop, index) => )} 25 | 26 | }> 31 | 32 | 33 | 34 | {scrollBottom && scrollBottom.map((ScrollBottom, index) => )} 35 | 36 | 37 | {bottom && bottom.map((Bottom, index) => )} 38 | 39 | ); 40 | }); 41 | 42 | export default TextGenerationPage; 43 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Plugins/Preview/Preview.tsx: -------------------------------------------------------------------------------- 1 | import {isEmpty} from 'lodash'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {Plugins_Icon} from '../../../../../../assets/icons/SvgIcons/SvgIcons'; 5 | import {usePluginsState} from '../../../../../Redux/Reducer/PluginsReducer'; 6 | import {ContainersBg} from '../../../../../Utils/CrossStyle'; 7 | import PreviewBody from './Body'; 8 | import PreviewHeader from './Header'; 9 | 10 | const Preview = memo(() => { 11 | const selectedPlugin = usePluginsState('selectedPlugin'); 12 | const installed = usePluginsState('installedList'); 13 | const installedExt = useMemo( 14 | () => installed.find(item => item.id === selectedPlugin?.metadata.id), 15 | [installed, selectedPlugin], 16 | ); 17 | return ( 18 |
24 | {isEmpty(selectedPlugin) ? ( 25 |
30 | 31 | Select a plugin from the list to begin your exploration. 32 |
33 | ) : ( 34 | <> 35 | 36 | 37 | 38 | )} 39 |
40 | ); 41 | }); 42 | 43 | export default Preview; 44 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/ImageGenerationPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {PageID} from '../../../../../../cross/CrossConstants'; 5 | import {ImageGeneration_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 7 | import {GetComponentsByPath} from '../../Cards/Cards'; 8 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 9 | import Page from '../Page'; 10 | 11 | type Props = {show: boolean}; 12 | 13 | const ImageGenerationPage = memo(({show}: Props) => { 14 | const {top, scrollTop, scrollBottom, bottom, cardsContainer} = useMemo( 15 | () => extensionsData.customizePages.image.add, 16 | [], 17 | ); 18 | 19 | return ( 20 | 21 | {top && top.map((Top, index) => )} 22 | 23 | 24 | {scrollTop && scrollTop.map((ScrollTop, index) => )} 25 | 26 | }> 31 | 32 | 33 | 34 | {scrollBottom && scrollBottom.map((ScrollBottom, index) => )} 35 | 36 | 37 | {bottom && bottom.map((Bottom, index) => )} 38 | 39 | ); 40 | }); 41 | 42 | export default ImageGenerationPage; 43 | -------------------------------------------------------------------------------- /src/renderer/src/App/Redux/Reducer/TerminalReducer.ts: -------------------------------------------------------------------------------- 1 | import {createSlice, PayloadAction} from '@reduxjs/toolkit'; 2 | import {useSelector} from 'react-redux'; 3 | 4 | import StorageTypes from '../../../../../cross/StorageTypes'; 5 | import rendererIpc from '../../RendererIpc'; 6 | import {RootState} from '../Store'; 7 | 8 | type TerminalState = StorageTypes['terminal']; 9 | 10 | type TerminalStateTypes = { 11 | [K in keyof TerminalState]: TerminalState[K]; 12 | }; 13 | 14 | const initialState: TerminalState = await rendererIpc.storage.get('terminal'); 15 | 16 | const terminalSlice = createSlice({ 17 | initialState, 18 | name: 'terminal', 19 | reducers: { 20 | initState: (state: TerminalState, action: PayloadAction) => { 21 | state = action.payload; 22 | return state; 23 | }, 24 | setTerminalState: ( 25 | state: TerminalState, 26 | action: PayloadAction<{ 27 | key: K; 28 | value: TerminalState[K]; 29 | }>, 30 | ) => { 31 | state[action.payload.key] = action.payload.value; 32 | rendererIpc.storage.update('terminal', {[action.payload.key]: action.payload.value}); 33 | }, 34 | 35 | resetToDefaults: (state: TerminalState) => { 36 | state = initialState; 37 | rendererIpc.storage.update('terminal', initialState); 38 | return state; 39 | }, 40 | }, 41 | }); 42 | 43 | export const useTerminalState = (name: T): TerminalStateTypes[T] => 44 | useSelector((state: RootState) => state.terminal[name]); 45 | 46 | export const useTerminalStat = () => useSelector((state: RootState) => state.terminal); 47 | 48 | export const terminalActions = terminalSlice.actions; 49 | 50 | export default terminalSlice.reducer; 51 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/ContentPages/AudioGenerationPage.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollShadow} from '@heroui/react'; 2 | import {memo, useMemo} from 'react'; 3 | 4 | import {PageID} from '../../../../../../cross/CrossConstants'; 5 | import {AudioGeneration_Icon} from '../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {extensionsData} from '../../../Extensions/ExtensionLoader'; 7 | import {GetComponentsByPath} from '../../Cards/Cards'; 8 | import CardContainer, {CardContainerClasses} from '../CardContainer'; 9 | import Page from '../Page'; 10 | 11 | type Props = {show: boolean}; 12 | 13 | const AudioGenerationPage = memo(({show}: Props) => { 14 | const {top, scrollTop, scrollBottom, bottom, cardsContainer} = useMemo( 15 | () => extensionsData.customizePages.audio.add, 16 | [], 17 | ); 18 | 19 | return ( 20 | 21 | {top && top.map((Top, index) => )} 22 | 23 | 24 | {scrollTop && scrollTop.map((ScrollTop, index) => )} 25 | 26 | }> 31 | 32 | 33 | 34 | {scrollBottom && scrollBottom.map((ScrollBottom, index) => )} 35 | 36 | 37 | {bottom && bottom.map((Bottom, index) => )} 38 | 39 | ); 40 | }); 41 | 42 | export default AudioGenerationPage; 43 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/SettingsContainer.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | 3 | import {extensionsData} from '../../../../Extensions/ExtensionLoader'; 4 | import SettingsBrowser, {SettingsBrowserId} from './Content/Browser/SettingsBrowser'; 5 | import SettingsCard, {SettingsCardId} from './Content/Card/Settings-Card'; 6 | import SettingsGeneral, {SettingsGeneralId} from './Content/General/Settings-General'; 7 | import {HotkeySettings, SettingsHotkeysId} from './Content/HotkeySettings'; 8 | import SettingsClear, {SettingsClearId} from './Content/Settings-Clear'; 9 | import SettingsData, {SettingsDataId} from './Content/Settings-Data'; 10 | import SettingsDiscord, {SettingsDiscordId} from './Content/Settings-Discord'; 11 | import SettingsStartup, {SettingsStartupId} from './Content/Startup/Settings-Startup'; 12 | import SettingsTerminal, {SettingsTerminalId} from './Content/Terminal/Settings-Terminal'; 13 | 14 | export const settingsSectionId = { 15 | SettingsCardId, 16 | SettingsGeneralId, 17 | SettingsTerminalId, 18 | SettingsBrowserId, 19 | SettingsStartupId, 20 | SettingsClearId, 21 | SettingsDataId, 22 | SettingsDiscordId, 23 | SettingsHotkeysId, 24 | }; 25 | 26 | export const SettingsSections = () => { 27 | const content = useMemo(() => extensionsData.customizePages.settings.add.content, []); 28 | 29 | return ( 30 | <> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {content.map((Content, index) => ( 43 | 44 | ))} 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/renderer/dialog/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Input} from '@heroui/react'; 2 | import {useEffect, useState} from 'react'; 3 | 4 | export default function Dialog() { 5 | const [title, setTitle] = useState(''); 6 | const [inputValue, setInputValue] = useState(''); 7 | 8 | useEffect(() => { 9 | window.electron.ipcRenderer.on('dlg-title', (_e, result) => { 10 | setTitle(result); 11 | setInputValue(''); 12 | }); 13 | }, [setTitle]); 14 | 15 | const hide = () => { 16 | window.electron.ipcRenderer.send('dlg-hide'); 17 | }; 18 | 19 | const done = () => { 20 | window.electron.ipcRenderer.send('dlg-result', inputValue); 21 | hide(); 22 | }; 23 | 24 | return ( 25 |
26 |
27 | {title} 28 | 29 | { 31 | if (event.key === 'Enter') { 32 | done(); 33 | } else if (event.key === 'Escape') { 34 | hide(); 35 | } 36 | }} 37 | size="sm" 38 | value={inputValue} 39 | className="notDraggable" 40 | onValueChange={setInputValue} 41 | autoFocus 42 | /> 43 | 44 |
45 | 48 | 49 | 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/Managements/Plugin/Extensions/ExtensionApi.ts: -------------------------------------------------------------------------------- 1 | import {initPluginNodeSentry} from '../PluginSentry'; 2 | import {EMenuItem, ExtensionData_Main, ExtensionMainApi} from './ExtensionTypes_Main'; 3 | 4 | export default class ExtensionApi { 5 | private readonly extensionsData: ExtensionData_Main; 6 | private readonly extensionMainApi: ExtensionMainApi; 7 | 8 | constructor() { 9 | this.extensionsData = { 10 | listenForChannels: [], 11 | onAppReady: [], 12 | onReadyToShow: [], 13 | trayMenu_AddItem: [], 14 | }; 15 | 16 | this.extensionMainApi = { 17 | listenForChannels: fc => this.extensionsData.listenForChannels.push(fc), 18 | onAppReady: fc => this.extensionsData.onAppReady.push(fc), 19 | onReadyToShow: fc => this.extensionsData.onReadyToShow.push(fc), 20 | trayMenu_AddItem: fc => this.extensionsData.trayMenu_AddItem.push(fc), 21 | initNodeSentry: initPluginNodeSentry, 22 | }; 23 | } 24 | 25 | public getApi() { 26 | return this.extensionMainApi; 27 | } 28 | 29 | public listenForChannels() { 30 | for (const listenForChannels of this.extensionsData.listenForChannels) { 31 | listenForChannels(); 32 | } 33 | } 34 | 35 | public async onAppReady() { 36 | for (const onAppReady of this.extensionsData.onAppReady) { 37 | await onAppReady(); 38 | } 39 | } 40 | 41 | public onReadyToShow() { 42 | for (const onReadyToShow of this.extensionsData.onReadyToShow) { 43 | onReadyToShow(); 44 | } 45 | } 46 | 47 | public getTrayItems(staticItems: EMenuItem[]) { 48 | for (const addTrayItem of this.extensionsData.trayMenu_AddItem) { 49 | const item = addTrayItem(); 50 | staticItems.splice(item.index, 0, item.item); 51 | } 52 | return staticItems; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/Managements/Plugin/Extensions/ExtensionUtils.ts: -------------------------------------------------------------------------------- 1 | import pty from 'node-pty'; 2 | 3 | import DiscordRpcManager from '../../DiscordRpcManager'; 4 | import ElectronAppManager from '../../ElectronAppManager'; 5 | import StorageManager from '../../Storage/StorageManager'; 6 | import ModuleManager from '../Modules/ModuleManager'; 7 | import {MainExtensionUtils} from './ExtensionTypes_Main'; 8 | 9 | export default class ExtensionUtils implements MainExtensionUtils { 10 | private managerPromises: Map> = new Map(); 11 | private resolvers: Map void> = new Map(); 12 | public nodePty = pty; 13 | 14 | constructor() { 15 | const managers = ['storage', 'app', 'discordRpc', 'module']; 16 | managers.forEach(manager => { 17 | this.managerPromises.set( 18 | manager, 19 | new Promise(resolve => { 20 | this.resolvers.set(manager, resolve); 21 | }), 22 | ); 23 | }); 24 | } 25 | 26 | getStorageManager() { 27 | return this.managerPromises.get('storage')!; 28 | } 29 | 30 | getAppManager() { 31 | return this.managerPromises.get('app')!; 32 | } 33 | 34 | getDiscordRpcManager() { 35 | return this.managerPromises.get('discordRpc')!; 36 | } 37 | 38 | getModuleManager() { 39 | return this.managerPromises.get('module')!; 40 | } 41 | 42 | setStorageManager(manager: StorageManager) { 43 | this.resolvers.get('storage')!(manager); 44 | } 45 | 46 | setAppManager(manager: ElectronAppManager) { 47 | this.resolvers.get('app')!(manager); 48 | } 49 | 50 | setDiscordRpcManager(manager: DiscordRpcManager) { 51 | this.resolvers.get('discordRpc')!(manager); 52 | } 53 | 54 | setModuleManager(manager: ModuleManager) { 55 | this.resolvers.get('module')!(manager); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cross/plugin/PluginTypes.ts: -------------------------------------------------------------------------------- 1 | import {OsPlatforms, SubscribeStages} from '../CrossTypes'; 2 | 3 | export type PluginEngines = {moduleApi?: string; extensionApi?: string}; 4 | 5 | export type ChangelogSubItem = string | Record; 6 | export type ChangelogItem = Record; 7 | export type PluginChangelog = {version: string; date: string; items: ChangelogItem[]}; 8 | export type PluginChanges = PluginChangelog[]; 9 | 10 | export type VersionItem = { 11 | version: string; 12 | commit: string; 13 | stage: SubscribeStages; 14 | engines: PluginEngines; 15 | platforms: OsPlatforms[]; 16 | }; 17 | export type PluginVersions = VersionItem[]; 18 | export type PluginVersioning = {versions: PluginVersions; changes: PluginChanges}; 19 | 20 | export type PluginMetadata = { 21 | id: string; 22 | title: string; 23 | description: string; 24 | type: 'module' | 'extension'; 25 | }; 26 | 27 | type PluginCompatibility = { 28 | isCompatible: boolean; 29 | incompatibleReason?: string; 30 | }; 31 | export type VersionItemValidated = Omit & PluginCompatibility; 32 | export type PluginItem = { 33 | url: string; 34 | metadata: PluginMetadata; 35 | versions: VersionItemValidated[]; 36 | changes: PluginChanges; 37 | } & PluginCompatibility; 38 | 39 | export type PluginSyncItem = {id: string; type: 'downgrade' | 'upgrade'; version: string; commit: string}; 40 | export type PluginInstalledItem = {id: string; url: string; version: string}; 41 | export type UnloadedPlugins = {id: string; message: string}; 42 | export type PluginFilter = Set<'installed' | 'modules' | 'extensions'> | 'all'; 43 | export type ValidatedPlugins = {type: 'module' | 'extension'; folder: string}[]; 44 | export type PluginAddresses = {type: 'module' | 'extension'; address: string}[]; 45 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Browser_Terminal/Terminal/Terminal_Timer.tsx: -------------------------------------------------------------------------------- 1 | import NumberFlow, {NumberFlowGroup} from '@number-flow/react'; 2 | import {useEffect, useState} from 'react'; 3 | 4 | import {secondsElapsed} from '../../../../../../cross/CrossUtils'; 5 | import {useTabsState} from '../../../Redux/Reducer/TabsReducer'; 6 | 7 | const calc = (startTime: string) => { 8 | const startDate = new Date(startTime); 9 | 10 | if (isNaN(startDate.getTime())) { 11 | return {hh: 0, mm: 0, ss: 0}; 12 | } 13 | 14 | const seconds = secondsElapsed(startDate); 15 | return {hh: Math.floor(seconds / 3600), mm: Math.floor((seconds % 3600) / 60), ss: seconds % 60}; 16 | }; 17 | 18 | type TimeType = {hh: number; mm: number; ss: number}; 19 | 20 | type Props = {startTime: string}; 21 | export default function Terminal_Timer({startTime}: Props) { 22 | const activeTab = useTabsState('activeTab'); 23 | const [{ss, hh, mm}, setTime] = useState(calc(startTime)); 24 | 25 | useEffect(() => { 26 | setInterval(() => { 27 | setTime(calc(startTime)); 28 | }, 1000); 29 | }, []); 30 | 31 | return ( 32 | 33 |
38 | 39 | 40 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/src/App/Redux/Store.ts: -------------------------------------------------------------------------------- 1 | import {configureStore} from '@reduxjs/toolkit'; 2 | import {createReduxEnhancer} from '@sentry/react'; 3 | 4 | import {extensionsData} from '../Extensions/ExtensionLoader'; 5 | import appReducer from './Reducer/AppReducer'; 6 | import cardsReducer from './Reducer/CardsReducer'; 7 | import hotkeysReducer from './Reducer/HotkeysReducer'; 8 | import modalsReducer from './Reducer/ModalsReducer'; 9 | import pluginsReducer from './Reducer/PluginsReducer'; 10 | import settingsReducer from './Reducer/SettingsReducer'; 11 | import tabsReducer from './Reducer/TabsReducer'; 12 | import terminalReducer from './Reducer/TerminalReducer'; 13 | import userReducer from './Reducer/UserReducer'; 14 | 15 | const staticReducers = { 16 | app: appReducer, 17 | tabs: tabsReducer, 18 | user: userReducer, 19 | cards: cardsReducer, 20 | modals: modalsReducer, 21 | settings: settingsReducer, 22 | terminal: terminalReducer, 23 | hotkeys: hotkeysReducer, 24 | plugins: pluginsReducer, 25 | }; 26 | 27 | let store = configureStore({ 28 | reducer: staticReducers, 29 | }); 30 | 31 | const sentryReduxEnhancer = createReduxEnhancer({}); 32 | 33 | export const createStore = (collectError: boolean) => { 34 | const extensionReducers = extensionsData.addReducer.reduce((acc, reducer) => { 35 | acc[reducer.name] = reducer.reducer; 36 | return acc; 37 | }, {}); 38 | 39 | store = configureStore({ 40 | reducer: { 41 | ...staticReducers, 42 | ...extensionReducers, 43 | }, 44 | enhancers: collectError 45 | ? getDefaultEnhancers => { 46 | return getDefaultEnhancers().concat(sentryReduxEnhancer); 47 | } 48 | : undefined, 49 | }); 50 | 51 | return store; 52 | }; 53 | 54 | export type RootState = ReturnType; 55 | export type AppDispatch = typeof store.dispatch; 56 | -------------------------------------------------------------------------------- /src/renderer/loading/Backgrounds/RippleBG.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from '@heroui/react'; 2 | import React, {ComponentPropsWithoutRef, CSSProperties} from 'react'; 3 | 4 | interface RippleProps extends ComponentPropsWithoutRef<'div'> { 5 | mainCircleSize?: number; 6 | mainCircleOpacity?: number; 7 | numCircles?: number; 8 | } 9 | export const Ripple = React.memo(function Ripple({ 10 | mainCircleSize = 210, 11 | mainCircleOpacity = 0.24, 12 | numCircles = 8, 13 | className, 14 | ...props 15 | }: RippleProps) { 16 | return ( 17 |
23 | {Array.from({length: numCircles}, (_, i) => { 24 | const size = mainCircleSize + i * 70; 25 | const opacity = mainCircleOpacity - i * 0.03; 26 | const animationDelay = `${i * 0.06}s`; 27 | const borderStyle = 'solid'; 28 | return ( 29 |
48 | ); 49 | })} 50 |
51 | ); 52 | }); 53 | 54 | Ripple.displayName = 'Ripple'; 55 | 56 | export type {RippleProps}; 57 | -------------------------------------------------------------------------------- /src/renderer/src/App/Redux/Reducer/UserReducer.ts: -------------------------------------------------------------------------------- 1 | import {createSlice, PayloadAction} from '@reduxjs/toolkit'; 2 | import {useSelector} from 'react-redux'; 3 | 4 | import {PatreonUserData, SubscribeStages} from '../../../../../cross/CrossTypes'; 5 | import {RootState} from '../Store'; 6 | 7 | type UserState = { 8 | patreonUserData: PatreonUserData; 9 | patreonLoggedIn: boolean; 10 | updateChannel: SubscribeStages; 11 | }; 12 | 13 | type UserStateTypes = { 14 | [K in keyof UserState]: UserState[K]; 15 | }; 16 | 17 | const initialState: UserState = { 18 | patreonUserData: { 19 | tier: 'Not available', 20 | name: 'Guest', 21 | imageUrl: '', 22 | subscribeStage: 'public', 23 | }, 24 | patreonLoggedIn: false, 25 | updateChannel: 'public', 26 | }; 27 | 28 | const userSlice = createSlice({ 29 | name: 'user', 30 | initialState, 31 | reducers: { 32 | setUserState: ( 33 | state: UserState, 34 | action: PayloadAction<{ 35 | key: K; 36 | value: UserState[K]; 37 | }>, 38 | ) => { 39 | state[action.payload.key] = action.payload.value; 40 | }, 41 | resetUserState: (state: UserState, action: PayloadAction) => { 42 | state[action.payload] = initialState[action.payload]; 43 | }, 44 | setUpdateChannel: (state: UserState, action: PayloadAction) => { 45 | state.updateChannel = action.payload; 46 | }, 47 | }, 48 | }); 49 | 50 | /** 51 | * Hook to access app state 52 | * @param key - The key of the app state to retrieve 53 | * @returns The value of the specified app state 54 | */ 55 | export const useUserState = (key: K): UserStateTypes[K] => 56 | useSelector((state: RootState) => state.user[key]); 57 | 58 | export const userActions = userSlice.actions; 59 | 60 | export default userSlice.reducer; 61 | -------------------------------------------------------------------------------- /src/renderer/src/App/Utils/LocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import {RepoDetails} from '../../../../cross/CrossTypes'; 2 | import {extractGitUrl} from '../../../../cross/CrossUtils'; 3 | 4 | /** 5 | * Fetches repository details from GitHub or GitLab API or local storage. 6 | * @param url - The GitHub or GitLab repository URL 7 | * @returns A promise that resolves to RepoDetails or undefined 8 | */ 9 | export async function fetchRepoDetails(url: string): Promise { 10 | if (!url) return undefined; 11 | 12 | const {owner, repo, platform} = extractGitUrl(url); 13 | 14 | try { 15 | let apiUrl: string; 16 | let repoDetails: RepoDetails; 17 | 18 | if (platform === 'github') { 19 | apiUrl = `https://api.github.com/repos/${owner}/${repo}`; 20 | const response = await fetch(apiUrl); 21 | const {forks_count, open_issues_count, stargazers_count, size} = await response.json(); 22 | 23 | if (!stargazers_count || !forks_count || !open_issues_count || !size) return undefined; 24 | 25 | repoDetails = { 26 | forks: forks_count, 27 | issues: open_issues_count, 28 | stars: stargazers_count, 29 | size, 30 | }; 31 | } else if (platform === 'gitlab') { 32 | apiUrl = `https://gitlab.com/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`; 33 | const response = await fetch(apiUrl); 34 | const {forks_count, star_count} = await response.json(); 35 | 36 | if (!star_count || !forks_count) return undefined; 37 | 38 | repoDetails = { 39 | forks: forks_count, 40 | stars: star_count, 41 | }; 42 | } else { 43 | console.error(`Unsupported platform: ${platform}`); 44 | return undefined; 45 | } 46 | 47 | return repoDetails; 48 | } catch (error) { 49 | console.error('Error fetching repo details:', error); 50 | return undefined; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import '@xterm/xterm/css/xterm.css'; 3 | import 'overlayscrollbars/overlayscrollbars.css'; 4 | import '@ant-design/v5-patch-for-react-19'; 5 | import '../SentryInit'; 6 | 7 | import {reactErrorHandler} from '@sentry/react'; 8 | import log from 'electron-log/renderer'; 9 | import {createRoot, RootOptions} from 'react-dom/client'; 10 | import {ErrorBoundary} from 'react-error-boundary'; 11 | import {Provider as ReduxProvider} from 'react-redux'; 12 | 13 | import {isDev} from '../../cross/CrossUtils'; 14 | import {onBreadcrumbStateChange} from '../Breadcrumbs'; 15 | import App from './App/App'; 16 | import {loadExtensions} from './App/Extensions/Vite-Federation'; 17 | import loadModules from './App/Modules/ModuleLoader'; 18 | import {createStore} from './App/Redux/Store'; 19 | import rendererIpc from './App/RendererIpc'; 20 | import ErrorComponent from './ErrorComponent'; 21 | 22 | await loadModules(); 23 | await loadExtensions(); 24 | 25 | const {darkMode, collectErrors, addBreadcrumbs} = await rendererIpc.storage.get('app'); 26 | onBreadcrumbStateChange(addBreadcrumbs); 27 | document.documentElement.className = darkMode ? 'dark' : 'light'; 28 | 29 | if (!isDev()) { 30 | Object.assign(console, log.functions); 31 | } 32 | 33 | const rootOptions: RootOptions | undefined = collectErrors 34 | ? { 35 | onUncaughtError: reactErrorHandler((error, errorInfo) => { 36 | console.warn('Uncaught error', error, errorInfo.componentStack); 37 | }), 38 | onCaughtError: reactErrorHandler(), 39 | onRecoverableError: reactErrorHandler(), 40 | } 41 | : undefined; 42 | 43 | createRoot(document.getElementById('root') as HTMLElement, rootOptions).render( 44 | 45 | 46 | 47 | 48 | , 49 | ); 50 | -------------------------------------------------------------------------------- /extension/electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import federation from '@originjs/vite-plugin-federation'; 2 | import react from '@vitejs/plugin-react'; 3 | import {defineConfig, externalizeDepsPlugin} from 'electron-vite'; 4 | import {resolve} from 'path'; 5 | 6 | // import packageJson from './package.json'; 7 | 8 | export default defineConfig({ 9 | main: { 10 | root: resolve('extension/src/main'), 11 | plugins: [externalizeDepsPlugin()], 12 | build: { 13 | outDir: resolve('extension_out/main'), 14 | rollupOptions: { 15 | input: resolve('extension/src/main/lynxExtension.ts'), 16 | output: {entryFileNames: 'mainEntry.mjs'}, 17 | }, 18 | }, 19 | }, 20 | renderer: { 21 | root: resolve('extension/src/renderer'), 22 | plugins: [ 23 | react(), 24 | federation({ 25 | name: 'extension', 26 | filename: 'rendererEntry.mjs', 27 | exposes: { 28 | Extension: resolve('extension/src/renderer/Extension.tsx'), 29 | }, 30 | shared: { 31 | antd: {generate: false}, 32 | react: {generate: false}, 33 | lodash: {generate: false}, 34 | 'react-dom': {generate: false}, 35 | 'react-redux': {generate: false}, 36 | 'mobx-react-lite': {generate: false}, 37 | '@heroui/react': {generate: false}, 38 | }, 39 | }), 40 | ], 41 | build: { 42 | outDir: resolve('extension_out/renderer'), 43 | rollupOptions: { 44 | input: resolve('extension/src/renderer/index.html'), 45 | // external: Object.keys(packageJson.devDependencies), 46 | treeshake: {moduleSideEffects: false}, 47 | }, 48 | assetsDir: '', 49 | minify: false, 50 | target: 'esnext', 51 | cssCodeSplit: false, 52 | modulePreload: false, 53 | }, 54 | 55 | publicDir: resolve(__dirname, 'extension/src/renderer/Public'), 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /src/main/Managements/ToastWindowManager.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import {is} from '@electron-toolkit/utils'; 4 | import {app, BrowserWindow, ipcMain} from 'electron'; 5 | 6 | import icon from '../../../resources/icon.png?asset'; 7 | import {ToastWindow_MessageType} from '../../cross/CrossTypes'; 8 | import {appWindowChannels} from '../../cross/IpcChannelAndTypes'; 9 | import {RelaunchApp} from '../Utilities/Utils'; 10 | 11 | export default function ShowToastWindow( 12 | message: ToastWindow_MessageType, 13 | onBtnPress?: (id: string, window?: BrowserWindow) => void, 14 | ) { 15 | const show = () => { 16 | const window = new BrowserWindow({ 17 | frame: false, 18 | show: false, 19 | height: 250, 20 | width: 600, 21 | resizable: false, 22 | maximizable: false, 23 | icon, 24 | webPreferences: { 25 | preload: path.join(__dirname, '../preload/only_ipc.cjs'), 26 | sandbox: false, 27 | }, 28 | }); 29 | 30 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 31 | window.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/toast_window.html`); 32 | } else { 33 | window.loadFile(path.join(__dirname, `../renderer/toast_window.html`)); 34 | } 35 | 36 | window.on('ready-to-show', () => { 37 | window.show(); 38 | window.webContents.send('show_message', message); 39 | }); 40 | 41 | window.on('closed', () => { 42 | window.destroy(); 43 | }); 44 | 45 | ipcMain.on('close_toast', () => { 46 | window.close(); 47 | }); 48 | ipcMain.on('exit_app', () => { 49 | app.exit(); 50 | }); 51 | ipcMain.on('restart_app', () => RelaunchApp(false)); 52 | ipcMain.on(appWindowChannels.toastBtnPress, (_, id: string) => { 53 | if (onBtnPress) onBtnPress(id, window); 54 | }); 55 | }; 56 | 57 | if (app.isReady()) { 58 | show(); 59 | } else { 60 | app.whenReady().then(() => show()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Settings-Data.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {useCallback, useEffect, useState} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | 5 | import {Database_Icon, OpenFolder_Icon, Refresh_Icon} from '../../../../../../assets/icons/SvgIcons/SvgIcons'; 6 | import {AppDispatch} from '../../../../../Redux/Store'; 7 | import rendererIpc from '../../../../../RendererIpc'; 8 | import {lynxTopToast} from '../../../../../Utils/UtilHooks'; 9 | import SettingsSection from '../SettingsPage-ContentSection'; 10 | 11 | export const SettingsDataId = 'settings_data_elem'; 12 | 13 | /** Change app data directory */ 14 | export default function SettingsData() { 15 | const [currentPath, setCurrentPath] = useState(''); 16 | const dispatch = useDispatch(); 17 | 18 | const change = () => { 19 | rendererIpc.appData 20 | .selectAnother() 21 | .then(result => { 22 | lynxTopToast(dispatch).success(result); 23 | }) 24 | .catch(reason => { 25 | lynxTopToast(dispatch).error(reason); 26 | }); 27 | }; 28 | 29 | const openFolder = useCallback(() => { 30 | rendererIpc.file.openPath(currentPath); 31 | }, [currentPath]); 32 | 33 | useEffect(() => { 34 | rendererIpc.appData.getCurrentPath().then(result => { 35 | setCurrentPath(result); 36 | }); 37 | }, []); 38 | 39 | return ( 40 | } itemsCenter> 41 | App data, including extensions, modules and binaries, will be saved here. 42 | 43 | 46 | 47 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Initializer/components/CheckRow.tsx: -------------------------------------------------------------------------------- 1 | import {Chip, Spinner} from '@heroui/react'; 2 | import {motion} from 'framer-motion'; 3 | 4 | import {CheckResult, RowData} from '../types'; 5 | 6 | function getStatusProps(status: CheckResult): {color: 'success' | 'danger' | 'default'; label: string} { 7 | switch (status) { 8 | case 'ok': 9 | return {color: 'success', label: 'Ready'}; 10 | case 'failed': 11 | return {color: 'danger', label: 'Missing'}; 12 | case 'checking': 13 | return {color: 'default', label: 'Checking...'}; 14 | case 'installing': 15 | return {color: 'default', label: 'Installing...'}; 16 | default: 17 | return {color: 'default', label: 'Unknown'}; 18 | } 19 | } 20 | 21 | type Props = {label: string; description?: string; status: RowData}; 22 | 23 | export default function CheckRow({label, description, status}: Props) { 24 | const {color, label: statusLabelText} = getStatusProps(status.result); 25 | const isBusy = status.result === 'checking' || status.result === 'installing'; 26 | const labelText = status.result === 'ok' && status.label ? status.label : statusLabelText; 27 | 28 | return ( 29 | 34 |
35 |
{label}
36 | {description &&
{description}
} 37 |
38 | 39 | }> 47 | {labelText} 48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Pages/SettingsPages/Settings/Content/Terminal/SettingsTerminal-CursorInactiveStyle.tsx: -------------------------------------------------------------------------------- 1 | import {Select, Selection, SelectItem} from '@heroui/react'; 2 | import {useCallback} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | 5 | import {TerminalCursorInactiveStyle} from '../../../../../../../../../cross/IpcChannelAndTypes'; 6 | import {terminalActions, useTerminalState} from '../../../../../../Redux/Reducer/TerminalReducer'; 7 | import {AppDispatch} from '../../../../../../Redux/Store'; 8 | 9 | export default function SettingsTerminalCursorInactiveStyle() { 10 | const cursorInactiveStyle = useTerminalState('cursorInactiveStyle'); 11 | const dispatch = useDispatch(); 12 | 13 | const onChange = useCallback( 14 | (keys: Selection) => { 15 | if (keys !== 'all') { 16 | const value = keys.values().next().value as TerminalCursorInactiveStyle; 17 | dispatch(terminalActions.setTerminalState({key: 'cursorInactiveStyle', value})); 18 | } 19 | }, 20 | [dispatch], 21 | ); 22 | return ( 23 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/main/Managements/DialogManager.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import {is} from '@electron-toolkit/utils'; 4 | import {app, BrowserWindow, ipcMain} from 'electron'; 5 | 6 | import {toMs} from '../../cross/CrossUtils'; 7 | import {appManager} from '../index'; 8 | 9 | export default function DialogManager() { 10 | if (!appManager) { 11 | setTimeout(DialogManager, toMs(1, 'seconds')); 12 | return; 13 | } 14 | 15 | app.on('web-contents-created', (_event, contents) => { 16 | contents.on('will-attach-webview', (_e, webPreferences) => { 17 | webPreferences.preload = path.join(__dirname, '../preload/webview.cjs'); 18 | }); 19 | }); 20 | 21 | appManager.onCreateWindow = () => { 22 | let promptResponse: string | null; 23 | 24 | const promptWindow: BrowserWindow = new BrowserWindow({ 25 | width: 300, 26 | height: 150, 27 | show: false, 28 | parent: appManager?.getMainWindow(), 29 | resizable: false, 30 | movable: true, 31 | alwaysOnTop: true, 32 | frame: false, 33 | modal: true, 34 | webPreferences: { 35 | preload: path.join(__dirname, '../preload/dialog.cjs'), 36 | }, 37 | }); 38 | 39 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 40 | promptWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/dialog.html`); 41 | } else { 42 | promptWindow.loadFile(path.join(__dirname, `../renderer/dialog.html`)); 43 | } 44 | 45 | ipcMain.on('show-prompt', function (_event, arg) { 46 | promptResponse = null; 47 | 48 | promptWindow.show(); 49 | promptWindow.webContents.send('dlg-title', arg); 50 | 51 | promptWindow.on('hide', () => { 52 | _event.returnValue = promptResponse; 53 | }); 54 | }); 55 | 56 | ipcMain.on('dlg-result', (_e, value) => { 57 | promptResponse = value === '' ? null : value; 58 | }); 59 | 60 | ipcMain.on('dlg-hide', () => { 61 | promptWindow.hide(); 62 | }); 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/src/App/UIProviders.tsx: -------------------------------------------------------------------------------- 1 | import {StyleProvider} from '@ant-design/cssinjs'; 2 | import {HeroUIProvider, ToastProvider} from '@heroui/react'; 3 | import {ConfigProvider as AntDProvider, message, notification, theme} from 'antd'; 4 | import {ReactNode, useLayoutEffect, useMemo} from 'react'; 5 | 6 | import {useAppState} from './Redux/Reducer/AppReducer'; 7 | 8 | /** Config HeroUI and AntD and return providers */ 9 | export default function UIProviders({children}: {children: ReactNode}) { 10 | const darkMode = useAppState('darkMode'); 11 | const toastPlacement = useAppState('toastPlacement'); 12 | const algorithm = useMemo(() => (darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm), [darkMode]); 13 | const colorBgSpotlight = useMemo(() => (darkMode ? '#424242' : 'white'), [darkMode]); 14 | const colorTextLightSolid = useMemo(() => (darkMode ? 'white' : 'black'), [darkMode]); 15 | 16 | useLayoutEffect(() => { 17 | AntDProvider.config({ 18 | holderRender: children => ( 19 | 24 | {children} 25 | 26 | ), 27 | }); 28 | message.config({top: 38, duration: 2}); 29 | notification.config({duration: 4, placement: 'bottomRight'}); 30 | }, [algorithm]); 31 | 32 | return ( 33 |
34 | 35 | 36 | 45 | {children} 46 | 47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/cross/AccentColorGenerator.ts: -------------------------------------------------------------------------------- 1 | const GOLDEN_RATIO_CONJUGATE = 0.61803398875; 2 | 3 | function simpleHash(str: string): number { 4 | let hash = 0; 5 | for (let i = 0; i < str.length; i++) { 6 | const char = str.charCodeAt(i); 7 | hash = (hash << 5) - hash + char; 8 | hash |= 0; 9 | } 10 | return hash; 11 | } 12 | 13 | export const getAccentColorAdvanced = (title: string, developerName: string): string => { 14 | const combinedString = `${title.toLowerCase()}-${developerName.toLowerCase()}`; 15 | const hash = simpleHash(combinedString); 16 | 17 | const hue = (Math.abs(hash) * GOLDEN_RATIO_CONJUGATE * 360) % 360; 18 | 19 | const saturation = 100; 20 | const lightness = 60; 21 | 22 | return `hsl(${Math.floor(hue)}, ${saturation}%, ${lightness}%)`; 23 | }; 24 | 25 | function hslToHex(h: number, s: number, l: number): string { 26 | s /= 100; 27 | l /= 100; 28 | 29 | const c = (1 - Math.abs(2 * l - 1)) * s; 30 | const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); 31 | const m = l - c / 2; 32 | let r = 0, 33 | g = 0, 34 | b = 0; 35 | 36 | if (0 <= h && h < 60) { 37 | [r, g, b] = [c, x, 0]; 38 | } else if (60 <= h && h < 120) { 39 | [r, g, b] = [x, c, 0]; 40 | } else if (120 <= h && h < 180) { 41 | [r, g, b] = [0, c, x]; 42 | } else if (180 <= h && h < 240) { 43 | [r, g, b] = [0, x, c]; 44 | } else if (240 <= h && h < 300) { 45 | [r, g, b] = [x, 0, c]; 46 | } else if (300 <= h && h < 360) { 47 | [r, g, b] = [c, 0, x]; 48 | } 49 | 50 | const toHex = (c: number) => { 51 | const hex = Math.round((c + m) * 255).toString(16); 52 | return hex.length === 1 ? '0' + hex : hex; 53 | }; 54 | 55 | return `#${toHex(r)}${toHex(g)}${toHex(b)}`; 56 | } 57 | 58 | export const getAccentColorAsHex = (title: string, developerName: string): string => { 59 | const hslString = getAccentColorAdvanced(title, developerName); 60 | 61 | const [h, s, l] = hslString.match(/\d+/g)?.map(Number) ?? [0, 0, 0]; 62 | 63 | return hslToHex(h, s, l); 64 | }; 65 | -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/CustomNotification/CustomNotification.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@heroui/react'; 2 | import {notification} from 'antd'; 3 | import {useEffect} from 'react'; 4 | 5 | import rendererIpc from '../../../RendererIpc'; 6 | 7 | export default function CustomNotification() { 8 | const [api, contextHolder] = notification.useNotification(); 9 | 10 | useEffect(() => { 11 | const offOpen = rendererIpc.customNotification.onOpen((_, data) => { 12 | console.log('openeing this', data.key); 13 | api[data.type]({ 14 | closeIcon: null, 15 | placement: 'bottomRight', 16 | actions: ( 17 |
18 | {data.buttons?.map(btn => ( 19 | 28 | ))} 29 | {data.closeBtn && ( 30 | 38 | )} 39 |
40 | ), 41 | message: {data.message}, 42 | description: data.description, 43 | duration: 0, 44 | key: data.key, 45 | className: 'dark:bg-foreground-100 !shadow-medium !overflow-hidden rounded-xl', 46 | }); 47 | }); 48 | const offClose = rendererIpc.customNotification.onClose((_, key) => api.destroy(key)); 49 | 50 | return () => { 51 | offOpen(); 52 | offClose(); 53 | }; 54 | }, []); 55 | 56 | return <>{contextHolder}; 57 | } 58 | -------------------------------------------------------------------------------- /src/main/Managements/Ipc/Methods/IpcMethods-Repository.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import {ShallowCloneOptions} from '../../../../cross/GitTypes'; 4 | import {setupGitManagerListeners} from '../../Git/GitHelper'; 5 | import GitManager from '../../Git/GitManager'; 6 | 7 | let gitManager: GitManager | undefined; 8 | 9 | export function shallowClone(options: ShallowCloneOptions): void { 10 | gitManager = new GitManager(true); 11 | 12 | gitManager.shallowClone(options); 13 | 14 | setupGitManagerListeners(gitManager); 15 | } 16 | 17 | export async function shallowClonePromise(options: ShallowCloneOptions) { 18 | return new GitManager(true).shallowClone(options); 19 | } 20 | 21 | export async function getRepositoryInfo(dir: string) { 22 | return new GitManager(true).getRepositoryInfo(dir); 23 | } 24 | 25 | export async function stashDrop(dir: string): Promise<{message: string; type: 'error' | 'success' | 'info'}> { 26 | return new GitManager(true).stashAndDrop(dir); 27 | } 28 | 29 | export async function changeBranch(dir: string, branchName: string) { 30 | return new GitManager(true).changeBranch(dir, branchName); 31 | } 32 | 33 | export async function unShallow(dir: string) { 34 | return new GitManager(true).unShallow(dir); 35 | } 36 | 37 | export async function resetHard(dir: string) { 38 | return new GitManager(true).resetHard(dir); 39 | } 40 | 41 | /** 42 | * Pulls the latest changes from a repository. 43 | * @param dir - The directory of the repository. 44 | * @param id - The unique identifier for the repository. 45 | */ 46 | export function pullRepo(dir: string, id: string): void { 47 | gitManager = new GitManager(); 48 | 49 | gitManager.pull(path.resolve(dir)); 50 | 51 | setupGitManagerListeners(gitManager, id); 52 | } 53 | 54 | export async function validateGitDir(dir: string, url: string): Promise { 55 | try { 56 | const result = await GitManager.locateCard(url, dir); 57 | return !!result; 58 | } catch (e) { 59 | console.error('Error validating git directory: ', e); 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /notifications.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$schema": "./notifications.schema.json", 4 | "id": "new-extension_hw-monitor", 5 | "title": "Hardware Monitor", 6 | "icon": "https://raw.githubusercontent.com/KindaBrazy/LynxHub-Hardware-Monitor/refs/heads/compiled/resources/icon.png", 7 | "description": [ 8 | { 9 | "text": "Just dropped a new extension: Hardware Monitor!", 10 | "color": "success" 11 | }, 12 | { 13 | "text": "It's super easy to keep tabs on your computer's CPU, GPU, and Memory usage, all from your LynxHub status bar. Plus, you can tweak it however you like." 14 | }, 15 | { 16 | "text": "Quick note: This one needs the .NET 8 runtime, and if some numbers aren't popping up, try running LynxHub as an administrator.", 17 | "color": "warning" 18 | }, 19 | { 20 | "text": "Currently, it's just for Windows, but Linux support is coming soon!" 21 | } 22 | ], 23 | "buttons": [ 24 | { 25 | "title": "🧩 Extensions", 26 | "destination": "extensions" 27 | } 28 | ] 29 | }, 30 | { 31 | "id": "welcome_lh_v3", 32 | "title": "Welcome to LynxHub Version 3!", 33 | "titleColor": "success", 34 | "icon": "https://raw.githubusercontent.com/KindaBrazy/LynxHub/refs/heads/master/resources/icon.png", 35 | "description": [ 36 | { 37 | "text": "I hope you enjoy LynxHub v3's new features! Thanks for staying with the app. I hope it keeps being useful to you." 38 | } 39 | ] 40 | }, 41 | { 42 | "id": "collect_errors", 43 | "title": "Help Improve LynxHub!", 44 | "icon": "🪲", 45 | "description": [ 46 | { 47 | "text": "From Version 3, LynxHub will collect anonymous error reports. This helps me fix critical bugs and make the app better for you." 48 | }, 49 | { 50 | "text": "You can turn this on or off anytime in the Settings." 51 | } 52 | ], 53 | "buttons": [ 54 | { 55 | "title": "⚙️ Settings", 56 | "destination": "settings" 57 | } 58 | ] 59 | } 60 | ] -------------------------------------------------------------------------------- /src/renderer/src/App/Components/Modals/CardInfo/UseCardInfoApi.tsx: -------------------------------------------------------------------------------- 1 | import {isEmpty} from 'lodash'; 2 | import {useEffect} from 'react'; 3 | 4 | import {CardInfoApi, CardInfoCallback, CardInfoDescriptions} from '../../../../../../cross/plugin/ModuleTypes'; 5 | import {getCardMethod, useAllCardMethods} from '../../../Modules/ModuleLoader'; 6 | import rendererIpc from '../../../RendererIpc'; 7 | 8 | export default function useCardInfoApi( 9 | cardId: string, 10 | setOpenFolders: (folders: string[] | undefined) => void, 11 | setCardInfoDescriptions: (descriptions: CardInfoDescriptions) => void, 12 | dir?: string, 13 | ) { 14 | const allMethods = useAllCardMethods(); 15 | 16 | useEffect(() => { 17 | if (!isEmpty(cardId)) { 18 | const api: CardInfoApi = { 19 | installationFolder: dir, 20 | ipc: { 21 | on(channel: string, listener: any): () => void { 22 | return window.electron.ipcRenderer.on(channel, listener); 23 | }, 24 | send(channel: string, ...args: any[]) { 25 | return window.electron.ipcRenderer.send(channel, ...args); 26 | }, 27 | invoke(channel: string, ...args: any[]): Promise { 28 | return window.electron.ipcRenderer.invoke(channel, ...args); 29 | }, 30 | }, 31 | storage: { 32 | get: (key: string) => rendererIpc.storage.getCustom(key), 33 | set: (key: string, data: any) => rendererIpc.storage.setCustom(key, data), 34 | }, 35 | getFolderSize: (dir: string) => rendererIpc.file.calcFolderSize(dir), 36 | getFolderCreationTime: (dir: string) => rendererIpc.moduleApi.getFolderCreationTime(dir), 37 | getLastPulledDate: (dir: string) => rendererIpc.moduleApi.getLastPulledDate(dir), 38 | getCurrentReleaseTag: (dir: string) => rendererIpc.moduleApi.getCurrentReleaseTag(dir), 39 | }; 40 | 41 | const callBack: CardInfoCallback = {setOpenFolders, setDescription: setCardInfoDescriptions}; 42 | 43 | getCardMethod(allMethods, cardId, 'cardInfo')?.(api, callBack); 44 | } 45 | }, [cardId, dir, allMethods]); 46 | } 47 | --------------------------------------------------------------------------------