├── .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 |
12 |
13 |
14 |
15 |
16 | major update currently in active development!
17 |
18 |
19 |
20 |
21 | 
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 |
44 |
45 | ## acknowledgments
46 |
47 |
57 |
58 |
59 |
60 |
61 | design
62 | basicons
63 |
64 |
65 |
66 |
67 |
76 |
77 |
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 | You need to enable JavaScript to run this app.
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 |
344 |
345 |
346 |
= 1000
350 | }
351 | >
352 | {
356 | toggleSideBar();
357 | }}
358 | data-tooltip={translateText("open sidebar")}
359 | >
360 |
361 |
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 | {
399 | if (globalContext.libraryData.folders.steam !== undefined) {
400 | uiContext.showImportAndOverwriteConfirm()
401 | ? importSteamGames()
402 | : uiContext.setShowImportAndOverwriteConfirm(true);
403 |
404 | setTimeout(() => {
405 | uiContext.setShowImportAndOverwriteConfirm(false);
406 | }, 2500);
407 | } else {
408 | importSteamGames();
409 | }
410 | }}
411 | >
412 |
416 |
420 |
421 | {translateText("current 'steam' folder will be overwritten. confirm?")}
422 |
423 |
424 |
425 |
426 |
427 |
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 | {
30 | setTimeout(() => {
31 | e.srcElement.classList.add("dragging");
32 | }, 10);
33 | e.dataTransfer.setData("gameName", props.gameName);
34 |
35 | e.dataTransfer.setData("oldFolderName", props.folderName);
36 | }}
37 | onDragEnd={(e) => {
38 | e.srcElement.classList.remove("dragging");
39 | }}
40 | onClick={async (e) => {
41 | if (e.ctrlKey) {
42 | openGame(globalContext.libraryData.games[props.gameName].location);
43 | return;
44 | }
45 | await selectedDataContext.setSelectedGame(globalContext.libraryData.games[props.gameName]);
46 | openDialog("gamePopUp");
47 | }}
48 | >
49 |
50 |
61 |
62 |
63 | {props.gameName}
64 |
65 |
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 | {
33 | e.preventDefault();
34 | }}
35 | onClick={async (e) => {
36 | if (e.ctrlKey) {
37 | openGame(globalContext.libraryData.games[gameName].location);
38 | return;
39 | }
40 | await selectedDataContext.setSelectedGame(globalContext.libraryData.games[gameName]);
41 |
42 | openDialog("gamePopUp");
43 | }}
44 | >
45 |
49 |
53 |
54 | {gameName}
55 |
56 |
57 |
58 |
59 | }
60 | >
61 |
62 |
73 |
74 |
75 |
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 | {
43 | props.onSettingsPage
44 | ? uiContext.setShowSettingsLanguageSelector((x) => !x)
45 | : setShowLanguageSelector((x) => !x);
46 |
47 | document.getElementById("firstDropdownItem").focus();
48 | }}
49 | class={
50 | props.onSettingsPage
51 | ? "w-full p-0 text-left"
52 | : "standardButton !w-max !justify-between !p-4 !text-black hover:!bg-[#d6d6d6] dark:!text-white dark:hover:!bg-[#2b2b2b] relative flex cursor-pointer items-center bg-[#E8E8E8] dark:bg-[#232323]"
53 | }
54 | >
55 | [{translateText("language")}]
56 |
57 | {returnLanguageFullName(globalContext.libraryData.userSettings.language)}
58 |
59 | {
64 | props.onSettingsPage ? uiContext.setShowSettingsLanguageSelector(false) : setShowLanguageSelector(false);
65 | }}
66 | onKeyDown={(e) => {
67 | if (e.key === "Escape") {
68 | props.onSettingsPage ? uiContext.setShowSettingsLanguageSelector(false) : setShowLanguageSelector(false);
69 | }
70 | }}
71 | >
72 | {
77 | changeLanguage("en");
78 | }}
79 | >
80 | english
81 |
82 | {
86 | changeLanguage("fr");
87 | }}
88 | >
89 | Français [french]
90 |
91 | {
95 | changeLanguage("ru");
96 | }}
97 | >
98 | русский [russian]
99 |
100 | {
104 | changeLanguage("jp");
105 | }}
106 | >
107 | 日本語 [japanese]
108 |
109 | {
113 | changeLanguage("es");
114 | }}
115 | >
116 | Español [spanish]
117 |
118 | {
121 | if (e.key === "Tab") {
122 | props.onSettingsPage
123 | ? uiContext.setShowSettingsLanguageSelector(false)
124 | : setShowLanguageSelector(false);
125 | }
126 | }}
127 | class="p-0 text-left text-[#12121280] duration-150 hover:text-[#121212cc] motion-reduce:duration-0 dark:text-[#ffffff80] dark:hover:text-[#ffffffcc]"
128 | onClick={() => {
129 | changeLanguage("hi");
130 | }}
131 | >
132 | हिंदी [hindi]
133 |
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 |
9 |
16 |
23 |
24 | );
25 | }
26 |
27 | export function EyeClosed() {
28 | return (
29 |
30 |
37 |
44 |
51 |
52 | );
53 | }
54 |
55 | export function Edit() {
56 | return (
57 |
58 |
65 |
66 | );
67 | }
68 |
69 | export function GameController() {
70 | return (
71 |
80 |
86 |
87 | );
88 | }
89 |
90 | export function Folder() {
91 | return (
92 |
101 |
107 |
108 |
109 | );
110 | }
111 |
112 | export function Notepad() {
113 | return (
114 |
123 |
129 |
130 |
131 | );
132 | }
133 |
134 | export function UpdateDownload() {
135 | return (
136 |
145 |
151 |
157 |
158 | );
159 | }
160 |
161 | export function Settings() {
162 | return (
163 |
172 |
178 |
184 |
185 | );
186 | }
187 |
188 | export function Steam() {
189 | return (
190 |
191 |
195 |
199 |
204 |
210 |
215 |
216 | );
217 | }
218 |
219 | export function EmptyTray() {
220 | return (
221 |
222 |
229 |
230 | );
231 | }
232 |
233 | export function SaveDisk() {
234 | return (
235 |
236 |
243 |
250 |
257 |
258 | );
259 | }
260 |
261 | export function TrashDelete() {
262 | return (
263 |
264 |
271 |
272 |
273 |
274 | );
275 | }
276 |
277 | export function Close() {
278 | return (
279 |
280 |
287 |
288 | );
289 | }
290 |
291 | export function OpenExternal() {
292 | return (
293 |
294 |
301 |
302 | );
303 | }
304 |
305 | export function Play() {
306 | return (
307 |
308 |
315 |
316 | );
317 | }
318 |
319 | export function Loading() {
320 | return (
321 |
330 |
337 |
338 | );
339 | }
340 |
341 | export function ChevronArrow() {
342 | return (
343 |
344 |
351 |
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 | {
132 | uiContext.setShowEditFolderModal(false);
133 | }}
134 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent"
135 | >
136 |
137 |
138 |
141 |
142 |
143 | {translateText("edit")} {selectedDataContext.selectedFolder().name}
144 |
145 |
146 |
147 |
148 | {
151 | if (editedHideFolder() === undefined) {
152 | setEditedHideFolder(!selectedDataContext.selectedGame().hide);
153 | } else {
154 | setEditedHideFolder(!editedHideFolder());
155 | }
156 | }}
157 | class="relative cursor-pointer"
158 | >
159 |
160 |
161 | {translateText("hide in expanded view")}
}
164 | >
165 |
166 |
{translateText("hide in expanded view")}
167 |
168 | {translateText("hide in expanded view")}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
{translateText("hide in expanded view")}
177 |
{translateText("hide in expanded view")}
178 |
179 |
180 |
181 |
182 | {translateText("hide in expanded view")}
183 |
184 |
185 |
186 |
187 |
192 | {translateText("save")}
193 |
194 |
195 |
196 |
{
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 |
208 | {showDeleteConfirm() ? translateText("confirm?") : translateText("delete")}
209 |
210 |
211 |
212 |
213 |
{
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 | {translateText("hit again to confirm")}
230 | ) : (
231 |
232 | )}
233 |
234 |
235 |
236 |
237 |
238 | {
245 | setEditedFolderName(e.currentTarget.value);
246 | }}
247 | placeholder={translateText("name of folder")}
248 | value={selectedDataContext.selectedFolder().name}
249 | />
250 |
251 |
252 |
253 |
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 | {
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 |
346 |
347 |
348 |
349 | {translateText("edit")} {selectedDataContext.selectedGame().name}
350 |
351 |
352 |
353 | {
357 | if (editedFavouriteGame() === undefined) {
358 | setEditedFavouriteGame(!selectedDataContext.selectedGame().favourite);
359 | } else {
360 | setEditedFavouriteGame((x) => !x);
361 | }
362 | }}
363 | >
364 |
365 |
366 | {translateText("favourite")}
}
369 | >
370 |
371 |
{translateText("favourite")}
372 |
373 | {translateText("favourite")}
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
{translateText("favourite")}
382 |
{translateText("favourite")}
383 |
384 |
385 |
386 |
387 | favourite
388 |
389 |
390 |
391 |
396 | {translateText("save")}
397 |
398 |
399 |
{
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 |
411 | {showDeleteConfirm() ? translateText("confirm?") : translateText("delete")}
412 |
413 |
414 |
415 |
{
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 | {translateText("hit again to confirm")}
432 | ) : (
433 |
434 | )}
435 |
436 |
437 |
438 |
439 |
{
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 |
449 |
450 |
461 |
462 | {translateText("grid/cover")}
463 |
464 |
465 |
466 |
467 |
468 | {translateText("grid/cover")}
469 |
470 |
471 |
472 |
473 | {translateText("grid/cover")}
474 |
475 |
476 |
477 |
478 |
479 |
480 |
{
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 |
490 |
491 |
492 | {translateText("hero")}
493 |
494 |
495 |
496 |
507 |
518 |
519 |
520 |
525 |
530 |
531 |
532 |
533 |
534 | {translateText("hero")}
535 |
536 |
537 |
538 |
{
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 |
554 | }
555 | >
556 |
561 |
562 |
563 |
564 | {translateText("logo")}
565 |
566 |
567 | }
568 | >
569 |
{
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 |
579 |
580 |
591 |
592 |
593 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 | {translateText("logo")}
606 |
607 |
608 |
609 |
610 |
611 |
{
615 | setEditedLocatedIcon(null);
616 | }}
617 | class="group !bg-[#27272700] relative p-0"
618 | data-tooltip={translateText("logo")}
619 | >
620 |
621 |
622 | }
625 | >
626 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 | {translateText("icon")}
648 |
649 |
650 |
651 |
{
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 |
{
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 |
673 |
674 |
675 | {getExecutableFileName(selectedDataContext.selectedGame().location)}
676 |
677 |
678 | {translateText("locate game")}
679 | {getExecutableFileName(editedLocatedGame())}
680 |
681 |
682 |
683 |
689 | {
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 |
700 |
701 |
702 | {translateText("open containing folder")}
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 | {translateText("right click to empty image selection")}
711 |
712 |
713 |
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 | {
30 | uiContext.setShowGamePopUpModal(false);
31 | }}
32 | onDragStart={(e) => {
33 | e.preventDefault();
34 | }}
35 | >
36 |
37 |
48 |
49 |
50 |
51 |
52 |
60 | {translateText("play")}
61 |
64 |
65 | }
66 | >
67 | {
71 | openGame(selectedDataContext.selectedGame().location);
72 | }}
73 | >
74 | {translateText("play")}
75 |
76 |
77 |
78 |
79 |
{
83 | closeDialog("gamePopUp");
84 | openDialog("editGame");
85 | }}
86 | data-tooltip={translateText("settings")}
87 | >
88 |
89 |
90 |
{
94 | closeDialog("gamePopUp");
95 | }}
96 | data-tooltip={translateText("close")}
97 | >
98 |
99 |
100 |
101 |
}
104 | >
105 |
116 |
117 |
118 |
119 |
123 | }
124 | >
125 |
136 |
137 |
138 |
139 |
140 |
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 | {
18 | uiContext.setShowLoadingModal(false);
19 | }}
20 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent"
21 | >
22 |
23 |
24 | {translateText("loading")}{" "}
25 |
26 | {`${steamDataContext.totalImportedSteamGames()} / ${steamDataContext.totalSteamGames()}`}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 | {
92 | uiContext.setShowNewFolderModal(false);
93 | }}
94 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent"
95 | >
96 |
97 |
98 |
101 |
102 |
{translateText("add new folder")}
103 |
104 |
105 | {
108 | setHideFolder((x) => !x);
109 | }}
110 | class="relative cursor-pointer"
111 | >
112 | {translateText("hide in expanded view")}
}>
113 |
114 |
{translateText("hide in expanded view")}
115 |
{translateText("hide in expanded view")}
116 |
117 |
118 |
119 |
124 | {translateText("save")}
125 |
126 |
127 |
{
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 | {translateText("hit again to confirm")}
144 | ) : (
145 |
146 | )}
147 |
148 |
149 |
150 |
151 | {
158 | setFolderName(e.currentTarget.value);
159 | }}
160 | value={folderName() || ""}
161 | placeholder={translateText("name of folder")}
162 | />
163 |
164 |
165 |
166 |
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 | {
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 |
36 |
37 |
38 |
39 |
{translateText("notepad")}
40 |
41 |
42 |
{
46 | closeDialog("notepad");
47 | getData();
48 | }}
49 | data-tooltip={translateText("close")}
50 | >
51 |
52 |
53 |
54 |
65 |
66 |
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 | {
35 | uiContext.setShowSettingsLanguageSelector(false);
36 | uiContext.setShowSettingsModal(false);
37 | }}
38 | class="!p-0 h-screen w-screen overflow-visible backdrop:bg-transparent"
39 | >
40 |
41 |
42 |
43 |
44 |
{translateText("settings")}
45 |
46 |
47 |
{
51 | closeDialog("settings");
52 | getData();
53 | }}
54 | data-tooltip={translateText("close")}
55 | >
56 |
57 |
58 |
59 |
60 |
61 | {
64 | globalContext.setLibraryData("userSettings", "roundedBorders", (x) => !x);
65 |
66 | await updateData();
67 | }}
68 | class="relative cursor-pointer p-0 text-left"
69 | >
70 | {translateText("rounded borders")}
}
73 | >
74 |
75 |
{translateText("rounded borders")}
76 |
{translateText("rounded borders")}
77 |
78 |
79 |
80 |
{
83 | globalContext.setLibraryData("userSettings", "gameTitle", (x) => !x);
84 |
85 | await updateData();
86 | }}
87 | class="relative cursor-pointer p-0 text-left"
88 | >
89 | {translateText("game title")} }
92 | >
93 |
94 |
{translateText("game title")}
95 |
{translateText("game title")}
96 |
97 |
98 |
99 |
{
102 | globalContext.setLibraryData("userSettings", "folderTitle", (x) => !x);
103 |
104 | await updateData();
105 | }}
106 | class="relative cursor-pointer p-0 text-left"
107 | >
108 | {translateText("folder title")} }
111 | >
112 |
113 |
{translateText("folder title")}
114 |
{translateText("folder title")}
115 |
116 |
117 |
118 | {
121 | globalContext.setLibraryData("userSettings", "quitAfterOpen", (x) => !x);
122 |
123 | await updateData();
124 | }}
125 | class="relative cursor-pointer p-0 text-left"
126 | >
127 | {translateText("quit after opening game")}}
130 | >
131 |
132 |
{translateText("quit after opening game")}
133 |
134 | {translateText("quit after opening game")}
135 |
136 |
137 |
138 |
139 |
140 | {
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 | [{translateText("font")}]
159 |
160 | {translateText(globalContext.libraryData.userSettings.fontName) || translateText("sans serif")}
161 |
162 |
163 | {
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 | [{translateText("theme")}]
175 |
176 | {translateText(globalContext.libraryData.userSettings.currentTheme) || translateText("dark")}
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | {
190 | invoke("open_location", {
191 | location: "https://clear.adithya.zip/update",
192 | });
193 | }}
194 | >
195 | {translateText("new update available!")}
196 | v{applicationStateContext.latestVersion()}
197 |
198 |
199 |
200 |
201 |
202 |
203 | {
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 |
225 |
229 |
230 | {translateText("current 'steam' folder will be overwritten. confirm?")}
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 | {
244 | const appDataDirPath = await appDataDir();
245 |
246 | invoke("open_location", {
247 | location: appDataDirPath,
248 | });
249 | }}
250 | >
251 | {translateText("open library location")}
252 |
253 |
254 | {translateText("these are all the files that the app stores on your pc")}
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | clear{" "}
264 | v{applicationStateContext.appVersion()}
265 |
266 |
{
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 |
277 |
{
280 | invoke("open_location", {
281 | location: "https://clear.adithya.zip/",
282 | });
283 | }}
284 | class="cursor-pointer p-0 underline"
285 | >
286 | {translateText("website")}
287 |
288 |
289 | {translateText("made by")}{" "}
290 | {
293 | invoke("open_location", {
294 | location: "https://adithya.zip/",
295 | });
296 | }}
297 | class="cursor-pointer p-0 underline"
298 | >
299 | {" "}
300 | adithya
301 |
302 |
303 |
{
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 |
314 |
315 |
316 |
317 |
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 |
--------------------------------------------------------------------------------