├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── request-a-feature.md ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── UNLICENSE ├── biome.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── rust-toolchain.toml ├── scripts ├── installer.nsi └── windows_bundle.bat ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ └── icon.ico ├── icons_store │ ├── macos │ │ ├── 128x128.png │ │ └── icon.ico │ └── windows │ │ ├── 128x128.png │ │ └── icon.ico ├── src │ └── main.rs └── tauri.conf.json ├── src ├── App.css ├── App.jsx ├── Globals.jsx ├── SideBar.jsx ├── Text.js ├── components │ ├── GameCardSideBar.jsx │ ├── GameCards.jsx │ ├── Hotkeys.jsx │ └── LanguageSelector.jsx ├── index.jsx ├── libraries │ ├── Icons.jsx │ ├── fuzzysearch.js │ └── parseVDF.js └── modals │ ├── EditFolder.jsx │ ├── EditGame.jsx │ ├── GamePopUp.jsx │ ├── Loading.jsx │ ├── NewFolder.jsx │ ├── NewGame.jsx │ ├── Notepad.jsx │ └── Settings.jsx ├── tailwind.config.js └── vite.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: File a bug report to help improve the app! 4 | title: '' 5 | labels: '' 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 | **More Info (please complete the following information):** 27 | - Version of the app [e.g. 1.0.0] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request-a-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request a Feature 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 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | z 4 | *.exe 5 | .env 6 | .env* 7 | upx.exe 8 | builds 9 | log 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # clear contributing guide 2 | 3 | thank you so much for considering contributing to 'clear'! not only will you be 4 | helping create an amazing experience for people who play video games, but also 5 | contribute to a body that is open source and dedicated to the public domain. 6 | 7 | ## if you would like to contribute to the translations: 8 | 9 | you would need to look at the file 10 | [Text.js](https://github.com/adithyasource/clear/blob/main/src/Text.js). here 11 | you can find the translations for each language. some languages were originally 12 | generated using google translate in order to get the ball rolling. 13 | 14 | | language | status | 15 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | 16 | | french | ✅ completed (by [@jer3m01](https://github.com/adithyasource/clear/pull/2)) | 17 | | russian | ✅ completed (by [@vladbrox](https://github.com/adithyasource/clear/issues/3)) | 18 | | japanese | google translate | 19 | | spanish | google translate | 20 | | hindi | ✅ completed (by [me](https://github.com/adithyasource/clear/commit/27fb8cf35fa3cbf12e3599de5067d64a83d3aed4), please feel free to improve) | 21 | 22 | to update the file with more accurate translations you'll have to fork the 23 | repository and create a new branch with your changes after which you can create 24 | a pull request.\ 25 | \ 26 | you can add a new language by adding a new simple 2-3 letter key to the JSON for 27 | every language.\ 28 | \ 29 | for example, to add a new language, say hebrew (shortened to he), change all 30 | text snippets like this 31 | 32 | ``` 33 | "import Steam games": { 34 | jp: "Steam ゲームをインポートする", 35 | . 36 | . 37 | . 38 | he: "Your translation goes here" 39 | }, 40 | ``` 41 | 42 | if you would like to contribute to the translations but do not know how to do so 43 | by modifying JSON or using Git/GitHub, you can create a 44 | [new issue](https://github.com/adithyasource/clear/issues) with all the 45 | improved/new translations for all the text. 46 | 47 | ## if you would like to contribute to improving the code: 48 | 49 | a bunch of ideas and features that i'm working on / will work on are mentioned 50 | in 51 | [this github projects kanban board](https://github.com/users/adithyasource/projects/3/views/9). 52 | if you find something that interests you, it'd be great if you could implement 53 | it! if you need any assistance, you can always open up a 54 | [new issue](https://github.com/adithyasource/clear/issues)\ 55 | \ 56 | please make sure that you do not implement any major new features that are not 57 | on the kanban board before opening an issue discussing it. this is in order to 58 | make that that the clear's original purpose of being clean, minimalistic and 59 | simple to use stays true.\ 60 | \ 61 | you'll have to fork the repository and create a new branch with your changes 62 | after which you can create a pull request.\ 63 | \ 64 | if you find any bugs, you can always open a 65 | [new issue](https://github.com/adithyasource/clear/issues) or fix the bug 66 | yourself! 67 | 68 | ### getting started 69 | 70 | in order to set up your dev environment, you need to have nodejs, npm and rust 71 | installed\ 72 | \ 73 | when that is done you can run the code in development mode, by doing the 74 | following: 75 | 76 | ```sh 77 | git clone https://github.com/adithyasource/clear 78 | cd clear 79 | npm install 80 | npm run tauri dev 81 | ``` 82 | 83 | to build the app, you can run `npm run tauri build` 84 | 85 | ### formatting and linting style 86 | 87 | the project uses the [biome](https://biomejs.dev/) toolchain in order 88 | to format and lint code. make sure that it is installed and set up in your 89 | editor.\ 90 | \ 91 | install biome globally by using ```npm install -g @biomejs/biome``` and setup 92 | up biome for 93 | [neovim (with lspconfig)](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#biome), 94 | [vscode](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) 95 | or [zed](https://biomejs.dev/reference/zed/)\ 96 | \ 97 | make sure to format and lint the code before commiting, either by using your 98 | editor's features or by running ```npm lint``` and ```npm format``` 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clear 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
download> product page
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
major update currently in active development!
20 | 21 | ![image](https://github.com/adithyasource/clear/assets/140549783/bd4dae97-4b0b-466f-a1ff-570ae05a0eec) 22 | 23 | ## feedback 24 | 25 | if there are any features or bug fixes you'd like to suggest, please open a new 26 | issue in the "Issue" tab. 27 | 28 | ## contributing (+ getting started) / translations 29 | 30 | if you'd like to contribute to the code or help translating "clear", guidelines 31 | and getting started guides are outlined in 32 | [CONTRIBUTING.md](https://github.com/adithyasource/clear/blob/main/CONTRIBUTING.md) 33 | 34 | ## other repos for clear 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
clear-api steamgriddb proxy for clearclear-website landing page
44 | 45 | ## acknowledgments 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
techtaurisolidjstailwind
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
designbasicons
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
code snippetsfuzzy searchvalve vdf parser
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
apissteamgriddb
85 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space", 15 | "indentWidth": 2, 16 | "lineWidth": 120 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "nursery": { 26 | "useSortedClasses": "error" 27 | } 28 | } 29 | }, 30 | "javascript": { 31 | "formatter": { 32 | "quoteStyle": "double" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | clear 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clear", 3 | "version": "1.1.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "vite", 7 | "dev": "vite", 8 | "build": "cross-env GENERATE_SOURCEMAP=false vite build", 9 | "serve": "vite preview", 10 | "tauri": "tauri", 11 | "windows_build": "cargo tauri build --target x86_64-pc-windows-msvc -- -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort", 12 | "windows_bundle": ".\\scripts\\windows_bundle.bat", 13 | "format": "biome format --write ./src ", 14 | "lint": "biome check --fix --unsafe ./src" 15 | }, 16 | "license": "UNLICENSE", 17 | "dependencies": { 18 | "@tauri-apps/api": "^1.6.0", 19 | "solid-js": "^1.9.3" 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^1.6.3", 23 | "autoprefixer": "^10.4.20", 24 | "cross-env": "^7.0.3", 25 | "postcss": "^8.4.49", 26 | "tailwindcss": "^3.4.17", 27 | "vite": "^6.0.7", 28 | "vite-plugin-minify": "^2.1.0", 29 | "vite-plugin-solid": "^2.11.0" 30 | }, 31 | "packageManager": "pnpm@9.15.3" 32 | } 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-05-29" # The nightly release to use, you can update this to the most recent one if you want 3 | profile = "minimal" -------------------------------------------------------------------------------- /scripts/installer.nsi: -------------------------------------------------------------------------------- 1 | ; Setting metadata 2 | VIAddVersionKey "ProductName" "clear" 3 | VIAddVersionKey "FileDescription" "clear - video game library" 4 | VIProductVersion "1.1.0.0" 5 | VIAddVersionKey "FileVersion" "1.1.0" 6 | VIAddVersionKey "ProductVersion" "1.1.0" 7 | VIAddVersionKey "LegalCopyright" "Unlicense" 8 | 9 | ;-------------------------------- 10 | 11 | 12 | ; The name of the installer 13 | Name "clear" 14 | 15 | 16 | ; The setup icon 17 | Icon "${NSISDIR}\Contrib\Graphics\Icons\nsis1-install.ico" 18 | 19 | ; The uninstaller icon 20 | UninstallIcon "${NSISDIR}\Contrib\Graphics\Icons\nsis1-uninstall.ico" 21 | 22 | ; The default installation directory 23 | InstallDir $PROGRAMFILES\clear 24 | 25 | ; Registry key to check for directory (for writing over the old install) 26 | InstallDirRegKey HKLM "Software\clear" "Install_Dir" 27 | 28 | 29 | ;-------------------------------- 30 | 31 | 32 | ; Loads and sets languages 33 | 34 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\English.nlf" 35 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\Japanese.nlf" 36 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\Spanish.nlf" 37 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\Hindi.nlf" 38 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\Russian.nlf" 39 | LoadLanguageFile "${NSISDIR}\Contrib\Language files\French.nlf" 40 | 41 | LangString clearRunning ${LANG_ENGLISH} "'clear' is open ~ please close it and try again!" 42 | LangString clearRunning ${LANG_JAPANESE} "「クリア」が開いています ~ 閉じてもう一度お試しください。" 43 | LangString clearRunning ${LANG_SPANISH} "'clear' está abierto ~ ¡ciérrelo e inténtelo de nuevo!" 44 | LangString clearRunning ${LANG_HINDI} "'clear' खुला है ~ कृपया इसे बंद करें और पुनः प्रयास करें!" 45 | LangString clearRunning ${LANG_RUSSIAN} "« clear » открыто ~ закройте его и повторите попытку!" 46 | LangString clearRunning ${LANG_FRENCH} "« clear » est ouvert ~ veuillez le fermer et réessayer !" 47 | 48 | LangString clearLaunch ${LANG_ENGLISH} "launch 'clear' now?" 49 | LangString clearLaunch ${LANG_JAPANESE} "今すぐ「クリア」を起動しますか?" 50 | LangString clearLaunch ${LANG_SPANISH} "¿Lanzar 'clear' ahora?" 51 | LangString clearLaunch ${LANG_HINDI} "अभी 'clear' लॉन्च करें?" 52 | LangString clearLaunch ${LANG_RUSSIAN} "запустить « clear » сейчас?" 53 | LangString clearLaunch ${LANG_FRENCH} "lancer 'clear' maintenant ?" 54 | 55 | 56 | ;-------------------------------- 57 | 58 | 59 | ; Pages 60 | 61 | Page directory 62 | Page components 63 | Page instfiles 64 | 65 | UninstPage uninstConfirm 66 | UninstPage instfiles 67 | 68 | 69 | ;-------------------------------- 70 | 71 | 72 | Section "clear - video game library" 73 | 74 | ; Removes option to uncheck the app from installing 75 | SectionIn RO 76 | 77 | ; Set output path to the installation directory. 78 | SetOutPath $INSTDIR 79 | 80 | ; Put file there (you can add more File lines too) 81 | File "..\src-tauri\target\x86_64-pc-windows-msvc\release\clear.exe" 82 | 83 | ; Write the installation path into the registry 84 | WriteRegStr HKLM SOFTWARE\clear "Install_Dir" "$INSTDIR" 85 | 86 | ; Write the uninstall keys for Windows 87 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\clear" "DisplayName" "clear" 88 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\clear" "UninstallString" '"$INSTDIR\uninstall.exe"' 89 | WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\clear" "NoModify" 1 90 | WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\clear" "NoRepair" 1 91 | WriteUninstaller "$INSTDIR\uninstall.exe" 92 | 93 | SectionEnd 94 | 95 | 96 | ;-------------------------------- 97 | 98 | 99 | ; Following three sections are shown as checkboxes in the components page 100 | ; And their contents are executed if they are checked 101 | 102 | Section "desktop shortcut" 103 | 104 | ; Creates a shortcut on the desktop 105 | CreateShortCut "$DESKTOP\clear.lnk" "$INSTDIR\clear.exe" "" "$INSTDIR\clear.exe" 0 106 | 107 | SectionEnd 108 | 109 | Section "start menu shortcut" 110 | 111 | ; Creates start menu shortcut 112 | CreateDirectory "$SMPROGRAMS\clear" 113 | CreateShortcut "$SMPROGRAMS\clear\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 114 | CreateShortcut "$SMPROGRAMS\clear\clear.lnk" "$INSTDIR\clear.exe" "" "$INSTDIR\clear.exe" 0 115 | 116 | SectionEnd 117 | 118 | 119 | ;-------------------------------- 120 | 121 | 122 | ; Displays a dialogbox after installation 123 | 124 | Function .onInstSuccess 125 | MessageBox MB_YESNO "$(clearLaunch)" IDYES OpenApp IDNO NoOpen 126 | OpenApp: 127 | ExecShell "" '"$INSTDIR\clear.exe"' 128 | Goto EndDialog 129 | NoOpen: 130 | EndDialog: 131 | FunctionEnd 132 | 133 | 134 | ;-------------------------------- 135 | 136 | 137 | ; Runs when installer opens and asks to select language 138 | 139 | Function .onInit 140 | 141 | 142 | ;Language selection dialog 143 | 144 | Push "" 145 | Push ${LANG_ENGLISH} 146 | Push English 147 | Push ${LANG_JAPANESE} 148 | Push Japanese 149 | Push ${LANG_SPANISH} 150 | Push Spanish 151 | Push ${LANG_HINDI} 152 | Push Hindi 153 | Push ${LANG_RUSSIAN} 154 | Push Russian 155 | Push ${LANG_FRENCH} 156 | Push French 157 | Push A ; A means auto count languages 158 | ; for the auto count to work the first empty push (Push "") must remain 159 | LangDLL::LangDialog "installer language" "please select the language of the installer" 160 | 161 | Pop $LANGUAGE 162 | StrCmp $LANGUAGE "cancel" 0 +2 163 | Abort 164 | 165 | 166 | ; Checks if clear is already open 167 | 168 | 169 | FunctionEnd 170 | 171 | ; Function .onGUIInit 172 | ; FindWindow $0 "" "clear" 173 | ; StrCmp $0 0 notRunning 174 | ; MessageBox MB_OK|MB_ICONEXCLAMATION "$(clearRunning)" /SD IDOK 175 | ; Abort 176 | ; notRunning: 177 | ; FunctionEnd 178 | ;-------------------------------- 179 | 180 | 181 | ; Uninstaller 182 | 183 | Section "Uninstall" 184 | 185 | ; Remove registry keys 186 | DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\clear" 187 | DeleteRegKey HKLM SOFTWARE\clear 188 | 189 | ; Remove files and uninstaller 190 | Delete $INSTDIR\clear.exe 191 | Delete $INSTDIR\uninstall.exe 192 | 193 | ; Remove directories used (only deletes empty dirs) 194 | RMDir "$SMPROGRAMS\clear" 195 | RMDir "$INSTDIR" 196 | 197 | SectionEnd -------------------------------------------------------------------------------- /scripts/windows_bundle.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | upx --ultra-brute src-tauri\target\x86_64-pc-windows-msvc\release\clear.exe 3 | 4 | if not exist ".\builds\" mkdir ".\builds\" 5 | if not exist ".\builds\setup\" mkdir ".\builds\setup\" 6 | if not exist ".\builds\portable\" mkdir ".\builds\portable\" 7 | 8 | makensis "/XOutFile '..\builds\setup\clear v1.1.0 setup.exe'" scripts\installer.nsi 9 | copy "src-tauri\target\x86_64-pc-windows-msvc\release\clear.exe" "builds\portable\clear v1.1.0 portable.exe" 10 | echo ===---------------------------------------------------------------------------------------------------------------------------- 11 | echo === portable available in %cd%\builds\portable\ 12 | echo === setup available in %cd%\builds\setup\ 13 | echo === following are the tauri auto generated bundles 14 | echo ===---------------------------------------------------------------------------------------------------------------------------- 15 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clear" 3 | version = "1.1.0" 4 | description = "clear - video game library" 5 | authors = ["adithya.zip"] 6 | license = "Unlicense" 7 | repository = "https://github.com/adithyasource/clear" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "1.5.5", features = [] } 14 | 15 | [dependencies] 16 | tauri = { version = "1.8.1", features = [ "protocol-asset", "path-all", "fs-read-file", "fs-create-dir", "fs-copy-file", "fs-write-file", "dialog-open", "fs-exists", "objc-exception", "wry"], default-features = false} 17 | [target.'cfg(windows)'.dependencies] 18 | winreg = "0.52.0" 19 | 20 | [features] 21 | # this feature is used for production builds or when `devPath` points to the filesystem 22 | # DO NOT REMOVE!! 23 | custom-protocol = ["tauri/custom-protocol"] 24 | 25 | [profile.release] 26 | panic = "abort" # Strip expensive panic clean-up logic 27 | codegen-units = 1 # Compile crates one after another so the compiler can optimize better 28 | lto = true # Enables link to optimizations 29 | opt-level = "z" # Optimize for binary size 30 | strip = true # Remove debug symbols 31 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adithyasource/clear/001ca318c036ee8aee53fd649d43f09cce84d4d7/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adithyasource/clear/001ca318c036ee8aee53fd649d43f09cce84d4d7/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons_store/macos/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adithyasource/clear/001ca318c036ee8aee53fd649d43f09cce84d4d7/src-tauri/icons_store/macos/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons_store/macos/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adithyasource/clear/001ca318c036ee8aee53fd649d43f09cce84d4d7/src-tauri/icons_store/macos/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons_store/windows/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adithyasource/clear/001ca318c036ee8aee53fd649d43f09cce84d4d7/src-tauri/icons_store/windows/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons_store/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adithyasource/clear/001ca318c036ee8aee53fd649d43f09cce84d4d7/src-tauri/icons_store/windows/icon.ico -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // prevents additional console window on windows in release, do not remove!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use std::env; 5 | use std::fs; 6 | use std::io; 7 | use std::path::PathBuf; 8 | use std::process::{Command, Stdio}; 9 | use tauri::{Manager, Window}; 10 | #[cfg(target_os = "windows")] 11 | use winreg::enums::*; 12 | #[cfg(target_os = "windows")] 13 | use winreg::RegKey; 14 | 15 | #[tauri::command] 16 | fn open_location(location: &str) { 17 | let mut command = if cfg!(target_os = "windows") { 18 | let mut cmd = Command::new("cmd"); 19 | cmd.args(&["/C", "start", "", location]); 20 | cmd 21 | } else { 22 | let mut cmd = Command::new("open"); 23 | cmd.arg(location); 24 | cmd 25 | }; 26 | 27 | #[cfg(target_os = "windows")] 28 | { 29 | use std::os::windows::process::CommandExt; 30 | command.creation_flags(0x08000000); 31 | } 32 | 33 | let _ = command.stdout(Stdio::null()).stderr(Stdio::null()).spawn(); 34 | } 35 | 36 | #[tauri::command] 37 | fn close_app() { 38 | std::process::exit(0x0); 39 | } 40 | 41 | #[tauri::command] 42 | fn get_platform() -> String { 43 | let platform = env::consts::OS; 44 | return platform.to_string(); 45 | } 46 | 47 | #[cfg(target_os = "windows")] 48 | fn get_steam_path() -> io::Result { 49 | let hkcu = RegKey::predef(HKEY_CURRENT_USER); 50 | let steam_key = hkcu.open_subkey("SOFTWARE\\Valve\\Steam")?; 51 | let steam_path: String = steam_key.get_value("SteamPath")?; 52 | print!("found steam path: {}", steam_path); 53 | Ok(steam_path) 54 | } 55 | 56 | #[cfg(not(target_os = "windows"))] 57 | fn get_steam_path() -> io::Result { 58 | let home_dir = std::env::var("HOME").expect("HOME environment variable not set"); 59 | Ok(PathBuf::from(home_dir) 60 | .join("Library") 61 | .join("Application Support") 62 | .join("steam") 63 | .to_string_lossy() 64 | .to_string()) 65 | } 66 | 67 | #[tauri::command] 68 | fn read_steam_vdf() -> String { 69 | let vdf_location = match get_steam_path() { 70 | Ok(steam_path) => PathBuf::from(steam_path) 71 | .join("steamapps") 72 | .join("libraryfolders.vdf") 73 | .to_string_lossy() 74 | .to_string(), 75 | Err(err) => { 76 | if cfg!(target_os = "windows") { 77 | println!("error reading steam path: {}", err); 78 | PathBuf::from("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") 79 | .to_string_lossy() 80 | .to_string() 81 | } else { 82 | let home_dir = std::env::var("HOME").expect("HOME environment variable not set"); 83 | PathBuf::from(home_dir) 84 | .join("Library/Application Support/Steam/steamapps/libraryfolders.vdf") 85 | .to_string_lossy() 86 | .to_string() 87 | } 88 | } 89 | }; 90 | 91 | match fs::read_to_string(vdf_location) { 92 | Ok(file_contents) => file_contents.into(), 93 | Err(err) => { 94 | println!("error reading VDF file: {}", err); 95 | return "error".to_string(); 96 | } 97 | } 98 | } 99 | 100 | #[tauri::command] 101 | async fn show_window(window: Window) { 102 | window 103 | .get_window("main") 104 | .expect("no window labeled 'main' found") 105 | .show() 106 | .unwrap(); 107 | } 108 | 109 | #[tauri::command] 110 | fn download_image(link: &str, location: &str) { 111 | let command_str = format!("Invoke-WebRequest '{}' -Outfile '{}'", link, location); 112 | 113 | let mut command = if cfg!(target_os = "windows") { 114 | let mut cmd = Command::new("powershell"); 115 | cmd.arg("-Command").arg(command_str); 116 | cmd 117 | } else { 118 | let mut cmd = Command::new("curl"); 119 | cmd.args(&["-o", location, link]); 120 | cmd 121 | }; 122 | 123 | #[cfg(target_os = "windows")] 124 | { 125 | use std::os::windows::process::CommandExt; 126 | command.creation_flags(0x08000000); 127 | } 128 | 129 | let _ = command.stdout(Stdio::null()).stderr(Stdio::null()).spawn(); 130 | } 131 | 132 | #[tauri::command] 133 | fn delete_assets(hero_image: &str, grid_image: &str, logo: &str, icon: &str) { 134 | let files = [hero_image, grid_image, logo, icon]; 135 | 136 | for file in files.iter() { 137 | let mut command = if cfg!(target_os = "windows") { 138 | let command_str = format!(" Remove-Item -Force \"{}\"", file); 139 | 140 | let mut cmd = Command::new("powershell"); 141 | cmd.arg("-Command").arg(command_str); 142 | cmd 143 | } else { 144 | let mut cmd = Command::new("rm"); 145 | cmd.arg("-f").arg(file); 146 | cmd 147 | }; 148 | 149 | #[cfg(target_os = "windows")] 150 | { 151 | use std::os::windows::process::CommandExt; 152 | command.creation_flags(0x08000000); 153 | } 154 | 155 | let _ = command.stdout(Stdio::null()).stderr(Stdio::null()).spawn(); 156 | } 157 | } 158 | 159 | #[tauri::command] 160 | fn check_connection() -> String { 161 | let (command, args) = if cfg!(target_os = "windows") { 162 | ( 163 | "cmd", 164 | vec![ 165 | "/C", 166 | "ping -n 1 www.google.com > nul && echo true || echo false", 167 | ], 168 | ) 169 | } else { 170 | ( 171 | "sh", 172 | vec![ 173 | "-c", 174 | "ping -c 1 www.google.com > /dev/null && echo true || echo false", 175 | ], 176 | ) 177 | }; 178 | 179 | let output = Command::new(command) 180 | .args(args) 181 | .output() 182 | .expect("failed to execute process"); 183 | 184 | let output_str = String::from_utf8_lossy(&output.stdout); 185 | output_str.trim().to_string() 186 | } 187 | 188 | fn main() { 189 | tauri::Builder::default() 190 | .invoke_handler(tauri::generate_handler![ 191 | open_location, 192 | close_app, 193 | read_steam_vdf, 194 | show_window, 195 | download_image, 196 | check_connection, 197 | get_platform, 198 | delete_assets 199 | ]) 200 | .run(tauri::generate_context!()) 201 | .expect("error while running tauri application"); 202 | } 203 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "pnpm run dev", 4 | "beforeBuildCommand": "pnpm run build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist", 7 | "withGlobalTauri": false 8 | }, 9 | "package": { 10 | "productName": "clear", 11 | "version": "1.1.0" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "path": { 16 | "all": true 17 | }, 18 | "fs": { 19 | "readFile": true, 20 | "writeFile": true, 21 | "copyFile": true, 22 | "exists": true, 23 | "createDir": true, 24 | "scope": ["$APPDATA/**"] 25 | }, 26 | "dialog": { 27 | "open": true 28 | }, 29 | "protocol": { 30 | "asset": true, 31 | "assetScope": ["**"] 32 | } 33 | }, 34 | "bundle": { 35 | "active": true, 36 | "targets": ["nsis", "msi", "dmg", "app"], 37 | "identifier": "com.adithya.clear", 38 | "icon": ["icons/128x128.png", "icons/icon.ico"] 39 | }, 40 | "windows": [ 41 | { 42 | "title": "clear", 43 | "width": 1220, 44 | "height": 760, 45 | "fileDropEnabled": false, 46 | "minHeight": 390, 47 | "minWidth": 640, 48 | "theme": "Dark", 49 | "visible": false 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* setting up tailwind */ 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | /* tailwind styles (for inaccessable selectors in html) */ 8 | 9 | @layer components { 10 | html, 11 | body { 12 | @apply dark:bg-[#121212] bg-[#FFFFFC]; 13 | } 14 | 15 | [class*="tooltip-"]::after { 16 | @apply dark:bg-[#272727cc] bg-[#E8E8E8cc] dark:text-white text-black border-2 border-solid border-[#00000010] dark:border-[#ffffff10]; 17 | } 18 | 19 | *::-webkit-scrollbar-thumb { 20 | @apply bg-[#E8E8E8] dark:bg-[#1c1c1c]; 21 | } 22 | 23 | #sideBarFolders:hover::-webkit-scrollbar-thumb { 24 | @apply bg-[#E8E8E8] dark:bg-[#232323]; 25 | } 26 | } 27 | 28 | /* styling scrollbars */ 29 | 30 | #sideBarFolders:hover::-webkit-scrollbar { 31 | width: 5px; 32 | } 33 | 34 | #sideBarFolders:hover::-webkit-scrollbar { 35 | display: block; 36 | } 37 | 38 | #sideBarFolders:hover::-webkit-scrollbar-track { 39 | margin: 5px; 40 | } 41 | 42 | #sideBarFolders::-webkit-scrollbar { 43 | display: none; 44 | } 45 | 46 | .SGDBGamesContainer::-webkit-scrollbar { 47 | display: none; 48 | } 49 | 50 | ::-webkit-scrollbar { 51 | width: 10px; 52 | } 53 | 54 | ::-webkit-scrollbar-track { 55 | margin: 10px; 56 | background-color: #00000000; 57 | } 58 | 59 | /* animation keyframes */ 60 | 61 | @keyframes dialogFadeIn { 62 | from { 63 | opacity: 0; 64 | transform: scale(0.99); 65 | } 66 | 67 | to { 68 | opacity: 1; 69 | transform: scale(1); 70 | } 71 | } 72 | 73 | @keyframes dialogFadeOut { 74 | from { 75 | opacity: 1; 76 | transform: scale(1); 77 | } 78 | 79 | to { 80 | opacity: 0; 81 | transform: scale(0.99); 82 | } 83 | } 84 | 85 | @keyframes dialogFadeInShorter { 86 | from { 87 | opacity: 0.6; 88 | transform: scale(0.99); 89 | } 90 | 91 | to { 92 | opacity: 1; 93 | transform: scale(1); 94 | } 95 | } 96 | 97 | @keyframes loadingFadeOut { 98 | from { 99 | opacity: 1; 100 | } 101 | 102 | to { 103 | opacity: 0; 104 | } 105 | } 106 | 107 | @keyframes spin { 108 | 0% { 109 | transform: rotate(0deg); 110 | } 111 | 112 | 100% { 113 | transform: rotate(360deg); 114 | } 115 | } 116 | 117 | @keyframes normalFadeIn { 118 | from { 119 | opacity: 0; 120 | } 121 | 122 | to { 123 | opacity: 1; 124 | } 125 | } 126 | 127 | @keyframes normalFadeOut { 128 | from { 129 | opacity: 1; 130 | } 131 | 132 | to { 133 | opacity: 0; 134 | } 135 | } 136 | 137 | /* setting base styles */ 138 | 139 | * { 140 | padding: 0; 141 | margin: 0; 142 | font-size: 14px; 143 | font-weight: normal; 144 | user-select: none; 145 | color-scheme: dark; 146 | } 147 | 148 | html, 149 | body { 150 | height: 100%; 151 | margin: 0; 152 | overflow: hidden; 153 | } 154 | 155 | button:focus, 156 | input:focus, 157 | select:focus, 158 | dialog:focus { 159 | outline: none; 160 | } 161 | 162 | img { 163 | object-fit: cover !important; 164 | } 165 | 166 | select { 167 | appearance: none; 168 | } 169 | 170 | dialog { 171 | border: none !important; 172 | background-color: #00000000; 173 | opacity: 0; 174 | } 175 | 176 | button, 177 | input, 178 | .panelButton { 179 | padding: 10px; 180 | border: 0; 181 | } 182 | 183 | body { 184 | font-family: "Segoe UI", Arial, Helvetica, sans-serif; 185 | font-size: 16px; 186 | } 187 | 188 | .sideBarFolder { 189 | display: flex; 190 | flex-direction: column; 191 | justify-items: center; 192 | justify-content: space-between; 193 | padding: 10px; 194 | margin: 0px 0px 12px 0px; 195 | } 196 | 197 | .titleButton { 198 | width: 48px; 199 | height: 32px; 200 | padding: 0px 10px 0px 18px; 201 | border: 0; 202 | margin: 0; 203 | background: transparent; 204 | outline: 0; 205 | } 206 | 207 | .titleButton svg { 208 | width: 10px; 209 | height: 10px; 210 | } 211 | 212 | /* elements with animations */ 213 | 214 | .standardButton { 215 | display: flex; 216 | justify-items: center; 217 | justify-content: space-between; 218 | align-items: center; 219 | width: 100%; 220 | gap: 5px; 221 | transition: 0.2s; 222 | } 223 | 224 | input, 225 | #searchInput { 226 | transition: 0.2s; 227 | } 228 | 229 | dialog.showDialog { 230 | animation: dialogFadeIn 0.3s ease forwards; 231 | } 232 | 233 | dialog.hideDialog { 234 | animation: dialogFadeOut 0.2s ease forwards; 235 | } 236 | 237 | .loading { 238 | opacity: 1; 239 | animation: loadingFadeOut 0.3s; 240 | opacity: 0; 241 | } 242 | 243 | .loadingIcon { 244 | animation: spin 3s linear infinite; 245 | } 246 | 247 | .toast:popover-open { 248 | opacity: 1; 249 | animation: normalFadeIn 0.2s, normalFadeOut 0.2s 1.3s; 250 | } 251 | 252 | .toast { 253 | top: 95%; 254 | left: 50%; 255 | transform: translate(-50%, -50%); 256 | } 257 | 258 | /* * tooltip related */ 259 | 260 | /* 261 | 262 | - [class*="value"] will select if value exists in class 263 | - we aren't using the simpler [data-tooltip] because of ctrl-hold behaviours, 264 | - the tooltip should only be visible once the class has been added to the element 265 | 266 | */ 267 | 268 | /* original positioning of right tooltip */ 269 | [class*="tooltip-"] { 270 | position: relative; 271 | } 272 | 273 | /* position of :after elements determined by ancestor */ 274 | [class*="tooltip-"]::after { 275 | position: absolute; 276 | content: attr(data-tooltip); 277 | width: max-content; 278 | padding: 2px 8px; 279 | z-index: 100000000000000000 !important; 280 | backdrop-filter: blur(10px); 281 | opacity: 0; 282 | display: none; 283 | } 284 | 285 | [class*="tooltip-"]:hover::after { 286 | display: block; 287 | opacity: 0; 288 | animation: normalFadeIn 0.2s ease forwards; 289 | } 290 | 291 | [class*="tooltip-delayed-"]:hover::after { 292 | display: block; 293 | opacity: 0; 294 | animation: normalFadeIn 0.2s ease 0.4s forwards; 295 | } 296 | 297 | .tooltip-top::after, 298 | .tooltip-delayed-top::after { 299 | top: -100%; 300 | left: 50%; 301 | transform: translate(-50%, -100%); 302 | } 303 | 304 | .tooltip-bottom::after, 305 | .tooltip-delayed-bottom::after { 306 | top: 100%; 307 | left: 50%; 308 | transform: translate(-50%, 0%); 309 | margin-top: 5px; 310 | } 311 | 312 | .tooltip-center::after, 313 | .tooltip-delayed-center::after { 314 | top: 50%; 315 | left: 50%; 316 | transform: translate(-50%, -50%); 317 | } 318 | 319 | .tooltip-left::after, 320 | .tooltip-delayed-left::after { 321 | transform: translate(50%, -50%); 322 | top: 50%; 323 | right: 300%; 324 | margin-right: 5px; 325 | } 326 | 327 | .tooltip-right::after, 328 | .tooltip-delayed-right::after { 329 | transform: translate(0%, -17%); 330 | right: 5%; 331 | } 332 | 333 | body.user-is-tabbing { 334 | [class*="tooltip-"]:focus::after { 335 | display: block; 336 | opacity: 0; 337 | animation: normalFadeIn 0.2s ease forwards; 338 | } 339 | 340 | [class*="tooltip-delayed-"]:focus::after { 341 | display: block; 342 | opacity: 0; 343 | animation: normalFadeIn 0.2s ease 0.4s forwards; 344 | } 345 | } 346 | 347 | /* removing all animations for people with animations turned off */ 348 | 349 | @media (prefers-reduced-motion) { 350 | .standardButton { 351 | transition: 0s; 352 | } 353 | 354 | input, 355 | #searchInput { 356 | transition: 0s; 357 | } 358 | 359 | dialog.showDialog { 360 | animation: dialogFadeIn 0s ease forwards; 361 | } 362 | 363 | dialog.hideDialog { 364 | animation: dialogFadeOut 0s ease forwards; 365 | } 366 | 367 | .loading { 368 | opacity: 1; 369 | animation: loadingFadeOut 0s; 370 | opacity: 0; 371 | } 372 | 373 | .loadingIcon { 374 | animation: spin 0s linear infinite; 375 | } 376 | 377 | .toast { 378 | opacity: 1; 379 | animation: dialogFadeIn 0s, dialogFadeOut 0s 2.3s; 380 | } 381 | 382 | [class*="tooltip-"]:hover::after, 383 | [class*="tooltip-"]:focus::after { 384 | animation: normalFadeIn 0s ease forwards; 385 | } 386 | 387 | .toast:popover-open { 388 | opacity: 1; 389 | animation: normalFadeIn 0s, normalFadeOut 0s 1.3s; 390 | } 391 | 392 | [class*="tooltip-delayed-"]:hover::after { 393 | animation: normalFadeIn 0s ease 0.4s forwards; 394 | } 395 | 396 | body.user-is-tabbing { 397 | [class*="tooltip-"]:focus::after { 398 | animation: normalFadeIn 0s ease forwards; 399 | } 400 | 401 | [class*="tooltip-delayed-"]:focus::after { 402 | animation: normalFadeIn 0s ease 0.4s forwards; 403 | } 404 | } 405 | } 406 | 407 | /* dynamic styles */ 408 | 409 | * { 410 | font-family: var(--font-family); 411 | color: var(--text-color); 412 | } 413 | 414 | *:not(body, svg, #loading, .sideBarGame, .gameIconImage), 415 | [class*="tooltip-"]:after { 416 | border-radius: var(--border-radius); 417 | } 418 | 419 | .currentlyDragging { 420 | box-shadow: 0 -3px 0 0 #646464; 421 | border-top-left-radius: 0; 422 | border-top-right-radius: 0; 423 | } 424 | 425 | body.user-is-tabbing button:not(.invisible-button-gamepopup):focus, 426 | body.user-is-tabbing input:focus, 427 | body.user-is-tabbing select:focus { 428 | outline: 1px solid var(--outline-color); 429 | } 430 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | UIContext, 6 | checkIfConnectedToInternet, 7 | closeDialogImmediately, 8 | getData, 9 | importSteamGames, 10 | openDialog, 11 | toggleSideBar, 12 | translateText, 13 | triggerToast, 14 | updateData, 15 | } from "./Globals.jsx"; 16 | 17 | // importing components 18 | import { SideBar } from "./SideBar.jsx"; 19 | import { GameCards } from "./components/GameCards.jsx"; 20 | import { Hotkeys } from "./components/Hotkeys.jsx"; 21 | import { LanguageSelector } from "./components/LanguageSelector.jsx"; 22 | import { ChevronArrows, EmptyTray, Steam } from "./libraries/Icons.jsx"; 23 | import { EditFolder } from "./modals/EditFolder.jsx"; 24 | import { EditGame } from "./modals/EditGame.jsx"; 25 | import { GamePopUp } from "./modals/GamePopUp.jsx"; 26 | import { Loading } from "./modals/Loading.jsx"; 27 | import { NewFolder } from "./modals/NewFolder.jsx"; 28 | import { NewGame } from "./modals/NewGame.jsx"; 29 | import { Notepad } from "./modals/Notepad.jsx"; 30 | import { Settings } from "./modals/Settings.jsx"; 31 | 32 | import { invoke } from "@tauri-apps/api/tauri"; 33 | // importing code snippets and library functions 34 | import { For, Match, Show, Switch, createEffect, onMount, useContext } from "solid-js"; 35 | import { fuzzysearch } from "./libraries/fuzzysearch.js"; 36 | 37 | // importing style related files 38 | import "./App.css"; 39 | 40 | function App() { 41 | const globalContext = useContext(GlobalContext); 42 | const uiContext = useContext(UIContext); 43 | const applicationStateContext = useContext(ApplicationStateContext); 44 | 45 | // setting up effects for styles that can be changed in settings 46 | createEffect(() => { 47 | document.body.style.setProperty( 48 | "--text-color", 49 | globalContext.libraryData.userSettings.currentTheme === "light" ? "#000000" : "#ffffff", 50 | ); 51 | }); 52 | 53 | createEffect(() => { 54 | let fontFamily; 55 | switch (globalContext.libraryData.userSettings.fontName) { 56 | case "sans serif": 57 | fontFamily = "Helvetica, Arial, sans-serif"; 58 | break; 59 | case "serif": 60 | fontFamily = "Times New Roman"; 61 | break; 62 | case "mono": 63 | fontFamily = "IBM Plex Mono, Consolas"; 64 | break; 65 | } 66 | document.body.style.setProperty("--font-family", fontFamily); 67 | }); 68 | 69 | createEffect(() => { 70 | document.body.style.setProperty( 71 | "--border-radius", 72 | globalContext.libraryData.userSettings.roundedBorders ? "6px" : "0px", 73 | ); 74 | }); 75 | 76 | createEffect(() => { 77 | document.body.style.setProperty( 78 | "--outline-color", 79 | globalContext.libraryData.userSettings.currentTheme === "light" ? "#000000" : "#ffffff", 80 | ); 81 | }); 82 | 83 | function closeApp() { 84 | invoke("close_app"); 85 | } 86 | 87 | function handleTabAndMouseBehaviour() { 88 | // adds user-is-tabbing to body whenever tab is used 89 | // used for changing tooltip delay 90 | 91 | function handleFirstTab(e) { 92 | if (e.key === "Tab") { 93 | document.body.classList.add("user-is-tabbing"); 94 | self.removeEventListener("keydown", handleFirstTab); 95 | self.addEventListener("mousedown", handleMouseDown); 96 | uiContext.setUserIsTabbing(document.body.classList.contains("user-is-tabbing")); 97 | } 98 | } 99 | 100 | function handleMouseDown() { 101 | document.body.classList.remove("user-is-tabbing"); 102 | self.removeEventListener("mousedown", handleMouseDown); 103 | self.addEventListener("keydown", handleFirstTab); 104 | uiContext.setUserIsTabbing(document.body.classList.contains("user-is-tabbing")); 105 | } 106 | 107 | self.addEventListener("keydown", handleFirstTab); 108 | } 109 | 110 | function returnGridStyleForGameCard(zoomLevel, showSideBar) { 111 | switch (zoomLevel) { 112 | case 0: 113 | if (showSideBar) { 114 | return "grid-cols-4 medium:grid-cols-5 large:grid-cols-7"; 115 | } 116 | return "grid-cols-4 medium:grid-cols-6 large:grid-cols-8"; 117 | case 1: 118 | if (showSideBar) { 119 | return "grid-cols-3 medium:grid-cols-4 large:grid-cols-6"; 120 | } 121 | return "grid-cols-3 medium:grid-cols-5 large:grid-cols-7"; 122 | case 2: 123 | if (showSideBar) { 124 | return "grid-cols-2 medium:grid-cols-3 large:grid-cols-5"; 125 | } 126 | return "grid-cols-2 medium:grid-cols-4 large:grid-cols-6"; 127 | } 128 | } 129 | 130 | function addEventListeners() { 131 | handleTabAndMouseBehaviour(); 132 | 133 | // disabling right click 134 | document.addEventListener("contextmenu", (event) => event.preventDefault()); 135 | 136 | // storing window width in application state context 137 | self.addEventListener("resize", () => { 138 | applicationStateContext.setWindowWidth(self.innerWidth); 139 | }); 140 | 141 | // keyboard handling 142 | document.addEventListener("keydown", (e) => { 143 | const allDialogs = document.querySelectorAll("dialog"); 144 | let anyDialogOpen = false; 145 | let currentlyOpenDialog; 146 | 147 | // checks if any dialogs are open 148 | for (const dialog of allDialogs) { 149 | if (dialog.open) { 150 | anyDialogOpen = true; 151 | currentlyOpenDialog = dialog; 152 | } 153 | } 154 | 155 | if (e.key === "Escape") { 156 | e.preventDefault(); 157 | 158 | const currentlyOpenDialogName = currentlyOpenDialog.getAttribute("data-modal"); 159 | 160 | const modalTakesUserInput = ["newGame", "editGame", "newFolder", "editFolder"].includes( 161 | currentlyOpenDialogName, 162 | ); 163 | 164 | if (anyDialogOpen) { 165 | if (!modalTakesUserInput) { 166 | closeDialogImmediately(currentlyOpenDialog); 167 | } 168 | } 169 | } 170 | 171 | const modifierKey = applicationStateContext.systemPlatform() === "windows" ? "ctrlKey" : "metaKey"; 172 | 173 | if (e[modifierKey]) { 174 | // "play" tooltip added to sidebar game and game card if user is also hovering on a specific element 175 | for (const sideBarGame of document.querySelectorAll(".sideBarGame")) { 176 | sideBarGame.classList.add("tooltip-right"); 177 | sideBarGame.style.cursor = "pointer"; 178 | } 179 | for (const gameCard of document.querySelectorAll(".gameCard")) { 180 | gameCard.classList.add("tooltip-center"); 181 | } 182 | 183 | // if ctrl/cmd + another key held down 184 | switch (e.code) { 185 | // increase game card zoom level 186 | case "Equal": 187 | globalContext.setLibraryData("userSettings", "zoomLevel", (zoomLevel) => { 188 | return zoomLevel !== 2 ? zoomLevel + 1 : 2; 189 | }); 190 | updateData(); 191 | break; 192 | 193 | // decrease game card zoom level 194 | case "Minus": 195 | globalContext.setLibraryData("userSettings", "zoomLevel", (zoomLevel) => { 196 | return zoomLevel !== 0 ? zoomLevel - 1 : 0; 197 | }); 198 | updateData(); 199 | break; 200 | 201 | // closes the app 202 | case "KeyW": 203 | e.preventDefault(); 204 | closeApp(); 205 | break; 206 | 207 | // focuses game search bar 208 | case "KeyF": 209 | if (!anyDialogOpen) { 210 | e.preventDefault(); 211 | document.querySelector("#searchInput").focus(); 212 | } else { 213 | triggerToast(translateText("close current dialog before opening another")); 214 | } 215 | break; 216 | 217 | // opens new game modal 218 | case "KeyN": 219 | e.preventDefault(); 220 | if (!anyDialogOpen) { 221 | openDialog("newGame"); 222 | } else { 223 | if (!uiContext.showNewGameModal()) { 224 | triggerToast(translateText("close current dialog before opening another")); 225 | } 226 | } 227 | break; 228 | 229 | // opens new folder modal 230 | case "KeyM": 231 | e.preventDefault(); 232 | if (!anyDialogOpen) { 233 | openDialog("newFolder"); 234 | } else { 235 | if (!uiContext.showNewFolderModal()) { 236 | triggerToast(translateText("close current dialog before opening another")); 237 | } 238 | } 239 | break; 240 | 241 | // opens notepad modal 242 | case "KeyL": 243 | e.preventDefault(); 244 | if (!anyDialogOpen) { 245 | openDialog("notepad"); 246 | } else { 247 | if (!uiContext.showNotepadModal()) { 248 | triggerToast(translateText("close current dialog before opening another")); 249 | } 250 | } 251 | break; 252 | 253 | // opens settings modal 254 | case "Comma": 255 | if (!anyDialogOpen) { 256 | e.preventDefault(); 257 | openDialog("settings"); 258 | } else { 259 | if (!uiContext.showSettingsModal()) { 260 | triggerToast(translateText("close current dialog before opening another")); 261 | } 262 | } 263 | break; 264 | 265 | // toggles sidebar 266 | case "Backslash": 267 | if (!anyDialogOpen) { 268 | e.preventDefault(); 269 | toggleSideBar(); 270 | document.querySelector("#searchInput").blur(); 271 | } else { 272 | triggerToast(translateText("close current dialog before toggling sidebar")); 273 | } 274 | break; 275 | 276 | // reload shortcut doesn't work on macos for some reason 277 | case "KeyR": 278 | self.location.reload(); 279 | break; 280 | 281 | // disabling misc webview shortcuts 282 | case "KeyG": 283 | case "KeyP": 284 | case "KeyU": 285 | e.preventDefault(); 286 | break; 287 | } 288 | } 289 | }); 290 | } 291 | 292 | document.addEventListener("keyup", () => { 293 | // resets sidebar cursor back to grab when ctrl/cmd is let go of 294 | for (const sideBarGame of document.querySelectorAll(".sideBarGame")) { 295 | sideBarGame.style.cursor = "grab"; 296 | } 297 | 298 | // hides "play" tooltip from sidebar game / game card when ctrl/cmd is let go of 299 | for (const sideBarGame of document.querySelectorAll(".sideBarGame")) { 300 | sideBarGame.classList.remove("tooltip-right"); 301 | } 302 | for (const gameCard of document.querySelectorAll(".gameCard")) { 303 | gameCard.classList.remove("tooltip-center"); 304 | } 305 | }); 306 | 307 | onMount(async () => { 308 | // fetches library data and populates the ui 309 | getData(); 310 | 311 | // loading app by default in dark mode so there's no bright flash of white while getData fetches preferences 312 | document.documentElement.classList.add("dark"); 313 | 314 | // only shows the window after the ui has been rendered 315 | invoke("show_window"); 316 | 317 | addEventListeners(); 318 | applicationStateContext.setSystemPlatform(await invoke("get_platform")); 319 | 320 | if (await checkIfConnectedToInternet()) { 321 | try { 322 | // checks latest version and stores it in variable 323 | const response = await fetch(`${import.meta.env.VITE_CLEAR_API_URL}/?version=a`); 324 | const clearVersion = await response.json(); 325 | applicationStateContext.setLatestVersion(clearVersion.clearVersion); 326 | 327 | // shows new version indicators if update is available 328 | applicationStateContext.latestVersion().replaceAll(".", "") > 329 | applicationStateContext.appVersion().replaceAll(".", "") 330 | ? uiContext.setShowNewVersionAvailable(true) 331 | : uiContext.setShowNewVersionAvailable(false); 332 | } catch (error) { 333 | triggerToast(`could not check if newer version available: ${error.message.toLowerCase()}`); 334 | } 335 | } 336 | }); 337 | 338 | return ( 339 | <> 340 | {/* fading out bg color to make the app loading look a bit more smoother */} 341 |
342 |

