├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs ├── adding-icons.md └── icons.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── scripts └── optimize-svg.js ├── src-tauri ├── .gitignore ├── Cargo.toml ├── build.rs ├── capabilities │ ├── default.json │ └── desktop.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ └── icon.png ├── src │ ├── cache.rs │ ├── dlc_manager.rs │ ├── installer.rs │ ├── main.rs │ └── searcher.rs └── tauri.conf.json ├── src ├── App.tsx ├── assets │ ├── fonts │ │ ├── Roboto.ttf │ │ ├── Satoshi.ttf │ │ └── WorkSans.ttf │ ├── react.svg │ └── screenshot.png ├── components │ ├── buttons │ │ ├── ActionButton.tsx │ │ ├── AnimatedCheckbox.tsx │ │ ├── Button.tsx │ │ └── index.ts │ ├── common │ │ ├── LoadingIndicator.tsx │ │ └── index.ts │ ├── dialogs │ │ ├── Dialog.tsx │ │ ├── DialogActions.tsx │ │ ├── DialogBody.tsx │ │ ├── DialogFooter.tsx │ │ ├── DialogHeader.tsx │ │ ├── DlcSelectionDialog.tsx │ │ ├── ProgressDialog.tsx │ │ └── index.ts │ ├── games │ │ ├── GameItem.tsx │ │ ├── GameList.tsx │ │ ├── ImagePreloader.tsx │ │ └── index.ts │ ├── icons │ │ ├── Icon.tsx │ │ ├── IconFactory.ts │ │ ├── brands │ │ │ ├── discord.svg │ │ │ ├── github.svg │ │ │ ├── index.ts │ │ │ ├── linux.svg │ │ │ ├── proton.svg │ │ │ ├── steam.svg │ │ │ └── windows.svg │ │ ├── index.ts │ │ └── ui │ │ │ ├── bold │ │ │ ├── arrow-up.svg │ │ │ ├── check.svg │ │ │ ├── close.svg │ │ │ ├── controller.svg │ │ │ ├── copy.svg │ │ │ ├── diamond.svg │ │ │ ├── download.svg │ │ │ ├── download1.svg │ │ │ ├── edit.svg │ │ │ ├── error.svg │ │ │ ├── index.ts │ │ │ ├── info.svg │ │ │ ├── layers.svg │ │ │ ├── refresh.svg │ │ │ ├── search.svg │ │ │ ├── trash.svg │ │ │ ├── warning.svg │ │ │ └── wine.svg │ │ │ └── outline │ │ │ ├── arrow-up.svg │ │ │ ├── check.svg │ │ │ ├── close.svg │ │ │ ├── controller.svg │ │ │ ├── copy.svg │ │ │ ├── diamond.svg │ │ │ ├── download.svg │ │ │ ├── download1.svg │ │ │ ├── edit.svg │ │ │ ├── error.svg │ │ │ ├── index.ts │ │ │ ├── info.svg │ │ │ ├── layers.svg │ │ │ ├── refresh.svg │ │ │ ├── search.svg │ │ │ ├── trash.svg │ │ │ ├── warning.svg │ │ │ └── wine.svg │ ├── layout │ │ ├── AnimatedBackground.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Header.tsx │ │ ├── InitialLoadingScreen.tsx │ │ ├── Sidebar.tsx │ │ └── index.ts │ ├── notifications │ │ ├── Toast.tsx │ │ ├── ToastContainer.tsx │ │ └── index.ts │ └── updater │ │ ├── UpdateNotifier.tsx │ │ └── index.ts ├── contexts │ ├── AppContext.tsx │ ├── AppProvider.tsx │ ├── index.ts │ └── useAppContext.ts ├── hooks │ ├── index.ts │ ├── useAppLogic.ts │ ├── useDlcManager.ts │ ├── useGameActions.ts │ ├── useGames.ts │ ├── useToasts.ts │ └── useUpdateChecker.ts ├── main.tsx ├── services │ ├── ImageService.ts │ └── index.ts ├── styles │ ├── abstracts │ │ ├── _fonts.scss │ │ ├── _index.scss │ │ ├── _layout.scss │ │ ├── _mixins.scss │ │ ├── _reset.scss │ │ └── _variables.scss │ ├── components │ │ ├── buttons │ │ │ ├── _action_button.scss │ │ │ ├── _animated_checkbox.scss │ │ │ ├── _button.scss │ │ │ └── _index.scss │ │ ├── common │ │ │ ├── _index.scss │ │ │ ├── _loading.scss │ │ │ └── _updater.scss │ │ ├── dialogs │ │ │ ├── _dialog.scss │ │ │ ├── _dlc_dialog.scss │ │ │ ├── _index.scss │ │ │ └── _progress_dialog.scss │ │ ├── games │ │ │ ├── _gamecard.scss │ │ │ ├── _gamelist.scss │ │ │ └── _index.scss │ │ ├── icons │ │ │ ├── _icon.scss │ │ │ └── _index.scss │ │ ├── layout │ │ │ ├── _background.scss │ │ │ ├── _header.scss │ │ │ ├── _index.scss │ │ │ ├── _loading_screen.scss │ │ │ └── _sidebar.scss │ │ └── notifications │ │ │ ├── _index.scss │ │ │ └── _toast.scss │ ├── main.scss │ ├── pages │ │ └── _home.scss │ └── themes │ │ ├── _dark.scss │ │ └── _index.scss ├── types │ ├── DlcInfo.ts │ ├── Game.ts │ ├── index.ts │ └── svg.d.ts ├── utils │ ├── helpers.ts │ └── index.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help improve CreamLinux 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: 'Novattz' 7 | --- 8 | 9 | ## Bug Description 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps To Reproduce 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected Behavior 21 | 22 | A clear and concise description of what you expected to happen. 23 | 24 | ## Screenshots 25 | 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | ## System Information 29 | 30 | - OS: [e.g. Ubuntu 22.04, Arch Linux, etc.] 31 | - Desktop Environment: [e.g. GNOME, KDE, etc.] 32 | - CreamLinux Version: [e.g. 0.1.0] 33 | - Steam Version: [e.g. latest] 34 | 35 | ## Game Information 36 | 37 | - Game name: 38 | - Game ID (if known): 39 | - Native Linux or Proton: 40 | - Steam installation path: 41 | 42 | ## Additional Context 43 | 44 | Add any other context about the problem here. 45 | 46 | ## Logs 47 | 48 | If possible, include the contents of `~/.cache/creamlinux/creamlinux.log` or attach the file. 49 | 50 | ``` 51 | Paste log content here 52 | ``` 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for CreamLinux 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: 'Novattz' 7 | --- 8 | 9 | ## Feature Description 10 | 11 | A clear and concise description of what you want to happen. 12 | 13 | ## Problem This Feature Solves 14 | 15 | Is your feature request related to a problem? Please describe. 16 | Ex. I'm always frustrated when [...] 17 | 18 | ## Alternatives You've Considered 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | ## Additional Context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | 26 | ## Implementation Ideas (Optional) 27 | 28 | If you have any ideas on how this feature could be implemented, please share them here. 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | docs 14 | *.local 15 | *.lock 16 | .env 17 | CHANGELOG.md 18 | scripts/prepare-release.js 19 | scripts/update-server.js 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | .DS_Store 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | src-tauri/target -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tickbase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CreamLinux 2 | 3 | CreamLinux is a GUI application for Linux that simplifies the management of DLC in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games) and SmokeAPI (for Windows games running through Proton). 4 | 5 | ## Watch the demo here: 6 | [![Watch the demo](./src/assets/screenshot.png)](https://www.youtube.com/watch?v=ZunhZnKFLlg) 7 | ## Beta Status 8 | 9 | ⚠️ **IMPORTANT**: CreamLinux is currently in BETA. This means: 10 | 11 | - Some features may be incomplete or subject to change 12 | - You might encounter bugs or unexpected behavior 13 | - The application is under active development 14 | - Your feedback and bug reports are invaluable 15 | 16 | While the core functionality is working, please be aware that this is an early release. Im continuously working to improve stability, add features, and enhance the user experience. Please report any issues you encounter on [GitHub Issues page](https://github.com/Novattz/creamlinux-installer/issues). 17 | 18 | ## Features 19 | 20 | - **Auto-discovery**: Automatically finds Steam games installed on your system 21 | - **Native support**: Installs CreamLinux for native Linux games 22 | - **Proton support**: Installs SmokeAPI for Windows games running through Proton 23 | - **DLC management**: Easily select which DLCs to enable 24 | - **Modern UI**: Clean, responsive interface that's easy to use 25 | 26 | ## Installation 27 | 28 | ### AppImage (Recommended) 29 | 30 | 1. Download the latest `CreamLinux.AppImage` from the [Releases](https://github.com/Novattz/creamlinux-installer/releases) page 31 | 2. Make it executable: 32 | ```bash 33 | chmod +x CreamLinux.AppImage 34 | ``` 35 | 3. Run it: 36 | ```bash 37 | ./CreamLinux.AppImage 38 | ``` 39 | 40 | For Nvidia users use this command: 41 | ``` 42 | WEBKIT_DISABLE_DMABUF_RENDERER=1 ./creamlinux.appimage 43 | ``` 44 | 45 | ### Building from Source 46 | 47 | #### Prerequisites 48 | 49 | - Rust 1.77.2 or later 50 | - Node.js 18 or later 51 | - npm or yarn 52 | 53 | #### Steps 54 | 55 | 1. Clone the repository: 56 | 57 | ```bash 58 | git clone https://github.com/novattz/creamlinux.git 59 | cd creamlinux 60 | ``` 61 | 62 | 2. Install dependencies: 63 | 64 | ```bash 65 | npm install # or yarn 66 | ``` 67 | 68 | 3. Build the application: 69 | 70 | ```bash 71 | NO_STRIP=true npm run tauri build 72 | ``` 73 | 74 | 4. The compiled binary will be available in `src-tauri/target/release/creamlinux` 75 | 76 | ### Desktop Integration 77 | 78 | If you're using the AppImage version, you can integrate it into your desktop environment: 79 | 80 | 1. Create a desktop entry file: 81 | 82 | ```bash 83 | mkdir -p ~/.local/share/applications 84 | ``` 85 | 86 | 2. Create `~/.local/share/applications/creamlinux.desktop` with the following content (adjust the path to your AppImage): 87 | 88 | ``` 89 | [Desktop Entry] 90 | Name=Creamlinux 91 | Exec=/absolute/path/to/CreamLinux.AppImage 92 | Icon=/absolute/path/to/creamlinux-icon.png 93 | Type=Application 94 | Categories=Game;Utility; 95 | Comment=DLC Manager for Steam games on Linux 96 | ``` 97 | 98 | 3. Update your desktop database so creamlinux appears in your app launcher: 99 | 100 | ```bash 101 | update-desktop-database ~/.local/share/applications 102 | ``` 103 | 104 | ## Troubleshooting 105 | 106 | ### Common Issues 107 | 108 | - **Game doesn't load**: Make sure the launch options are correctly set in Steam 109 | - **DLCs not showing up**: Try refreshing the game list and reinstalling 110 | - **Cannot find Steam**: Ensure Steam is installed and you've launched it at least once (Flatpak is not supported yet) 111 | 112 | ### Debug Logs 113 | 114 | Logs are stored at: `~/.cache/creamlinux/creamlinux.log` 115 | 116 | ## License 117 | 118 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. 119 | 120 | ## Credits 121 | 122 | - [Creamlinux](https://github.com/anticitizn/creamlinux) - Native DLC support 123 | - [SmokeAPI](https://github.com/acidicoala/SmokeAPI) - Proton support 124 | - [Tauri](https://tauri.app/) - Framework for building the desktop application 125 | - [React](https://reactjs.org/) - UI library 126 | -------------------------------------------------------------------------------- /docs/adding-icons.md: -------------------------------------------------------------------------------- 1 | # Adding New Icons to Creamlinux 2 | 3 | This guide explains how to add new icons to the Creamlinux project. 4 | 5 | ## Prerequisites 6 | 7 | - Basic knowledge of SVG files 8 | - Node.js and npm installed 9 | - Creamlinux project set up 10 | 11 | ## Step 1: Find or Create SVG Icons 12 | 13 | You can: 14 | 15 | - Create your own SVG icons using tools like Figma, Sketch, or Illustrator 16 | - Download icons from libraries like Heroicons, Material Icons, or Feather Icons 17 | - Use existing SVG files 18 | 19 | Ideally, icons should: 20 | 21 | - Be 24x24px or have a viewBox of "0 0 24 24" 22 | - Have a consistent style with existing icons 23 | - Use stroke-width of 2 for outline variants 24 | - Use solid fills for bold variants 25 | 26 | ## Step 2: Optimize SVG Files 27 | 28 | We have a script to optimize SVG files for the icon system: 29 | 30 | ```bash 31 | # Install dependencies 32 | npm install 33 | 34 | # Optimize a single SVG 35 | npm run optimize-svg path/to/icon.svg 36 | 37 | # Optimize all SVGs in a directory 38 | npm run optimize-svg src/components/icons/ui/outline 39 | ``` 40 | 41 | The optimizer will: 42 | 43 | - Remove unnecessary attributes 44 | - Set the viewBox to "0 0 24 24" 45 | - Add currentColor for fills/strokes for proper color inheritance 46 | - Remove width and height attributes for flexible sizing 47 | 48 | ## Step 3: Add SVG Files to the Project 49 | 50 | 1. Decide if your icon is a "bold" (filled) or "outline" (stroked) variant 51 | 2. Place the file in the appropriate directory: 52 | - For outline variants: `src/components/icons/ui/outline/` 53 | - For bold variants: `src/components/icons/ui/bold/` 54 | 3. Use a descriptive name like `download.svg` or `settings.svg` 55 | 56 | ## Step 4: Export the Icons 57 | 58 | 1. Open the index.ts file in the respective directory: 59 | 60 | - `src/components/icons/ui/outline/index.ts` for outline variants 61 | - `src/components/icons/ui/bold/index.ts` for bold variants 62 | 63 | 2. Add an export statement for your new icon: 64 | 65 | ```typescript 66 | // For outline variant 67 | export { ReactComponent as NewIconOutlineIcon } from './new-icon.svg' 68 | 69 | // For bold variant 70 | export { ReactComponent as NewIconBoldIcon } from './new-icon.svg' 71 | ``` 72 | 73 | Use a consistent naming pattern: 74 | 75 | - CamelCase 76 | - Descriptive name 77 | - Suffix with BoldIcon or OutlineIcon based on variant 78 | 79 | ## Step 5: Use the Icon in Your Components 80 | 81 | Now you can use your new icon in any component: 82 | 83 | ```tsx 84 | import { Icon } from '@/components/icons' 85 | import { NewIconOutlineIcon, NewIconBoldIcon } from '@/components/icons' 86 | 87 | // In your component: 88 | 89 | 90 | ``` 91 | 92 | ## Best Practices 93 | 94 | 1. **Create both variants**: When possible, create both bold and outline variants for consistency. 95 | 96 | 2. **Use semantic names**: Name icons based on their meaning, not appearance (e.g., "success" instead of "checkmark"). 97 | 98 | 3. **Be consistent**: Follow the existing icon style for visual harmony. 99 | 100 | 4. **Test different sizes**: Ensure icons look good at all standard sizes: xs, sm, md, lg, xl. 101 | 102 | 5. **Optimize manually if needed**: Sometimes automatic optimization may not work perfectly. You might need to manually edit SVG files. 103 | 104 | 6. **Add accessibility**: When using icons, provide proper accessibility: 105 | 106 | ```tsx 107 | 108 | ``` 109 | 110 | ## Troubleshooting 111 | 112 | **Problem**: Icon doesn't change color with CSS 113 | **Solution**: Make sure your SVG uses `currentColor` for fill or stroke 114 | 115 | **Problem**: Icon looks pixelated 116 | **Solution**: Ensure your SVG has a proper viewBox attribute 117 | 118 | **Problem**: Icon sizing is inconsistent 119 | **Solution**: Use the standard size props (xs, sm, md, lg, xl) instead of custom sizes 120 | 121 | **Problem**: SVG has complex gradients or effects that don't render correctly 122 | **Solution**: Simplify the SVG design; complex effects aren't ideal for UI icons 123 | 124 | ## Additional Resources 125 | 126 | - [SVGR documentation](https://react-svgr.com/docs/what-is-svgr/) 127 | - [SVGO documentation](https://github.com/svg/svgo) 128 | - [SVG MDN documentation](https://developer.mozilla.org/en-US/docs/Web/SVG) 129 | -------------------------------------------------------------------------------- /docs/icons.md: -------------------------------------------------------------------------------- 1 | # Icon Usage Methods 2 | 3 | There are two ways to use icons in Creamlinux, both fully supported and completely interchangeable. 4 | 5 | ## Method 1: Using Icon component with name prop 6 | 7 | This approach uses the `Icon` component with a `name` prop: 8 | 9 | ```tsx 10 | import { Icon, refresh, check, info, steam } from '@/components/icons' 11 | 12 | 13 | 14 | 15 | {/* Brand icons auto-detect the variant */} 16 | ``` 17 | 18 | ## Method 2: Using direct icon components 19 | 20 | This approach imports pre-configured icon components directly: 21 | 22 | ```tsx 23 | import { RefreshIcon, CheckBoldIcon, InfoIcon, SteamIcon } from '@/components/icons' 24 | 25 | {/* Outline variant */} 26 | {/* Bold variant */} 27 | 28 | {/* Brand icon */} 29 | ``` 30 | 31 | ## When to use each method 32 | 33 | ### Use Method 1 (Icon + name) when: 34 | 35 | - You have dynamic icon selection based on data or state 36 | - You want to keep your imports list shorter 37 | - You're working with icons in loops or maps 38 | - You want to change variants dynamically 39 | 40 | Example of dynamic icon selection: 41 | 42 | ```tsx 43 | import { Icon } from '@/components/icons' 44 | 45 | function StatusIndicator({ status }) { 46 | const iconName = 47 | status === 'success' 48 | ? 'Check' 49 | : status === 'warning' 50 | ? 'Warning' 51 | : status === 'error' 52 | ? 'Close' 53 | : 'Info' 54 | 55 | return 56 | } 57 | ``` 58 | 59 | ### Use Method 2 (direct components) when: 60 | 61 | - You want the most concise syntax 62 | - You're using a fixed set of icons that won't change 63 | - You want specific variants (like InfoBoldIcon vs InfoIcon) 64 | - You prefer more explicit component names in your JSX 65 | 66 | Example of fixed icon usage: 67 | 68 | ```tsx 69 | import { InfoIcon, CloseIcon } from '@/components/icons' 70 | 71 | function ModalHeader({ title, onClose }) { 72 | return ( 73 |
74 |
75 | 76 |

