├── 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 |
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 | }>
24 | Plugins Page
25 |
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 | } fullWidth>
26 | Reset to Defaults
27 |
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 |
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 | }
27 | onPress={() => window.open(ISSUE_PAGE)}
28 | endContent={}
29 | className="mt-4 font-medium transition-all duration-200 hover:shadow-lg"
30 | fullWidth>
31 | Open an Issue
32 |
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 | }>
44 | {currentPath}
45 |
46 |
47 | }>
48 | Change (Restart Required)
49 |
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 |
--------------------------------------------------------------------------------