343 |

344 | 345 |
346 | = 1000 350 | } 351 | > 352 | 362 | 363 | = 1000} 365 | > 366 | 367 | 368 | 369 | 375 |
= 1000 378 | ? "large:pl-[17%] pl-[23%]" 379 | : "large:pl-[30px] pl-[30px]" 380 | }`} 381 | > 382 |
383 |

384 | {translateText("hey there! thank you so much for using clear")} 385 |
386 |
- {translateText("add some new games using the sidebar buttons")} 387 |
388 |
- {translateText("create new folders and drag and drop your games into them")} 389 |
390 |
- {translateText("don't forget to check out the settings!")} 391 |

392 | 393 |
394 | 428 | 429 | 430 |
431 | 432 | 433 |
434 |
435 |
436 |
= 1000 439 | ? "large:pl-[17%] pl-[23%]" 440 | : "large:pl-[30px] pl-[30px]" 441 | }`} 442 | > 443 | 446 | 447 | {(folderName) => { 448 | const folder = globalContext.libraryData.folders[folderName]; 449 | 450 | return ( 451 | 452 |
453 | 454 |

{folder.name}

455 |
456 |
462 | 463 |
464 |
465 |
466 | ); 467 | }} 468 |
469 |
470 | 471 | 474 | {() => { 475 | const searchResults = []; 476 | const allGameNames = []; 477 | 478 | if (applicationStateContext.searchValue() !== "" && applicationStateContext.searchValue() !== undefined) { 479 | for (const key in globalContext.libraryData.games) { 480 | allGameNames.push(key); 481 | } 482 | } 483 | 484 | for (const libraryGame of Object.keys(globalContext.libraryData.games)) { 485 | const result = fuzzysearch( 486 | applicationStateContext.searchValue(), 487 | libraryGame.toLowerCase().replace("-", " "), 488 | ); 489 | if (result === true) { 490 | searchResults.push(libraryGame); 491 | } 492 | } 493 | 494 | return ( 495 |
496 |
502 | 503 |
504 |
505 | 506 |
507 | 508 | {translateText("no games found")} 509 |
510 |
511 |
512 |
513 | ); 514 | }} 515 |
516 |
517 |
518 | 519 |
523 | {applicationStateContext.toastMessage()} 524 |
525 | 526 |
527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 |
554 | 555 | ); 556 | } 557 | 558 | export default App; 559 | -------------------------------------------------------------------------------- /src/Globals.jsx: -------------------------------------------------------------------------------- 1 | import { BaseDirectory, createDir, exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"; 2 | import { appDataDir } from "@tauri-apps/api/path"; 3 | import { invoke } from "@tauri-apps/api/tauri"; 4 | // importing code snippets and library functions 5 | import { createContext, createSignal } from "solid-js"; 6 | import { createStore, produce } from "solid-js/store"; 7 | import { parseVDF } from "./libraries/parseVDF.js"; 8 | 9 | // importing text snippets for different languages 10 | import { textLanguages } from "./Text.js"; 11 | 12 | export const GlobalContext = createContext(); 13 | export const UIContext = createContext(); 14 | export const SelectedDataContext = createContext(); 15 | export const ApplicationStateContext = createContext(); 16 | export const SteamDataContext = createContext(); 17 | 18 | // creating store for library data 19 | export const [libraryData, setLibraryData] = createStore({ 20 | // default values 21 | games: {}, 22 | folders: {}, 23 | notepad: "", 24 | userSettings: { 25 | roundedBorders: true, 26 | showSideBar: true, 27 | gameTitle: true, 28 | folderTitle: true, 29 | quitAfterOpen: true, 30 | fontName: "sans serif", 31 | language: "en", 32 | currentTheme: "dark", 33 | zoomLevel: 1, 34 | }, 35 | }); 36 | 37 | // ui signals 38 | const [showSettingsLanguageSelector, setShowSettingsLanguageSelector] = createSignal(false); 39 | const [showImportAndOverwriteConfirm, setShowImportAndOverwriteConfirm] = createSignal(false); 40 | const [showNewVersionAvailable, setShowNewVersionAvailable] = createSignal(false); 41 | const [showNewGameModal, setShowNewGameModal] = createSignal(false); 42 | const [showEditGameModal, setShowEditGameModal] = createSignal(false); 43 | const [showNewFolderModal, setShowNewFolderModal] = createSignal(false); 44 | const [showEditFolderModal, setShowEditFolderModal] = createSignal(false); 45 | const [showGamePopUpModal, setShowGamePopUpModal] = createSignal(false); 46 | const [showNotepadModal, setShowNotepadModal] = createSignal(false); 47 | const [showSettingsModal, setShowSettingsModal] = createSignal(false); 48 | const [showLoadingModal, setShowLoadingModal] = createSignal(false); 49 | const [userIsTabbing, setUserIsTabbing] = createSignal(false); 50 | 51 | // selected data signals 52 | const [selectedGame, setSelectedGame] = createSignal({}); 53 | const [selectedFolder, setSelectedFolder] = createSignal([]); 54 | const [selectedGameId, setSelectedGameId] = createSignal(); 55 | 56 | // application state signals 57 | const [currentGames, setCurrentGames] = createSignal([]); 58 | const [currentFolders, setCurrentFolders] = createSignal([]); 59 | const [searchValue, setSearchValue] = createSignal(); 60 | const [toastMessage, setToastMessage] = createSignal(""); 61 | const [appVersion, setAppVersion] = createSignal("1.1.0"); 62 | const [systemPlatform, setSystemPlatform] = createSignal(""); 63 | const [latestVersion, setLatestVersion] = createSignal(""); 64 | const [appDataDirPath, setAppDataDirPath] = createSignal({}); 65 | const [windowWidth, setWindowWidth] = createSignal(self.innerWidth); 66 | 67 | // steam data signals 68 | const [totalSteamGames, setTotalSteamGames] = createSignal(0); 69 | const [totalImportedSteamGames, setTotalImportedSteamGames] = createSignal(0); 70 | 71 | // adding signals to and exporting their respective context providers 72 | 73 | export function GlobalContextProvider(props) { 74 | const context = { 75 | libraryData, 76 | setLibraryData, 77 | }; 78 | return {props.children}; 79 | } 80 | 81 | export function UIContextProvider(props) { 82 | const context = { 83 | showSettingsLanguageSelector, 84 | setShowSettingsLanguageSelector, 85 | showImportAndOverwriteConfirm, 86 | setShowImportAndOverwriteConfirm, 87 | showNewVersionAvailable, 88 | setShowNewVersionAvailable, 89 | showNewGameModal, 90 | setShowNewGameModal, 91 | showEditGameModal, 92 | setShowEditGameModal, 93 | showNewFolderModal, 94 | setShowNewFolderModal, 95 | showEditFolderModal, 96 | setShowEditFolderModal, 97 | showGamePopUpModal, 98 | setShowGamePopUpModal, 99 | showNotepadModal, 100 | setShowNotepadModal, 101 | showSettingsModal, 102 | setShowSettingsModal, 103 | showLoadingModal, 104 | setShowLoadingModal, 105 | userIsTabbing, 106 | setUserIsTabbing, 107 | }; 108 | 109 | return {props.children}; 110 | } 111 | 112 | export function SelectedDataContextProvider(props) { 113 | const context = { 114 | selectedGame, 115 | setSelectedGame, 116 | selectedFolder, 117 | setSelectedFolder, 118 | selectedGameId, 119 | setSelectedGameId, 120 | }; 121 | 122 | return {props.children}; 123 | } 124 | 125 | export function ApplicationStateContextProvider(props) { 126 | const context = { 127 | currentGames, 128 | setCurrentGames, 129 | currentFolders, 130 | setCurrentFolders, 131 | searchValue, 132 | setSearchValue, 133 | toastMessage, 134 | setToastMessage, 135 | appVersion, 136 | setAppVersion, 137 | systemPlatform, 138 | setSystemPlatform, 139 | latestVersion, 140 | setLatestVersion, 141 | appDataDirPath, 142 | setAppDataDirPath, 143 | windowWidth, 144 | setWindowWidth, 145 | }; 146 | 147 | return {props.children}; 148 | } 149 | 150 | export function SteamDataContextProvider(props) { 151 | const context = { 152 | totalSteamGames, 153 | setTotalSteamGames, 154 | totalImportedSteamGames, 155 | setTotalImportedSteamGames, 156 | }; 157 | 158 | return {props.children}; 159 | } 160 | 161 | // global helper functions 162 | 163 | async function setupFoldersForImages() { 164 | await createDir("heroes", { 165 | dir: BaseDirectory.AppData, 166 | recursive: true, 167 | }); 168 | await createDir("grids", { 169 | dir: BaseDirectory.AppData, 170 | recursive: true, 171 | }); 172 | await createDir("logos", { 173 | dir: BaseDirectory.AppData, 174 | recursive: true, 175 | }); 176 | await createDir("icons", { 177 | dir: BaseDirectory.AppData, 178 | recursive: true, 179 | }); 180 | 181 | updateData(); 182 | } 183 | 184 | export async function getData() { 185 | setAppDataDirPath(await appDataDir()); 186 | 187 | if (await exists("data.json", { dir: BaseDirectory.AppData })) { 188 | const getLibraryData = await readTextFile("data.json", { 189 | dir: BaseDirectory.AppData, 190 | }); 191 | 192 | // WARN potential footgun here cause you're not checking if games are empty 193 | if (getLibraryData !== "" && JSON.parse(getLibraryData).folders !== "") { 194 | setLibraryData(JSON.parse(getLibraryData)); 195 | 196 | const correctOrderOfFolders = []; 197 | const folders = libraryData.folders; 198 | for (let x = 0; x < Object.keys(folders).length; x++) { 199 | for (const key in folders) { 200 | if (folders[key].index === x) { 201 | correctOrderOfFolders.push(key); 202 | } 203 | } 204 | } 205 | 206 | // i actually have no idea but this somehow fixes all issues with drag and drop ¯\_(ツ)_/¯ 207 | setCurrentFolders(""); 208 | setCurrentGames(""); 209 | 210 | setCurrentFolders(correctOrderOfFolders); 211 | 212 | setCurrentGames(Object.keys(libraryData.games)); 213 | 214 | // checks current theme and adds it to the document classlist for tailwind 215 | if (libraryData.userSettings.currentTheme === "light") { 216 | document.documentElement.classList.remove("dark"); 217 | } else { 218 | document.documentElement.classList.add("dark"); 219 | } 220 | 221 | console.log("data fetched"); 222 | } else setupFoldersForImages(); 223 | } else { 224 | setupFoldersForImages(); 225 | } 226 | } 227 | 228 | export function openGame(gameLocation) { 229 | if (gameLocation === undefined) { 230 | triggerToast(translateText("no game file provided!")); 231 | return; 232 | } 233 | 234 | invoke("open_location", { 235 | location: gameLocation, 236 | }); 237 | 238 | if (libraryData.userSettings.quitAfterOpen === true || libraryData.userSettings.quitAfterOpen === undefined) { 239 | setTimeout(() => { 240 | invoke("close_app"); 241 | }, 500); 242 | } else { 243 | triggerToast(translateText("game launched! enjoy your session!")); 244 | } 245 | } 246 | 247 | export function generateRandomString() { 248 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 249 | 250 | let result = ""; 251 | const charactersLength = characters.length; 252 | for (let i = 0; i < 5; i++) { 253 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 254 | } 255 | 256 | return result; 257 | } 258 | 259 | export async function importSteamGames() { 260 | openDialog("loading"); 261 | 262 | const connectedToInternet = await checkIfConnectedToInternet(); 263 | 264 | if (!connectedToInternet) { 265 | closeDialog("loading"); 266 | triggerToast(translateText("you're not connected to the internet :(")); 267 | 268 | return; 269 | } 270 | 271 | const steamVDFData = await invoke("read_steam_vdf"); 272 | 273 | if (steamVDFData === "error") { 274 | closeDialog("loading"); 275 | triggerToast(translateText("sorry but there was an error \n reading your Steam library :(")); 276 | 277 | return; 278 | } 279 | 280 | const steamData = parseVDF(steamVDFData); 281 | 282 | const steamGameIds = []; 283 | 284 | for (let x = 0; x < Object.keys(steamData.libraryfolders).length; x++) { 285 | steamGameIds.push(...Object.keys(steamData.libraryfolders[x].apps)); 286 | } 287 | 288 | // exclude steam redistrutables from the game library 289 | const index = steamGameIds.indexOf("228980"); 290 | index !== -1 ? steamGameIds.splice(index, 1) : null; 291 | 292 | setTotalSteamGames(steamGameIds.length); 293 | 294 | const allGameNames = []; 295 | 296 | setLibraryData((data) => { 297 | data.folders.steam = undefined; 298 | return data; 299 | }); 300 | 301 | await updateData(); 302 | 303 | for (const steamId of steamGameIds) { 304 | let gameData = await fetch(`${import.meta.env.VITE_CLEAR_API_URL}/?steamID=${steamId}`); 305 | 306 | gameData = await gameData.json(); 307 | 308 | const gameSGDBID = gameData.data.id; 309 | const name = gameData.data.name; 310 | allGameNames.push(name); 311 | 312 | let gridImageFileName = `${generateRandomString()}.png`; 313 | let heroImageFileName = `${generateRandomString()}.png`; 314 | let logoImageFileName = `${generateRandomString()}.png`; 315 | let iconImageFileName = `${generateRandomString()}.png`; 316 | 317 | let assetsData = await fetch(`${import.meta.env.VITE_CLEAR_API_URL}/?assets=${gameSGDBID}&length=1`); 318 | 319 | assetsData = await assetsData.json(); 320 | 321 | if (assetsData.grids.length !== 0) { 322 | await invoke("download_image", { 323 | link: assetsData.grids[0], 324 | location: locationJoin([appDataDirPath(), "grids", gridImageFileName]), 325 | }); 326 | } else { 327 | gridImageFileName = undefined; 328 | } 329 | 330 | if (assetsData.heroes.length !== 0) { 331 | await invoke("download_image", { 332 | link: assetsData.heroes[0], 333 | location: locationJoin([appDataDirPath(), "heroes", heroImageFileName]), 334 | }); 335 | } else { 336 | heroImageFileName = undefined; 337 | } 338 | 339 | if (assetsData.logos.length !== 0) { 340 | await invoke("download_image", { 341 | link: assetsData.logos[0], 342 | location: locationJoin([appDataDirPath(), "logos", logoImageFileName]), 343 | }); 344 | } else { 345 | logoImageFileName = undefined; 346 | } 347 | 348 | if (assetsData.icons.length !== 0) { 349 | await invoke("download_image", { 350 | link: assetsData.icons[0], 351 | location: locationJoin([appDataDirPath(), "icons", iconImageFileName]), 352 | }); 353 | } else { 354 | iconImageFileName = undefined; 355 | } 356 | 357 | setLibraryData((data) => { 358 | data.games[name] = { 359 | location: `steam://rungameid/${steamId}`, 360 | name: name, 361 | heroImage: heroImageFileName, 362 | gridImage: gridImageFileName, 363 | logo: logoImageFileName, 364 | icon: iconImageFileName, 365 | favourite: false, 366 | }; 367 | return data; 368 | }); 369 | 370 | setTotalImportedSteamGames((x) => x + 1); 371 | } 372 | 373 | setLibraryData( 374 | produce((data) => { 375 | data.folders.steam = { 376 | name: "steam", 377 | hide: false, 378 | games: allGameNames, 379 | index: currentFolders().length, 380 | }; 381 | return data; 382 | }), 383 | ); 384 | 385 | setTimeout(async () => { 386 | await updateData().then(() => { 387 | closeDialog("loading"); 388 | closeDialog("settings"); 389 | setTotalImportedSteamGames(0); 390 | setTotalSteamGames(0); 391 | }); 392 | }, 1000); 393 | } 394 | 395 | export function translateText(text) { 396 | if (!Object.prototype.hasOwnProperty.call(textLanguages, text)) { 397 | console.trace(`missing text translation '${text}'`); 398 | 399 | return "undefined"; 400 | } 401 | 402 | const translatedText = textLanguages[text][libraryData.userSettings.language]; 403 | 404 | if (libraryData.userSettings.language === "en" || translatedText === "") { 405 | return text; 406 | } 407 | 408 | return translatedText; 409 | } 410 | 411 | export async function updateData() { 412 | await writeTextFile( 413 | { 414 | path: "data.json", 415 | contents: JSON.stringify(libraryData, null, 4), 416 | }, 417 | { 418 | dir: BaseDirectory.AppData, 419 | }, 420 | ).then(getData()); 421 | } 422 | 423 | let toastTimeout = setTimeout(() => {}, 0); 424 | 425 | export function triggerToast(message) { 426 | document.querySelector(".toast").hidePopover(); 427 | setTimeout(() => { 428 | document.querySelector(".toast").showPopover(); 429 | }, 20); 430 | 431 | setToastMessage(message); 432 | clearTimeout(toastTimeout); 433 | toastTimeout = setTimeout(() => { 434 | document.querySelector(".toast").hidePopover(); 435 | }, 1500); 436 | } 437 | 438 | export function openDialog(dialogData) { 439 | switch (dialogData) { 440 | case "newGame": 441 | setShowNewGameModal(true); 442 | break; 443 | 444 | case "editGame": 445 | setShowEditGameModal(true); 446 | break; 447 | 448 | case "newFolder": 449 | setShowNewFolderModal(true); 450 | break; 451 | 452 | case "editFolder": 453 | setShowEditFolderModal(true); 454 | break; 455 | 456 | case "gamePopUp": 457 | setShowGamePopUpModal(true); 458 | break; 459 | 460 | case "notepad": 461 | setShowNotepadModal(true); 462 | break; 463 | 464 | case "settings": 465 | setShowSettingsModal(true); 466 | break; 467 | 468 | case "loading": 469 | setShowLoadingModal(true); 470 | break; 471 | } 472 | 473 | const dialogRef = document.querySelector(`[data-modal="${dialogData}"]`); 474 | 475 | dialogRef.classList.remove("hideDialog"); 476 | dialogRef.showModal(); 477 | dialogRef.classList.add("showDialog"); 478 | 479 | document.activeElement.focus(); 480 | document.activeElement.blur(); 481 | } 482 | 483 | export function closeDialog(dialogData, ref) { 484 | function updateModalShowState() { 485 | switch (dialogData) { 486 | case "newGame": 487 | setShowNewGameModal(false); 488 | break; 489 | 490 | case "editGame": 491 | setShowEditGameModal(false); 492 | break; 493 | 494 | case "newFolder": 495 | setShowNewFolderModal(false); 496 | break; 497 | 498 | case "editFolder": 499 | setShowEditFolderModal(false); 500 | break; 501 | 502 | case "gamePopUp": 503 | setShowGamePopUpModal(false); 504 | break; 505 | 506 | case "notepad": 507 | setShowNotepadModal(false); 508 | break; 509 | 510 | case "settings": 511 | setShowSettingsModal(false); 512 | break; 513 | 514 | case "loading": 515 | setShowLoadingModal(false); 516 | break; 517 | } 518 | } 519 | 520 | if (ref !== undefined) { 521 | ref.addEventListener("keydown", (e) => { 522 | if (e.key === "Escape") { 523 | e.stopPropagation(); 524 | 525 | ref.classList.remove("showDialog"); 526 | ref.classList.add("hideDialog"); 527 | setTimeout(() => { 528 | ref.close(); 529 | updateModalShowState(); 530 | }, 200); 531 | } 532 | }); 533 | } else { 534 | const dialogRef = document.querySelector(`[data-modal="${dialogData}"]`); 535 | 536 | dialogRef.classList.remove("showDialog"); 537 | dialogRef.classList.add("hideDialog"); 538 | setTimeout(() => { 539 | dialogRef.close(); 540 | updateModalShowState(); 541 | }, 200); 542 | } 543 | } 544 | 545 | export function closeDialogImmediately(ref) { 546 | ref.classList.remove("showDialog"); 547 | ref.classList.add("hideDialog"); 548 | setTimeout(() => { 549 | ref.close(); 550 | }, 200); 551 | } 552 | 553 | export async function toggleSideBar() { 554 | setSearchValue(""); 555 | 556 | setLibraryData("userSettings", "showSideBar", (x) => !x); 557 | 558 | await updateData(); 559 | } 560 | 561 | export async function checkIfConnectedToInternet() { 562 | let connectedToInternet = await invoke("check_connection"); 563 | 564 | connectedToInternet = connectedToInternet === "true"; 565 | 566 | if (!connectedToInternet) { 567 | triggerToast("not connected to the internet :("); 568 | } 569 | 570 | return connectedToInternet; 571 | } 572 | 573 | export function locationJoin(locationsList) { 574 | if (systemPlatform() === "windows") { 575 | return locationsList.join("\\"); 576 | } 577 | 578 | return locationsList.join("/"); 579 | } 580 | 581 | export function getExecutableFileName(location) { 582 | // splits both / and \ paths since a library created on windows can be 583 | // viewed on macos and vice versa 584 | return location.toString().split("\\").slice(-1).toString().split("/").slice(-1); 585 | } 586 | 587 | export function getExecutableParentFolder(location) { 588 | if (systemPlatform() === "windows") { 589 | return location.toString().split("\\").slice(0, -1).join("\\"); 590 | } 591 | 592 | return location.toString().split("/").slice(0, -1).join("/"); 593 | } 594 | -------------------------------------------------------------------------------- /src/SideBar.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | SelectedDataContext, 6 | UIContext, 7 | getData, 8 | openDialog, 9 | toggleSideBar, 10 | translateText, 11 | triggerToast, 12 | updateData, 13 | } from "./Globals.jsx"; 14 | 15 | // importing components 16 | import { GameCardSideBar } from "./components/GameCardSideBar.jsx"; 17 | 18 | // importing code snippets and library functions 19 | import { For, Show, createSignal, onMount, useContext } from "solid-js"; 20 | import { produce } from "solid-js/store"; 21 | 22 | // importing style related files 23 | import { 24 | ChevronArrows, 25 | Edit, 26 | EyeClosed, 27 | Folder, 28 | GameController, 29 | Notepad, 30 | Settings, 31 | UpdateDownload, 32 | } from "./libraries/Icons.jsx"; 33 | 34 | export function SideBar() { 35 | const globalContext = useContext(GlobalContext); 36 | const uiContext = useContext(UIContext); 37 | const selectedDataContext = useContext(SelectedDataContext); 38 | const applicationStateContext = useContext(ApplicationStateContext); 39 | 40 | const [showContentSkipButton, setShowContentSkipButton] = createSignal(false); 41 | 42 | let scrollY = " "; 43 | 44 | async function moveFolder(folderName, toPosition) { 45 | const pastPositionOfFolder = applicationStateContext.currentFolders().indexOf(folderName); 46 | const currentFolders = applicationStateContext.currentFolders(); 47 | 48 | // removing folder from its past position 49 | currentFolders.splice(pastPositionOfFolder, 1); 50 | 51 | // pushing it into proper position relative to its past 52 | if (toPosition === -1) { 53 | currentFolders.push(folderName); 54 | } else { 55 | if (toPosition > pastPositionOfFolder) { 56 | currentFolders.splice(toPosition - 1, 0, folderName); 57 | } else { 58 | currentFolders.splice(toPosition, 0, folderName); 59 | } 60 | } 61 | 62 | // reordering folders in library data based on new current folders order 63 | for (const currentFolderName of currentFolders) { 64 | for (const folderName of Object.keys(globalContext.libraryData.folders)) { 65 | if (currentFolderName === folderName) { 66 | globalContext.setLibraryData( 67 | produce((data) => { 68 | Object.values(data.folders)[Object.keys(globalContext.libraryData.folders).indexOf(folderName)].index = 69 | currentFolders.indexOf(currentFolderName); 70 | 71 | return data; 72 | }), 73 | ); 74 | } 75 | } 76 | } 77 | 78 | await updateData(); 79 | } 80 | 81 | function moveGameInCurrentFolder(gameName, toPosition, currentFolderName) { 82 | const pastPositionOfGame = globalContext.libraryData.folders[currentFolderName].games.indexOf(gameName); 83 | 84 | // removing game from its past position 85 | globalContext.setLibraryData( 86 | produce((data) => { 87 | data.folders[currentFolderName].games.splice(data.folders[currentFolderName].games.indexOf(gameName), 1); 88 | return data; 89 | }), 90 | ); 91 | 92 | // pushing it into proper position relative to its past 93 | if (toPosition === -1) { 94 | globalContext.setLibraryData( 95 | produce((data) => { 96 | data.folders[currentFolderName].games.push(gameName); 97 | return data; 98 | }), 99 | ); 100 | } else { 101 | if (toPosition > pastPositionOfGame) { 102 | globalContext.setLibraryData( 103 | produce((data) => { 104 | data.folders[currentFolderName].games.splice(toPosition - 1, 0, gameName); 105 | return data; 106 | }), 107 | ); 108 | } else { 109 | globalContext.setLibraryData( 110 | produce((data) => { 111 | data.folders[currentFolderName].games.splice(toPosition, 0, gameName); 112 | return data; 113 | }), 114 | ); 115 | } 116 | } 117 | } 118 | 119 | function moveGameToAnotherFolder(gameName, toPosition, currentFolderName, destinationFolderName) { 120 | if (currentFolderName !== "uncategorized") { 121 | globalContext.setLibraryData( 122 | produce((data) => { 123 | data.folders[currentFolderName].games.splice(data.folders[currentFolderName].games.indexOf(gameName), 1); 124 | return data; 125 | }), 126 | ); 127 | } 128 | 129 | if (toPosition === -1) { 130 | globalContext.setLibraryData( 131 | produce((data) => { 132 | data.folders[destinationFolderName].games.push(gameName); 133 | return data; 134 | }), 135 | ); 136 | } else { 137 | globalContext.setLibraryData( 138 | produce((data) => { 139 | data.folders[destinationFolderName].games.splice(toPosition, 0, gameName); 140 | return data; 141 | }), 142 | ); 143 | } 144 | } 145 | 146 | function folderContainerDragOverHandler(e) { 147 | e.preventDefault(); 148 | 149 | if (document.querySelectorAll(".sideBarFolder:is(.dragging)")[0] !== undefined) { 150 | const siblings = [...e.srcElement.querySelectorAll(".sideBarFolder:not(.dragging)")]; 151 | 152 | const allGames = document.querySelectorAll(".sideBarFolder"); 153 | 154 | for (const game of allGames) { 155 | game.classList.remove("currentlyDragging"); 156 | } 157 | 158 | const nextSibling = siblings.find((sibling) => { 159 | let compensatedY = ""; 160 | compensatedY = e.clientY + scrollY; 161 | 162 | return compensatedY <= sibling.offsetTop + sibling.offsetHeight / 2; 163 | }); 164 | 165 | try { 166 | nextSibling.classList.add("currentlyDragging"); 167 | } catch (_error) { 168 | // do nothing 169 | } 170 | } 171 | } 172 | 173 | async function folderContainerDropHandler(e) { 174 | const folderName = e.dataTransfer.getData("folderName"); 175 | 176 | if (document.querySelectorAll(".sideBarFolder:is(.dragging)")[0] !== undefined) { 177 | const siblings = [...e.srcElement.querySelectorAll(".sideBarFolder:not(.dragging)")]; 178 | 179 | const nextSibling = siblings.find((sibling) => { 180 | let compensatedY = ""; 181 | compensatedY = e.clientY + scrollY; 182 | 183 | return compensatedY <= sibling.offsetTop + sibling.offsetHeight / 2; 184 | }); 185 | 186 | try { 187 | moveFolder(folderName, applicationStateContext.currentFolders().indexOf(nextSibling.id)); 188 | document.querySelector("#uncategorizedFolder").classList.remove("currentlyDragging"); 189 | setTimeout(() => { 190 | getData(); 191 | }, 100); 192 | } catch (_error) { 193 | getData(); 194 | } 195 | 196 | await updateData(); 197 | } 198 | } 199 | 200 | function gamesFolderDragOverHandler(e) { 201 | e.preventDefault(); 202 | 203 | console.log("dragging over game"); 204 | 205 | if (document.querySelectorAll(".sideBarFolder:is(.dragging)")[0] === undefined) { 206 | const siblings = [...e.srcElement.querySelectorAll(".sideBarGame:not(.dragging)")]; 207 | 208 | const allGames = document.querySelectorAll(".sideBarGame"); 209 | 210 | for (const game of allGames) { 211 | game.classList.remove("currentlyDragging"); 212 | } 213 | 214 | const nextSibling = siblings.find((sibling) => { 215 | let compensatedY = ""; 216 | compensatedY = e.clientY + scrollY; 217 | 218 | return compensatedY <= sibling.offsetTop + sibling.offsetHeight / 2; 219 | }); 220 | 221 | try { 222 | nextSibling.classList.add("currentlyDragging"); 223 | } catch (_error) { 224 | // do nothing 225 | } 226 | } 227 | } 228 | 229 | async function gamesFolderDropHandler(e, folderName) { 230 | const oldFolderName = e.dataTransfer.getData("oldFolderName"); 231 | 232 | if (document.querySelectorAll(".sideBarFolder:is(.dragging)")[0] === undefined) { 233 | const draggingItem = document.querySelector(".dragging"); 234 | const siblings = [...e.srcElement.querySelectorAll(".sideBarGame:not(.dragging)")]; 235 | 236 | const nextSibling = siblings.find((sibling) => { 237 | let compensatedY = ""; 238 | compensatedY = e.clientY + scrollY; 239 | 240 | return compensatedY <= sibling.offsetTop + sibling.offsetHeight / 2; 241 | }); 242 | 243 | const currentDraggingItem = draggingItem.textContent; 244 | 245 | let nextSiblingItem; 246 | let toPosition; 247 | 248 | try { 249 | nextSiblingItem = nextSibling.textContent; 250 | toPosition = globalContext.libraryData.folders[folderName].games.indexOf(nextSiblingItem); 251 | } catch { 252 | toPosition = -1; 253 | } 254 | 255 | if (oldFolderName === folderName) { 256 | moveGameInCurrentFolder(currentDraggingItem, toPosition, folderName); 257 | } else { 258 | moveGameToAnotherFolder(currentDraggingItem, toPosition, oldFolderName, folderName); 259 | } 260 | } 261 | 262 | await updateData(); 263 | } 264 | 265 | onMount(() => { 266 | document.getElementById("sideBarFolders").addEventListener("scroll", () => { 267 | scrollY = document.getElementById("sideBarFolders").scrollTop; 268 | }); 269 | }); 270 | 271 | return ( 272 | <> 273 | 551 | 552 | ); 553 | } 554 | -------------------------------------------------------------------------------- /src/Text.js: -------------------------------------------------------------------------------- 1 | export const textLanguages = { 2 | "hey there! thank you so much for using clear": { 3 | jp: "ちょっと、そこ!いつもクリアをご利用いただき誠にありがとうございます", 4 | es: "¡hola! muchas gracias por usar claro", 5 | hi: "सुनो! clear का उपयोग करने के लिए आपका धन्यवाद ", 6 | ru: "Привет! большое спасибо за использование clear", 7 | fr: "Salut! Merci beaucoup d'utiliser clear", 8 | }, 9 | "add some new games using the sidebar buttons": { 10 | jp: "サイドバーボタンを使用して新しいゲームを追加します", 11 | es: "agregue algunos juegos nuevos usando los botones de la barra lateral", 12 | hi: "साइडबार बटनों का उपयोग करके कुछ नए गेम डालो", 13 | ru: "Добавьте новые игры с помощью кнопок на боковой панели", 14 | fr: "Ajoute de nouveaux jeux en utilisant les boutons de la barre latérale", 15 | }, 16 | "create new folders and drag and drop your games into them": { 17 | jp: "新しいフォルダーを作成し、そこにゲームをドラッグ アンド ドロップします。", 18 | es: "Crea nuevas carpetas y arrastra y suelta tus juegos en ellas.", 19 | hi: "नए फ़ोल्डर बनाएं और अपने गेम को उनमें उठाकर डालो", 20 | ru: "Создавайте новые папки и перетаскивайте в них свои игры", 21 | fr: "Créer de nouveaux dossiers et glisse-y des jeux", 22 | }, 23 | "don't forget to check out the settings!": { 24 | jp: "設定を確認することを忘れないでください", 25 | es: "¡No olvides revisar la configuración!", 26 | hi: "सेटिंग्स की जांच करना न भूलें!", 27 | ru: "Не забудьте проверить настройки!", 28 | fr: "N'oublie pas de visiter les paramètres !", 29 | }, 30 | "import Steam games": { 31 | jp: "Steam ゲームをインポートする", 32 | es: "importar juegos de steam", 33 | hi: "steam से गेम clear में लाये", 34 | ru: "Импорт игр из Steam", 35 | fr: "Importer des jeux Steam", 36 | }, 37 | "might not work perfectly!": { 38 | jp: "完全に動作しない可能性があります!", 39 | es: "¡Puede que no funcione perfectamente!", 40 | hi: "हो सकता है कि यह पूरी तरह से काम न करे!", 41 | ru: "Может работать не идеально!", 42 | fr: "Pourrait ne pas fonctionner parfaitement !", 43 | }, 44 | "new game": { 45 | jp: "新しいゲーム", 46 | es: "nuevo juego", 47 | hi: "गेम डालो", 48 | ru: "Новая игра", 49 | fr: "Nouveau jeu", 50 | }, 51 | "open settings": { 52 | jp: "設定を開く", 53 | es: "configuración abierta", 54 | hi: "सेटिंग्स खोलो", 55 | ru: "Открыть настройки", 56 | fr: "Ouvrir les paramètres", 57 | }, 58 | "new folder": { 59 | jp: "新しいフォルダ", 60 | es: "nueva carpeta", 61 | hi: "फोल्डर बनाओ", 62 | ru: "Новая папка", 63 | fr: "Nouveau dossier", 64 | }, 65 | "open notepad": { 66 | jp: "メモ帳を開く", 67 | es: "abrir el bloc de notas", 68 | hi: "नोटपैड खोलो", 69 | ru: "Открыть блокнот", 70 | fr: "Ouvrir le bloc-notes", 71 | }, 72 | "close app": { 73 | jp: "アプリを閉じる", 74 | es: "cerrar app", 75 | hi: "ऐप बंद करें", 76 | ru: "Закрыть приложение", 77 | fr: "Fermer l'application", 78 | }, 79 | "change zoom": { 80 | jp: "ズームを変更する", 81 | es: "cambiar zoom", 82 | hi: "ज़ूम बदलें", 83 | ru: "Изменить масштаб", 84 | fr: "Changer le zoom", 85 | }, 86 | "search bar": { 87 | jp: "検索バー", 88 | es: "barra de búsqueda", 89 | hi: "गेम ढूंढे", 90 | ru: "Окно поиска", 91 | fr: "Barre de recherche", 92 | }, 93 | "hide sidebar": { 94 | jp: "サイドバーを隠す", 95 | es: "esconder barra lateral", 96 | hi: "साइडबार छुपाओ", 97 | ru: "Скрыть боковую панель", 98 | fr: "Masquer la barre latérale", 99 | }, 100 | "quick open game": { 101 | jp: "クイックオープンゲーム", 102 | es: "juego abierto rápido", 103 | hi: "गेम जल्दी खोलो", 104 | ru: "Быстро открыть игру", 105 | fr: "Lancement rapide du jeu", 106 | }, 107 | "no game file": { 108 | jp: "ファイルがない", 109 | es: "ningún archivo", 110 | hi: "कोई गेम फ़ाइल नहीं", 111 | ru: "Отсутствует файл игры", 112 | fr: "Pas de fichier de jeu", 113 | }, 114 | "no games found": { 115 | jp: "ゲームが見つかりません", 116 | es: "no se han encontrado juegos", 117 | hi: "कोई गेम नहीं मिला", 118 | ru: "Игры не найдены", 119 | fr: "Aucun jeu trouvé", 120 | }, 121 | uncategorized: { 122 | jp: "未分類", 123 | es: "sin categoría", 124 | hi: "अवर्गीकृत", 125 | ru: "Без категории", 126 | fr: "Non classé", 127 | }, 128 | "add game": { 129 | jp: "ゲームを追加", 130 | es: "agregar juego", 131 | hi: "गेम डालो", 132 | ru: "Добавить игру", 133 | fr: "Ajouter un jeu", 134 | }, 135 | "add folder": { 136 | jp: "フォルダーを追加", 137 | es: "agregar carpeta", 138 | hi: "फ़ोल्डर बनाओ", 139 | ru: "Добавить папку", 140 | fr: "Ajouter un dossier", 141 | }, 142 | notepad: { 143 | jp: "メモ帳", 144 | es: "bloc", 145 | hi: "नोटपैड", 146 | ru: "Блокнот", 147 | fr: "Bloc-notes", 148 | }, 149 | settings: { 150 | jp: "設定", 151 | es: "ajustes", 152 | hi: "सेटिंग्स", 153 | ru: "Настройки", 154 | fr: "Paramètres", 155 | }, 156 | search: { 157 | jp: "検索", 158 | es: "buscar", 159 | hi: "गेम ढूंढो", 160 | ru: "Поиск", 161 | fr: "Rechercher", 162 | }, 163 | "new update available!": { 164 | jp: "新しいアップデートが利用可能になりました!", 165 | es: "¡Nueva actualización disponible!", 166 | hi: "नई संस्करण उपलब्ध है!", 167 | ru: "Доступно новое обновление!", 168 | fr: "Nouvelle mise à jour disponible!", 169 | }, 170 | "rounded borders": { 171 | jp: "丸い境界線", 172 | es: "bordes redondeados", 173 | hi: "गोलाकार सीमाएँ", 174 | ru: "Закругленные границы", 175 | fr: "Bordures arrondies", 176 | }, 177 | "game title": { 178 | jp: "ゲームタイトル", 179 | es: "título del juego", 180 | hi: "खेल का नाम", 181 | ru: "Название игры", 182 | fr: "Titre du jeu", 183 | }, 184 | "folder title": { 185 | jp: "フォルダタイトル", 186 | es: "título de la carpeta", 187 | hi: "फ़ोल्डर का नाम", 188 | ru: "Название папки", 189 | fr: "Titre du dossier", 190 | }, 191 | "quit after opening game": { 192 | jp: "ゲーム開始後に終了", 193 | es: "salir después de abrir el juego", 194 | hi: "खेल खुन्लने पर clear बांध करो", 195 | ru: "Выйти после открытия игры", 196 | fr: "Quitter après avoir lancer le jeu", 197 | }, 198 | font: { 199 | jp: "フォント", 200 | es: "fuente", 201 | hi: "फ़ॉन्ट", 202 | ru: "Шрифт", 203 | fr: "Police", 204 | }, 205 | "sans serif": { 206 | jp: "サンセリフ", 207 | es: "sans serif", 208 | hi: "सान्स सेरिफ़", 209 | ru: "Без засечек", 210 | fr: "Sans sérif", 211 | }, 212 | mono: { 213 | jp: "単核症", 214 | es: "mononucleosis infecciosa", 215 | hi: "मोनो", 216 | ru: "Моно", 217 | fr: "mono", 218 | }, 219 | serif: { 220 | jp: "セリフ", 221 | es: "serifa", 222 | hi: "सेरिफ़", 223 | ru: "С засечками", 224 | fr: "Sérif", 225 | }, 226 | light: { 227 | jp: "ライト", 228 | es: "Ligero", 229 | hi: "रोशनी", 230 | ru: "Светлая", 231 | fr: "Clair", 232 | }, 233 | dark: { 234 | jp: "暗い", 235 | es: "oscuro", 236 | hi: "अँधेरा", 237 | ru: "Темная", 238 | fr: "Sombre", 239 | }, 240 | theme: { 241 | jp: "テーマ", 242 | es: "tema", 243 | hi: "विषय", 244 | ru: "Тема", 245 | fr: "Thème", 246 | }, 247 | "open library location": { 248 | jp: "図書館の場所を開く", 249 | es: "ubicación de la biblioteca abierta", 250 | hi: "clear के डाटा का स्थान खोलें", 251 | ru: "Расположение открытой библиотеки", 252 | fr: "Ouvrir le dossier de bibliothèque", 253 | }, 254 | "these are all the files that the app stores on your pc": { 255 | jp: "これらはすべて、アプリが PC に保存するファイルです", 256 | es: "Estos son todos los archivos que la aplicación almacena en tu PC", 257 | hi: "ये सभी फ़ाइलें हैं जिन्हें ऐप आपके पीसी पर संग्रहीत करता है", 258 | ru: "Это все файлы, которые приложение хранит на вашем компьютере", 259 | fr: "Tous les fichiers de l’application sur le PC", 260 | }, 261 | feedback: { 262 | jp: "フィードバック", 263 | es: "comentario", 264 | hi: "प्रतिक्रिया", 265 | ru: "Обратная связь", 266 | fr: "Commentaire", 267 | }, 268 | website: { 269 | jp: "ウェブサイト", 270 | es: "sitio web", 271 | hi: "वेबसाइट", 272 | ru: "Веб-сайт", 273 | fr: "Site web", 274 | }, 275 | "made by": { 276 | jp: "作られた", 277 | es: "hecho por", 278 | hi: "बनाने वाला आदमी:", 279 | ru: "Сделан", 280 | fr: "Fait par", 281 | }, 282 | "buy me a coffee": { 283 | jp: "コーヒーを買ってください", 284 | es: "cómprame un café", 285 | hi: "मेरे लिए एक कॉफ़ी खरीदो", 286 | ru: "Купить мне кофе", 287 | fr: "Achètez-moi un café", 288 | }, 289 | "write anything you want over here!": { 290 | jp: "ここに何でも書いてください!", 291 | es: "¡Escribe lo que quieras aquí!", 292 | hi: "यहां पर आप जो चाहें लिखें!", 293 | ru: "Пишите сюда все, что хотите!", 294 | fr: "Écrivez tout ce que vous voulez ici !", 295 | }, 296 | "add new game": { 297 | jp: "新しいゲームを追加する", 298 | es: "agregar nuevo juego", 299 | hi: "नया गेम डालें", 300 | ru: "Добавить новую игру", 301 | fr: "Ajouter un nouveau jeu", 302 | }, 303 | favourite: { 304 | jp: "お気に入り", 305 | es: "favorita", 306 | hi: "मेरा पसंदीदा हे", 307 | ru: "Избранное", 308 | fr: "Favoris", 309 | }, 310 | save: { 311 | jp: "保存", 312 | es: "ahorrar", 313 | hi: "डालदो", 314 | ru: "Сохранить", 315 | fr: "Sauvegarder", 316 | }, 317 | "grid/cover": { 318 | jp: "グリッド/カバー", 319 | es: "rejilla/cubierta", 320 | hi: "कवर चित्र", 321 | ru: "Обложка", 322 | fr: "grille/couvercle", 323 | }, 324 | hero: { 325 | jp: "ヒーロー", 326 | es: "héroe", 327 | hi: "नायक चित्र", 328 | ru: "Фон", 329 | fr: "Bannière", 330 | }, 331 | logo: { 332 | jp: "ロゴ", 333 | es: "logo", 334 | hi: "लोगो", 335 | ru: "Логотип", 336 | fr: "Logo", 337 | }, 338 | icon: { 339 | jp: "アイコン", 340 | es: "icono", 341 | hi: "आइकन", 342 | ru: "Иконка", 343 | fr: "Icône", 344 | }, 345 | "name of game": { 346 | jp: "ゲームの名前", 347 | es: "nombre del juego", 348 | hi: "गेम का नाम", 349 | ru: "Название игры", 350 | fr: "Nom du jeu", 351 | }, 352 | "auto find assets": { 353 | jp: "アセットの自動検索", 354 | es: "búsqueda automática", 355 | hi: "स्वत: चित्र ढूंढो", 356 | ru: "Автоматический поиск", 357 | fr: "Recherche automatique", 358 | }, 359 | "find assets": { 360 | jp: "資産を見つける", 361 | es: "encontrar activos", 362 | hi: "चित्र खुद ढूंढो", 363 | ru: "Найти объекты", 364 | fr: "Trouver des éléments graphiques", 365 | }, 366 | "locate game": { 367 | jp: "ゲームを探す", 368 | es: "localizar juego", 369 | hi: "खेल का पता लगाएं", 370 | ru: "Расположение игры", 371 | fr: "Localiser le jeu", 372 | }, 373 | "right click to empty image selection": { 374 | jp: "右クリックして画像の選択を空にします", 375 | es: "clic derecho para vaciar la selección de imágenes", 376 | hi: "चित्र निकलने के लिए राइट क्लिक करें", 377 | ru: "Щелкните правой кнопкой мыши, чтобы удалить изображение", 378 | fr: "Clic droit pour vider la sélection d'images", 379 | }, 380 | "add new folder": { 381 | jp: "新しいフォルダーを追加する", 382 | es: "agregar nueva carpeta", 383 | hi: "नया फ़ोल्डर बनाओ", 384 | ru: "Создать папку", 385 | fr: "Ajouter un nouveau dossier", 386 | }, 387 | "hide in expanded view": { 388 | jp: "展開表示で非表示にする", 389 | es: "ocultar en vista ampliada", 390 | hi: "विस्तारित दृश्य में छिपाएँ", 391 | ru: "Скрыть в развернутом виде", 392 | fr: "Masquer dans la vue développée", 393 | }, 394 | "name of folder": { 395 | jp: "フォルダの名前", 396 | es: "nombre de la carpeta", 397 | hi: "फ़ोल्डर का नाम", 398 | ru: "Имя папки", 399 | fr: "Nom du dossier", 400 | }, 401 | loading: { 402 | jp: "読み込み中", 403 | es: "cargando", 404 | hi: "लोड हो रहा है", 405 | ru: "Загрузка", 406 | fr: "Chargement", 407 | }, 408 | play: { 409 | jp: "遊ぶ", 410 | es: "jugar", 411 | hi: "खेलो", 412 | ru: "Играть", 413 | fr: "Jouer", 414 | }, 415 | edit: { 416 | jp: "編集", 417 | es: "editar", 418 | hi: "बदलो", 419 | ru: "Редактировать", 420 | fr: "Modifier", 421 | }, 422 | delete: { 423 | jp: "消去", 424 | es: "borrar", 425 | hi: "निकल्दो", 426 | ru: "Удалить", 427 | fr: "Supprimer", 428 | }, 429 | "confirm?": { 430 | jp: "確認する?", 431 | es: "¿confirmar?", 432 | hi: "सच्चिमे?", 433 | ru: "Вы уверены?", 434 | fr: "Confirmer ?", 435 | }, 436 | "scroll on the image to select a different asset": { 437 | jp: "画像をスクロールして別のアセットを選択します", 438 | es: "desplácese por la imagen para seleccionar un activo diferente", 439 | hi: "नया चित्र चुनने के लिए स्क्रॉल करना", 440 | ru: "Прокрутите изображение, чтобы выбрать другой объект", 441 | fr: "Utilisez la molette sur l'image pour en sélectionner une autre", 442 | }, 443 | "select the official name of your game": { 444 | jp: "ゲームの正式名を選択してください", 445 | es: "selecciona el nombre oficial de tu juego", 446 | hi: "अपने गेम का ऑफिसियल नाम डाले", 447 | ru: "Выберите официальное название вашей игры", 448 | fr: "Sélectionnez le nom officiel de votre jeu", 449 | }, 450 | "open containing folder": { 451 | jp: "含まれているフォルダーを開く", 452 | es: "carpeta que contiene abierto", 453 | hi: "गेम होने वाले फोल्डर को खोलना", 454 | ru: "Открыть папку с игрой", 455 | fr: "Ouvrir le dossier contenant", 456 | }, 457 | "current 'steam' folder will be overwritten. confirm?": { 458 | jp: "現在の「steam」フォルダーは上書きされます。確認する?", 459 | es: "Se sobrescribirá la carpeta 'Steam' actual. ¿confirmar?", 460 | hi: "अभी होने वाला steam फ़ोल्डर मिट जायेगा. चलेगा?", 461 | ru: "Текущая папка Steam будет перезаписана. Вы уверены?", 462 | fr: "Le dossier « Steam » actuel sera écrasé. Confirmer ?", 463 | }, 464 | "no game file provided!": { 465 | jp: "ゲームファイルが提供されていません!", 466 | es: "¡No se proporciona ningún archivo de juego!", 467 | hi: "कोई गेम फ़ाइल दिया नहीं गया!", 468 | ru: "Файл игры не предоставлен!", 469 | fr: "Aucun fichier de jeu fourni !", 470 | }, 471 | "game launched! enjoy your session!": { 472 | jp: "ゲームが起動しました!セッションをお楽しみください!", 473 | es: "¡Juego lanzado! ¡Disfruta tu sesión!", 474 | hi: "गेम लॉन्च हो गया! मजे करो!", 475 | ru: "Игра запущена! Наслаждайтесь сеансом!", 476 | fr: "Jeu lancé ! Bonne séance !", 477 | }, 478 | "you're not connected to the internet :(": { 479 | jp: "インターネットに接続していません:(", 480 | es: "No estas conectado a internet :(", 481 | hi: "आप इंटरनेट से कनेक्ट नहीं हैं :(", 482 | ru: "Отсутсвует подключение к интернету :(", 483 | fr: "Pas de connection à internet :(", 484 | }, 485 | "no folder name": { 486 | jp: "フォルダ名がありません", 487 | es: "sin nombre de carpeta", 488 | hi: "फोल्डर का नाम नहीं दिया गया है", 489 | ru: "Отсутсвует имя папки", 490 | fr: "Pas de nom de dossier", 491 | }, 492 | "is already in your library": { 493 | jp: "すでにライブラリにあります", 494 | es: "ya está en tu biblioteca", 495 | hi: "आपके लाइब्रेरी में कबका है", 496 | ru: "Уже есть в вашей библиотеке", 497 | fr: "est déjà dans votre bibliothèque", 498 | }, 499 | "no game name": { 500 | jp: "ゲーム名なし", 501 | es: "sin nombre de juego", 502 | hi: "गेम का नाम नहीं दिया गया है", 503 | ru: "Отсутсвует имя игры", 504 | fr: "Pas de nom de jeu", 505 | }, 506 | "couldn't find that game :(": { 507 | jp: "そのゲームは見つかりませんでした:(", 508 | es: "no pude encontrar ese juego :(", 509 | hi: "वह गेम नहीं मिल सका :(", 510 | ru: "Игра не найдена :(", 511 | fr: "Jeu non trouvé :(", 512 | }, 513 | "couldn't find any assets :(": { 514 | jp: "アセットが見つかりませんでした:(", 515 | es: "no pude encontrar ningún activo :(", 516 | hi: "कोई चित्र नहीं मिल पाए :(", 517 | ru: "Не нашел никаких объектов :(", 518 | fr: "Aucun élément trouvé :(", 519 | }, 520 | "couldn't find": { 521 | jp: "見つかりませんでした", 522 | es: "no pude encontrar", 523 | hi: "यह नहीं मिल पाया", 524 | ru: "Не найдено", 525 | fr: "Pas trouver", 526 | }, 527 | "sorry but there was an error \n reading your Steam library :(": { 528 | jp: "申し訳ありませんが、Steam ライブラリの読み取り中にエラーが発生しました:(", 529 | es: "Lo siento pero hubo un error \n al leer tu biblioteca de Steam :(", 530 | hi: "क्षमा करें, लेकिन आपकी steam लाइब्रेरी को clear पद नहीं पाया :(", 531 | ru: "Извините, но при чтении вашей библиотеки Steam произошла ошибка :(", 532 | fr: "Désolé mais il y a eu une erreur lors de la lecture de votre bibliothèque Steam :(", 533 | }, 534 | language: { 535 | jp: "言語", 536 | es: "idioma", 537 | hi: "भाषा", 538 | ru: "Язык", 539 | fr: "Langue", 540 | }, 541 | "skip to games": { 542 | jp: "", 543 | es: "", 544 | hi: "", 545 | ru: "", 546 | fr: "", 547 | }, 548 | scroll: { 549 | jp: "", 550 | es: "", 551 | hi: "", 552 | ru: "", 553 | fr: "", 554 | }, 555 | "close current dialog before opening another": { 556 | jp: "", 557 | es: "", 558 | hi: "", 559 | ru: "", 560 | fr: "", 561 | }, 562 | "close current dialog before toggling sidebar": { 563 | jp: "", 564 | es: "", 565 | hi: "", 566 | ru: "", 567 | fr: "", 568 | }, 569 | "open sidebar": { 570 | jp: "", 571 | es: "", 572 | hi: "", 573 | ru: "", 574 | fr: "", 575 | }, 576 | "close sidebar": { 577 | jp: "", 578 | es: "", 579 | hi: "", 580 | ru: "", 581 | fr: "", 582 | }, 583 | hidden: { 584 | jp: "", 585 | es: "", 586 | hi: "", 587 | ru: "", 588 | fr: "", 589 | }, 590 | close: { 591 | jp: "", 592 | es: "", 593 | hi: "", 594 | ru: "", 595 | fr: "", 596 | }, 597 | "scroll left": { 598 | jp: "", 599 | es: "", 600 | hi: "", 601 | ru: "", 602 | fr: "", 603 | }, 604 | "scroll right": { 605 | jp: "", 606 | es: "", 607 | hi: "", 608 | ru: "", 609 | fr: "", 610 | }, 611 | "hit again to confirm": { 612 | jp: "", 613 | es: "", 614 | hi: "", 615 | ru: "", 616 | fr: "", 617 | }, 618 | "arrow keys": { 619 | jp: "", 620 | es: "", 621 | hi: "", 622 | ru: "", 623 | fr: "", 624 | }, 625 | "not implemented on macOS yet! sorry :(": { 626 | jp: "", 627 | es: "", 628 | hi: "", 629 | ru: "", 630 | fr: "", 631 | }, 632 | }; 633 | -------------------------------------------------------------------------------- /src/components/GameCardSideBar.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | SelectedDataContext, 6 | locationJoin, 7 | openDialog, 8 | openGame, 9 | translateText, 10 | } from "../Globals.jsx"; 11 | 12 | import { convertFileSrc } from "@tauri-apps/api/tauri"; 13 | // importing code snippets and library functions 14 | import { Show, useContext } from "solid-js"; 15 | 16 | export function GameCardSideBar(props) { 17 | const globalContext = useContext(GlobalContext); 18 | const selectedDataContext = useContext(SelectedDataContext); 19 | const applicationStateContext = useContext(ApplicationStateContext); 20 | 21 | return ( 22 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/GameCards.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | SelectedDataContext, 6 | locationJoin, 7 | openDialog, 8 | openGame, 9 | translateText, 10 | } from "../Globals.jsx"; 11 | 12 | import { convertFileSrc } from "@tauri-apps/api/tauri"; 13 | // importing code snippets and library functions 14 | import { For, Show, useContext } from "solid-js"; 15 | 16 | export function GameCards(props) { 17 | const globalContext = useContext(GlobalContext); 18 | const applicationStateContext = useContext(ApplicationStateContext); 19 | const selectedDataContext = useContext(SelectedDataContext); 20 | 21 | return ( 22 | 23 | {(gameName, index) => { 24 | return ( 25 |
76 | } 77 | > 78 |
79 | 83 | 84 | {gameName} 85 | 86 |
87 |
88 | } 89 | > 90 | 102 |
103 | 104 |
105 | 116 |
117 |
118 |
119 | 120 | 121 |
122 | {gameName} 123 |
124 |
125 | 126 | ); 127 | }} 128 | 129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /src/components/Hotkeys.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { ApplicationStateContext, translateText } from "../Globals.jsx"; 3 | 4 | // importing code snippets and library functions 5 | import { Show, useContext } from "solid-js"; 6 | 7 | export function Hotkeys(props) { 8 | const applicationStateContext = useContext(ApplicationStateContext); 9 | 10 | const modifierKeyPrefix = applicationStateContext.systemPlatform() === "windows" ? "ctrl" : "⌘"; 11 | 12 | return ( 13 | <> 14 |
15 |
16 |
17 | {modifierKeyPrefix} + n 18 |
19 | 20 | {translateText("new game")} 21 |
22 | 23 |
24 |
25 | {modifierKeyPrefix} + , 26 |
27 | 28 | {translateText("open settings")} 29 |
30 |
31 |
32 | {modifierKeyPrefix} + f 33 |
34 | 35 | {translateText("search bar")} 36 |
37 |
38 |
39 | {modifierKeyPrefix} + m 40 |
41 | 42 | {translateText("new folder")} 43 |
44 |
45 |
46 | {modifierKeyPrefix} + l 47 |
48 | 49 | {translateText("open notepad")} 50 |
51 |
52 |
53 | {modifierKeyPrefix} + \\ 54 |
55 | 56 | {translateText("hide sidebar")} 57 |
58 | 59 | 60 |
61 |
62 | {modifierKeyPrefix} + w 63 |
64 | 65 | {translateText("close app")} 66 |
67 |
68 |
69 | {modifierKeyPrefix} - / = 70 |
71 | 72 | {translateText("change zoom")} 73 |
74 | 75 |
76 |
77 | {modifierKeyPrefix} + click 78 |
79 | 80 | {translateText("quick open game")} 81 |
82 |
83 |
84 | 85 | 86 |
87 |
88 |
89 | {modifierKeyPrefix} + f 90 |
91 | 92 | {translateText("search bar")} 93 |
94 |
95 |
96 | {modifierKeyPrefix} + \\ 97 |
98 | 99 | {translateText("hide sidebar")} 100 |
101 |
102 |
103 | {modifierKeyPrefix} + click 104 |
105 | 106 | {translateText("quick open game")} 107 |
108 |
109 |
110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/LanguageSelector.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { GlobalContext, UIContext, translateText, updateData } from "../Globals.jsx"; 3 | 4 | // importing code snippets and library functions 5 | import { Show, createSignal, useContext } from "solid-js"; 6 | 7 | export function LanguageSelector(props) { 8 | const globalContext = useContext(GlobalContext); 9 | const uiContext = useContext(UIContext); 10 | 11 | const [showLanguageSelector, setShowLanguageSelector] = createSignal(false); 12 | 13 | async function changeLanguage(lang) { 14 | globalContext.setLibraryData("userSettings", "language", lang); 15 | 16 | await updateData(); 17 | setShowLanguageSelector(false); 18 | uiContext.setShowSettingsLanguageSelector(false); 19 | } 20 | 21 | function returnLanguageFullName(shortName) { 22 | switch (shortName) { 23 | case "en": 24 | return "english"; 25 | case "jp": 26 | return "日本語"; 27 | case "es": 28 | return "Español"; 29 | case "hi": 30 | return "हिंदी"; 31 | case "ru": 32 | return "русский"; 33 | case "fr": 34 | return "Français"; 35 | } 36 | return "english"; 37 | } 38 | 39 | return ( 40 | 82 | 91 | 100 | 109 | 118 | 134 |
135 | 136 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | 3 | // importing globals 4 | import { 5 | ApplicationStateContextProvider, 6 | GlobalContextProvider, 7 | SelectedDataContextProvider, 8 | SteamDataContextProvider, 9 | UIContextProvider, 10 | } from "./Globals.jsx"; 11 | 12 | // importing code snippets and library functions 13 | import { render } from "solid-js/web"; 14 | import App from "./App.jsx"; 15 | 16 | render( 17 | () => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ), 30 | document.getElementById("root"), 31 | ); 32 | -------------------------------------------------------------------------------- /src/libraries/Icons.jsx: -------------------------------------------------------------------------------- 1 | // icons from https://basicons.xyz/ 2 | 3 | // INFO use when no text equivalent is present 4 | // INFO else use aria-hidden="true" to hide from assistive technologies 5 | 6 | export function ChevronArrows({ classProp }) { 7 | return ( 8 | <svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class={classProp}> 9 | <path 10 | d="M6 11L1 6L6 1" 11 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 12 | stroke-width="1.3" 13 | stroke-linecap="round" 14 | stroke-linejoin="round" 15 | /> 16 | <path 17 | d="M11 11L6 6L11 1" 18 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 19 | stroke-width="1.3" 20 | stroke-linecap="round" 21 | stroke-linejoin="round" 22 | /> 23 | </svg> 24 | ); 25 | } 26 | 27 | export function EyeClosed() { 28 | return ( 29 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 30 | <path 31 | d="M2 2L22 22" 32 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 33 | stroke-width="1.5" 34 | stroke-linecap="round" 35 | stroke-linejoin="round" 36 | /> 37 | <path 38 | d="M6.71277 6.7226C3.66479 8.79527 2 12 2 12C2 12 5.63636 19 12 19C14.0503 19 15.8174 18.2734 17.2711 17.2884M11 5.05822C11.3254 5.02013 11.6588 5 12 5C18.3636 5 22 12 22 12C22 12 21.3082 13.3317 20 14.8335" 39 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 40 | stroke-width="1.5" 41 | stroke-linecap="round" 42 | stroke-linejoin="round" 43 | /> 44 | <path 45 | d="M14 14.2362C13.4692 14.7112 12.7684 15.0001 12 15.0001C10.3431 15.0001 9 13.657 9 12.0001C9 11.1764 9.33193 10.4303 9.86932 9.88818" 46 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 47 | stroke-width="1.5" 48 | stroke-linecap="round" 49 | stroke-linejoin="round" 50 | /> 51 | </svg> 52 | ); 53 | } 54 | 55 | export function Edit() { 56 | return ( 57 | <svg width="14" height="14" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 58 | <path 59 | d="M2.5 11.5L12.5 1.50002C13.3284 0.671585 14.6716 0.671585 15.5 1.50002C16.3284 2.32845 16.3284 3.67159 15.5 4.50002L5.5 14.5L1.5 15.5L2.5 11.5Z" 60 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 61 | stroke-width="1.5" 62 | stroke-linecap="round" 63 | stroke-linejoin="round" 64 | /> 65 | </svg> 66 | ); 67 | } 68 | 69 | export function GameController() { 70 | return ( 71 | <svg 72 | aria-hidden="true" 73 | width="18" 74 | height="18" 75 | viewBox="0 0 24 24" 76 | class="stroke-[#000000] dark:stroke-[#ffffff]" 77 | fill="none" 78 | xmlns="http://www.w3.org/2000/svg" 79 | > 80 | <path 81 | d="M8 9V13M6 11H10M17 10.0161L17.0161 10M14 12.0161L14.0161 12M16.1836 5H7.81641C5.60774 5 3.71511 6.57359 3.32002 8.73845L2.0451 15.7241C1.84609 16.8145 2.31653 17.9185 3.24219 18.5333C4.3485 19.268 5.82159 19.1227 6.76177 18.1861L7.99615 16.9563C8.36513 16.5887 8.86556 16.3822 9.38737 16.3822H14.6126C15.1344 16.3822 15.6349 16.5887 16.0038 16.9563L17.2382 18.1861C18.1784 19.1227 19.6515 19.268 20.7578 18.5333C21.6835 17.9185 22.1539 16.8145 21.9549 15.7241L20.68 8.73845C20.2849 6.57359 18.3923 5 16.1836 5Z" 82 | stroke-width="1.5" 83 | stroke-linecap="round" 84 | stroke-linejoin="round" 85 | /> 86 | </svg> 87 | ); 88 | } 89 | 90 | export function Folder() { 91 | return ( 92 | <svg 93 | aria-hidden="true" 94 | width="18" 95 | height="18" 96 | class="stroke-[#000000] dark:stroke-[#ffffff]" 97 | viewBox="0 0 24 24" 98 | fill="none" 99 | xmlns="http://www.w3.org/2000/svg" 100 | > 101 | <path 102 | d="M4 21H20C21.1046 21 22 20.1046 22 19V8C22 6.89543 21.1046 6 20 6H11L9.29687 3.4453C9.1114 3.1671 8.79917 3 8.46482 3H4C2.89543 3 2 3.89543 2 5V19C2 20.1046 2.89543 21 4 21Z" 103 | stroke-width="1.5" 104 | stroke-linecap="round" 105 | stroke-linejoin="round" 106 | /> 107 | <path d="M12 10V16M9 13H15" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> 108 | </svg> 109 | ); 110 | } 111 | 112 | export function Notepad() { 113 | return ( 114 | <svg 115 | aria-hidden="true" 116 | width="18" 117 | height="18" 118 | class="stroke-[#000000] dark:stroke-[#ffffff]" 119 | viewBox="0 0 24 24" 120 | fill="none" 121 | xmlns="http://www.w3.org/2000/svg" 122 | > 123 | <path 124 | d="M6 22H18C19.1046 22 20 21.1046 20 20V9.82843C20 9.29799 19.7893 8.78929 19.4142 8.41421L13.5858 2.58579C13.2107 2.21071 12.702 2 12.1716 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22Z" 125 | stroke-width="1.5" 126 | stroke-linecap="round" 127 | stroke-linejoin="round" 128 | /> 129 | <path d="M13 2.5V9H19" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> 130 | </svg> 131 | ); 132 | } 133 | 134 | export function UpdateDownload() { 135 | return ( 136 | <svg 137 | width="18" 138 | height="18" 139 | viewBox="0 0 24 26" 140 | class="stroke-[#000000] dark:stroke-[#ffffff]" 141 | fill="none" 142 | xmlns="http://www.w3.org/2000/svg" 143 | aria-hidden="true" 144 | > 145 | <path 146 | d="M3 14V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V14" 147 | stroke-width="1.5" 148 | stroke-linecap="round" 149 | stroke-linejoin="round" 150 | /> 151 | <path 152 | d="M12 3V17M12 17L7 11.5555M12 17L17 11.5556" 153 | stroke-width="1.5" 154 | stroke-linecap="round" 155 | stroke-linejoin="round" 156 | /> 157 | </svg> 158 | ); 159 | } 160 | 161 | export function Settings() { 162 | return ( 163 | <svg 164 | aria-hidden="true" 165 | width="18" 166 | height="18" 167 | viewBox="0 0 24 24" 168 | fill="none" 169 | class="stroke-[#000000] dark:stroke-[#ffffff] " 170 | xmlns="http://www.w3.org/2000/svg" 171 | > 172 | <path 173 | d="M10.0761 3.16311C10.136 2.50438 10.6883 2 11.3497 2H12.6503C13.3117 2 13.864 2.50438 13.9239 3.16311C13.9731 3.70392 14.3623 4.14543 14.8708 4.336C15.0015 4.38499 15.1307 4.43724 15.2582 4.49263C15.7613 4.71129 16.3531 4.66938 16.7745 4.31818C17.2953 3.8842 18.0611 3.91894 18.5404 4.39829L19.4584 5.31623C19.9154 5.77326 19.9485 6.50338 19.5347 6.99992C19.1901 7.41349 19.158 7.99745 19.3897 8.48341C19.49 8.69386 19.5816 8.90926 19.664 9.12916C19.8546 9.63767 20.2961 10.0269 20.8369 10.0761C21.4956 10.136 22 10.6883 22 11.3497V12.6503C22 13.3117 21.4956 13.864 20.8369 13.9239C20.2961 13.9731 19.8546 14.3623 19.664 14.8708C19.59 15.0682 19.5086 15.262 19.4202 15.4518C19.2053 15.913 19.2401 16.4637 19.5658 16.8546C19.962 17.33 19.9303 18.0291 19.4927 18.4667L18.4667 19.4927C18.0291 19.9303 17.33 19.962 16.8546 19.5658C16.4637 19.2401 15.913 19.2053 15.4518 19.4202C15.262 19.5086 15.0682 19.59 14.8708 19.664C14.3623 19.8546 13.9731 20.2961 13.9239 20.8369C13.864 21.4956 13.3117 22 12.6503 22H11.3497C10.6883 22 10.136 21.4956 10.0761 20.8369C10.0269 20.2961 9.63767 19.8546 9.12917 19.664C8.90927 19.5816 8.69387 19.49 8.48343 19.3897C7.99746 19.158 7.4135 19.1901 6.99992 19.5347C6.50338 19.9485 5.77325 19.9154 5.31622 19.4584L4.39829 18.5404C3.91893 18.0611 3.8842 17.2953 4.31818 16.7745C4.66939 16.3531 4.71129 15.7613 4.49263 15.2582C4.43724 15.1307 4.385 15.0016 4.336 14.8708C4.14544 14.3623 3.70392 13.9731 3.16311 13.9239C2.50438 13.864 2 13.3117 2 12.6503V11.3497C2 10.6883 2.50438 10.136 3.16311 10.0761C3.70393 10.0269 4.14544 9.63768 4.33601 9.12917C4.3936 8.9755 4.45568 8.82402 4.52209 8.67489C4.7571 8.14716 4.71804 7.52257 4.34821 7.07877C3.89722 6.53758 3.93332 5.74179 4.43145 5.24365L5.24364 4.43146C5.74178 3.93332 6.53757 3.89722 7.07876 4.34822C7.52256 4.71805 8.14715 4.7571 8.67488 4.52209C8.82401 4.45568 8.97549 4.3936 9.12916 4.33601C9.63767 4.14544 10.0269 3.70393 10.0761 3.16311Z" 174 | stroke-width="1.5" 175 | stroke-linecap="round" 176 | stroke-linejoin="round" 177 | /> 178 | <path 179 | d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z" 180 | stroke-width="1.5" 181 | stroke-linecap="round" 182 | stroke-linejoin="round" 183 | /> 184 | </svg> 185 | ); 186 | } 187 | 188 | export function Steam() { 189 | return ( 190 | <svg aria-hidden="true" width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg"> 191 | <path 192 | d="M9.33521 10.141L10.8362 11.166L11.8246 12.667L15.0096 10.3973L11.5683 6.88281" 193 | class="fill-[#00000080] dark:fill-[#ffffff80] " 194 | /> 195 | <path 196 | d="M15.0827 8.6404C16.0734 8.6404 16.8765 7.83728 16.8765 6.84657C16.8765 5.85586 16.0734 5.05273 15.0827 5.05273C14.0919 5.05273 13.2888 5.85586 13.2888 6.84657C13.2888 7.83728 14.0919 8.6404 15.0827 8.6404Z" 197 | class="fill-[#00000080] dark:fill-[#ffffff80] " 198 | /> 199 | <path 200 | d="M9.18868 15.0834C10.4624 15.0834 11.495 14.0508 11.495 12.7771C11.495 11.5033 10.4624 10.4707 9.18868 10.4707C7.91492 10.4707 6.88232 11.5033 6.88232 12.7771C6.88232 14.0508 7.91492 15.0834 9.18868 15.0834Z" 201 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 202 | stroke-width="0.695568" 203 | /> 204 | <path 205 | d="M1.97681 9.81055L9.11554 12.7759" 206 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 207 | stroke-width="2.92871" 208 | stroke-linecap="round" 209 | /> 210 | <path 211 | d="M15.0827 9.81149C16.7204 9.81149 18.0481 8.48388 18.0481 6.84618C18.0481 5.20848 16.7204 3.88086 15.0827 3.88086C13.445 3.88086 12.1174 5.20848 12.1174 6.84618C12.1174 8.48388 13.445 9.81149 15.0827 9.81149Z" 212 | class="stroke-[#00000080] dark:stroke-[#ffffff80] " 213 | stroke-width="1.17148" 214 | /> 215 | </svg> 216 | ); 217 | } 218 | 219 | export function EmptyTray() { 220 | return ( 221 | <svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 222 | <path 223 | d="M3.04819 12H8.44444L10.2222 14H13.7778L15.5556 12H20.9361M6.70951 5.4902L3.27942 11.2785C3.09651 11.5871 3 11.9393 3 12.2981V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V12.2981C21 11.9393 20.9035 11.5871 20.7206 11.2785L17.2905 5.4902C17.1104 5.18633 16.7834 5 16.4302 5H7.5698C7.21659 5 6.88958 5.18633 6.70951 5.4902Z" 224 | stroke="white" 225 | stroke-width="1.5" 226 | stroke-linecap="round" 227 | stroke-linejoin="round" 228 | /> 229 | </svg> 230 | ); 231 | } 232 | 233 | export function SaveDisk() { 234 | return ( 235 | <svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 236 | <path 237 | d="M5 21H19C20.1046 21 21 20.1046 21 19V8.82843C21 8.29799 20.7893 7.78929 20.4142 7.41421L16.5858 3.58579C16.2107 3.21071 15.702 3 15.1716 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21Z" 238 | class="stroke-black dark:stroke-white" 239 | stroke-width="1.5" 240 | stroke-linecap="round" 241 | stroke-linejoin="round" 242 | /> 243 | <path 244 | d="M7 3V8H15V3" 245 | class="stroke-black dark:stroke-white" 246 | stroke-width="1.5" 247 | stroke-linecap="round" 248 | stroke-linejoin="round" 249 | /> 250 | <path 251 | d="M7 21V15H17V21" 252 | class="stroke-black dark:stroke-white" 253 | stroke-width="1.5" 254 | stroke-linecap="round" 255 | stroke-linejoin="round" 256 | /> 257 | </svg> 258 | ); 259 | } 260 | 261 | export function TrashDelete() { 262 | return ( 263 | <svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 264 | <path 265 | d="M3 6H21M5 6V20C5 21.1046 5.89543 22 7 22H17C18.1046 22 19 21.1046 19 20V6M8 6V4C8 2.89543 8.89543 2 10 2H14C15.1046 2 16 2.89543 16 4V6" 266 | stroke="#FF3636" 267 | stroke-width="2" 268 | stroke-linecap="round" 269 | stroke-linejoin="round" 270 | /> 271 | <path d="M14 11V17" stroke="#FF3636" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> 272 | <path d="M10 11V17" stroke="#FF3636" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> 273 | </svg> 274 | ); 275 | } 276 | 277 | export function Close() { 278 | return ( 279 | <svg width="16" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 280 | <path 281 | d="M1 1L11 10.3369M1 10.3369L11 1" 282 | stroke="#FF3636" 283 | stroke-width="2" 284 | stroke-linecap="round" 285 | stroke-linejoin="round" 286 | /> 287 | </svg> 288 | ); 289 | } 290 | 291 | export function OpenExternal() { 292 | return ( 293 | <svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 294 | <path 295 | d="M5.63605 18.364L18.364 5.63603M18.364 5.63603L8.46446 5.63604M18.364 5.63603V15.5355" 296 | class="stroke-black dark:stroke-white" 297 | stroke-width="1.5" 298 | stroke-linecap="round" 299 | stroke-linejoin="round" 300 | /> 301 | </svg> 302 | ); 303 | } 304 | 305 | export function Play() { 306 | return ( 307 | <svg aria-hidden="true" width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 308 | <path 309 | d="M1.69727 14.3947V0.894745L12.1973 7.64474L1.69727 14.3947Z" 310 | class="stroke-black dark:stroke-white" 311 | stroke-width="1.5" 312 | stroke-linecap="round" 313 | stroke-linejoin="round" 314 | /> 315 | </svg> 316 | ); 317 | } 318 | 319 | export function Loading() { 320 | return ( 321 | <svg 322 | width="14" 323 | height="14" 324 | viewBox="0 0 24 24" 325 | fill="none" 326 | xmlns="http://www.w3.org/2000/svg" 327 | class="loadingIcon" 328 | aria-hidden="true" 329 | > 330 | <path 331 | d="M16 16L19 19M18 12H22M8 8L5 5M16 8L19 5M8 16L5 19M2 12H6M12 2V6M12 18V22" 332 | class="stroke-[#000000] dark:stroke-[#ffffff] " 333 | stroke-width="1.5" 334 | stroke-linecap="round" 335 | stroke-linejoin="round" 336 | /> 337 | </svg> 338 | ); 339 | } 340 | 341 | export function ChevronArrow() { 342 | return ( 343 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 344 | <path 345 | d="M14 8L10 12L14 16" 346 | stroke="rgba(255,255,255,0.5)" 347 | stroke-width="1.5" 348 | stroke-linecap="round" 349 | stroke-linejoin="round" 350 | /> 351 | </svg> 352 | ); 353 | } 354 | -------------------------------------------------------------------------------- /src/libraries/fuzzysearch.js: -------------------------------------------------------------------------------- 1 | // fuzzy search from https://github.com/bevacqua/fuzzysearch 2 | 3 | export function fuzzysearch(needle, haystack) { 4 | const hlen = haystack.length; 5 | const nlen = needle.length; 6 | if (nlen > hlen) { 7 | return false; 8 | } 9 | if (nlen === hlen) { 10 | return needle === haystack; 11 | } 12 | outer: for (let i = 0, j = 0; i < nlen; i++) { 13 | const nch = needle.charCodeAt(i); 14 | while (j < hlen) { 15 | if (haystack.charCodeAt(j++) === nch) { 16 | continue outer; 17 | } 18 | } 19 | return false; 20 | } 21 | return true; 22 | } 23 | -------------------------------------------------------------------------------- /src/libraries/parseVDF.js: -------------------------------------------------------------------------------- 1 | // vdf parser from https://github.com/node-steam/vdf 2 | 3 | export function parseVDF(text) { 4 | if (typeof text !== "string") { 5 | throw new TypeError("VDF | Parse: Expecting parameter to be a string"); 6 | } 7 | 8 | const lines = text.split("\n"); 9 | const object = {}; 10 | const stack = [object]; 11 | let expect = false; 12 | 13 | const regex = new RegExp( 14 | '^("((?:\\\\.|[^\\\\"])+)"|([a-z0-9\\-\\_]+))' + 15 | "([ \t]*(" + 16 | '"((?:\\\\.|[^\\\\"])*)(")?' + 17 | "|([a-z0-9\\-\\_]+)" + 18 | "))?", 19 | ); 20 | 21 | let i = 0; 22 | const j = lines.length; 23 | 24 | let comment = false; 25 | 26 | for (; i < j; i++) { 27 | let line = lines[i].trim(); 28 | 29 | if (line.startsWith("/*") && line.endsWith("*/")) { 30 | continue; 31 | } 32 | 33 | if (line.startsWith("/*")) { 34 | comment = true; 35 | continue; 36 | } 37 | 38 | if (line.endsWith("*/")) { 39 | comment = false; 40 | continue; 41 | } 42 | 43 | if (comment) { 44 | continue; 45 | } 46 | 47 | if (line === "" || line[0] === "/") { 48 | continue; 49 | } 50 | if (line[0] === "{") { 51 | expect = false; 52 | continue; 53 | } 54 | if (expect) { 55 | throw new SyntaxError(`VDF | Parse: Invalid syntax on line ${i + 1}`); 56 | } 57 | if (line[0] === "}") { 58 | stack.pop(); 59 | continue; 60 | } 61 | 62 | /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ 63 | 64 | while (true) { 65 | const m = regex.exec(line); 66 | if (m === null) { 67 | throw new SyntaxError(`VDF | Parse: Invalid syntax on line ${i + 1}`); 68 | } 69 | const key = m[2] !== undefined ? m[2] : m[3]; 70 | let val = m[6] !== undefined ? m[6] : m[8]; 71 | 72 | if (val === undefined) { 73 | if (stack[stack.length - 1][key] === undefined) { 74 | stack[stack.length - 1][key] = {}; 75 | } 76 | stack.push(stack[stack.length - 1][key]); 77 | expect = true; 78 | } else { 79 | if (m[7] === undefined && m[8] === undefined) { 80 | line += `\n${lines[++i]}`; 81 | continue; 82 | } 83 | 84 | if (val !== "" && !Number.isNaN(val)) val = +val; 85 | if (val === "true") val = true; 86 | if (val === "false") val = false; 87 | if (val === "null") val = null; 88 | if (val === "undefined") val = undefined; 89 | 90 | stack[stack.length - 1][key] = val; 91 | } 92 | break; 93 | } 94 | } 95 | 96 | if (stack.length !== 1) { 97 | throw new SyntaxError("VDF | Parse: Open parentheses somewhere"); 98 | } 99 | 100 | return object; 101 | } 102 | -------------------------------------------------------------------------------- /src/modals/EditFolder.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | GlobalContext, 4 | SelectedDataContext, 5 | UIContext, 6 | closeDialog, 7 | closeDialogImmediately, 8 | translateText, 9 | triggerToast, 10 | updateData, 11 | } from "../Globals.jsx"; 12 | 13 | // importing code snippets and library functions 14 | import { Match, Show, Switch, createSignal, onMount, useContext } from "solid-js"; 15 | import { produce } from "solid-js/store"; 16 | 17 | // importing style related files 18 | import { Close, SaveDisk, TrashDelete } from "../libraries/Icons.jsx"; 19 | 20 | export function EditFolder() { 21 | const globalContext = useContext(GlobalContext); 22 | const uiContext = useContext(UIContext); 23 | const selectedDataContext = useContext(SelectedDataContext); 24 | 25 | const [editedFolderName, setEditedFolderName] = createSignal(); 26 | const [editedHideFolder, setEditedHideFolder] = createSignal(); 27 | 28 | const [showCloseConfirm, setShowCloseConfirm] = createSignal(false); 29 | 30 | const [showDeleteConfirm, setShowDeleteConfirm] = createSignal(false); 31 | 32 | async function editFolder() { 33 | if (editedFolderName() === "") { 34 | triggerToast(translateText("no folder name")); 35 | return; 36 | } 37 | 38 | if (selectedDataContext.selectedFolder().name !== editedFolderName()) { 39 | let folderNameAlreadyExists = false; 40 | 41 | for (const folderName of Object.keys(globalContext.libraryData.folders)) { 42 | if (editedFolderName() === folderName) { 43 | folderNameAlreadyExists = true; 44 | } 45 | } 46 | 47 | if (folderNameAlreadyExists) { 48 | triggerToast(`${editedFolderName()} ${translateText("is already in your library")}`); 49 | return; 50 | } 51 | } 52 | 53 | globalContext.setLibraryData( 54 | produce((data) => { 55 | delete data.folders[selectedDataContext.selectedFolder().name]; 56 | 57 | return data; 58 | }), 59 | ); 60 | 61 | if (!editedFolderName()) { 62 | setEditedFolderName(selectedDataContext.selectedFolder().name); 63 | } 64 | 65 | globalContext.setLibraryData( 66 | produce((data) => { 67 | data.folders[editedFolderName()] = { 68 | ...selectedDataContext.selectedFolder(), 69 | name: editedFolderName(), 70 | hide: editedHideFolder(), 71 | }; 72 | 73 | return data; 74 | }), 75 | ); 76 | 77 | await updateData(); 78 | closeDialog("editFolder"); 79 | } 80 | 81 | async function deleteFolder() { 82 | for (let x = 0; x < Object.keys(globalContext.libraryData.folders).length; x++) { 83 | if (x > globalContext.libraryData.folders[selectedDataContext.selectedFolder().name].index) { 84 | globalContext.setLibraryData( 85 | produce((data) => { 86 | Object.values(data.folders)[x].index -= 1; 87 | 88 | return data; 89 | }), 90 | ); 91 | } 92 | } 93 | 94 | globalContext.setLibraryData( 95 | produce((data) => { 96 | delete data.folders[selectedDataContext.selectedFolder().name]; 97 | 98 | return data; 99 | }), 100 | ); 101 | 102 | await updateData(); 103 | 104 | closeDialog("editFolder"); 105 | } 106 | 107 | onMount(() => { 108 | document.addEventListener("keydown", (e) => { 109 | if (e.key === "Escape") { 110 | e.preventDefault(); 111 | if (showCloseConfirm()) { 112 | closeDialogImmediately(document.querySelector("[data-modal='editFolder']")); 113 | 114 | setShowCloseConfirm(false); 115 | } else { 116 | setShowCloseConfirm(true); 117 | 118 | const closeConfirmTimer = setTimeout(() => { 119 | clearTimeout(closeConfirmTimer); 120 | 121 | setShowCloseConfirm(false); 122 | }, 1500); 123 | } 124 | } 125 | }); 126 | }); 127 | 128 | return ( 129 | <dialog 130 | data-modal="editFolder" 131 | onClose={() => { 132 | uiContext.setShowEditFolderModal(false); 133 | }} 134 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 135 | > 136 | <div class="flex h-screen w-screen items-center justify-center bg-[#d1d1d166] align-middle dark:bg-[#12121266]"> 137 | <div class="w-[60%] border-2 border-[#1212121f] border-solid bg-[#FFFFFC] p-6 dark:border-[#ffffff1f] dark:bg-[#121212]"> 138 | <div 139 | class={`flex justify-between ${globalContext.libraryData.userSettings.language !== "en" ? "large:flex-row flex-col" : ""} `} 140 | > 141 | <div> 142 | <p class="text-[#000000] text-[25px] dark:text-[#ffffff80]"> 143 | {translateText("edit")} {selectedDataContext.selectedFolder().name} 144 | </p> 145 | </div> 146 | 147 | <div class="flex items-center gap-5"> 148 | <button 149 | type="button" 150 | onClick={() => { 151 | if (editedHideFolder() === undefined) { 152 | setEditedHideFolder(!selectedDataContext.selectedGame().hide); 153 | } else { 154 | setEditedHideFolder(!editedHideFolder()); 155 | } 156 | }} 157 | class="relative cursor-pointer" 158 | > 159 | <Switch> 160 | <Match when={editedHideFolder() === undefined}> 161 | <Show 162 | when={selectedDataContext.selectedFolder().hide} 163 | fallback={<div class="">{translateText("hide in expanded view")}</div>} 164 | > 165 | <div class="relative"> 166 | <div class="">{translateText("hide in expanded view")}</div> 167 | <div class="absolute inset-0 opacity-70 blur-[5px]"> 168 | {translateText("hide in expanded view")} 169 | </div> 170 | </div> 171 | </Show> 172 | </Match> 173 | 174 | <Match when={editedHideFolder() === true}> 175 | <div class="relative"> 176 | <div class="">{translateText("hide in expanded view")}</div> 177 | <div class="absolute inset-0 opacity-70 blur-[5px]">{translateText("hide in expanded view")}</div> 178 | </div> 179 | </Match> 180 | 181 | <Match when={editedHideFolder() === false}> 182 | <div class="">{translateText("hide in expanded view")}</div> 183 | </Match> 184 | </Switch> 185 | </button> 186 | 187 | <button 188 | type="button" 189 | onClick={editFolder} 190 | class="standardButton !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 191 | > 192 | {translateText("save")} 193 | <SaveDisk /> 194 | </button> 195 | 196 | <button 197 | type="button" 198 | onClick={() => { 199 | showDeleteConfirm() ? deleteFolder() : setShowDeleteConfirm(true); 200 | 201 | setTimeout(() => { 202 | setShowDeleteConfirm(false); 203 | }, 1500); 204 | }} 205 | class="standardButton !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 206 | > 207 | <span class="text-[#FF3636]"> 208 | {showDeleteConfirm() ? translateText("confirm?") : translateText("delete")} 209 | </span> 210 | <TrashDelete /> 211 | </button> 212 | 213 | <button 214 | type="button" 215 | class="standardButton !w-max !h-full !gap-0 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 216 | onClick={() => { 217 | if (showCloseConfirm()) { 218 | closeDialog("editFolder"); 219 | } else { 220 | setShowCloseConfirm(true); 221 | } 222 | setTimeout(() => { 223 | setShowCloseConfirm(false); 224 | }, 1500); 225 | }} 226 | data-tooltip={translateText("close")} 227 | > 228 | {showCloseConfirm() ? ( 229 | <span class="whitespace-nowrap text-[#FF3636]">{translateText("hit again to confirm")}</span> 230 | ) : ( 231 | <Close /> 232 | )} 233 | </button> 234 | </div> 235 | </div> 236 | 237 | <div class="mt-6 flex items-end gap-6"> 238 | <input 239 | aria-autocomplete="none" 240 | type="text" 241 | name="" 242 | id="" 243 | class="!text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] w-full bg-[#E8E8E8] dark:bg-[#232323]" 244 | onInput={(e) => { 245 | setEditedFolderName(e.currentTarget.value); 246 | }} 247 | placeholder={translateText("name of folder")} 248 | value={selectedDataContext.selectedFolder().name} 249 | /> 250 | </div> 251 | </div> 252 | </div> 253 | </dialog> 254 | ); 255 | } 256 | -------------------------------------------------------------------------------- /src/modals/EditGame.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | SelectedDataContext, 6 | UIContext, 7 | closeDialog, 8 | closeDialogImmediately, 9 | generateRandomString, 10 | getExecutableFileName, 11 | getExecutableParentFolder, 12 | locationJoin, 13 | translateText, 14 | triggerToast, 15 | updateData, 16 | } from "../Globals.jsx"; 17 | 18 | import { invoke } from "@tauri-apps/api"; 19 | import { open } from "@tauri-apps/api/dialog"; 20 | import { BaseDirectory, copyFile } from "@tauri-apps/api/fs"; 21 | import { convertFileSrc } from "@tauri-apps/api/tauri"; 22 | // importing code snippets and library functions 23 | import { Match, Show, Switch, createSignal, onMount, useContext } from "solid-js"; 24 | import { produce } from "solid-js/store"; 25 | 26 | // importing style related files 27 | import { Close, OpenExternal, SaveDisk, TrashDelete } from "../libraries/Icons.jsx"; 28 | 29 | export function EditGame() { 30 | const globalContext = useContext(GlobalContext); 31 | const uiContext = useContext(UIContext); 32 | const selectedDataContext = useContext(SelectedDataContext); 33 | const applicationStateContext = useContext(ApplicationStateContext); 34 | 35 | const [editedGameName, setEditedGameName] = createSignal(); 36 | const [editedFavouriteGame, setEditedFavouriteGame] = createSignal(); 37 | const [editedLocatedHeroImage, setEditedLocatedHeroImage] = createSignal(); 38 | const [editedLocatedGridImage, setEditedLocatedGridImage] = createSignal(); 39 | const [editedLocatedLogo, setEditedLocatedLogo] = createSignal(); 40 | const [editedLocatedIcon, setEditedLocatedIcon] = createSignal(); 41 | const [editedLocatedGame, setEditedlocatedGame] = createSignal(); 42 | 43 | const [showCloseConfirm, setShowCloseConfirm] = createSignal(false); 44 | 45 | const [showDeleteConfirm, setShowDeleteConfirm] = createSignal(false); 46 | 47 | async function locateEditedGame() { 48 | setEditedlocatedGame( 49 | await open({ 50 | multiple: false, 51 | filters: [ 52 | { 53 | name: "Executable", 54 | extensions: ["exe", "lnk", "url", "app"], 55 | }, 56 | ], 57 | }), 58 | ); 59 | } 60 | 61 | async function locateEditedHeroImage() { 62 | setEditedLocatedHeroImage( 63 | await open({ 64 | multiple: false, 65 | filters: [ 66 | { 67 | name: "Image", 68 | extensions: ["png", "jpg", "jpeg", "webp"], 69 | }, 70 | ], 71 | }), 72 | ); 73 | } 74 | 75 | async function locateEditedGridImage() { 76 | setEditedLocatedGridImage( 77 | await open({ 78 | multiple: false, 79 | filters: [ 80 | { 81 | name: "Image", 82 | extensions: ["png", "jpg", "jpeg", "webp"], 83 | }, 84 | ], 85 | }), 86 | ); 87 | } 88 | 89 | async function locateEditedLogo() { 90 | setEditedLocatedLogo( 91 | await open({ 92 | multiple: false, 93 | filters: [ 94 | { 95 | name: "Image", 96 | extensions: ["png", "jpg", "jpeg", "webp"], 97 | }, 98 | ], 99 | }), 100 | ); 101 | } 102 | 103 | async function locateEditedIcon() { 104 | setEditedLocatedIcon( 105 | await open({ 106 | multiple: false, 107 | filters: [ 108 | { 109 | name: "Image", 110 | extensions: ["png", "jpg", "jpeg", "ico"], 111 | }, 112 | ], 113 | }), 114 | ); 115 | } 116 | 117 | async function updateGame() { 118 | let previousIndex; 119 | 120 | if (editedGameName() === "") { 121 | triggerToast(translateText("no game name")); 122 | return; 123 | } 124 | 125 | if (selectedDataContext.selectedGame().name !== editedGameName()) { 126 | let gameNameAlreadyExists = false; 127 | 128 | for (const gameName of Object.keys(globalContext.libraryData.games)) { 129 | if (editedGameName() === gameName) { 130 | gameNameAlreadyExists = true; 131 | } 132 | } 133 | 134 | if (gameNameAlreadyExists) { 135 | triggerToast(`${editedGameName()} ${translateText("is already in your library")}`); 136 | return; 137 | } 138 | } 139 | 140 | for (const folder of Object.values(globalContext.libraryData.folders)) { 141 | for (const gameName of folder.games) { 142 | if (gameName === selectedDataContext.selectedGame().name) { 143 | previousIndex = folder.games.indexOf(gameName); 144 | } 145 | } 146 | } 147 | 148 | globalContext.setLibraryData((data) => { 149 | delete data.games[selectedDataContext.selectedGame().name]; 150 | return data; 151 | }); 152 | 153 | if (!editedGameName()) { 154 | setEditedGameName(selectedDataContext.selectedGame().name); 155 | } 156 | 157 | if (editedFavouriteGame() === undefined) { 158 | setEditedFavouriteGame(selectedDataContext.selectedGame().favourite); 159 | } 160 | 161 | if (editedLocatedGame() === undefined) { 162 | setEditedlocatedGame(selectedDataContext.selectedGame().location); 163 | } else { 164 | if (editedLocatedGame() === null) { 165 | setEditedlocatedGame(undefined); 166 | } 167 | } 168 | 169 | if (editedLocatedGridImage() === undefined) { 170 | setEditedLocatedGridImage(selectedDataContext.selectedGame().gridImage); 171 | } else { 172 | if (editedLocatedGridImage() === null) { 173 | setEditedLocatedGridImage(undefined); 174 | } else { 175 | const gridImageFileName = `${generateRandomString()}.${ 176 | editedLocatedGridImage().split(".")[editedLocatedGridImage().split(".").length - 1] 177 | }`; 178 | 179 | await copyFile(editedLocatedGridImage(), locationJoin(["grids", gridImageFileName]), { 180 | dir: BaseDirectory.AppData, 181 | }).then(() => { 182 | setEditedLocatedGridImage(gridImageFileName); 183 | }); 184 | } 185 | } 186 | 187 | if (editedLocatedHeroImage() === undefined) { 188 | setEditedLocatedHeroImage(selectedDataContext.selectedGame().heroImage); 189 | } else { 190 | if (editedLocatedHeroImage() === null) { 191 | setEditedLocatedHeroImage(undefined); 192 | } else { 193 | const heroImageFileName = `${generateRandomString()}.${ 194 | editedLocatedHeroImage().split(".")[editedLocatedHeroImage().split(".").length - 1] 195 | }`; 196 | 197 | await copyFile(editedLocatedHeroImage(), locationJoin(["heroes", heroImageFileName]), { 198 | dir: BaseDirectory.AppData, 199 | }).then(() => { 200 | setEditedLocatedHeroImage(heroImageFileName); 201 | }); 202 | } 203 | } 204 | 205 | if (editedLocatedLogo() === undefined) { 206 | setEditedLocatedLogo(selectedDataContext.selectedGame().logo); 207 | } else { 208 | if (editedLocatedLogo() === null) { 209 | setEditedLocatedLogo(undefined); 210 | } else { 211 | const logoFileName = `${generateRandomString()}.${ 212 | editedLocatedLogo().split(".")[editedLocatedLogo().split(".").length - 1] 213 | }`; 214 | 215 | await copyFile(editedLocatedLogo(), locationJoin(["logos", logoFileName]), { 216 | dir: BaseDirectory.AppData, 217 | }).then(() => { 218 | setEditedLocatedLogo(logoFileName); 219 | }); 220 | } 221 | } 222 | 223 | if (editedLocatedIcon() === undefined) { 224 | setEditedLocatedIcon(selectedDataContext.selectedGame().icon); 225 | } else { 226 | if (editedLocatedIcon() === null) { 227 | setEditedLocatedIcon(undefined); 228 | } else { 229 | const iconFileName = `${generateRandomString()}.${ 230 | editedLocatedIcon().split(".")[editedLocatedIcon().split(".").length - 1] 231 | }`; 232 | 233 | await copyFile(editedLocatedIcon(), locationJoin(["icons", iconFileName]), { 234 | dir: BaseDirectory.AppData, 235 | }).then(() => { 236 | setEditedLocatedIcon(iconFileName); 237 | }); 238 | } 239 | } 240 | 241 | globalContext.setLibraryData("games", editedGameName(), { 242 | location: editedLocatedGame(), 243 | name: editedGameName(), 244 | heroImage: editedLocatedHeroImage(), 245 | gridImage: editedLocatedGridImage(), 246 | logo: editedLocatedLogo(), 247 | icon: editedLocatedIcon(), 248 | favourite: editedFavouriteGame(), 249 | }); 250 | 251 | for (const folder of Object.values(globalContext.libraryData.folders)) { 252 | for (const gameName of folder.games) { 253 | if (gameName === selectedDataContext.selectedGame().name) { 254 | if (gameName === selectedDataContext.selectedGame().name) { 255 | globalContext.setLibraryData( 256 | produce((data) => { 257 | data.folders[folder.name].games.splice(folder.games.indexOf(gameName), 1); 258 | data.folders[folder.name].games.splice(previousIndex, 0, editedGameName()); 259 | 260 | return data; 261 | }), 262 | ); 263 | } 264 | } 265 | } 266 | } 267 | 268 | await updateData(); 269 | closeDialog("editGame"); 270 | } 271 | 272 | async function deleteGame() { 273 | await invoke("delete_assets", { 274 | heroImage: locationJoin([ 275 | applicationStateContext.appDataDirPath(), 276 | "heroes", 277 | selectedDataContext.selectedGame().heroImage, 278 | ]), 279 | gridImage: locationJoin([ 280 | applicationStateContext.appDataDirPath(), 281 | "grids", 282 | selectedDataContext.selectedGame().gridImage, 283 | ]), 284 | logo: locationJoin([applicationStateContext.appDataDirPath(), "logos", selectedDataContext.selectedGame().logo]), 285 | icon: locationJoin([applicationStateContext.appDataDirPath(), "icons", selectedDataContext.selectedGame().icon]), 286 | }); 287 | 288 | setTimeout(async () => { 289 | globalContext.setLibraryData((data) => { 290 | delete data.games[selectedDataContext.selectedGame().name]; 291 | return data; 292 | }); 293 | 294 | for (const folder of Object.values(globalContext.libraryData.folders)) { 295 | for (const gameName of folder.games) { 296 | if (gameName === selectedDataContext.selectedGame().name) { 297 | globalContext.setLibraryData( 298 | produce((data) => { 299 | data.folders[folder.name].games.splice(folder.games.indexOf(gameName), 1); 300 | 301 | return data; 302 | }), 303 | ); 304 | } 305 | } 306 | } 307 | 308 | await updateData(); 309 | closeDialog("editGame"); 310 | }, 100); 311 | } 312 | 313 | onMount(() => { 314 | document.addEventListener("keydown", (e) => { 315 | if (e.key === "Escape") { 316 | e.preventDefault(); 317 | if (showCloseConfirm()) { 318 | closeDialogImmediately(document.querySelector("[data-modal='editGame']")); 319 | 320 | setShowCloseConfirm(false); 321 | } else { 322 | setShowCloseConfirm(true); 323 | 324 | const closeConfirmTimer = setTimeout(() => { 325 | clearTimeout(closeConfirmTimer); 326 | 327 | setShowCloseConfirm(false); 328 | }, 1500); 329 | } 330 | } 331 | }); 332 | }); 333 | 334 | return ( 335 | <dialog 336 | data-modal="editGame" 337 | onDragStart={(e) => { 338 | e.preventDefault(); 339 | }} 340 | onClose={() => { 341 | uiContext.setShowEditGameModal(false); 342 | }} 343 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 344 | > 345 | <div class="flex h-screen w-screen flex-col items-center justify-center gap-3 bg-[#d1d1d1cc] dark:bg-[#121212cc]"> 346 | <div class="flex w-[84rem] justify-between max-large:w-[61rem]"> 347 | <div> 348 | <p class="text-[#000000] text-[25px] dark:text-[#ffffff80]"> 349 | {translateText("edit")} {selectedDataContext.selectedGame().name} 350 | </p> 351 | </div> 352 | <div class="flex items-center gap-4"> 353 | <button 354 | type="button" 355 | class="cursor-pointer" 356 | onClick={() => { 357 | if (editedFavouriteGame() === undefined) { 358 | setEditedFavouriteGame(!selectedDataContext.selectedGame().favourite); 359 | } else { 360 | setEditedFavouriteGame((x) => !x); 361 | } 362 | }} 363 | > 364 | <Switch> 365 | <Match when={editedFavouriteGame() === undefined}> 366 | <Show 367 | when={selectedDataContext.selectedGame().favourite} 368 | fallback={<div class="!w-max">{translateText("favourite")}</div>} 369 | > 370 | <div class="relative"> 371 | <div class="!w-max">{translateText("favourite")}</div> 372 | <div class="-z-10 !w-max absolute inset-0 opacity-70 blur-[5px]"> 373 | {translateText("favourite")} 374 | </div> 375 | </div> 376 | </Show> 377 | </Match> 378 | 379 | <Match when={editedFavouriteGame() === true}> 380 | <div class="relative"> 381 | <div class="!w-max">{translateText("favourite")}</div> 382 | <div class="-z-10 !w-max absolute inset-0 opacity-70 blur-[5px]">{translateText("favourite")}</div> 383 | </div> 384 | </Match> 385 | 386 | <Match when={editedFavouriteGame() === false}> 387 | <div class="!w-max">favourite</div> 388 | </Match> 389 | </Switch> 390 | </button> 391 | <button 392 | type="button" 393 | onClick={updateGame} 394 | class="standardButton !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 395 | > 396 | <div class="!w-max">{translateText("save")}</div> 397 | <SaveDisk /> 398 | </button> 399 | <button 400 | type="button" 401 | onClick={() => { 402 | showDeleteConfirm() ? deleteGame() : setShowDeleteConfirm(true); 403 | 404 | setTimeout(() => { 405 | setShowDeleteConfirm(false); 406 | }, 1500); 407 | }} 408 | class="standardButton !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 409 | > 410 | <span class="w-max text-[#FF3636]"> 411 | {showDeleteConfirm() ? translateText("confirm?") : translateText("delete")} 412 | </span> 413 | <TrashDelete /> 414 | </button> 415 | <button 416 | type="button" 417 | class="standardButton !w-max !h-full !gap-0 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 418 | onClick={() => { 419 | if (showCloseConfirm()) { 420 | closeDialog("editGame"); 421 | } else { 422 | setShowCloseConfirm(true); 423 | } 424 | setTimeout(() => { 425 | setShowCloseConfirm(false); 426 | }, 1500); 427 | }} 428 | data-tooltip={translateText("close")} 429 | > 430 | {showCloseConfirm() ? ( 431 | <span class="whitespace-nowrap text-[#FF3636]">{translateText("hit again to confirm")}</span> 432 | ) : ( 433 | <Close /> 434 | )} 435 | </button> 436 | </div> 437 | </div> 438 | <div class="flex gap-3"> 439 | <button 440 | type="button" 441 | onClick={locateEditedGridImage} 442 | onContextMenu={() => { 443 | setEditedLocatedGridImage(null); 444 | }} 445 | class="panelButton locatingGridImg group relative aspect-[2/3] h-full cursor-pointer overflow-hidden bg-[#f1f1f1] dark:bg-[#1c1c1c]" 446 | data-tooltip={translateText("grid/cover")} 447 | > 448 | <Switch> 449 | <Match when={editedLocatedGridImage() === undefined}> 450 | <img 451 | class="absolute inset-0 aspect-[2/3]" 452 | src={convertFileSrc( 453 | locationJoin([ 454 | applicationStateContext.appDataDirPath(), 455 | "grids", 456 | selectedDataContext.selectedGame().gridImage, 457 | ]), 458 | )} 459 | alt="" 460 | /> 461 | <span class="absolute top-[47%] left-[35%] opacity-0 group-hover:opacity-100 max-large:top-[45%] max-large:left-[30%]"> 462 | {translateText("grid/cover")} 463 | </span> 464 | </Match> 465 | <Match when={editedLocatedGridImage()}> 466 | <img class="absolute inset-0 aspect-[2/3]" src={convertFileSrc(editedLocatedGridImage())} alt="" /> 467 | <span class="absolute top-[47%] left-[35%] opacity-0 group-hover:opacity-100 max-large:top-[45%] max-large:left-[30%]"> 468 | {translateText("grid/cover")} 469 | </span> 470 | </Match> 471 | <Match when={editedLocatedGridImage() === null}> 472 | <span class="absolute top-[47%] left-[35%] opacity-0 group-hover:opacity-100 max-large:top-[45%] max-large:left-[30%]"> 473 | {translateText("grid/cover")} 474 | </span> 475 | </Match> 476 | </Switch> 477 | </button> 478 | 479 | <div class="relative flex flex-col gap-3"> 480 | <button 481 | type="button" 482 | onClick={locateEditedHeroImage} 483 | onContextMenu={() => { 484 | setEditedLocatedHeroImage(null); 485 | }} 486 | class="panelButton group relative m-0 aspect-[67/26] h-[350px] cursor-pointer bg-[#f1f1f1] p-0 max-large:h-[250px] dark:bg-[#1c1c1c]" 487 | data-tooltip={translateText("hero")} 488 | > 489 | <Switch> 490 | <Match when={editedLocatedHeroImage() === null} class="absolute inset-0 overflow-hidden"> 491 | <span class=" absolute top-[47%] left-[45%] opacity-0 group-hover:opacity-100 max-large:top-[45%] max-large:left-[42%]"> 492 | {translateText("hero")} 493 | </span> 494 | </Match> 495 | <Match when={editedLocatedHeroImage() === undefined} class="absolute inset-0 overflow-hidden"> 496 | <img 497 | src={convertFileSrc( 498 | locationJoin([ 499 | applicationStateContext.appDataDirPath(), 500 | "heroes", 501 | selectedDataContext.selectedGame().heroImage, 502 | ]), 503 | )} 504 | alt="" 505 | class="absolute inset-0 aspect-[96/31] h-full" 506 | /> 507 | <img 508 | src={convertFileSrc( 509 | locationJoin([ 510 | applicationStateContext.appDataDirPath(), 511 | "heroes", 512 | selectedDataContext.selectedGame().heroImage, 513 | ]), 514 | )} 515 | alt="" 516 | class="-z-10 absolute inset-0 aspect-[96/31] h-full opacity-[0.6] blur-[80px]" 517 | /> 518 | </Match> 519 | <Match when={editedLocatedHeroImage()} class="absolute inset-0 overflow-hidden"> 520 | <img 521 | src={convertFileSrc(editedLocatedHeroImage())} 522 | alt="" 523 | class="absolute inset-0 aspect-[96/31] h-full" 524 | /> 525 | <img 526 | src={convertFileSrc(editedLocatedHeroImage())} 527 | alt="" 528 | class="-z-10 absolute inset-0 aspect-[96/31] h-full opacity-[0.6] blur-[80px]" 529 | /> 530 | </Match> 531 | </Switch> 532 | 533 | <span class="absolute top-[47%] left-[45%] opacity-0 group-hover:opacity-100 max-large:top-[45%] max-large:left-[42%]"> 534 | {translateText("hero")} 535 | </span> 536 | </button> 537 | 538 | <Show 539 | when={selectedDataContext.selectedGame().logo} 540 | fallback={ 541 | <button 542 | type="button" 543 | onClick={locateEditedLogo} 544 | onContextMenu={() => { 545 | setEditedLocatedLogo(null); 546 | }} 547 | class={`panelButton group !bg-[#27272700] absolute bottom-[60px] left-[20px] cursor-pointer bg-[#f1f1f1] max-large:bottom-[40px] dark:bg-[#1c1c1c] ${selectedDataContext.selectedGame().logo ? "" : "!h-[65px] !w-[200px]"} `} 548 | data-tooltip={translateText("logo")} 549 | > 550 | <Show 551 | when={editedLocatedLogo()} 552 | fallback={ 553 | <div class="dark:!bg-[#272727] relative h-[90px] w-[250px] bg-[#E8E8E8] max-large:h-[70px] max-large:w-[170px]" /> 554 | } 555 | > 556 | <img 557 | src={convertFileSrc(editedLocatedLogo())} 558 | alt="" 559 | class="relative max-h-[100px] max-w-[400px] max-large:max-h-[70px] max-large:max-w-[300px]" 560 | /> 561 | </Show> 562 | 563 | <span class=" absolute top-[65%] left-[55%] opacity-0 group-hover:opacity-100 max-large:top-[45%] max-large:left-[35%]"> 564 | {translateText("logo")} 565 | </span> 566 | </button> 567 | } 568 | > 569 | <button 570 | type="button" 571 | onClick={locateEditedLogo} 572 | onContextMenu={() => { 573 | setEditedLocatedLogo(null); 574 | }} 575 | class={`panelButton group !bg-[#27272700] absolute bottom-[70px] left-[20px] cursor-pointer bg-[#f1f1f1] dark:bg-[#1c1c1c] ${selectedDataContext.selectedGame().logo ? "" : "!h-[65px] !w-[200px]"} `} 576 | data-tooltip={translateText("logo")} 577 | > 578 | <Switch> 579 | <Match when={editedLocatedLogo() === undefined}> 580 | <img 581 | src={convertFileSrc( 582 | locationJoin([ 583 | applicationStateContext.appDataDirPath(), 584 | "logos", 585 | selectedDataContext.selectedGame().logo, 586 | ]), 587 | )} 588 | alt="" 589 | class="relative max-h-[100px] max-w-[400px] max-large:max-h-[70px] max-large:max-w-[300px]" 590 | /> 591 | </Match> 592 | <Match when={editedLocatedLogo()}> 593 | <img 594 | src={convertFileSrc(editedLocatedLogo())} 595 | alt="" 596 | class="relative max-h-[100px] max-w-[400px] max-large:max-h-[70px] max-large:max-w-[300px]" 597 | /> 598 | </Match> 599 | <Match when={editedLocatedLogo() === null}> 600 | <div class="dark:!bg-[#272727] relative h-[90px] w-[250px] bg-[#E8E8E8] max-large:h-[70px] max-large:w-[170px]" /> 601 | </Match> 602 | </Switch> 603 | 604 | <span class=" absolute top-[35%] left-[40%] opacity-0 group-hover:opacity-100 max-large:top-[30%] max-large:left-[35%]"> 605 | {translateText("logo")} 606 | </span> 607 | </button> 608 | </Show> 609 | 610 | <div class="flex cursor-pointer items-center gap-3"> 611 | <button 612 | type="button" 613 | onClick={locateEditedIcon} 614 | onContextMenu={() => { 615 | setEditedLocatedIcon(null); 616 | }} 617 | class="group !bg-[#27272700] relative p-0" 618 | data-tooltip={translateText("logo")} 619 | > 620 | <Switch> 621 | <Match when={editedLocatedIcon() === undefined}> 622 | <Show 623 | when={selectedDataContext.selectedGame().icon} 624 | fallback={<div class="!bg-[#E8E8E8] dark:!bg-[#272727] h-[40px] w-[40px]" />} 625 | > 626 | <img 627 | src={convertFileSrc( 628 | locationJoin([ 629 | applicationStateContext.appDataDirPath(), 630 | "icons", 631 | selectedDataContext.selectedGame().icon, 632 | ]), 633 | )} 634 | alt="" 635 | class="h-[40px] w-[40px] " 636 | /> 637 | </Show> 638 | </Match> 639 | <Match when={editedLocatedIcon()}> 640 | <img src={convertFileSrc(editedLocatedIcon())} alt="" class="h-[40px] w-[40px]" /> 641 | </Match> 642 | <Match when={editedLocatedIcon() === null}> 643 | <div class="!bg-[#E8E8E8] dark:!bg-[#272727] h-[40px] w-[40px]" /> 644 | </Match> 645 | </Switch> 646 | <span class=" absolute top-[120%] left-[-10%] z-[10000] opacity-0 group-hover:opacity-100"> 647 | {translateText("icon")} 648 | </span> 649 | </button> 650 | 651 | <input 652 | aria-autocomplete="none" 653 | type="text" 654 | style={{ "flex-grow": "1" }} 655 | name="" 656 | id="" 657 | onInput={(e) => { 658 | setEditedGameName(e.currentTarget.value); 659 | }} 660 | class="bg-[#E8E8E8cc] backdrop-blur-[10px] dark:bg-[#272727cc]" 661 | placeholder={translateText("name of game")} 662 | value={selectedDataContext.selectedGame().name} 663 | /> 664 | <button 665 | type="button" 666 | onClick={locateEditedGame} 667 | onContextMenu={() => { 668 | setEditedlocatedGame(null); 669 | }} 670 | class="standardButton !mt-0 !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] bg-[#E8E8E8] dark:bg-[#232323]" 671 | > 672 | <Switch> 673 | <Match when={editedLocatedGame() === undefined}> 674 | <Show when={selectedDataContext.selectedGame().location} fallback={translateText("locate game")}> 675 | {getExecutableFileName(selectedDataContext.selectedGame().location)} 676 | </Show> 677 | </Match> 678 | <Match when={editedLocatedGame() === null}>{translateText("locate game")}</Match> 679 | <Match when={editedLocatedGame()}>{getExecutableFileName(editedLocatedGame())}</Match> 680 | </Switch> 681 | </button> 682 | 683 | <Show 684 | when={ 685 | selectedDataContext.selectedGame().location && 686 | selectedDataContext.selectedGame().location.split("//")[0] !== "steam:" 687 | } 688 | > 689 | <button 690 | type="button" 691 | onClick={() => { 692 | invoke("open_location", { 693 | location: getExecutableParentFolder(selectedDataContext.selectedGame().location), 694 | }); 695 | }} 696 | class="standardButton group !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] relative bg-[#E8E8E8] dark:bg-[#232323]" 697 | data-tooltip={translateText("logo")} 698 | > 699 | <OpenExternal /> 700 | 701 | <span class="absolute top-[120%] left-[-150%] opacity-0 group-hover:opacity-100"> 702 | {translateText("open containing folder")} 703 | </span> 704 | </button> 705 | </Show> 706 | </div> 707 | </div> 708 | </div> 709 | <div class="flex w-[84rem] justify-between max-large:w-[61rem]"> 710 | <span class="opacity-50">{translateText("right click to empty image selection")}</span> 711 | </div> 712 | </div> 713 | </dialog> 714 | ); 715 | } 716 | -------------------------------------------------------------------------------- /src/modals/GamePopUp.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | SelectedDataContext, 5 | UIContext, 6 | closeDialog, 7 | locationJoin, 8 | openDialog, 9 | openGame, 10 | translateText, 11 | } from "../Globals.jsx"; 12 | 13 | import { convertFileSrc } from "@tauri-apps/api/tauri"; 14 | // importing code snippets and library functions 15 | import { Show, useContext } from "solid-js"; 16 | 17 | // importing style related files 18 | import { Close, Play, Settings } from "../libraries/Icons.jsx"; 19 | 20 | export function GamePopUp() { 21 | const selectedDataContext = useContext(SelectedDataContext); 22 | const applicationStateContext = useContext(ApplicationStateContext); 23 | const uiContext = useContext(UIContext); 24 | 25 | return ( 26 | <dialog 27 | data-modal="gamePopUp" 28 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 29 | onClose={() => { 30 | uiContext.setShowGamePopUpModal(false); 31 | }} 32 | onDragStart={(e) => { 33 | e.preventDefault(); 34 | }} 35 | > 36 | <div class="flex h-screen w-screen flex-col items-center justify-center bg-[#d1d1d166] px-[40px] dark:bg-[#12121266] "> 37 | <img 38 | src={convertFileSrc( 39 | locationJoin([ 40 | applicationStateContext.appDataDirPath(), 41 | "heroes", 42 | selectedDataContext.selectedGame().heroImage, 43 | ]), 44 | )} 45 | alt="" 46 | class="-z-10 absolute h-[350px] opacity-[0.4] blur-[80px] max-large:h-[270px]" 47 | /> 48 | <div class="relative"> 49 | <div class="absolute right-[30px] bottom-[30px] flex gap-[15px]"> 50 | <button type="button" class="invisible-button-gamepopup pointer-events-none" /> 51 | 52 | <Show 53 | when={selectedDataContext.selectedGame().location} 54 | fallback={ 55 | <button 56 | type="button" 57 | class="!flex standardButton !bg-opacity-80 !text-black !backdrop-blur-[10px] hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-bottom focus:!bg-[#d6d6d6] dark:focus:!bg-[#2b2b2b] w-max cursor-not-allowed bg-[#E8E8E8] hover:backdrop-blur-[5px] focus:backdrop-blur-[5px] dark:bg-[#232323]" 58 | data-tooltip={translateText("no game file")} 59 | > 60 | <div class="!w-max opacity-50">{translateText("play")}</div> 61 | <div class="opacity-50"> 62 | <Play /> 63 | </div> 64 | </button> 65 | } 66 | > 67 | <button 68 | type="button" 69 | class="standardButton !bg-opacity-80 !text-black !backdrop-blur-[10px] hover:!bg-[#d6d6d6] focus:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] dark:focus:!bg-[#2b2b2b] bg-[#E8E8E8] hover:backdrop-blur-[5px] focus:backdrop-blur-[5px] dark:bg-[#232323]" 70 | onClick={() => { 71 | openGame(selectedDataContext.selectedGame().location); 72 | }} 73 | > 74 | <div class="!w-max">{translateText("play")}</div> 75 | <Play /> 76 | </button> 77 | </Show> 78 | 79 | <button 80 | type="button" 81 | class="standardButton !bg-opacity-80 !text-black !backdrop-blur-[10px] hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom focus:!bg-[#d6d6d6] dark:focus:!bg-[#2b2b2b] bg-[#E8E8E8] hover:backdrop-blur-[5px] focus:backdrop-blur-[5px] dark:bg-[#232323]" 82 | onClick={() => { 83 | closeDialog("gamePopUp"); 84 | openDialog("editGame"); 85 | }} 86 | data-tooltip={translateText("settings")} 87 | > 88 | <Settings /> 89 | </button> 90 | <button 91 | type="button" 92 | class="standardButton !bg-opacity-80 !text-black !backdrop-blur-[10px] hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom focus:!bg-[#d6d6d6] dark:focus:!bg-[#2b2b2b] bg-[#E8E8E8] hover:backdrop-blur-[5px] focus:backdrop-blur-[5px] dark:bg-[#232323]" 93 | onClick={() => { 94 | closeDialog("gamePopUp"); 95 | }} 96 | data-tooltip={translateText("close")} 97 | > 98 | <Close /> 99 | </button> 100 | </div> 101 | <Show 102 | when={selectedDataContext.selectedGame().heroImage} 103 | fallback={<div class="aspect-[96/31] h-[350px] bg-[#f1f1f1] max-large:h-[270px] dark:bg-[#1c1c1c]" />} 104 | > 105 | <img 106 | src={convertFileSrc( 107 | locationJoin([ 108 | applicationStateContext.appDataDirPath(), 109 | "heroes", 110 | selectedDataContext.selectedGame().heroImage, 111 | ]), 112 | )} 113 | alt="" 114 | class="aspect-[96/31] h-[350px] max-large:h-[270px]" 115 | /> 116 | </Show> 117 | 118 | <div class="absolute bottom-[30px] left-[25px] flex h-[70px] w-[300px] items-center align-middle max-large:bottom-[15px]"> 119 | <Show 120 | when={selectedDataContext.selectedGame().logo} 121 | fallback={ 122 | <div class="dark:!bg-[#272727] absolute bottom-[5px] h-[90px] w-[250px] bg-[#E8E8E8] max-large:h-[70px] max-large:w-[170px]" /> 123 | } 124 | > 125 | <img 126 | src={convertFileSrc( 127 | locationJoin([ 128 | applicationStateContext.appDataDirPath(), 129 | "logos", 130 | selectedDataContext.selectedGame().logo, 131 | ]), 132 | )} 133 | alt="" 134 | class=" relative aspect-auto max-h-[100px] max-w-[400px] max-large:max-h-[70px] max-large:max-w-[300px]" 135 | /> 136 | </Show> 137 | </div> 138 | </div> 139 | </div> 140 | </dialog> 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /src/modals/Loading.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { SteamDataContext, UIContext, translateText } from "../Globals.jsx"; 3 | 4 | // importing code snippets and library functions 5 | import { Show, useContext } from "solid-js"; 6 | 7 | // importing style related files 8 | import { Loading as LoadingIcon } from "../libraries/Icons.jsx"; 9 | 10 | export function Loading() { 11 | const steamDataContext = useContext(SteamDataContext); 12 | const uiContext = useContext(UIContext); 13 | 14 | return ( 15 | <dialog 16 | data-modal="loading" 17 | onClose={() => { 18 | uiContext.setShowLoadingModal(false); 19 | }} 20 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 21 | > 22 | <div class="flex h-screen w-screen items-center justify-center bg-[#d1d1d166] align-middle dark:bg-[#12121266]"> 23 | <div class="flex w-max items-center justify-between gap-2 border-2 border-[#1212121f] border-solid bg-[#FFFFFC] p-3 dark:border-[#ffffff1f] dark:bg-[#121212]"> 24 | {translateText("loading")}{" "} 25 | <Show when={steamDataContext.totalSteamGames() !== 0}> 26 | {`${steamDataContext.totalImportedSteamGames()} / ${steamDataContext.totalSteamGames()}`} 27 | </Show> 28 | <div class="relative"> 29 | <LoadingIcon /> 30 | </div> 31 | </div> 32 | </div> 33 | </dialog> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/modals/NewFolder.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | UIContext, 6 | closeDialog, 7 | closeDialogImmediately, 8 | translateText, 9 | triggerToast, 10 | updateData, 11 | } from "../Globals.jsx"; 12 | 13 | // importing code snippets and library functions 14 | import { Show, createSignal, onMount, useContext } from "solid-js"; 15 | import { produce } from "solid-js/store"; 16 | 17 | // importing style related files 18 | import { Close, SaveDisk } from "../libraries/Icons.jsx"; 19 | 20 | export function NewFolder() { 21 | const globalContext = useContext(GlobalContext); 22 | const applicationStateContext = useContext(ApplicationStateContext); 23 | const uiContext = useContext(UIContext); 24 | 25 | const [folderName, setFolderName] = createSignal(); 26 | const [hideFolder, setHideFolder] = createSignal(false); 27 | 28 | const [showCloseConfirm, setShowCloseConfirm] = createSignal(false); 29 | 30 | async function addFolder() { 31 | if (folderName() === "" || folderName() === undefined) { 32 | triggerToast(translateText("no folder name")); 33 | return; 34 | } 35 | 36 | let folderNameAlreadyExists = false; 37 | 38 | for (const name of Object.keys(globalContext.libraryData.folders)) { 39 | if (folderName() === name) { 40 | folderNameAlreadyExists = true; 41 | } 42 | } 43 | 44 | if (folderNameAlreadyExists) { 45 | triggerToast(`${folderName()} ${translateText("is already in your library")}`); 46 | return; 47 | } 48 | 49 | globalContext.setLibraryData( 50 | produce((data) => { 51 | data.folders[folderName()] = { 52 | name: folderName(), 53 | hide: hideFolder(), 54 | games: [], 55 | index: applicationStateContext.currentFolders().length, 56 | }; 57 | 58 | return data; 59 | }), 60 | ); 61 | 62 | await updateData(); 63 | 64 | closeDialog("newFolder"); 65 | } 66 | 67 | onMount(() => { 68 | document.addEventListener("keydown", (e) => { 69 | if (e.key === "Escape") { 70 | e.preventDefault(); 71 | if (showCloseConfirm()) { 72 | closeDialogImmediately(document.querySelector("[data-modal='newFolder']")); 73 | 74 | setShowCloseConfirm(false); 75 | } else { 76 | setShowCloseConfirm(true); 77 | 78 | const closeConfirmTimer = setTimeout(() => { 79 | clearTimeout(closeConfirmTimer); 80 | 81 | setShowCloseConfirm(false); 82 | }, 1500); 83 | } 84 | } 85 | }); 86 | }); 87 | 88 | return ( 89 | <dialog 90 | data-modal="newFolder" 91 | onClose={() => { 92 | uiContext.setShowNewFolderModal(false); 93 | }} 94 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 95 | > 96 | <div class="flex h-screen w-screen items-center justify-center bg-[#d1d1d166] align-middle dark:bg-[#12121266]"> 97 | <div class="w-[60%] border-2 border-[#1212121f] border-solid bg-[#FFFFFC] p-6 dark:border-[#ffffff1f] dark:bg-[#121212]"> 98 | <div 99 | class={`flex justify-between ${globalContext.libraryData.userSettings.language !== "en" ? "large:flex-row flex-col" : ""} `} 100 | > 101 | <div> 102 | <p class="text-[#000000] text-[25px] dark:text-[#ffffff80]">{translateText("add new folder")}</p> 103 | </div> 104 | <div class="flex items-center gap-5"> 105 | <button 106 | type="button" 107 | onClick={() => { 108 | setHideFolder((x) => !x); 109 | }} 110 | class="relative cursor-pointer" 111 | > 112 | <Show when={hideFolder()} fallback={<div class="">{translateText("hide in expanded view")}</div>}> 113 | <div class="relative"> 114 | <div class="">{translateText("hide in expanded view")}</div> 115 | <div class="absolute inset-0 opacity-70 blur-[5px]">{translateText("hide in expanded view")}</div> 116 | </div> 117 | </Show> 118 | </button> 119 | <button 120 | type="button" 121 | onClick={addFolder} 122 | class="standardButton !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 123 | > 124 | {translateText("save")} 125 | <SaveDisk /> 126 | </button> 127 | <button 128 | type="button" 129 | class="standardButton !w-max !h-full !gap-0 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 130 | onClick={() => { 131 | if (showCloseConfirm()) { 132 | closeDialog("newFolder"); 133 | } else { 134 | setShowCloseConfirm(true); 135 | } 136 | setTimeout(() => { 137 | setShowCloseConfirm(false); 138 | }, 1500); 139 | }} 140 | data-tooltip={translateText("close")} 141 | > 142 | {showCloseConfirm() ? ( 143 | <span class="whitespace-nowrap text-[#FF3636]">{translateText("hit again to confirm")}</span> 144 | ) : ( 145 | <Close /> 146 | )} 147 | </button> 148 | </div> 149 | </div> 150 | <div class="mt-6 flex items-end gap-6"> 151 | <input 152 | aria-autocomplete="none" 153 | type="text" 154 | name="" 155 | id="" 156 | class="!text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] w-full bg-[#E8E8E8] dark:bg-[#232323]" 157 | onInput={(e) => { 158 | setFolderName(e.currentTarget.value); 159 | }} 160 | value={folderName() || ""} 161 | placeholder={translateText("name of folder")} 162 | /> 163 | </div> 164 | </div> 165 | </div> 166 | </dialog> 167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /src/modals/Notepad.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { GlobalContext, UIContext, closeDialog, getData, translateText, updateData } from "../Globals.jsx"; 3 | 4 | // importing code snippets and library functions 5 | import { createSignal, useContext } from "solid-js"; 6 | 7 | // importing style related files 8 | import { Close } from "../libraries/Icons.jsx"; 9 | 10 | export function Notepad() { 11 | const globalContext = useContext(GlobalContext); 12 | const uiContext = useContext(UIContext); 13 | 14 | const [notepadValue, setNotepadValue] = createSignal(""); 15 | 16 | async function saveNotepad() { 17 | globalContext.setLibraryData("notepad", notepadValue()); 18 | await updateData(); 19 | } 20 | 21 | setTimeout(() => { 22 | setNotepadValue(globalContext.libraryData.notepad || ""); 23 | }, 50); 24 | 25 | return ( 26 | <> 27 | <dialog 28 | data-modal="notepad" 29 | onClose={() => { 30 | setNotepadValue(globalContext.libraryData.notepad || ""); 31 | uiContext.setShowNotepadModal(false); 32 | }} 33 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 34 | > 35 | <div class="flex h-screen w-screen items-center justify-center bg-[#d1d1d166] align-middle dark:bg-[#12121266]"> 36 | <div class="w-[50%] border-2 border-[#1212121f] border-solid bg-[#FFFFFC] p-6 dark:border-[#ffffff1f] dark:bg-[#121212]"> 37 | <div class="flex justify-between"> 38 | <div> 39 | <p class="text-[#000000] text-[25px] dark:text-[#ffffff80]">{translateText("notepad")}</p> 40 | </div> 41 | 42 | <button 43 | type="button" 44 | class="standardButton !w-max !gap-0 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom aspect-square bg-[#E8E8E8] dark:bg-[#232323]" 45 | onClick={() => { 46 | closeDialog("notepad"); 47 | getData(); 48 | }} 49 | data-tooltip={translateText("close")} 50 | > 51 | <Close /> 52 | </button> 53 | </div> 54 | <textarea 55 | onInput={(e) => { 56 | setNotepadValue(e.target.value); 57 | saveNotepad(); 58 | }} 59 | class="mt-6 h-[40vh] w-full resize-none bg-transparent focus:outline-none" 60 | placeholder={translateText("write anything you want over here!")} 61 | spellcheck="false" 62 | value={notepadValue()} 63 | /> 64 | </div> 65 | </div> 66 | </dialog> 67 | </> 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/modals/Settings.jsx: -------------------------------------------------------------------------------- 1 | // importing globals 2 | import { 3 | ApplicationStateContext, 4 | GlobalContext, 5 | UIContext, 6 | closeDialog, 7 | getData, 8 | importSteamGames, 9 | translateText, 10 | updateData, 11 | } from "../Globals.jsx"; 12 | 13 | import { Hotkeys } from "../components/Hotkeys.jsx"; 14 | // importing components 15 | import { LanguageSelector } from "../components/LanguageSelector.jsx"; 16 | 17 | import { appDataDir } from "@tauri-apps/api/path"; 18 | import { invoke } from "@tauri-apps/api/tauri"; 19 | // importing code snippets and library functions 20 | import { Show, useContext } from "solid-js"; 21 | 22 | // importing style related files 23 | import { Close, Steam } from "../libraries/Icons.jsx"; 24 | 25 | export function Settings() { 26 | const globalContext = useContext(GlobalContext); 27 | const uiContext = useContext(UIContext); 28 | const applicationStateContext = useContext(ApplicationStateContext); 29 | 30 | return ( 31 | <> 32 | <dialog 33 | data-modal="settings" 34 | onClose={() => { 35 | uiContext.setShowSettingsLanguageSelector(false); 36 | uiContext.setShowSettingsModal(false); 37 | }} 38 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent" 39 | > 40 | <div class="flex h-screen w-screen items-center justify-center bg-[#d1d1d166] align-middle dark:bg-[#12121266]"> 41 | <div class="w-[70%] border-2 border-[#1212121f] border-solid bg-[#FFFFFC] p-6 dark:border-[#ffffff1f] dark:bg-[#121212]"> 42 | <div class="flex justify-between"> 43 | <div> 44 | <p class="text-[#000000] text-[25px] dark:text-[#ffffff80]">{translateText("settings")}</p> 45 | </div> 46 | 47 | <button 48 | type="button" 49 | class="standardButton !w-max !gap-0 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] tooltip-delayed-bottom aspect-square bg-[#E8E8E8] dark:bg-[#232323]" 50 | onClick={() => { 51 | closeDialog("settings"); 52 | getData(); 53 | }} 54 | data-tooltip={translateText("close")} 55 | > 56 | <Close /> 57 | </button> 58 | </div> 59 | 60 | <div class="mt-[25px] grid grid-cols-3 gap-y-4"> 61 | <button 62 | type="button" 63 | onClick={async () => { 64 | globalContext.setLibraryData("userSettings", "roundedBorders", (x) => !x); 65 | 66 | await updateData(); 67 | }} 68 | class="relative cursor-pointer p-0 text-left" 69 | > 70 | <Show 71 | when={globalContext.libraryData.userSettings.roundedBorders} 72 | fallback={<div class="">{translateText("rounded borders")}</div>} 73 | > 74 | <div class="relative "> 75 | <div class="">{translateText("rounded borders")}</div> 76 | <div class="absolute inset-0 opacity-70 blur-[5px]">{translateText("rounded borders")}</div> 77 | </div> 78 | </Show> 79 | </button> 80 | <button 81 | type="button" 82 | onClick={async () => { 83 | globalContext.setLibraryData("userSettings", "gameTitle", (x) => !x); 84 | 85 | await updateData(); 86 | }} 87 | class="relative cursor-pointer p-0 text-left" 88 | > 89 | <Show 90 | when={globalContext.libraryData.userSettings.gameTitle} 91 | fallback={<div class="">{translateText("game title")}</div>} 92 | > 93 | <div class="relative"> 94 | <div class="">{translateText("game title")}</div> 95 | <div class="absolute inset-0 opacity-70 blur-[5px]">{translateText("game title")}</div> 96 | </div> 97 | </Show> 98 | </button> 99 | <button 100 | type="button" 101 | onClick={async () => { 102 | globalContext.setLibraryData("userSettings", "folderTitle", (x) => !x); 103 | 104 | await updateData(); 105 | }} 106 | class="relative cursor-pointer p-0 text-left" 107 | > 108 | <Show 109 | when={globalContext.libraryData.userSettings.folderTitle} 110 | fallback={<div class="">{translateText("folder title")}</div>} 111 | > 112 | <div class="relative"> 113 | <div class="">{translateText("folder title")}</div> 114 | <div class="absolute inset-0 opacity-70 blur-[5px]">{translateText("folder title")}</div> 115 | </div> 116 | </Show> 117 | </button> 118 | <button 119 | type="button" 120 | onClick={async () => { 121 | globalContext.setLibraryData("userSettings", "quitAfterOpen", (x) => !x); 122 | 123 | await updateData(); 124 | }} 125 | class="relative cursor-pointer p-0 text-left" 126 | > 127 | <Show 128 | when={globalContext.libraryData.userSettings.quitAfterOpen} 129 | fallback={<div class="">{translateText("quit after opening game")}</div>} 130 | > 131 | <div class="relative"> 132 | <div class="">{translateText("quit after opening game")}</div> 133 | <div class="absolute inset-0 opacity-70 blur-[5px] "> 134 | {translateText("quit after opening game")} 135 | </div> 136 | </div> 137 | </Show> 138 | </button> 139 | 140 | <button 141 | type="button" 142 | onClick={async () => { 143 | switch (globalContext.libraryData.userSettings.fontName) { 144 | case "sans serif": 145 | globalContext.setLibraryData("userSettings", "fontName", "serif"); 146 | break; 147 | case "serif": 148 | globalContext.setLibraryData("userSettings", "fontName", "mono"); 149 | break; 150 | case "mono": 151 | globalContext.setLibraryData("userSettings", "fontName", "sans serif"); 152 | } 153 | 154 | await updateData(); 155 | }} 156 | class="flex cursor-pointer gap-2 p-0 text-left" 157 | > 158 | <span class="text-[#12121280] dark:text-[#ffffff80]">[{translateText("font")}]</span> 159 | <div class=""> 160 | {translateText(globalContext.libraryData.userSettings.fontName) || translateText("sans serif")} 161 | </div> 162 | </button> 163 | <button 164 | type="button" 165 | onClick={async () => { 166 | globalContext.libraryData.userSettings.currentTheme === "dark" 167 | ? globalContext.setLibraryData("userSettings", "currentTheme", "light") 168 | : globalContext.setLibraryData("userSettings", "currentTheme", "dark"); 169 | 170 | await updateData(); 171 | }} 172 | class="flex cursor-pointer gap-2 p-0 text-left" 173 | > 174 | <span class="text-[#12121280] dark:text-[#ffffff80]">[{translateText("theme")}]</span> 175 | <div class=""> 176 | {translateText(globalContext.libraryData.userSettings.currentTheme) || translateText("dark")} 177 | </div> 178 | </button> 179 | <div class="relative flex cursor-pointer gap-2"> 180 | <LanguageSelector onSettingsPage={true} /> 181 | </div> 182 | </div> 183 | 184 | <Show when={uiContext.showNewVersionAvailable()}> 185 | <div class="mt-[35px] flex items-start gap-3"> 186 | <button 187 | type="button" 188 | class="standardButton !m-0 !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 189 | onClick={() => { 190 | invoke("open_location", { 191 | location: "https://clear.adithya.zip/update", 192 | }); 193 | }} 194 | > 195 | {translateText("new update available!")} 196 | <span class="text-[#12121280] dark:text-[#ffffff80]">v{applicationStateContext.latestVersion()}</span> 197 | </button> 198 | </div> 199 | </Show> 200 | 201 | <div class="mt-[35px] flex flex-row items-start gap-4"> 202 | <div> 203 | <button 204 | type="button" 205 | class="standardButton tooltip-bottom !flex !w-max !gap-3 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] bg-[#E8E8E8] dark:bg-[#232323] " 206 | data-tooltip={translateText("might not work perfectly!")} 207 | onClick={() => { 208 | if (globalContext.libraryData.folders.steam !== undefined) { 209 | uiContext.showImportAndOverwriteConfirm() 210 | ? importSteamGames() 211 | : uiContext.setShowImportAndOverwriteConfirm(true); 212 | 213 | setTimeout(() => { 214 | uiContext.setShowImportAndOverwriteConfirm(false); 215 | }, 2500); 216 | } else { 217 | importSteamGames(); 218 | } 219 | }} 220 | > 221 | <Show 222 | when={globalContext.libraryData.folders.steam !== undefined} 223 | fallback={translateText("import Steam games")} 224 | > 225 | <Show 226 | when={uiContext.showImportAndOverwriteConfirm() === true} 227 | fallback={translateText("import Steam games")} 228 | > 229 | <span class="text-[#FF3636]"> 230 | {translateText("current 'steam' folder will be overwritten. confirm?")} 231 | </span> 232 | </Show> 233 | </Show> 234 | 235 | <Steam /> 236 | </button> 237 | </div> 238 | 239 | <div class="flex items-start gap-3"> 240 | <button 241 | type="button" 242 | class="standardButton !m-0 !w-max !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] flex items-center bg-[#E8E8E8] dark:bg-[#232323]" 243 | onClick={async () => { 244 | const appDataDirPath = await appDataDir(); 245 | 246 | invoke("open_location", { 247 | location: appDataDirPath, 248 | }); 249 | }} 250 | > 251 | {translateText("open library location")} 252 | </button> 253 | <span class="w-[50%] text-[#12121280] dark:text-[#ffffff80]"> 254 | {translateText("these are all the files that the app stores on your pc")} 255 | </span> 256 | </div> 257 | </div> 258 | 259 | <Hotkeys onSettingsPage={true} /> 260 | 261 | <div class="mt-[35px] flex justify-between "> 262 | <div> 263 | clear{" "} 264 | <span class="text-[#12121280] dark:text-[#ffffff80]">v{applicationStateContext.appVersion()}</span> 265 | </div> 266 | <button 267 | type="button" 268 | onClick={() => { 269 | invoke("open_location", { 270 | location: "https://clear.adithya.zip/feedback", 271 | }); 272 | }} 273 | class="cursor-pointer p-0 underline" 274 | > 275 | {translateText("feedback")} 276 | </button> 277 | <button 278 | type="button" 279 | onClick={() => { 280 | invoke("open_location", { 281 | location: "https://clear.adithya.zip/", 282 | }); 283 | }} 284 | class="cursor-pointer p-0 underline" 285 | > 286 | {translateText("website")} 287 | </button> 288 | <div> 289 | {translateText("made by")}{" "} 290 | <button 291 | type="button" 292 | onClick={() => { 293 | invoke("open_location", { 294 | location: "https://adithya.zip/", 295 | }); 296 | }} 297 | class="cursor-pointer p-0 underline" 298 | > 299 | {" "} 300 | adithya 301 | </button> 302 | </div> 303 | <button 304 | type="button" 305 | onClick={() => { 306 | invoke("open_location", { 307 | location: "https://ko-fi.com/adithyasource", 308 | }); 309 | }} 310 | class="cursor-pointer p-0 underline" 311 | > 312 | {translateText("buy me a coffee")} 313 | </button> 314 | </div> 315 | </div> 316 | </div> 317 | </dialog> 318 | </> 319 | ); 320 | } 321 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | screens: { 6 | thin: "850px", 7 | medium: "1000px", 8 | large: "1500px", 9 | }, 10 | }, 11 | plugins: [], 12 | darkMode: "class", 13 | }; 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { ViteMinifyPlugin } from "vite-plugin-minify"; 3 | import solidPlugin from "vite-plugin-solid"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig(async () => ({ 7 | plugins: [solidPlugin(), ViteMinifyPlugin({})], 8 | 9 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 10 | // 11 | // 1. prevent vite from obscuring rust errors 12 | clearScreen: false, 13 | // 2. tauri expects a fixed port, fail if that port is not available 14 | server: { 15 | port: 1420, 16 | strictPort: true, 17 | }, 18 | // 3. to make use of `TAURI_DEBUG` and other env variables 19 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand 20 | envPrefix: ["VITE_", "TAURI_"], 21 | build: { 22 | sourcemap: false, 23 | }, 24 | })); 25 | --------------------------------------------------------------------------------