{title}

77 |
78 | 81 |
82 | ) 83 | } 84 | ``` 85 | 86 | ## Available Icon Component Exports 87 | 88 | ### UI Icons (Outline variant by default) 89 | 90 | ```tsx 91 | import { 92 | ArrowUpIcon, 93 | CheckIcon, 94 | CloseIcon, 95 | ControllerIcon, 96 | CopyIcon, 97 | DownloadIcon, 98 | EditIcon, 99 | InfoIcon, 100 | LayersIcon, 101 | RefreshIcon, 102 | SearchIcon, 103 | TrashIcon, 104 | WarningIcon, 105 | WineIcon, 106 | } from '@/components/icons' 107 | ``` 108 | 109 | ### Bold Variants 110 | 111 | ```tsx 112 | import { CheckBoldIcon, InfoBoldIcon, WarningBoldIcon } from '@/components/icons' 113 | ``` 114 | 115 | ### Brand Icons 116 | 117 | ```tsx 118 | import { DiscordIcon, GitHubIcon, LinuxIcon, SteamIcon, WindowsIcon } from '@/components/icons' 119 | ``` 120 | 121 | ## Combining Methods 122 | 123 | Both methods work perfectly together and can be mixed in the same component: 124 | 125 | ```tsx 126 | import { 127 | Icon, 128 | refresh, // Method 1 129 | CheckBoldIcon, // Method 2 130 | } from '@/components/icons' 131 | 132 | function MyComponent() { 133 | return ( 134 |
135 | 136 | 137 |
138 | ) 139 | } 140 | ``` 141 | 142 | ## Props are Identical 143 | 144 | Both methods accept the same props: 145 | 146 | ```tsx 147 | // These are equivalent: 148 | 149 | 150 | ``` 151 | 152 | Available props in both cases: 153 | 154 | - `size`: "xs" | "sm" | "md" | "lg" | "xl" | number 155 | - `variant`: "outline" | "bold" | "brand" (only for Icon + name method) 156 | - `fillColor`: CSS color string 157 | - `strokeColor`: CSS color string 158 | - `className`: CSS class string 159 | - `title`: Accessibility title 160 | - ...plus all standard SVG attributes 161 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist', 'node_modules', 'src-tauri/target'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 23 | }, 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Creamlinux 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "creamlinux", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "author": "Tickbase", 7 | "repository": "https://github.com/Novattz/creamlinux-installer", 8 | "license": "MIT", 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "tsc -b && vite build", 12 | "lint": "eslint .", 13 | "preview": "vite preview", 14 | "tauri": "tauri", 15 | "optimize-svg": "node scripts/optimize-svg.js", 16 | "prepare-release": "node scripts/prepare-release.js" 17 | }, 18 | "dependencies": { 19 | "@tauri-apps/api": "^2.5.0", 20 | "@tauri-apps/plugin-process": "^2.2.1", 21 | "@tauri-apps/plugin-updater": "^2.7.1", 22 | "react": "^19.0.0", 23 | "react-dom": "^19.0.0", 24 | "sass": "^1.89.0", 25 | "uuid": "^11.1.0" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.22.0", 29 | "@semantic-release/changelog": "^6.0.3", 30 | "@semantic-release/git": "^10.0.1", 31 | "@semantic-release/github": "^11.0.2", 32 | "@svgr/core": "^8.1.0", 33 | "@svgr/webpack": "^8.1.0", 34 | "@tauri-apps/cli": "^2.5.0", 35 | "@types/node": "^20.10.0", 36 | "@types/react": "^19.0.10", 37 | "@types/react-dom": "^19.0.4", 38 | "@vitejs/plugin-react": "^4.3.4", 39 | "dotenv": "^16.5.0", 40 | "eslint": "^9.22.0", 41 | "eslint-plugin-react-hooks": "^5.2.0", 42 | "eslint-plugin-react-refresh": "^0.4.19", 43 | "glob": "^11.0.2", 44 | "globals": "^16.0.0", 45 | "node-fetch": "^3.3.2", 46 | "sass-embedded": "^1.86.3", 47 | "semantic-release": "^24.2.4", 48 | "typescript": "~5.7.2", 49 | "typescript-eslint": "^8.26.1", 50 | "vite": "^6.3.5", 51 | "vite-plugin-svgr": "^4.3.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/optimize-svg.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * SVG Optimizer for Creamlinux 5 | * 6 | * This script optimizes SVG files for use in the icon system. 7 | * Run it with `node optimize-svg.js path/to/svg` 8 | */ 9 | 10 | import fs from 'fs' 11 | import path from 'path' 12 | import optimize from 'svgo' 13 | 14 | // Check if a file path is provided 15 | if (process.argv.length < 3) { 16 | console.error('Please provide a path to an SVG file or directory') 17 | process.exit(1) 18 | } 19 | 20 | const inputPath = process.argv[2] 21 | 22 | // SVGO configuration 23 | const svgoConfig = { 24 | plugins: [ 25 | { 26 | name: 'preset-default', 27 | params: { 28 | overrides: { 29 | // Keep viewBox attribute 30 | removeViewBox: false, 31 | // Don't remove IDs 32 | cleanupIDs: false, 33 | // Don't minify colors 34 | convertColors: false, 35 | }, 36 | }, 37 | }, 38 | // Add currentColor for path fill if not specified 39 | { 40 | name: 'addAttributesToSVGElement', 41 | params: { 42 | attributes: [ 43 | { 44 | fill: 'currentColor', 45 | }, 46 | ], 47 | }, 48 | }, 49 | // Remove width and height 50 | { 51 | name: 'removeAttrs', 52 | params: { 53 | attrs: ['width', 'height'], 54 | }, 55 | }, 56 | // Make sure viewBox is 0 0 24 24 for consistent sizing 57 | { 58 | name: 'addAttributesToSVGElement', 59 | params: { 60 | attributes: [ 61 | { 62 | viewBox: '0 0 24 24', 63 | }, 64 | ], 65 | }, 66 | }, 67 | ], 68 | } 69 | 70 | // Function to optimize a single SVG file 71 | function optimizeSVG(filePath) { 72 | try { 73 | const svg = fs.readFileSync(filePath, 'utf8') 74 | const result = optimize(svg, svgoConfig) 75 | 76 | // Write the optimized SVG back to the file 77 | fs.writeFileSync(filePath, result.data) 78 | console.log(`✅ Optimized: ${filePath}`) 79 | 80 | return true 81 | } catch (error) { 82 | console.error(`❌ Error optimizing ${filePath}:`, error) 83 | return false 84 | } 85 | } 86 | 87 | // Function to process a directory of SVG files 88 | function processDirectory(dirPath) { 89 | try { 90 | const files = fs.readdirSync(dirPath) 91 | let optimizedCount = 0 92 | 93 | for (const file of files) { 94 | const filePath = path.join(dirPath, file) 95 | const stat = fs.statSync(filePath) 96 | 97 | if (stat.isDirectory()) { 98 | // Recursively process subdirectories 99 | optimizedCount += processDirectory(filePath) 100 | } else if (path.extname(file).toLowerCase() === '.svg') { 101 | // Process SVG files 102 | if (optimizeSVG(filePath)) { 103 | optimizedCount++ 104 | } 105 | } 106 | } 107 | 108 | return optimizedCount 109 | } catch (error) { 110 | console.error(`Error processing directory ${dirPath}:`, error) 111 | return 0 112 | } 113 | } 114 | 115 | // Main execution 116 | try { 117 | const stat = fs.statSync(inputPath) 118 | 119 | if (stat.isDirectory()) { 120 | const count = processDirectory(inputPath) 121 | console.log(`\nOptimized ${count} SVG files in ${inputPath}`) 122 | } else if (path.extname(inputPath).toLowerCase() === '.svg') { 123 | optimizeSVG(inputPath) 124 | } else { 125 | console.error('The provided path is not an SVG file or directory') 126 | process.exit(1) 127 | } 128 | } catch (error) { 129 | console.error('Error:', error) 130 | process.exit(1) 131 | } 132 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/schemas 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "1.0.0" 4 | description = "DLC Manager for Steam games on Linux" 5 | authors = ["tickbase"] 6 | license = "MIT" 7 | repository = "https://github.com/Novattz/creamlinux-installer" 8 | edition = "2021" 9 | rust-version = "1.77.2" 10 | 11 | [build-dependencies] 12 | tauri-build = { version = "2.2.0", features = [] } 13 | 14 | [dependencies] 15 | serde_json = { version = "1.0", features = ["raw_value"] } 16 | serde = { version = "1.0", features = ["derive"] } 17 | regex = "1" 18 | xdg = "2" 19 | log = "0.4" 20 | log4rs = "1.2" 21 | reqwest = { version = "0.11", features = ["json"] } 22 | tokio = { version = "1", features = ["full"] } 23 | zip = "0.6" 24 | tempfile = "3.8" 25 | walkdir = "2.3" 26 | parking_lot = "0.12" 27 | tauri = { version = "2.5.0", features = [] } 28 | tauri-plugin-log = "2.0.0-rc" 29 | tauri-plugin-shell = "2.0.0-rc" 30 | tauri-plugin-dialog = "2.0.0-rc" 31 | tauri-plugin-fs = "2.0.0-rc" 32 | num_cpus = "1.16.0" 33 | tauri-plugin-process = "2" 34 | 35 | [features] 36 | custom-protocol = ["tauri/custom-protocol"] 37 | 38 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 39 | tauri-plugin-updater = "2" 40 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "enables the default permissions", 5 | "windows": ["main"], 6 | "permissions": ["core:default"] 7 | } 8 | -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "updater:default" 13 | ] 14 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/cache.rs: -------------------------------------------------------------------------------- 1 | // This is a placeholder file - cache functionality has been removed 2 | // and now only exists in memory within the App state 3 | 4 | pub fn cache_dlcs(_game_id: &str, _dlcs: &[crate::dlc_manager::DlcInfoWithState]) -> std::io::Result<()> { 5 | // This function is kept only for compatibility, but now does nothing 6 | // The DLCs are only cached in memory 7 | log::info!("Cache functionality has been removed - DLCs are only stored in memory"); 8 | Ok(()) 9 | } 10 | 11 | pub fn load_cached_dlcs(_game_id: &str) -> Option> { 12 | // This function is kept only for compatibility, but now always returns None 13 | log::info!("Cache functionality has been removed - DLCs are only stored in memory"); 14 | None 15 | } 16 | 17 | pub fn clear_all_caches() -> std::io::Result<()> { 18 | // This function is kept only for compatibility, but now does nothing 19 | log::info!("Cache functionality has been removed - DLCs are only stored in memory"); 20 | Ok(()) 21 | } -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", 3 | "build": { 4 | "frontendDist": "../dist", 5 | "devUrl": "http://localhost:1420", 6 | "beforeDevCommand": "npm run dev", 7 | "beforeBuildCommand": "npm run build" 8 | }, 9 | "bundle": { 10 | "active": true, 11 | "targets": "all", 12 | "category": "Utility", 13 | "createUpdaterArtifacts": true, 14 | "icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"] 15 | }, 16 | "productName": "Creamlinux", 17 | "mainBinaryName": "creamlinux", 18 | "version": "1.0.0", 19 | "identifier": "com.creamlinux.dev", 20 | "plugins": { 21 | "updater": { 22 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJDNEI1NzBBRDUxODQ3RjEKUldUeFJ4alZDbGRMTE5Vc241NG5yL080UklnaW1iUGdUWElPRXloRGtKZ3M2SWkzK0RGSDh3Q2kK", 23 | "endpoints": [ 24 | "https://github.com/Novattz/creamlinux-installer/releases/latest/download/latest.json" 25 | ], 26 | "windows": { 27 | "installMode": "passive" 28 | } 29 | } 30 | }, 31 | "app": { 32 | "withGlobalTauri": false, 33 | "windows": [ 34 | { 35 | "title": "Creamlinux", 36 | "width": 1000, 37 | "height": 700, 38 | "minWidth": 800, 39 | "minHeight": 600, 40 | "resizable": true, 41 | "fullscreen": false 42 | } 43 | ], 44 | "security": { 45 | "csp": null 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '@/contexts/useAppContext' 2 | import { UpdateNotifier } from '@/components/updater' 3 | import { useAppLogic } from '@/hooks' 4 | import './styles/main.scss' 5 | 6 | // Layout components 7 | import { Header, Sidebar, InitialLoadingScreen, ErrorBoundary } from '@/components/layout' 8 | import AnimatedBackground from '@/components/layout/AnimatedBackground' 9 | 10 | // Dialog components 11 | import { ProgressDialog, DlcSelectionDialog } from '@/components/dialogs' 12 | 13 | // Game components 14 | import { GameList } from '@/components/games' 15 | 16 | /** 17 | * Main application component 18 | */ 19 | function App() { 20 | // Get application logic from hook 21 | const { 22 | filter, 23 | setFilter, 24 | searchQuery, 25 | handleSearchChange, 26 | isInitialLoad, 27 | scanProgress, 28 | filteredGames, 29 | handleRefresh, 30 | isLoading, 31 | error, 32 | } = useAppLogic({ autoLoad: true }) 33 | 34 | // Get action handlers from context 35 | const { 36 | dlcDialog, 37 | handleDlcDialogClose, 38 | handleProgressDialogClose, 39 | progressDialog, 40 | handleGameAction, 41 | handleDlcConfirm, 42 | handleGameEdit, 43 | } = useAppContext() 44 | 45 | // Show loading screen during initial load 46 | if (isInitialLoad) { 47 | return 48 | } 49 | 50 | return ( 51 | 52 |
53 | {/* Animated background */} 54 | 55 | 56 | {/* Header with search */} 57 |
63 | 64 |
65 | {/* Sidebar for filtering */} 66 | 67 | 68 | {/* Show error or game list */} 69 | {error ? ( 70 |
71 |

Error Loading Games

72 |

{error}

73 | 74 |
75 | ) : ( 76 | 82 | )} 83 |
84 | 85 | {/* Progress Dialog */} 86 | 95 | 96 | {/* DLC Selection Dialog */} 97 | 108 | 109 | {/* Simple update notifier that uses toast - no UI component */} 110 | 111 |
112 |
113 | ) 114 | } 115 | 116 | export default App -------------------------------------------------------------------------------- /src/assets/fonts/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src/assets/fonts/Roboto.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Satoshi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src/assets/fonts/Satoshi.ttf -------------------------------------------------------------------------------- /src/assets/fonts/WorkSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src/assets/fonts/WorkSans.ttf -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Novattz/creamlinux-installer/116e2cfea0fdae0f9daffc7b483122d543b9a508/src/assets/screenshot.png -------------------------------------------------------------------------------- /src/components/buttons/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import Button, { ButtonVariant } from '../buttons/Button' 3 | import { Icon, layers, download } from '@/components/icons' 4 | 5 | // Define available action types 6 | export type ActionType = 'install_cream' | 'uninstall_cream' | 'install_smoke' | 'uninstall_smoke' 7 | 8 | interface ActionButtonProps { 9 | action: ActionType 10 | isInstalled: boolean 11 | isWorking: boolean 12 | onClick: () => void 13 | disabled?: boolean 14 | className?: string 15 | } 16 | 17 | /** 18 | * Specialized button for game installation actions 19 | */ 20 | const ActionButton: FC = ({ 21 | action, 22 | isInstalled, 23 | isWorking, 24 | onClick, 25 | disabled = false, 26 | className = '', 27 | }) => { 28 | // Determine button text based on state 29 | const getButtonText = () => { 30 | if (isWorking) return 'Working...' 31 | 32 | const isCream = action.includes('cream') 33 | const product = isCream ? 'CreamLinux' : 'SmokeAPI' 34 | 35 | return isInstalled ? `Uninstall ${product}` : `Install ${product}` 36 | } 37 | 38 | // Map to button variant 39 | const getButtonVariant = (): ButtonVariant => { 40 | // For uninstall actions, use danger variant 41 | if (isInstalled) return 'danger' 42 | // For install actions, use success variant 43 | return 'success' 44 | } 45 | 46 | // Select appropriate icon based on action type and state 47 | const getIconInfo = () => { 48 | const isCream = action.includes('cream') 49 | 50 | if (isInstalled) { 51 | // Uninstall actions 52 | return { name: layers, variant: 'bold' } 53 | } else { 54 | // Install actions 55 | return { name: download, variant: isCream ? 'bold' : 'outline' } 56 | } 57 | } 58 | 59 | const iconInfo = getIconInfo() 60 | 61 | return ( 62 | 75 | ) 76 | } 77 | 78 | export default ActionButton 79 | -------------------------------------------------------------------------------- /src/components/buttons/AnimatedCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, check } from '@/components/icons' 2 | 3 | interface AnimatedCheckboxProps { 4 | checked: boolean 5 | onChange: () => void 6 | label?: string 7 | sublabel?: string 8 | className?: string 9 | } 10 | 11 | /** 12 | * Animated checkbox component with optional label and sublabel 13 | */ 14 | const AnimatedCheckbox = ({ 15 | checked, 16 | onChange, 17 | label, 18 | sublabel, 19 | className = '', 20 | }: AnimatedCheckboxProps) => { 21 | return ( 22 | 36 | ) 37 | } 38 | 39 | export default AnimatedCheckbox 40 | -------------------------------------------------------------------------------- /src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ButtonHTMLAttributes } from 'react' 2 | 3 | export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning' 4 | export type ButtonSize = 'small' | 'medium' | 'large' 5 | 6 | interface ButtonProps extends ButtonHTMLAttributes { 7 | variant?: ButtonVariant 8 | size?: ButtonSize 9 | isLoading?: boolean 10 | leftIcon?: React.ReactNode 11 | rightIcon?: React.ReactNode 12 | fullWidth?: boolean 13 | } 14 | 15 | /** 16 | * Button component with different variants, sizes and states 17 | */ 18 | const Button: FC = ({ 19 | children, 20 | variant = 'primary', 21 | size = 'medium', 22 | isLoading = false, 23 | leftIcon, 24 | rightIcon, 25 | fullWidth = false, 26 | className = '', 27 | disabled, 28 | ...props 29 | }) => { 30 | // Size class mapping 31 | const sizeClass = { 32 | small: 'btn-sm', 33 | medium: 'btn-md', 34 | large: 'btn-lg', 35 | }[size] 36 | 37 | // Variant class mapping 38 | const variantClass = { 39 | primary: 'btn-primary', 40 | secondary: 'btn-secondary', 41 | danger: 'btn-danger', 42 | success: 'btn-success', 43 | warning: 'btn-warning', 44 | }[variant] 45 | 46 | return ( 47 | 64 | ) 65 | } 66 | 67 | export default Button 68 | -------------------------------------------------------------------------------- /src/components/buttons/index.ts: -------------------------------------------------------------------------------- 1 | // Export all button components 2 | export { default as Button } from './Button' 3 | export { default as ActionButton } from './ActionButton' 4 | export { default as AnimatedCheckbox } from './AnimatedCheckbox' 5 | 6 | // Export types 7 | export type { ButtonVariant, ButtonSize } from './Button' 8 | export type { ActionType } from './ActionButton' 9 | -------------------------------------------------------------------------------- /src/components/common/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export type LoadingType = 'spinner' | 'dots' | 'progress' 4 | export type LoadingSize = 'small' | 'medium' | 'large' 5 | 6 | interface LoadingIndicatorProps { 7 | size?: LoadingSize 8 | type?: LoadingType 9 | message?: string 10 | progress?: number 11 | className?: string 12 | } 13 | 14 | /** 15 | * Versatile loading indicator component 16 | * Supports multiple visual styles and sizes 17 | */ 18 | const LoadingIndicator = ({ 19 | size = 'medium', 20 | type = 'spinner', 21 | message, 22 | progress = 0, 23 | className = '', 24 | }: LoadingIndicatorProps) => { 25 | // Size class mapping 26 | const sizeClass = { 27 | small: 'loading-small', 28 | medium: 'loading-medium', 29 | large: 'loading-large', 30 | }[size] 31 | 32 | // Render loading indicator based on type 33 | const renderLoadingIndicator = (): ReactNode => { 34 | switch (type) { 35 | case 'spinner': 36 | return
37 | 38 | case 'dots': 39 | return ( 40 |
41 |
42 |
43 |
44 |
45 | ) 46 | 47 | case 'progress': 48 | return ( 49 |
50 |
51 |
55 |
56 | {progress > 0 &&
{Math.round(progress)}%
} 57 |
58 | ) 59 | 60 | default: 61 | return
62 | } 63 | } 64 | 65 | return ( 66 |
67 | {renderLoadingIndicator()} 68 | {message &&

{message}

} 69 |
70 | ) 71 | } 72 | 73 | export default LoadingIndicator 74 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LoadingIndicator } from './LoadingIndicator' 2 | 3 | export type { LoadingSize, LoadingType } from './LoadingIndicator' 4 | -------------------------------------------------------------------------------- /src/components/dialogs/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from 'react' 2 | 3 | export interface DialogProps { 4 | visible: boolean 5 | onClose?: () => void 6 | className?: string 7 | preventBackdropClose?: boolean 8 | children: ReactNode 9 | size?: 'small' | 'medium' | 'large' 10 | showAnimationOnUnmount?: boolean 11 | } 12 | 13 | /** 14 | * Base Dialog component that serves as a container for dialog content 15 | * Used with DialogHeader, DialogBody, and DialogFooter components 16 | */ 17 | const Dialog = ({ 18 | visible, 19 | onClose, 20 | className = '', 21 | preventBackdropClose = false, 22 | children, 23 | size = 'medium', 24 | showAnimationOnUnmount = true, 25 | }: DialogProps) => { 26 | const [showContent, setShowContent] = useState(false) 27 | const [shouldRender, setShouldRender] = useState(visible) 28 | 29 | // Handle visibility changes with animations 30 | useEffect(() => { 31 | if (visible) { 32 | setShouldRender(true) 33 | // Small delay to trigger entrance animation after component is mounted 34 | const timer = setTimeout(() => { 35 | setShowContent(true) 36 | }, 50) 37 | return () => clearTimeout(timer) 38 | } else if (showAnimationOnUnmount) { 39 | // First hide content with animation 40 | setShowContent(false) 41 | // Then unmount after animation completes 42 | const timer = setTimeout(() => { 43 | setShouldRender(false) 44 | }, 300) // Match this with your CSS transition duration 45 | return () => clearTimeout(timer) 46 | } else { 47 | // Immediately unmount without animation 48 | setShowContent(false) 49 | setShouldRender(false) 50 | } 51 | }, [visible, showAnimationOnUnmount]) 52 | 53 | const handleBackdropClick = (e: React.MouseEvent) => { 54 | if (e.target === e.currentTarget && !preventBackdropClose && onClose) { 55 | onClose() 56 | } 57 | } 58 | 59 | // Don't render anything if dialog shouldn't be shown 60 | if (!shouldRender) return null 61 | 62 | const sizeClass = { 63 | small: 'dialog-small', 64 | medium: 'dialog-medium', 65 | large: 'dialog-large', 66 | }[size] 67 | 68 | return ( 69 |
70 |
71 | {children} 72 |
73 |
74 | ) 75 | } 76 | 77 | export default Dialog 78 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogActions.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export interface DialogActionsProps { 4 | children: ReactNode 5 | className?: string 6 | align?: 'start' | 'center' | 'end' 7 | } 8 | 9 | /** 10 | * Actions container for dialog footers 11 | * Provides consistent spacing and alignment for action buttons 12 | */ 13 | const DialogActions = ({ children, className = '', align = 'end' }: DialogActionsProps) => { 14 | const alignClass = { 15 | start: 'justify-start', 16 | center: 'justify-center', 17 | end: 'justify-end', 18 | }[align] 19 | 20 | return
{children}
21 | } 22 | 23 | export default DialogActions 24 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogBody.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export interface DialogBodyProps { 4 | children: ReactNode 5 | className?: string 6 | } 7 | 8 | /** 9 | * Body component for dialogs 10 | * Contains the main content with scrolling capability 11 | */ 12 | const DialogBody = ({ children, className = '' }: DialogBodyProps) => { 13 | return
{children}
14 | } 15 | 16 | export default DialogBody 17 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogFooter.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export interface DialogFooterProps { 4 | children: ReactNode 5 | className?: string 6 | } 7 | 8 | /** 9 | * Footer component for dialogs 10 | * Contains action buttons and optional status information 11 | */ 12 | const DialogFooter = ({ children, className = '' }: DialogFooterProps) => { 13 | return
{children}
14 | } 15 | 16 | export default DialogFooter 17 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export interface DialogHeaderProps { 4 | children: ReactNode 5 | className?: string 6 | onClose?: () => void 7 | hideCloseButton?: boolean; 8 | } 9 | 10 | /** 11 | * Header component for dialogs 12 | * Contains the title and optional close button 13 | */ 14 | const DialogHeader = ({ children, className = '', onClose, hideCloseButton = false }: DialogHeaderProps) => { 15 | return ( 16 |
17 | {children} 18 | {onClose && !hideCloseButton && ( 19 | 22 | )} 23 |
24 | ) 25 | } 26 | 27 | export default DialogHeader 28 | -------------------------------------------------------------------------------- /src/components/dialogs/ProgressDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import Dialog from './Dialog' 3 | import DialogHeader from './DialogHeader' 4 | import DialogBody from './DialogBody' 5 | import DialogFooter from './DialogFooter' 6 | import DialogActions from './DialogActions' 7 | import { Button } from '@/components/buttons' 8 | 9 | export interface InstallationInstructions { 10 | type: string 11 | command: string 12 | game_title: string 13 | dlc_count?: number 14 | } 15 | 16 | export interface ProgressDialogProps { 17 | visible: boolean 18 | title: string 19 | message: string 20 | progress: number 21 | showInstructions?: boolean 22 | instructions?: InstallationInstructions 23 | onClose?: () => void 24 | } 25 | 26 | /** 27 | * ProgressDialog component 28 | * Shows installation progress with a progress bar and optional instructions 29 | */ 30 | const ProgressDialog = ({ 31 | visible, 32 | title, 33 | message, 34 | progress, 35 | showInstructions = false, 36 | instructions, 37 | onClose, 38 | }: ProgressDialogProps) => { 39 | const [copySuccess, setCopySuccess] = useState(false) 40 | 41 | const handleCopyCommand = () => { 42 | if (instructions?.command) { 43 | navigator.clipboard.writeText(instructions.command) 44 | setCopySuccess(true) 45 | 46 | // Reset the success message after 2 seconds 47 | setTimeout(() => { 48 | setCopySuccess(false) 49 | }, 2000) 50 | } 51 | } 52 | 53 | // Determine if we should show the copy button (for CreamLinux but not SmokeAPI) 54 | const showCopyButton = 55 | instructions?.type === 'cream_install' || instructions?.type === 'cream_uninstall' 56 | 57 | // Format instruction message based on type 58 | const getInstructionText = () => { 59 | if (!instructions) return null 60 | 61 | switch (instructions.type) { 62 | case 'cream_install': 63 | return ( 64 | <> 65 |

66 | In Steam, set the following launch options for{' '} 67 | {instructions.game_title}: 68 |

69 | {instructions.dlc_count !== undefined && ( 70 |
71 | {instructions.dlc_count} DLCs have been enabled! 72 |
73 | )} 74 | 75 | ) 76 | case 'cream_uninstall': 77 | return ( 78 |

79 | For {instructions.game_title}, open Steam properties and remove the 80 | following launch option: 81 |

82 | ) 83 | case 'smoke_install': 84 | return ( 85 | <> 86 |

87 | SmokeAPI has been installed for {instructions.game_title} 88 |

89 | {instructions.dlc_count !== undefined && ( 90 |
91 | {instructions.dlc_count} Steam API files have been patched. 92 |
93 | )} 94 | 95 | ) 96 | case 'smoke_uninstall': 97 | return ( 98 |

99 | SmokeAPI has been uninstalled from {instructions.game_title} 100 |

101 | ) 102 | default: 103 | return ( 104 |

105 | Done processing {instructions.game_title} 106 |

107 | ) 108 | } 109 | } 110 | 111 | // Determine the CSS class for the command box based on instruction type 112 | const getCommandBoxClass = () => { 113 | return instructions?.type.includes('smoke') ? 'command-box command-box-smoke' : 'command-box' 114 | } 115 | 116 | // Determine if close button should be enabled 117 | const isCloseButtonEnabled = showInstructions || progress >= 100 118 | 119 | return ( 120 | 126 | 127 |

{title}

128 |
129 | 130 | 131 |

{message}

132 | 133 |
134 |
135 |
136 |
{Math.round(progress)}%
137 | 138 | {showInstructions && instructions && ( 139 |
140 |

141 | {instructions.type.includes('uninstall') 142 | ? 'Uninstallation Instructions' 143 | : 'Installation Instructions'} 144 |

145 | {getInstructionText()} 146 | 147 |
148 |
{instructions.command}
149 |
150 |
151 | )} 152 | 153 | 154 | 155 | 156 | {showInstructions && showCopyButton && ( 157 | 160 | )} 161 | 162 | {isCloseButtonEnabled && ( 163 | 166 | )} 167 | 168 | 169 |
170 | ) 171 | } 172 | 173 | export default ProgressDialog 174 | -------------------------------------------------------------------------------- /src/components/dialogs/index.ts: -------------------------------------------------------------------------------- 1 | // Export all dialog components 2 | export { default as Dialog } from './Dialog' 3 | export { default as DialogHeader } from './DialogHeader' 4 | export { default as DialogBody } from './DialogBody' 5 | export { default as DialogFooter } from './DialogFooter' 6 | export { default as DialogActions } from './DialogActions' 7 | export { default as ProgressDialog } from './ProgressDialog' 8 | export { default as DlcSelectionDialog } from './DlcSelectionDialog' 9 | 10 | // Export types 11 | export type { DialogProps } from './Dialog' 12 | export type { DialogHeaderProps } from './DialogHeader' 13 | export type { DialogBodyProps } from './DialogBody' 14 | export type { DialogFooterProps } from './DialogFooter' 15 | export type { DialogActionsProps } from './DialogActions' 16 | export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog' 17 | export type { DlcSelectionDialogProps } from './DlcSelectionDialog' 18 | -------------------------------------------------------------------------------- /src/components/games/GameItem.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { findBestGameImage } from '@/services/ImageService' 3 | import { Game } from '@/types' 4 | import { ActionButton, ActionType, Button } from '@/components/buttons' 5 | 6 | interface GameItemProps { 7 | game: Game 8 | onAction: (gameId: string, action: ActionType) => Promise 9 | onEdit?: (gameId: string) => void 10 | } 11 | 12 | /** 13 | * Individual game card component 14 | * Displays game information and action buttons 15 | */ 16 | const GameItem = ({ game, onAction, onEdit }: GameItemProps) => { 17 | const [imageUrl, setImageUrl] = useState(null) 18 | const [isLoading, setIsLoading] = useState(true) 19 | const [hasError, setHasError] = useState(false) 20 | 21 | useEffect(() => { 22 | // Function to fetch the game cover/image 23 | const fetchGameImage = async () => { 24 | // First check if we already have it (to prevent flickering on re-renders) 25 | if (imageUrl) return 26 | 27 | setIsLoading(true) 28 | try { 29 | // Try to find the best available image for this game 30 | const bestImageUrl = await findBestGameImage(game.id) 31 | 32 | if (bestImageUrl) { 33 | setImageUrl(bestImageUrl) 34 | setHasError(false) 35 | } else { 36 | setHasError(true) 37 | } 38 | } catch (error) { 39 | console.error('Error fetching game image:', error) 40 | setHasError(true) 41 | } finally { 42 | setIsLoading(false) 43 | } 44 | } 45 | 46 | if (game.id) { 47 | fetchGameImage() 48 | } 49 | }, [game.id, imageUrl]) 50 | 51 | // Determine if we should show CreamLinux buttons (only for native games) 52 | const shouldShowCream = game.native === true 53 | 54 | // Determine if we should show SmokeAPI buttons (only for non-native games with API files) 55 | const shouldShowSmoke = !game.native && game.api_files && game.api_files.length > 0 56 | 57 | // Check if this is a Proton game without API files 58 | const isProtonNoApi = !game.native && (!game.api_files || game.api_files.length === 0) 59 | 60 | const handleCreamAction = () => { 61 | if (game.installing) return 62 | const action: ActionType = game.cream_installed ? 'uninstall_cream' : 'install_cream' 63 | onAction(game.id, action) 64 | } 65 | 66 | const handleSmokeAction = () => { 67 | if (game.installing) return 68 | const action: ActionType = game.smoke_installed ? 'uninstall_smoke' : 'install_smoke' 69 | onAction(game.id, action) 70 | } 71 | 72 | // Handle edit button click 73 | const handleEdit = () => { 74 | if (onEdit && game.cream_installed) { 75 | onEdit(game.id) 76 | } 77 | } 78 | 79 | // Determine background image 80 | const backgroundImage = 81 | !isLoading && imageUrl 82 | ? `url(${imageUrl})` 83 | : hasError 84 | ? 'linear-gradient(135deg, #232323, #1A1A1A)' 85 | : 'linear-gradient(135deg, #232323, #1A1A1A)' 86 | 87 | return ( 88 |
96 |
97 |
98 | 99 | {game.native ? 'Native' : 'Proton'} 100 | 101 | {game.cream_installed && CreamLinux} 102 | {game.smoke_installed && SmokeAPI} 103 |
104 | 105 |
106 |

{game.title}

107 |
108 | 109 |
110 | {/* Show CreamLinux button only for native games */} 111 | {shouldShowCream && ( 112 | 118 | )} 119 | 120 | {/* Show SmokeAPI button only for Proton/Windows games with API files */} 121 | {shouldShowSmoke && ( 122 | 128 | )} 129 | 130 | {/* Show message for Proton games without API files */} 131 | {isProtonNoApi && ( 132 |
133 | Steam API DLL not found 134 | 142 |
143 | )} 144 | 145 | {/* Edit button - only enabled if CreamLinux is installed */} 146 | {game.cream_installed && ( 147 | 157 | )} 158 |
159 |
160 |
161 | ) 162 | } 163 | 164 | export default GameItem 165 | -------------------------------------------------------------------------------- /src/components/games/GameList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from 'react' 2 | import { GameItem, ImagePreloader } from '@/components/games' 3 | import { ActionType } from '@/components/buttons' 4 | import { Game } from '@/types' 5 | import LoadingIndicator from '../common/LoadingIndicator' 6 | 7 | interface GameListProps { 8 | games: Game[] 9 | isLoading: boolean 10 | onAction: (gameId: string, action: ActionType) => Promise 11 | onEdit?: (gameId: string) => void 12 | } 13 | 14 | /** 15 | * Main game list component 16 | * Displays games in a grid with search and filtering applied 17 | */ 18 | const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => { 19 | const [imagesPreloaded, setImagesPreloaded] = useState(false) 20 | 21 | // Sort games alphabetically by title 22 | const sortedGames = useMemo(() => { 23 | return [...games].sort((a, b) => a.title.localeCompare(b.title)) 24 | }, [games]) 25 | 26 | // Reset preloaded state when games change 27 | useEffect(() => { 28 | setImagesPreloaded(false) 29 | }, [games]) 30 | 31 | const handlePreloadComplete = () => { 32 | setImagesPreloaded(true) 33 | } 34 | 35 | if (isLoading) { 36 | return ( 37 |
38 | 39 |
40 | ) 41 | } 42 | 43 | return ( 44 |
45 |

Games ({games.length})

46 | 47 | {!imagesPreloaded && games.length > 0 && ( 48 | game.id)} 50 | onComplete={handlePreloadComplete} 51 | /> 52 | )} 53 | 54 | {games.length === 0 ? ( 55 |
No games found
56 | ) : ( 57 |
58 | {sortedGames.map((game) => ( 59 | 60 | ))} 61 |
62 | )} 63 |
64 | ) 65 | } 66 | 67 | export default GameList 68 | -------------------------------------------------------------------------------- /src/components/games/ImagePreloader.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { findBestGameImage } from '@/services/ImageService' 3 | 4 | interface ImagePreloaderProps { 5 | gameIds: string[] 6 | onComplete?: () => void 7 | } 8 | 9 | /** 10 | * Preloads game images to prevent flickering 11 | * Only used internally by GameList component 12 | */ 13 | const ImagePreloader = ({ gameIds, onComplete }: ImagePreloaderProps) => { 14 | useEffect(() => { 15 | const preloadImages = async () => { 16 | try { 17 | // Only preload the first batch for performance (10 images max) 18 | const batchToPreload = gameIds.slice(0, 10) 19 | 20 | // Track loading progress 21 | let loadedCount = 0 22 | const totalImages = batchToPreload.length 23 | 24 | // Load images in parallel 25 | await Promise.allSettled( 26 | batchToPreload.map(async (id) => { 27 | await findBestGameImage(id) 28 | loadedCount++ 29 | 30 | // If all images are loaded, call onComplete 31 | if (loadedCount === totalImages && onComplete) { 32 | onComplete() 33 | } 34 | }) 35 | ) 36 | 37 | // Fallback if Promise.allSettled doesn't trigger onComplete 38 | if (onComplete) { 39 | onComplete() 40 | } 41 | } catch (error) { 42 | console.error('Error preloading images:', error) 43 | // Continue even if there's an error 44 | if (onComplete) { 45 | onComplete() 46 | } 47 | } 48 | } 49 | 50 | if (gameIds.length > 0) { 51 | preloadImages() 52 | } else if (onComplete) { 53 | onComplete() 54 | } 55 | }, [gameIds, onComplete]) 56 | 57 | // Invisible component that just handles preloading 58 | return null 59 | } 60 | 61 | export default ImagePreloader 62 | -------------------------------------------------------------------------------- /src/components/games/index.ts: -------------------------------------------------------------------------------- 1 | // Export all game components 2 | export { default as GameList } from './GameList' 3 | export { default as GameItem } from './GameItem' 4 | export { default as ImagePreloader } from './ImagePreloader' 5 | -------------------------------------------------------------------------------- /src/components/icons/Icon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Icon component for displaying SVG icons with standardized properties 3 | */ 4 | import React from 'react' 5 | 6 | // Import all icon variants 7 | import * as OutlineIcons from './ui/outline' 8 | import * as BoldIcons from './ui/bold' 9 | import * as BrandIcons from './brands' 10 | 11 | export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number 12 | export type IconVariant = 'bold' | 'outline' | 'brand' | undefined 13 | export type IconName = keyof typeof OutlineIcons | keyof typeof BoldIcons | keyof typeof BrandIcons 14 | 15 | // Sets of icon names by type for determining default variants 16 | const BRAND_ICON_NAMES = new Set(Object.keys(BrandIcons)) 17 | const OUTLINE_ICON_NAMES = new Set(Object.keys(OutlineIcons)) 18 | const BOLD_ICON_NAMES = new Set(Object.keys(BoldIcons)) 19 | 20 | export interface IconProps extends React.SVGProps { 21 | /** Name of the icon to render */ 22 | name: IconName | string 23 | /** Size of the icon */ 24 | size?: IconSize 25 | /** Icon variant - bold, outline, or brand */ 26 | variant?: IconVariant | string 27 | /** Title for accessibility */ 28 | title?: string 29 | /** Fill color (if not specified by the SVG itself) */ 30 | fillColor?: string 31 | /** Stroke color (if not specified by the SVG itself) */ 32 | strokeColor?: string 33 | /** Additional CSS class names */ 34 | className?: string 35 | } 36 | 37 | /** 38 | * Convert size string to pixel value 39 | */ 40 | const getSizeValue = (size: IconSize): string => { 41 | if (typeof size === 'number') return `${size}px` 42 | 43 | const sizeMap: Record = { 44 | xs: '12px', 45 | sm: '16px', 46 | md: '24px', 47 | lg: '32px', 48 | xl: '48px', 49 | } 50 | 51 | return sizeMap[size] || sizeMap.md 52 | } 53 | 54 | /** 55 | * Gets the icon component based on name and variant 56 | */ 57 | const getIconComponent = ( 58 | name: string, 59 | variant: IconVariant | string 60 | ): React.ComponentType> | null => { 61 | // Normalize variant to ensure it's a valid IconVariant 62 | const normalizedVariant = 63 | variant === 'bold' || variant === 'outline' || variant === 'brand' 64 | ? (variant as IconVariant) 65 | : undefined 66 | 67 | // Try to get the icon from the specified variant 68 | switch (normalizedVariant) { 69 | case 'outline': 70 | return OutlineIcons[name as keyof typeof OutlineIcons] || null 71 | case 'bold': 72 | return BoldIcons[name as keyof typeof BoldIcons] || null 73 | case 'brand': 74 | return BrandIcons[name as keyof typeof BrandIcons] || null 75 | default: 76 | // If no variant specified, determine best default 77 | if (BRAND_ICON_NAMES.has(name)) { 78 | return BrandIcons[name as keyof typeof BrandIcons] || null 79 | } else if (OUTLINE_ICON_NAMES.has(name)) { 80 | return OutlineIcons[name as keyof typeof OutlineIcons] || null 81 | } else if (BOLD_ICON_NAMES.has(name)) { 82 | return BoldIcons[name as keyof typeof BoldIcons] || null 83 | } 84 | return null 85 | } 86 | } 87 | 88 | /** 89 | * Icon component 90 | * Renders SVG icons with consistent sizing and styling 91 | */ 92 | const Icon: React.FC = ({ 93 | name, 94 | size = 'md', 95 | variant, 96 | title, 97 | fillColor, 98 | strokeColor, 99 | className = '', 100 | ...rest 101 | }) => { 102 | // Determine default variant based on icon type if no variant provided 103 | let defaultVariant: IconVariant | string = variant 104 | 105 | if (defaultVariant === undefined) { 106 | if (BRAND_ICON_NAMES.has(name)) { 107 | defaultVariant = 'brand' 108 | } else { 109 | defaultVariant = 'bold' // Default to bold for non-brand icons 110 | } 111 | } 112 | 113 | // Get the icon component based on name and variant 114 | let finalIconComponent = getIconComponent(name, defaultVariant) 115 | let finalVariant = defaultVariant 116 | 117 | // Try fallbacks if the icon doesn't exist in the requested variant 118 | if (!finalIconComponent && defaultVariant !== 'outline') { 119 | finalIconComponent = getIconComponent(name, 'outline') 120 | finalVariant = 'outline' 121 | } 122 | 123 | if (!finalIconComponent && defaultVariant !== 'bold') { 124 | finalIconComponent = getIconComponent(name, 'bold') 125 | finalVariant = 'bold' 126 | } 127 | 128 | if (!finalIconComponent && defaultVariant !== 'brand') { 129 | finalIconComponent = getIconComponent(name, 'brand') 130 | finalVariant = 'brand' 131 | } 132 | 133 | // If still no icon found, return null 134 | if (!finalIconComponent) { 135 | console.warn(`Icon not found: ${name} (${defaultVariant})`) 136 | return null 137 | } 138 | 139 | const sizeValue = getSizeValue(size) 140 | const combinedClassName = `icon icon-${size} icon-${finalVariant} ${className}`.trim() 141 | 142 | const IconComponentToRender = finalIconComponent 143 | 144 | return ( 145 | 156 | ) 157 | } 158 | 159 | export default Icon 160 | -------------------------------------------------------------------------------- /src/components/icons/IconFactory.ts: -------------------------------------------------------------------------------- 1 | // BROKEN 2 | 3 | //import React from 'react' 4 | //import Icon from './Icon' 5 | //import type { IconProps, IconVariant } from './Icon' 6 | // 7 | //export const createIconComponent = ( 8 | // name: string, 9 | // defaultVariant: IconVariant = 'outline' 10 | //): React.FC> => { 11 | // const IconComponent: React.FC> = (props) => { 12 | // return ( 13 | // 18 | // ) 19 | // } 20 | // 21 | // IconComponent.displayName = `${name}Icon` 22 | // return IconComponent 23 | //} 24 | // 25 | -------------------------------------------------------------------------------- /src/components/icons/brands/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/brands/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/brands/index.ts: -------------------------------------------------------------------------------- 1 | // Bold variant icons 2 | export { ReactComponent as Linux } from './linux.svg' 3 | export { ReactComponent as Steam } from './steam.svg' 4 | export { ReactComponent as Windows } from './windows.svg' 5 | export { ReactComponent as Github } from './github.svg' 6 | export { ReactComponent as Discord } from './discord.svg' 7 | export { ReactComponent as Proton } from './proton.svg' 8 | -------------------------------------------------------------------------------- /src/components/icons/brands/linux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/brands/steam.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/brands/windows.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | // import { createIconComponent } from './IconFactory' <-- Broken atm 2 | export { default as Icon } from './Icon' 3 | export type { IconProps, IconSize, IconVariant, IconName } from './Icon' 4 | 5 | // Re-export all icons by category for convenience 6 | import * as OutlineIcons from './ui/outline' 7 | import * as BoldIcons from './ui/bold' 8 | import * as BrandIcons from './brands' 9 | 10 | export { OutlineIcons, BoldIcons, BrandIcons } 11 | 12 | // Export individual icon names as constants 13 | // UI icons 14 | export const arrowUp = 'ArrowUp' 15 | export const check = 'Check' 16 | export const close = 'Close' 17 | export const controller = 'Controller' 18 | export const copy = 'Copy' 19 | export const download = 'Download' 20 | export const download1 = 'Download1' 21 | export const edit = 'Edit' 22 | export const error = 'Error' 23 | export const info = 'Info' 24 | export const layers = 'Layers' 25 | export const refresh = 'Refresh' 26 | export const search = 'Search' 27 | export const trash = 'Trash' 28 | export const warning = 'Warning' 29 | export const wine = 'Wine' 30 | export const diamond = 'Diamond' 31 | 32 | // Brand icons 33 | export const discord = 'Discord' 34 | export const github = 'GitHub' 35 | export const linux = 'Linux' 36 | export const proton = 'Proton' 37 | export const steam = 'Steam' 38 | export const windows = 'Windows' 39 | 40 | // Keep the IconNames object for backward compatibility and autocompletion 41 | export const IconNames = { 42 | // UI icons 43 | ArrowUp: arrowUp, 44 | Check: check, 45 | Close: close, 46 | Controller: controller, 47 | Copy: copy, 48 | Download: download, 49 | Download1: download1, 50 | Edit: edit, 51 | Error: error, 52 | Info: info, 53 | Layers: layers, 54 | Refresh: refresh, 55 | Search: search, 56 | Trash: trash, 57 | Warning: warning, 58 | Wine: wine, 59 | Diamond: diamond, 60 | 61 | // Brand icons 62 | Discord: discord, 63 | GitHub: github, 64 | Linux: linux, 65 | Proton: proton, 66 | Steam: steam, 67 | Windows: windows, 68 | } as const 69 | 70 | // Export direct icon components using createIconComponent from IconFactory 71 | // UI icons (outline variant by default) 72 | //export const ArrowUpIcon = createIconComponent(arrowUp, 'outline') 73 | //export const CheckIcon = createIconComponent(check, 'outline') 74 | //export const CloseIcon = createIconComponent(close, 'outline') 75 | //export const ControllerIcon = createIconComponent(controller, 'outline') 76 | //export const CopyIcon = createIconComponent(copy, 'outline') 77 | //export const DownloadIcon = createIconComponent(download, 'outline') 78 | //export const Download1Icon = createIconComponent(download1, 'outline') 79 | //export const EditIcon = createIconComponent(edit, 'outline') 80 | //export const ErrorIcon = createIconComponent(error, 'outline') 81 | //export const InfoIcon = createIconComponent(info, 'outline') 82 | //export const LayersIcon = createIconComponent(layers, 'outline') 83 | //export const RefreshIcon = createIconComponent(refresh, 'outline') 84 | //export const SearchIcon = createIconComponent(search, 'outline') 85 | //export const TrashIcon = createIconComponent(trash, 'outline') 86 | //export const WarningIcon = createIconComponent(warning, 'outline') 87 | //export const WineIcon = createIconComponent(wine, 'outline') 88 | 89 | // Brand icons 90 | //export const DiscordIcon = createIconComponent(discord, 'brand') 91 | //export const GitHubIcon = createIconComponent(github, 'brand') 92 | //export const LinuxIcon = createIconComponent(linux, 'brand') 93 | //export const SteamIcon = createIconComponent(steam, 'brand') 94 | //export const WindowsIcon = createIconComponent(windows, 'brand') 95 | 96 | // Bold variants for common icons 97 | //export const CheckBoldIcon = createIconComponent(check, 'bold') 98 | //export const InfoBoldIcon = createIconComponent(info, 'bold') 99 | //export const WarningBoldIcon = createIconComponent(warning, 'bold') 100 | //export const ErrorBoldIcon = createIconComponent(error, 'bold') 101 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/controller.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/diamond.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/download1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/index.ts: -------------------------------------------------------------------------------- 1 | // Bold variant icons 2 | export { ReactComponent as ArrowUp } from './arrow-up.svg' 3 | export { ReactComponent as Check } from './check.svg' 4 | export { ReactComponent as Close } from './close.svg' 5 | export { ReactComponent as Controller } from './controller.svg' 6 | export { ReactComponent as Copy } from './copy.svg' 7 | export { ReactComponent as Download } from './download.svg' 8 | export { ReactComponent as Download1 } from './download1.svg' 9 | export { ReactComponent as Edit } from './edit.svg' 10 | export { ReactComponent as Error } from './error.svg' 11 | export { ReactComponent as Info } from './info.svg' 12 | export { ReactComponent as Layers } from './layers.svg' 13 | export { ReactComponent as Refresh } from './refresh.svg' 14 | export { ReactComponent as Search } from './search.svg' 15 | export { ReactComponent as Trash } from './trash.svg' 16 | export { ReactComponent as Warning } from './warning.svg' 17 | export { ReactComponent as Wine } from './wine.svg' 18 | export { ReactComponent as Diamond } from './diamond.svg' 19 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/layers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/bold/wine.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/controller.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/diamond.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/download1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/index.ts: -------------------------------------------------------------------------------- 1 | // Outline variant icons 2 | export { ReactComponent as ArrowUp } from './arrow-up.svg' 3 | export { ReactComponent as Check } from './check.svg' 4 | export { ReactComponent as Close } from './close.svg' 5 | export { ReactComponent as Controller } from './controller.svg' 6 | export { ReactComponent as Copy } from './copy.svg' 7 | export { ReactComponent as Download } from './download.svg' 8 | export { ReactComponent as Download1 } from './download1.svg' 9 | export { ReactComponent as Edit } from './edit.svg' 10 | export { ReactComponent as Error } from './error.svg' 11 | export { ReactComponent as Info } from './info.svg' 12 | export { ReactComponent as Layers } from './layers.svg' 13 | export { ReactComponent as Refresh } from './refresh.svg' 14 | export { ReactComponent as Search } from './search.svg' 15 | export { ReactComponent as Trash } from './trash.svg' 16 | export { ReactComponent as Warning } from './warning.svg' 17 | export { ReactComponent as Wine } from './wine.svg' 18 | export { ReactComponent as Diamond } from './diamond.svg' 19 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/layers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/ui/outline/wine.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/layout/AnimatedBackground.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * Animated background component that draws an interactive particle effect 5 | */ 6 | const AnimatedBackground = () => { 7 | const canvasRef = useRef(null) 8 | 9 | useEffect(() => { 10 | const canvas = canvasRef.current 11 | if (!canvas) return 12 | 13 | const ctx = canvas.getContext('2d') 14 | if (!ctx) return 15 | 16 | // Set canvas size to match window 17 | const setCanvasSize = () => { 18 | canvas.width = window.innerWidth 19 | canvas.height = window.innerHeight 20 | } 21 | 22 | setCanvasSize() 23 | window.addEventListener('resize', setCanvasSize) 24 | 25 | // Create particles 26 | const particles: Particle[] = [] 27 | const particleCount = 30 28 | 29 | interface Particle { 30 | x: number 31 | y: number 32 | size: number 33 | speedX: number 34 | speedY: number 35 | opacity: number 36 | color: string 37 | } 38 | 39 | // Color palette matching theme 40 | const colors = [ 41 | 'rgba(74, 118, 196, 0.5)', // primary blue 42 | 'rgba(155, 125, 255, 0.5)', // purple 43 | 'rgba(251, 177, 60, 0.5)', // gold 44 | ] 45 | 46 | // Create initial particles 47 | for (let i = 0; i < particleCount; i++) { 48 | particles.push({ 49 | x: Math.random() * canvas.width, 50 | y: Math.random() * canvas.height, 51 | size: Math.random() * 3 + 1, 52 | speedX: Math.random() * 0.2 - 0.1, 53 | speedY: Math.random() * 0.2 - 0.1, 54 | opacity: Math.random() * 0.07 + 0.03, 55 | color: colors[Math.floor(Math.random() * colors.length)], 56 | }) 57 | } 58 | 59 | // Animation loop 60 | const animate = () => { 61 | // Clear canvas with transparent black to create fade effect 62 | ctx.fillStyle = 'rgba(15, 15, 15, 0.1)' 63 | ctx.fillRect(0, 0, canvas.width, canvas.height) 64 | 65 | // Update and draw particles 66 | particles.forEach((particle) => { 67 | // Update position 68 | particle.x += particle.speedX 69 | particle.y += particle.speedY 70 | 71 | // Wrap around edges 72 | if (particle.x < 0) particle.x = canvas.width 73 | if (particle.x > canvas.width) particle.x = 0 74 | if (particle.y < 0) particle.y = canvas.height 75 | if (particle.y > canvas.height) particle.y = 0 76 | 77 | // Draw particle 78 | ctx.beginPath() 79 | ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2) 80 | ctx.fillStyle = particle.color.replace('0.5', `${particle.opacity}`) 81 | ctx.fill() 82 | 83 | // Connect particles that are close to each other 84 | particles.forEach((otherParticle) => { 85 | const dx = particle.x - otherParticle.x 86 | const dy = particle.y - otherParticle.y 87 | const distance = Math.sqrt(dx * dx + dy * dy) 88 | 89 | if (distance < 100) { 90 | ctx.beginPath() 91 | ctx.strokeStyle = particle.color.replace('0.5', `${particle.opacity * 0.5}`) 92 | ctx.lineWidth = 0.2 93 | ctx.moveTo(particle.x, particle.y) 94 | ctx.lineTo(otherParticle.x, otherParticle.y) 95 | ctx.stroke() 96 | } 97 | }) 98 | }) 99 | 100 | requestAnimationFrame(animate) 101 | } 102 | 103 | // Start animation 104 | animate() 105 | 106 | // Cleanup 107 | return () => { 108 | window.removeEventListener('resize', setCanvasSize) 109 | } 110 | }, []) 111 | 112 | return