├── src ├── js │ ├── renderers │ │ ├── indexRenderer.js │ │ ├── solveRenderer.js │ │ ├── algsRenderer.js │ │ ├── render.js │ │ ├── settingsRenderer.js │ │ ├── statsRenderer.js │ │ └── timerRenderer.js │ ├── loadSidebar.js │ ├── index.js │ └── preload.js ├── backup │ └── cubyData │ │ ├── solves.json │ │ └── theme.json ├── img │ ├── icon.ico │ ├── icon.png │ ├── img.png │ ├── img_1.png │ └── img_2.png ├── css │ ├── titlebar.css │ ├── input.css │ └── tailwind │ │ └── output.css └── pages │ ├── splash.html │ ├── algs.html │ ├── solver.html │ ├── components │ └── sidebar.html │ ├── settings.html │ ├── index.html │ ├── timer.html │ └── stats.html ├── ideas.md ├── tailwind.config.js ├── .gitignore ├── LICENSE ├── forge.config.js ├── package.json ├── README.md └── .github └── workflows └── codeql.yml /src/js/renderers/indexRenderer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/renderers/solveRenderer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backup/cubyData/solves.json: -------------------------------------------------------------------------------- 1 | { 2 | "solves": [] 3 | } -------------------------------------------------------------------------------- /src/backup/cubyData/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "dark" 3 | } -------------------------------------------------------------------------------- /src/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cuby-Project/Cuby-Client/HEAD/src/img/icon.ico -------------------------------------------------------------------------------- /src/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cuby-Project/Cuby-Client/HEAD/src/img/icon.png -------------------------------------------------------------------------------- /src/img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cuby-Project/Cuby-Client/HEAD/src/img/img.png -------------------------------------------------------------------------------- /src/img/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cuby-Project/Cuby-Client/HEAD/src/img/img_1.png -------------------------------------------------------------------------------- /src/img/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cuby-Project/Cuby-Client/HEAD/src/img/img_2.png -------------------------------------------------------------------------------- /src/js/loadSidebar.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function() { 2 | fetch('components/sidebar.html') 3 | .then(response => response.text()) 4 | .then(data => { 5 | document.getElementById('sidebar-container').innerHTML = data; 6 | }) 7 | .catch(error => console.error('Error loading sidebar:', error)); 8 | }); -------------------------------------------------------------------------------- /src/js/renderers/algsRenderer.js: -------------------------------------------------------------------------------- 1 | // Set theme on load of page 2 | appdata.getTheme().then( 3 | (theme => { 4 | if (theme === "dark") { 5 | document.querySelector("html").classList.add('dark') 6 | } else { 7 | document.querySelector("html").classList.remove('dark') 8 | } 9 | } 10 | )); -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | # design ideas: 2 | - for the preview, display an eye icon that when clicked, shows the cube --> DONE 3 | - rework the settings page 4 | 5 | # functional ideas: 6 | - add a cube preview next to the scramble --> DONE 7 | - add a fully customizable ui (colors, fonts, etc) --> theme DONE 8 | - let the user chose in the settings how does he wants to input the time (timer, manual input, etc) --> UI DONE -------------------------------------------------------------------------------- /src/js/renderers/render.js: -------------------------------------------------------------------------------- 1 | const buttonClose = document.querySelector('#close'); 2 | const buttonMaximize = document.querySelector('#maximize'); 3 | const buttonMinimize = document.querySelector('#minimize'); 4 | 5 | buttonClose.addEventListener('click', api.closeWindow) 6 | buttonMaximize.addEventListener('click', api.maximizeWindow) 7 | buttonMinimize.addEventListener('click', api.minimizeWindow) 8 | 9 | // Set theme on load of page 10 | appdata.getTheme().then( 11 | (theme => { 12 | if (theme === "dark") { 13 | document.querySelector("html").classList.add('dark') 14 | } else { 15 | document.querySelector("html").classList.remove('dark') 16 | } 17 | } 18 | )); 19 | 20 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["src/pages/*.html", "src/js/**/*.js"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'custom-gray-0': '#424549', 8 | 'custom-gray-1': '#36393e', 9 | 'custom-gray-2': '#282b30', 10 | 'custom-gray-3': '#1e2124', 11 | 'custom-blue': '#009FFD', 12 | }, 13 | cardHeight: { 14 | 'card': '500px', 15 | }, 16 | screens: { 17 | 'hXL': {'raw': '(min-height: 970px)'} 18 | }, 19 | fontFamily: { 20 | "Montserrat": ['Montserrat', 'sans-serif'] 21 | } 22 | }, 23 | }, 24 | plugins: [], 25 | darkMode: 'class', 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/css/titlebar.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | text-align: justify; 5 | } 6 | 7 | 8 | .draggable { 9 | padding: 2px; 10 | font-family: sans-serif; 11 | font-size: 14px; 12 | display: flex; 13 | justify-content: end; 14 | -webkit-app-region: drag; 15 | } 16 | 17 | .controls { 18 | -webkit-app-region: no-drag; 19 | user-select: none; 20 | padding: 5px 0 0; 21 | } 22 | 23 | .button { 24 | display: inline-block; 25 | width: 15px; 26 | height: 15px; 27 | border-radius: 100%; 28 | cursor: pointer; 29 | } 30 | 31 | .button.close { background: #F03823; } 32 | .button.minimize { background: #FCA101; margin: 0 3px; } 33 | .button.maximize { background: #66E017; } 34 | 35 | .button.close:hover { background: #CC2411; } 36 | .button.minimize:hover { background: #D48802; } 37 | .button.maximize:hover { background: #4EBD06; } 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .gclient_done 4 | **/.npmrc 5 | .tags* 6 | .vs/ 7 | .vscode/ 8 | *.log 9 | *.pyc 10 | *.sln 11 | *.swp 12 | *.VC.db 13 | *.VC.VC.opendb 14 | *.vcxproj 15 | *.vcxproj.filters 16 | *.vcxproj.user 17 | *.xcodeproj 18 | /.idea/ 19 | /dist/ 20 | node_modules/ 21 | SHASUMS256.txt 22 | **/package-lock.json 23 | compile_commands.json 24 | .envrc 25 | 26 | # npm package 27 | /npm/dist 28 | /npm/path.txt 29 | /npm/checksums.json 30 | 31 | .npmrc 32 | 33 | # Generated API definitions 34 | electron-api.json 35 | electron.d.ts 36 | 37 | # Spec hash calculation 38 | spec/.hash 39 | 40 | # Eslint Cache 41 | .eslintcache* 42 | 43 | # Generated native addon files 44 | /spec/fixtures/native-addon/echo/build/ 45 | 46 | # If someone runs tsc this is where stuff will end up 47 | ts-gen 48 | 49 | # Used to accelerate CI builds 50 | .depshash 51 | .depshash-target 52 | 53 | # Used to accelerate builds after sync 54 | patches/mtime-cache.json 55 | 56 | spec/fixtures/logo.png 57 | 58 | # idea 59 | .idea/ 60 | 61 | out/ -------------------------------------------------------------------------------- /src/pages/splash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Loading... 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 |
24 | 25 |

Loading...

26 |
27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 quentinformatique 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publishers: [ 3 | { 4 | name: '@electron-forge/publisher-github', 5 | config: { 6 | repository: { 7 | owner: 'quentinformatique', 8 | name: 'Cuby' 9 | }, 10 | prerelease: false, 11 | draft: true 12 | } 13 | } 14 | ], 15 | packagerConfig: { 16 | asar: true, 17 | }, 18 | rebuildConfig: {}, 19 | makers: [ 20 | { 21 | name: '@electron-forge/maker-squirrel', 22 | config: {}, 23 | }, 24 | { 25 | name: '@electron-forge/maker-zip', 26 | platforms: ['darwin'], 27 | }, 28 | { 29 | name: '@electron-forge/maker-deb', 30 | config: {}, 31 | }, 32 | { 33 | name: '@electron-forge/maker-rpm', 34 | config: {}, 35 | }, 36 | ], 37 | plugins: [ 38 | { 39 | name: '@electron-forge/plugin-auto-unpack-natives', 40 | config: {}, 41 | }, 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /src/js/renderers/settingsRenderer.js: -------------------------------------------------------------------------------- 1 | let buttonTheme = document.querySelector('button#toggleDarkMode'); 2 | 3 | 4 | function changeTheme() { 5 | appdata.getTheme().then( 6 | (theme => { 7 | if (theme === "dark") { 8 | buttonTheme.innerHTML = "change to dark Mode"; 9 | document.querySelector("html").classList.remove('dark') 10 | } else { 11 | buttonTheme.innerHTML = "change to light Mode"; 12 | document.querySelector("html").classList.add('dark') 13 | } 14 | appdata.changeTheme(); 15 | }) 16 | ); 17 | } 18 | 19 | // Set theme on load of page 20 | appdata.getTheme().then( 21 | (theme => { 22 | if (theme === "dark") { 23 | document.querySelector("html").classList.add('dark') 24 | buttonTheme.innerHTML = "change to light Mode"; 25 | } else { 26 | document.querySelector("html").classList.remove('dark') 27 | buttonTheme.innerHTML = "change to dark Mode"; 28 | } 29 | } 30 | )); 31 | 32 | buttonTheme.addEventListener("click", changeTheme); 33 | 34 | document.querySelector('#github').addEventListener('click', () => { 35 | openWindowApi.openUrl('https://github.com/quentinformatique/Cuby'); 36 | }); -------------------------------------------------------------------------------- /src/pages/algs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Cuby 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 | 29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /src/pages/solver.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Cuby 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /src/pages/components/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 24 | 25 | 26 | 29 | 30 | 31 | 34 | 35 |
36 |
-------------------------------------------------------------------------------- /src/css/input.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400&display=swap'); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | 7 | * { 8 | @apply font-Montserrat font-light; 9 | } 10 | 11 | .side-bar-item { 12 | @apply relative flex items-center justify-center 13 | h-12 w-12 mt-2 mb-2 mx-auto hover:bg-custom-blue bg-white dark:bg-custom-gray-0 text-custom-blue hover:rounded-xl rounded-3xl 14 | cursor-pointer transition-all duration-300 ease-linear shadow-lg; 15 | } 16 | 17 | .side-bar-item-disabled { 18 | @apply relative flex items-center justify-center 19 | h-12 w-12 mt-2 mb-2 mx-auto bg-custom-gray-0 text-custom-gray-1 rounded-3xl 20 | cursor-not-allowed; 21 | } 22 | 23 | .side-bar-main { 24 | @apply relative flex items-center justify-center 25 | h-12 w-12 mt-2 mb-2 mx-auto cursor-pointer hover:scale-90 shadow-none; 26 | } 27 | 28 | .current-item { 29 | @apply rounded-xl; 30 | } 31 | 32 | .tableTr { 33 | @apply text-center ; 34 | } 35 | 36 | .tableTd { 37 | @apply text-center border-custom-gray-1 border-2 font-medium; 38 | } 39 | 40 | .tableTh { 41 | @apply border-custom-gray-1 border-2 font-bold sticky top-0 ; 42 | } 43 | 44 | .grid-cell { 45 | @apply text-center border-custom-gray-1 dark:border-2 border p-1 dark:font-medium font-bold; 46 | } 47 | 48 | ::-webkit-scrollbar { 49 | width: 10px; 50 | } 51 | 52 | /* Track */ 53 | ::-webkit-scrollbar-track { 54 | @apply bg-custom-gray-0 rounded-xl; 55 | } 56 | 57 | /* Handle */ 58 | ::-webkit-scrollbar-thumb { 59 | @apply bg-custom-blue rounded-xl; 60 | } 61 | 62 | /* Handle on hover */ 63 | ::-webkit-scrollbar-thumb:hover { 64 | @apply bg-[#108DD7FF] 65 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cuby", 3 | "version": "1.0.0", 4 | "description": "A Rubik's cube app", 5 | "main": "src/js/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "tailwind:watch": "npx tailwindcss -i ./src/css/input.css -o ./src/css/tailwind/output.css --watch" 13 | 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/quentinformatique/Cuby.git" 18 | }, 19 | "keywords": [ 20 | "rubik's cube" 21 | ], 22 | "author": "quentiformatique", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/quentinformatique/Cuby/issues" 26 | }, 27 | "homepage": "https://github.com/quentinformatique/Cuby#readme", 28 | "devDependencies": { 29 | "@electron-forge/cli": "^7.2.0", 30 | "@electron-forge/maker-deb": "^7.2.0", 31 | "@electron-forge/maker-rpm": "^7.2.0", 32 | "@electron-forge/maker-squirrel": "^7.2.0", 33 | "@electron-forge/maker-zip": "^7.2.0", 34 | "@electron-forge/plugin-auto-unpack-natives": "^7.2.0", 35 | "@electron-forge/publisher-github": "^7.2.0", 36 | "electron": "^26.3.0", 37 | "tailwindcss": "^3.3.3" 38 | }, 39 | "dependencies": { 40 | "@fortawesome/fontawesome-free": "^6.4.2", 41 | "axios": "^1.6.5", 42 | "chart.js": "^4.4.1", 43 | "cubing": "^0.43.4", 44 | "electron-squirrel-startup": "^1.0.0", 45 | "fs": "^0.0.1-security", 46 | "fs-extra": "^11.1.1", 47 | "moment": "^2.29.4", 48 | "shell": "^0.5.1", 49 | "sr-puzzlegen": "^1.0.4", 50 | "tailwindcss-typography": "^3.1.0" 51 | }, 52 | "build": { 53 | "win": { 54 | "icon": "src/img/icon.ico" 55 | }, 56 | "mac": { 57 | "icon": "src/img/icon.ico" 58 | } 59 | }, 60 | "files": [ 61 | "./build/**/*", 62 | "./dist/**/*", 63 | "./node_modules/**/*", 64 | "./src/img/*", 65 | "*.js" 66 | ], 67 | "directories": { 68 | "buildResources": "src/img/*" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require('electron') 2 | const path = require('path') 3 | 4 | let win; 5 | function createWindow () { 6 | // Create the browser window for the splash screen 7 | let splash = new BrowserWindow({ 8 | width: 500, 9 | height: 300, 10 | transparent: true, 11 | frame: false, 12 | alwaysOnTop: true 13 | }); 14 | 15 | // Load the splash screen html 16 | splash.loadFile('src/pages/splash.html'); 17 | splash.setIcon(path.join(__dirname, '../img/icon.ico')); 18 | splash.center(); 19 | 20 | 21 | win = new BrowserWindow({ 22 | width: 1000, 23 | height: 790, 24 | minWidth: 1000, 25 | minHeight: 790, 26 | frame: false, 27 | backgroundColor: '#282c34', 28 | webPreferences: { 29 | preload: path.join(__dirname, 'preload.js'), 30 | 31 | nodeIntegration: true, 32 | } 33 | }) 34 | 35 | win.loadFile('src/pages/index.html'); 36 | win.setIcon(path.join(__dirname, '../img/icon.ico')); 37 | 38 | 39 | win.once('ready-to-show', () => { 40 | splash.destroy(); 41 | win.show(); 42 | }); 43 | 44 | 45 | } 46 | 47 | app.whenReady().then(() => { 48 | createWindow() 49 | 50 | app.on('activate', () => { 51 | if (BrowserWindow.getAllWindows().length === 0) { 52 | createWindow() 53 | } 54 | }) 55 | 56 | ipcMain.handle('closeWindow', (event) => { 57 | app.quit(); 58 | }) 59 | 60 | ipcMain.handle('maximizeWindow', (event) => { 61 | if (win.isMaximized()) { 62 | win.restore(); 63 | } else { 64 | win.maximize(); 65 | } 66 | }) 67 | 68 | ipcMain.handle('minimizeWindow', (event) => { 69 | console.log('minimize') 70 | win.minimize(); 71 | }) 72 | }) 73 | 74 | 75 | app.on('window-all-closed', () => { 76 | if (process.platform !== 'darwin') { 77 | app.quit() 78 | } 79 | }) 80 | 81 | let moves = ["U", "D", "F", "B", "R", "L"] 82 | let variations = ["", "'", "2"] 83 | let minMoves = 20; 84 | let maxMoves = 25; 85 | let lengthScramble = Math.random() * (maxMoves - minMoves) + minMoves; 86 | 87 | const { ipcRenderer } = require('electron'); 88 | 89 | function generateScramble() { 90 | let scramble = ""; 91 | let lastMove = ""; 92 | for (let i = 0; i < lengthScramble; i++) { 93 | let move = moves[Math.floor(Math.random() * moves.length)]; 94 | while (move === lastMove) { 95 | move = moves[Math.floor(Math.random() * moves.length)]; 96 | } 97 | scramble += move + variations[Math.floor(Math.random() * variations.length)] + " "; 98 | lastMove = move; 99 | } 100 | return scramble; 101 | } 102 | 103 | ipcMain.handle('generateScramble', (event) => { 104 | return generateScramble(); 105 | }); 106 | 107 | app.whenReady() 108 | .then(() => { 109 | ipcMain.handle("getDeviceUserDataPath", () => { 110 | return app.getPath("userData"); 111 | }) 112 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧊 Cuby 2 | 3 |
4 | 5 | [![GitHub stars](https://img.shields.io/github/stars/quentinformatique/Cuby.svg)](https://github.com/quentinformatique/Cuby/stargazers) 6 | [![GitHub forks](https://img.shields.io/github/forks/quentinformatique/Cuby.svg)](https://github.com/quentinformatique/Cuby/network) 7 | [![GitHub issues](https://img.shields.io/github/issues/quentinformatique/Cuby.svg)](https://github.com/quentinformatique/Cuby/issues) 8 | [![GitHub last commit](https://img.shields.io/github/last-commit/quentinformatique/Cuby.svg)](https://github.com/quentinformatique/Cuby/commits/main) 9 | 10 |
11 | 12 | ## 📝 Description 13 | 14 | Cuby is a comprehensive ElectronJS application for Rubik's Cube enthusiasts. It provides everything you need to solve, time, learn, and explore Rubik's Cubes. Built with ElectronJS, this application is currently in active development but already offers a range of useful features. 15 | 16 | ## 🔗 Related Projects 17 | 18 | - [Cuby Mobile App](https://github.com/Cuby-Project/Cuby-mobile-app) - Mobile version of Cuby 19 | - [Cuby Recognition API](https://github.com/Cuby-Project/Cuby-recognition-API) - Color detection API 20 | - [Cuby Solve API](https://github.com/Cuby-Project/Cuby-solve-API) - Cube solving algorithm API 21 | - [Cuby Capture API](https://github.com/Cuby-Project/Cuby-capture-API) - Cube state capture API 22 | - [Cuby Capture Website](https://github.com/Cuby-Project/Cuby-capture-website) - Web interface for cube capture 23 | 24 | ## ✨ Features 25 | 26 | - ⏱️ Timer with statistics 27 | - 🔄 Scramble generator 28 | - 📊 Detailed statistics and analytics 29 | - 🎯 Multiple cube support 30 | - 📚 Algorithm library (in development) 31 | - 🤖 Auto-solve feature (in development) 32 | 33 | ## 🖼️ Screenshots 34 | 35 | ![Timer View](src/img/img_1.png) 36 | ![Statistics View](src/img/img.png) 37 | ![Cube View](src/img/img_2.png) 38 | 39 | ## 🚀 Installation 40 | 41 | ### Option 1: Download Release 42 | 43 | Download the latest release from the [releases page](https://github.com/quentinformatique/Cuby/releases). 44 | 45 | ### Option 2: Build from Source 46 | 47 | 1. Clone the repository: 48 | 49 | ```bash 50 | git clone https://github.com/quentinformatique/Cuby.git 51 | ``` 52 | 53 | 2. Install dependencies: 54 | 55 | ```bash 56 | npm install 57 | ``` 58 | 59 | 3. Start the development server: 60 | 61 | ```bash 62 | npm run start 63 | ``` 64 | 65 | 4. For development with Tailwind CSS: 66 | 67 | ```bash 68 | npx tailwindcss -i ./src/css/input.css -o ./src/css/tailwind/output.css --watch 69 | ``` 70 | 71 | ## 🤝 Contributing 72 | 73 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 74 | 75 | 1. Fork the Project 76 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 77 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 78 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 79 | 5. Open a Pull Request 80 | 81 | ## 📄 License 82 | 83 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 84 | 85 | ## 📞 Support 86 | 87 | - [Report a bug](https://github.com/quentinformatique/Cuby/issues/new/choose) 88 | - [Request a feature](https://github.com/quentinformatique/Cuby/issues/new/choose) 89 | 90 | ## 👨‍💻 Author 91 | 92 | *quentinformatique* 93 | 94 | ## 📞 Support 95 | 96 | - [Report a bug](https://github.com/quentinformatique/Cuby/issues/new/choose) 97 | - [Request a feature](https://github.com/quentinformatique/Cuby/issues/new/choose) 98 | 99 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | # Runner size impacts CodeQL analysis time. To learn more, please see: 24 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 25 | # - https://gh.io/supported-runners-and-hardware-resources 26 | # - https://gh.io/using-larger-runners 27 | # Consider using larger runners for possible analysis time improvements. 28 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 29 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 30 | permissions: 31 | # required for all workflows 32 | security-events: write 33 | 34 | # only required for workflows in private repositories 35 | actions: read 36 | contents: read 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'javascript-typescript' ] 42 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 43 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 44 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 45 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@v3 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | 64 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 65 | # If this step fails, then you should remove it and run the build manually (see below) 66 | - name: Autobuild 67 | uses: github/codeql-action/autobuild@v3 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 71 | 72 | # If the Autobuild fails above, remove it and uncomment the following three lines. 73 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 74 | 75 | # - run: | 76 | # echo "Run, Build Application using script" 77 | # ./location_of_script_within_repo/buildscript.sh 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v3 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /src/pages/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Cuby 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
Customize the app style
31 |
32 |
33 |
Change color theme :
34 | 36 |
37 |
38 |
Customize the time input method
39 |
40 |
41 |
Change the time input method :
42 |
43 | 44 |
45 | 46 | 47 |
48 |
49 |
50 |
Other informations
51 |
52 |
53 |
Github repository :
54 | 57 |
58 |
59 |
60 | created by : 61 |
Quentinformatique, all rights reserved
62 |
63 |
64 |
contact :
65 |
66 | 67 | 68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | -------------------------------------------------------------------------------- /src/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Cuby 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 |
29 | 77 |
78 |
79 | 80 | -------------------------------------------------------------------------------- /src/pages/timer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Cuby 15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 37 |
38 |
39 | 43 |
44 |
45 | scramble 46 |
47 | 50 |
51 | 52 |
53 | 54 | 68 |
69 |
70 |
71 |
72 |

00:00,00

73 |
74 |
75 |
76 |
77 |
Personal best :
78 |
Average :
79 |
solves :
80 |
81 |
82 | 83 | 84 |
85 |
86 |
87 |
88 |
89 | 90 | -------------------------------------------------------------------------------- /src/pages/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Cuby 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 | 51 |
52 |
53 | 56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 | Best solve : 00:11.34 65 |
66 |
67 | Average : 00:17.90 68 |
69 |
70 | Solves : 30 71 |
72 |
73 |
74 |
75 | Best Ao5 : 00:11.34 76 |
77 |
78 | Best Ao12 : 00:17.90 79 |
80 |
81 |
82 |
83 | First solve the 16/12/2023 84 |
85 |
86 | Last solve the 14/01/2024 87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 | -------------------------------------------------------------------------------- /src/js/renderers/statsRenderer.js: -------------------------------------------------------------------------------- 1 | const CHANGE_BUTTON = document.getElementById("changeView"); 2 | const CUBE_SELECT = document.getElementById("selectCube"); 3 | const NB_SOLVE_SELECT = document.getElementById("displayedNumber"); 4 | const CONTENT = document.getElementById("content"); 5 | const bestSolveElement = document.querySelector('.fa-star + span'); 6 | const averageElement = document.querySelector('.fa-chart-line + span'); 7 | const solvesElement = document.querySelector('.fa-signal + span'); 8 | const bestAo5Element = document.querySelector('.fa-gauge-simple + span'); 9 | const bestAo12Element = document.querySelector('.fa-gauge-high + span'); 10 | const firstSolveDateElement = document.querySelector('.fa-calendar-minus + span'); 11 | const lastSolveDateElement = document.querySelector('.fa-calendar-plus + span'); 12 | 13 | function displayContent() { 14 | if (CONTENT.getAttribute('display') === "chart") { 15 | displayChart(CUBE_SELECT.value); 16 | } else { 17 | displayTable(CUBE_SELECT.value); 18 | } 19 | 20 | solvesDataAPI.getBestSolve(CUBE_SELECT.value).then(bestSolve => { 21 | bestSolveElement.textContent = timeAPI.formatDuration(bestSolve); 22 | }); 23 | 24 | solvesDataAPI.getAverage(CUBE_SELECT.value).then(average => { 25 | averageElement.textContent = timeAPI.formatDuration(average); 26 | }); 27 | 28 | solvesDataAPI.getNbSolve(CUBE_SELECT.value).then(nbSolve => { 29 | solvesElement.textContent = nbSolve; 30 | }); 31 | 32 | solvesDataAPI.getBestAo5(CUBE_SELECT.value).then(bestAo5 => { 33 | bestAo5Element.textContent = timeAPI.formatDuration(bestAo5); 34 | }); 35 | 36 | solvesDataAPI.getBestAo12(CUBE_SELECT.value).then(bestAo12 => { 37 | bestAo12Element.textContent = timeAPI.formatDuration(bestAo12); 38 | }); 39 | 40 | solvesDataAPI.getFirstSolveDate(CUBE_SELECT.value).then(firstSolveDate => { 41 | firstSolveDateElement.textContent =firstSolveDate 42 | }); 43 | 44 | solvesDataAPI.getLastSolveDate(CUBE_SELECT.value).then(lastSolveDate => { 45 | lastSolveDateElement.textContent = lastSolveDate 46 | console.log(lastSolveDate) 47 | }); 48 | } 49 | 50 | function displayChart(cube) { 51 | CONTENT.innerHTML = '' 52 | chartAPI.getChart(cube, document.getElementById('myChart')); 53 | } 54 | 55 | async function displayTable(cube) { 56 | let solves = await solvesDataAPI.getCubeSolves(cube); 57 | CONTENT.innerHTML = ` 58 |
59 |
60 |
solve number
61 |
time
62 |
gap to average
63 |
date
64 |
edit
65 |
66 |
67 | ${ 68 | solves.reverse().map((solve) => { 69 | let average = 0; 70 | solves.forEach((solve) => { 71 | average += solve.time; 72 | }); 73 | average = average / solves.length; 74 | let negative = false; 75 | let gapToAverage; 76 | if (solve.time > average) { 77 | gapToAverage = solve.time - average; 78 | negative = true; 79 | } else { 80 | gapToAverage = average - solve.time; 81 | } 82 | return ` 83 |
${solve.solveNumber}
84 |
${timeAPI.formatDuration(solve.time)}
85 | ${negative ? `
+ ${timeAPI.formatDuration(gapToAverage)}
` 86 | : `
- ${timeAPI.formatDuration(gapToAverage)}
`} 87 |
${solve.date}
88 |
`; 89 | }).join(``)} 90 |
91 |
92 | `; 93 | } 94 | 95 | CUBE_SELECT.addEventListener('change', function () { 96 | displayContent(); 97 | }); 98 | 99 | 100 | CHANGE_BUTTON.addEventListener('click', function () { 101 | if (CONTENT.getAttribute('display') === "chart") { 102 | CONTENT.setAttribute('display', "table"); 103 | CHANGE_BUTTON.innerHTML = " Chart"; 104 | } else { 105 | CONTENT.setAttribute('display', "chart") 106 | CHANGE_BUTTON.innerHTML = " Table"; 107 | } 108 | displayContent(); 109 | 110 | }); 111 | 112 | displayContent(); -------------------------------------------------------------------------------- /src/js/renderers/timerRenderer.js: -------------------------------------------------------------------------------- 1 | const buttonGenerate = document.querySelector('#regenerate'); 2 | const buttonDisplayScramble = document.querySelector("#displayScramble"); 3 | const scramble = document.querySelector('#scramble'); 4 | const selectCube = document.querySelector('#selectCube'); 5 | 6 | // previews items 7 | const previewContainer = document.querySelector('#previewContainer'); 8 | const previewScramble = document.querySelector("#previewScramble"); 9 | const previewIcon = document.querySelector("#previewIcon"); 10 | 11 | // tumer items 12 | const timerDisplay = document.getElementById("timer"); 13 | 14 | function refreshScramble() { 15 | // if the preview is displayed, we hide it 16 | if (!previewContainer.classList.contains("hidden")) { 17 | changeDisplay(); 18 | } 19 | api.generateScramble() 20 | .then(data => { 21 | scramble.innerHTML = data; 22 | }); 23 | } 24 | 25 | function getScramble() { 26 | return scramble.innerHTML; 27 | } 28 | 29 | function generatePreview(puzzle, alg) { 30 | let preview = ""; 33 | return preview; 34 | } 35 | 36 | function changeDisplay() { 37 | if (previewIcon.classList.contains("fa-eye")) { 38 | previewContainer.classList.remove("hidden"); 39 | previewIcon.classList.replace("fa-eye", "fa-eye-slash"); 40 | } else { 41 | previewContainer.classList.add("hidden"); 42 | previewIcon.classList.replace("fa-eye-slash", "fa-eye"); 43 | } 44 | } 45 | 46 | function displayPreview() { 47 | let alg = scramble.innerHTML; 48 | let puzzle = selectCube.value; 49 | previewScramble.innerHTML = generatePreview(puzzle, alg); 50 | changeDisplay(); 51 | } 52 | 53 | buttonGenerate.addEventListener('click', refreshScramble) 54 | refreshScramble(); 55 | 56 | buttonDisplayScramble.addEventListener('click', displayPreview) 57 | 58 | buttonGenerate.addEventListener('click', refreshScramble) 59 | refreshScramble(); 60 | 61 | buttonDisplayScramble.addEventListener('click', displayPreview) 62 | 63 | // if the scramble preview is display, we refresh it when the cube is changed 64 | selectCube.addEventListener('change', () => { 65 | if (!previewContainer.classList.contains("hidden")) { 66 | displayPreview(); 67 | } 68 | refreshStatistics(); 69 | refreshScramble(); 70 | }); 71 | 72 | 73 | let time = 0; 74 | let timerInterval; 75 | let startTime; 76 | let spacePressed = false; 77 | let isRunning = false; 78 | let startCalled = false; 79 | let releasedTooEarly = false; 80 | let timerStart; 81 | 82 | function colorWaiter() { 83 | timerDisplay.classList.add("text-red-500"); 84 | setTimeout(() => { 85 | timerDisplay.classList.remove("text-red-500"); 86 | if (!releasedTooEarly) { 87 | timerDisplay.classList.add("text-green-500"); 88 | } else { 89 | releasedTooEarly = false; 90 | } 91 | }, 1000); 92 | } 93 | 94 | document.addEventListener('keydown', (event) => { 95 | if (event.key === ' ' && !spacePressed) { 96 | // When the space bar is pressed for the first time, start the timer 97 | startTime = Date.now(); 98 | spacePressed = true; 99 | startCalled = false; // Reset the startCalled variable 100 | releasedTooEarly = false; 101 | // If the timer is already running, stop it 102 | if (isRunning) { 103 | stop(); 104 | } else { 105 | colorWaiter(); 106 | } 107 | } 108 | }); 109 | 110 | document.addEventListener('keyup', (event) => { 111 | if (event.key === ' ' && spacePressed) { 112 | // When the space bar is released, check the duration 113 | const timeHeld = Date.now() - startTime; 114 | 115 | if (timeHeld >= 1000) { 116 | if (!startCalled) { 117 | // If the space bar was held for at least one second and start hasn't been called, call the start function 118 | start(); 119 | timerDisplay.classList.remove("text-green-500"); 120 | timerDisplay.classList.remove("text-red-500"); 121 | startCalled = true; 122 | } 123 | } else { 124 | releasedTooEarly = true; 125 | startCalled = false; 126 | if (startCalled) { 127 | // If the space bar is released after start has been called, call the stop function 128 | stop(); 129 | refreshStatistics(); 130 | } 131 | } 132 | 133 | // Reset the variables 134 | spacePressed = false; 135 | startTime = 0; 136 | } 137 | }); 138 | 139 | function start() { 140 | isRunning = true; 141 | timerDisplay.innerHTML = "00:00,00"; // Reset the display 142 | timerStart = timeAPI.now(); 143 | timerInterval = setInterval(updateTimer, 10); 144 | } 145 | 146 | function updateTimer() { 147 | timerDisplay.innerHTML = timeAPI.formatDuration(timeAPI.getDuration(timerStart)); 148 | } 149 | 150 | 151 | function stop() { 152 | clearInterval(timerInterval); 153 | isRunning = false; 154 | refreshScramble(); // when we stop the timer, a new scramble is proposed 155 | 156 | time = timeAPI.getDuration(timerStart); 157 | timeAPI.registerTime(time, selectCube.value, getScramble(), refreshStatistics); 158 | } 159 | 160 | // statistics at the bottom of the page : 161 | 162 | function displayAverage() { 163 | solvesDataAPI.getAverage(selectCube.value) 164 | .then(data => { 165 | if (isNaN(data) || data === 0) { 166 | document.querySelector("#average").innerHTML = "No solve yet"; 167 | return; 168 | } 169 | document.querySelector("#average").innerHTML = timeAPI.formatDuration(data); 170 | }); 171 | } 172 | 173 | function displayBest() { 174 | solvesDataAPI.getBestSolve(selectCube.value) 175 | .then(data => { 176 | if (data === 0) { 177 | document.querySelector("#best").innerHTML = "No solve yet"; 178 | return; 179 | } 180 | document.querySelector("#best").innerHTML = timeAPI.formatDuration(data); 181 | }); 182 | } 183 | 184 | 185 | function displaySolveNumber() { 186 | solvesDataAPI.getCubeNbSolves(selectCube.value) 187 | .then(data => { 188 | document.querySelector("#solveNumber").innerHTML = data; 189 | }); 190 | } 191 | 192 | function refreshStatistics() { 193 | displayAverage(); 194 | displayBest(); 195 | displaySolveNumber(); 196 | displaySolvesHistory() 197 | } 198 | 199 | selectCube.addEventListener("change", refreshStatistics) 200 | 201 | // display the solves history in a table 202 | 203 | function displaySolvesHistory() { 204 | solvesDataAPI.getCubeSolves(selectCube.value) 205 | .then(data => { 206 | let average = 0; 207 | for (let i = 0; i < data.length; i++) { 208 | average += data[i].time; 209 | } 210 | average = average / data.length; 211 | 212 | let table = document.querySelector("#solvesHistory"); 213 | table.innerHTML = "Solve numberTimeGap to averageEdit"; 214 | 215 | // we only keep the last 5 solves, 216 | let last5Solves = data.length >= 5 217 | ? data.reverse().slice(0, 5) 218 | : data.reverse(); 219 | 220 | last5Solves.forEach(solve => { 221 | let row = document.createElement("tr"); 222 | let negative = false; 223 | // calculate the gap to average 224 | let gapToAverage; 225 | if (solve.time > average) { 226 | gapToAverage = solve.time - average; 227 | negative = true; 228 | } else { 229 | gapToAverage = average - solve.time; 230 | } 231 | 232 | row.classList.add("tableTr"); 233 | row.innerHTML = "" + solve.solveNumber + ""; 234 | row.innerHTML += "" + timeAPI.formatDuration(solve.time) + ""; 235 | if (negative) { 236 | row.innerHTML += "+ " + timeAPI.formatDuration(gapToAverage) + ""; 237 | } else { 238 | row.innerHTML += "- " + timeAPI.formatDuration(gapToAverage) + ""; 239 | } 240 | row.innerHTML += ""; 241 | 242 | table.appendChild(row); 243 | }); 244 | }); 245 | } 246 | 247 | refreshStatistics(); -------------------------------------------------------------------------------- /src/js/preload.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer, contextBridge} = require('electron'); 2 | const fs = require('fs'); 3 | const path = require("path"); 4 | const fse = require("fs-extra") 5 | const {shell} = require('electron'); 6 | const moment = require("moment"); 7 | const {Chart, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale} = require('chart.js'); 8 | Chart.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale); 9 | 10 | const api = { 11 | closeWindow: () => ipcRenderer.invoke("closeWindow"), 12 | maximizeWindow: () => ipcRenderer.invoke("maximizeWindow"), 13 | minimizeWindow: () => ipcRenderer.invoke("minimizeWindow"), 14 | generateScramble: async () => await ipcRenderer.invoke("generateScramble"), 15 | }; 16 | 17 | const appdata = { 18 | initialize() { 19 | ipcRenderer.invoke("getDeviceUserDataPath").then((appData) => { 20 | const pathSource = path.join(__dirname, "../backup"); 21 | fse.copySync(pathSource, appData); 22 | }); 23 | }, 24 | 25 | async appIsInitialized() { 26 | return await ipcRenderer.invoke("getDeviceUserDataPath"); 27 | }, 28 | 29 | changeTheme() { 30 | ipcRenderer.invoke("getDeviceUserDataPath").then((data) => { 31 | const themePath = path.join(data, "cubyData/theme.json"); 32 | const content = fs.readFileSync(themePath); 33 | const theme = JSON.parse(content); 34 | theme.theme = theme.theme === "dark" ? "light" : "dark"; 35 | fs.writeFileSync(themePath, JSON.stringify(theme)); 36 | }); 37 | }, 38 | 39 | async getTheme() { 40 | const data = await ipcRenderer.invoke("getDeviceUserDataPath"); 41 | const themePath = path.join(data, "cubyData/theme.json"); 42 | const content = fs.readFileSync(themePath); 43 | return JSON.parse(content).theme; 44 | }, 45 | }; 46 | 47 | const timeAPI = { 48 | now: () => moment(), 49 | 50 | formatDuration(duration) { 51 | if (duration === 0) { 52 | return "DNF"; 53 | } 54 | return duration >= 0 55 | ? moment(duration).format("mm:ss,SS") 56 | : moment(duration).format("-mm:ss,SS"); 57 | }, 58 | 59 | getDuration(start) { 60 | return moment().diff(start); 61 | }, 62 | 63 | registerTime(time, cube, scramble, callback = () => { 64 | }) { 65 | ipcRenderer.invoke("getDeviceUserDataPath").then((data) => { 66 | const solvesPath = path.join(data, "cubyData/solves.json"); 67 | const content = fs.readFileSync(solvesPath, {encoding: "utf8"}); 68 | const parsedContent = JSON.parse(content); 69 | const solvesTable = parsedContent.solves; 70 | const now = moment().format("DD/MM/YYYY"); 71 | 72 | solvesDataAPI.getCubeSolves(cube).then((data) => { 73 | const solveNumber = data.length + 1; 74 | const solve = { 75 | date: now, 76 | time: time, 77 | scramble: scramble, 78 | cube, 79 | solveNumber, 80 | }; 81 | 82 | solvesTable.push(solve); 83 | 84 | fs.writeFile(solvesPath, JSON.stringify(parsedContent), callback); 85 | }); 86 | }); 87 | }, 88 | }; 89 | 90 | const solvesDataAPI = { 91 | async getSolves() { 92 | const data = await ipcRenderer.invoke("getDeviceUserDataPath"); 93 | const solvesPath = path.join(data, "cubyData/solves.json"); 94 | const content = fs.readFileSync(solvesPath); 95 | return JSON.parse(content).solves; 96 | }, 97 | 98 | async getAverage(cube) { 99 | const solves = await solvesDataAPI.getSolves(); 100 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 101 | const sum = solvesOfCube.reduce((acc, solve) => acc + solve.time, 0); 102 | return sum / solvesOfCube.length; 103 | }, 104 | 105 | async getNbSolve() { 106 | const solves = await solvesDataAPI.getSolves(); 107 | return solves.length; 108 | }, 109 | 110 | async getCubeSolves(cube) { 111 | const solves = await solvesDataAPI.getSolves(); 112 | return solves.filter((solve) => solve.cube === cube); 113 | }, 114 | 115 | async getCubeNbSolves(cube) { 116 | const solves = await solvesDataAPI.getSolves(); 117 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 118 | return solvesOfCube.length; 119 | }, 120 | 121 | async getBestSolve(cube) { 122 | const solves = await solvesDataAPI.getSolves(); 123 | let bestSolve = 0; 124 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 125 | solvesOfCube.forEach((solve) => { 126 | if (solve.time < bestSolve || bestSolve === 0) { 127 | bestSolve = solve.time; 128 | } 129 | }); 130 | return bestSolve; 131 | }, 132 | 133 | async deleteSolve(id) { 134 | const solves = await solvesDataAPI.getSolves(); 135 | const solvesWithoutDeleted = solves.filter((solve) => solve.id !== id); 136 | const solvesData = {solves: solvesWithoutDeleted}; 137 | 138 | const data = await ipcRenderer.invoke("getDeviceUserDataPath"); 139 | const solvesPath = path.join(data, "cubyData/solves.json"); 140 | fs.writeFileSync(solvesPath, JSON.stringify(solvesData)); 141 | }, 142 | async getBestSolve(cube) { 143 | const solves = await solvesDataAPI.getSolves(); 144 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 145 | return Math.min(...solvesOfCube.map(solve => solve.time)); 146 | }, 147 | 148 | async getAverage(cube) { 149 | const solves = await solvesDataAPI.getSolves(); 150 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 151 | const sum = solvesOfCube.reduce((acc, solve) => acc + solve.time, 0); 152 | return sum / solvesOfCube.length; 153 | }, 154 | 155 | async getNbSolve(cube) { 156 | const solves = await solvesDataAPI.getSolves(); 157 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 158 | return solvesOfCube.length; 159 | }, 160 | 161 | async getBestAo5(cube) { 162 | const solves = await solvesDataAPI.getSolves(); 163 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 164 | const averages = []; 165 | for (let i = 0; i < solvesOfCube.length - 4; i++) { 166 | const sum = solvesOfCube.slice(i, i + 5).reduce((acc, solve) => acc + solve.time, 0); 167 | averages.push(sum / 5); 168 | } 169 | return Math.min(...averages); 170 | }, 171 | 172 | async getBestAo12(cube) { 173 | const solves = await solvesDataAPI.getSolves(); 174 | const solvesOfCube = solves.filter((solve) => solve.cube === cube); 175 | const averages = []; 176 | for (let i = 0; i < solvesOfCube.length - 11; i++) { 177 | const sum = solvesOfCube.slice(i, i + 12).reduce((acc, solve) => acc + solve.time, 0); 178 | averages.push(sum / 12); 179 | } 180 | return Math.min(...averages); 181 | }, 182 | 183 | async getFirstSolveDate(cube) { 184 | const solves = await solvesDataAPI.getSolves(); 185 | const firstSolve = solves.find((solve) => solve.cube === cube); 186 | return firstSolve.date 187 | }, 188 | 189 | async getLastSolveDate(cube) { 190 | // we get the last solve of the cube 191 | const solves = await solvesDataAPI.getSolves(); 192 | const lastSolve = solves.filter((solve) => solve.cube === cube).pop(); 193 | return lastSolve.date 194 | }, 195 | }; 196 | 197 | const openWindowApi = { 198 | openUrl: (url) => shell.openExternal(url), 199 | }; 200 | 201 | const chartAPI = { 202 | chart: null, 203 | 204 | getChart: (cube, element) => { 205 | solvesDataAPI.getCubeSolves(cube).then((data) => { 206 | if (chartAPI.chart !== null) { 207 | chartAPI.chart.clear(); 208 | chartAPI.chart.destroy(); 209 | } 210 | 211 | chartAPI.chart = new Chart(element, { 212 | type: "line", 213 | data: { 214 | labels: data.map((solve) => solve.solveNumber), 215 | datasets: [ 216 | { 217 | label: "Solves for " + cube, 218 | data: data.map((solve) => solve.time), 219 | borderColor: "#009FFD", 220 | fill: false, 221 | tension: 0.1, 222 | }, 223 | ], 224 | }, 225 | options: { 226 | scales: { 227 | y: { 228 | ticks: { 229 | callback: function (value, index, values) { 230 | return timeAPI.formatDuration(value); 231 | }, 232 | color: "white", 233 | font: { 234 | size: 14, 235 | }, 236 | }, 237 | }, 238 | x: { 239 | ticks: { 240 | color: "white", 241 | font: { 242 | size: 14, 243 | }, 244 | }, 245 | }, 246 | }, 247 | elements: { 248 | point: { 249 | radius: 4, 250 | backgroundColor: "#009FFD", 251 | hoverRadius: 5, 252 | hoverBorderWidth: 2, 253 | hoverBackgroundColor: "#009FFD", 254 | hitRadius: 5, 255 | borderWidth: 2, 256 | borderColor: "#009FFD", 257 | }, 258 | }, 259 | }, 260 | }); 261 | }); 262 | }, 263 | }; 264 | 265 | 266 | appdata.appIsInitialized().then((data) => { 267 | const state = fs.existsSync(path.join(data, "cubyData")); 268 | if (!state) { 269 | appdata.initialize(); 270 | } 271 | contextBridge.exposeInMainWorld("api", api); 272 | contextBridge.exposeInMainWorld("openWindowApi", openWindowApi); 273 | contextBridge.exposeInMainWorld("appdata", appdata); 274 | contextBridge.exposeInMainWorld("timeAPI", timeAPI); 275 | contextBridge.exposeInMainWorld("solvesDataAPI", solvesDataAPI); 276 | contextBridge.exposeInMainWorld("chartAPI", chartAPI); 277 | }); -------------------------------------------------------------------------------- /src/css/tailwind/output.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400&display=swap'); 2 | 3 | /* 4 | ! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com 5 | */ 6 | 7 | /* 8 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 9 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 10 | */ 11 | 12 | *, 13 | ::before, 14 | ::after { 15 | box-sizing: border-box; 16 | /* 1 */ 17 | border-width: 0; 18 | /* 2 */ 19 | border-style: solid; 20 | /* 2 */ 21 | border-color: #e5e7eb; 22 | /* 2 */ 23 | } 24 | 25 | ::before, 26 | ::after { 27 | --tw-content: ''; 28 | } 29 | 30 | /* 31 | 1. Use a consistent sensible line-height in all browsers. 32 | 2. Prevent adjustments of font size after orientation changes in iOS. 33 | 3. Use a more readable tab size. 34 | 4. Use the user's configured `sans` font-family by default. 35 | 5. Use the user's configured `sans` font-feature-settings by default. 36 | 6. Use the user's configured `sans` font-variation-settings by default. 37 | */ 38 | 39 | html { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 50 | /* 4 */ 51 | font-feature-settings: normal; 52 | /* 5 */ 53 | font-variation-settings: normal; 54 | /* 6 */ 55 | } 56 | 57 | /* 58 | 1. Remove the margin in all browsers. 59 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 60 | */ 61 | 62 | body { 63 | margin: 0; 64 | /* 1 */ 65 | line-height: inherit; 66 | /* 2 */ 67 | } 68 | 69 | /* 70 | 1. Add the correct height in Firefox. 71 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 72 | 3. Ensure horizontal rules are visible by default. 73 | */ 74 | 75 | hr { 76 | height: 0; 77 | /* 1 */ 78 | color: inherit; 79 | /* 2 */ 80 | border-top-width: 1px; 81 | /* 3 */ 82 | } 83 | 84 | /* 85 | Add the correct text decoration in Chrome, Edge, and Safari. 86 | */ 87 | 88 | abbr:where([title]) { 89 | -webkit-text-decoration: underline dotted; 90 | text-decoration: underline dotted; 91 | } 92 | 93 | /* 94 | Remove the default font size and weight for headings. 95 | */ 96 | 97 | h1, 98 | h2, 99 | h3, 100 | h4, 101 | h5, 102 | h6 { 103 | font-size: inherit; 104 | font-weight: inherit; 105 | } 106 | 107 | /* 108 | Reset links to optimize for opt-in styling instead of opt-out. 109 | */ 110 | 111 | a { 112 | color: inherit; 113 | text-decoration: inherit; 114 | } 115 | 116 | /* 117 | Add the correct font weight in Edge and Safari. 118 | */ 119 | 120 | b, 121 | strong { 122 | font-weight: bolder; 123 | } 124 | 125 | /* 126 | 1. Use the user's configured `mono` font family by default. 127 | 2. Correct the odd `em` font sizing in all browsers. 128 | */ 129 | 130 | code, 131 | kbd, 132 | samp, 133 | pre { 134 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 135 | /* 1 */ 136 | font-size: 1em; 137 | /* 2 */ 138 | } 139 | 140 | /* 141 | Add the correct font size in all browsers. 142 | */ 143 | 144 | small { 145 | font-size: 80%; 146 | } 147 | 148 | /* 149 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 150 | */ 151 | 152 | sub, 153 | sup { 154 | font-size: 75%; 155 | line-height: 0; 156 | position: relative; 157 | vertical-align: baseline; 158 | } 159 | 160 | sub { 161 | bottom: -0.25em; 162 | } 163 | 164 | sup { 165 | top: -0.5em; 166 | } 167 | 168 | /* 169 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 170 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 171 | 3. Remove gaps between table borders by default. 172 | */ 173 | 174 | table { 175 | text-indent: 0; 176 | /* 1 */ 177 | border-color: inherit; 178 | /* 2 */ 179 | border-collapse: collapse; 180 | /* 3 */ 181 | } 182 | 183 | /* 184 | 1. Change the font styles in all browsers. 185 | 2. Remove the margin in Firefox and Safari. 186 | 3. Remove default padding in all browsers. 187 | */ 188 | 189 | button, 190 | input, 191 | optgroup, 192 | select, 193 | textarea { 194 | font-family: inherit; 195 | /* 1 */ 196 | font-feature-settings: inherit; 197 | /* 1 */ 198 | font-variation-settings: inherit; 199 | /* 1 */ 200 | font-size: 100%; 201 | /* 1 */ 202 | font-weight: inherit; 203 | /* 1 */ 204 | line-height: inherit; 205 | /* 1 */ 206 | color: inherit; 207 | /* 1 */ 208 | margin: 0; 209 | /* 2 */ 210 | padding: 0; 211 | /* 3 */ 212 | } 213 | 214 | /* 215 | Remove the inheritance of text transform in Edge and Firefox. 216 | */ 217 | 218 | button, 219 | select { 220 | text-transform: none; 221 | } 222 | 223 | /* 224 | 1. Correct the inability to style clickable types in iOS and Safari. 225 | 2. Remove default button styles. 226 | */ 227 | 228 | button, 229 | [type='button'], 230 | [type='reset'], 231 | [type='submit'] { 232 | -webkit-appearance: button; 233 | /* 1 */ 234 | background-color: transparent; 235 | /* 2 */ 236 | background-image: none; 237 | /* 2 */ 238 | } 239 | 240 | /* 241 | Use the modern Firefox focus style for all focusable elements. 242 | */ 243 | 244 | :-moz-focusring { 245 | outline: auto; 246 | } 247 | 248 | /* 249 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 250 | */ 251 | 252 | :-moz-ui-invalid { 253 | box-shadow: none; 254 | } 255 | 256 | /* 257 | Add the correct vertical alignment in Chrome and Firefox. 258 | */ 259 | 260 | progress { 261 | vertical-align: baseline; 262 | } 263 | 264 | /* 265 | Correct the cursor style of increment and decrement buttons in Safari. 266 | */ 267 | 268 | ::-webkit-inner-spin-button, 269 | ::-webkit-outer-spin-button { 270 | height: auto; 271 | } 272 | 273 | /* 274 | 1. Correct the odd appearance in Chrome and Safari. 275 | 2. Correct the outline style in Safari. 276 | */ 277 | 278 | [type='search'] { 279 | -webkit-appearance: textfield; 280 | /* 1 */ 281 | outline-offset: -2px; 282 | /* 2 */ 283 | } 284 | 285 | /* 286 | Remove the inner padding in Chrome and Safari on macOS. 287 | */ 288 | 289 | ::-webkit-search-decoration { 290 | -webkit-appearance: none; 291 | } 292 | 293 | /* 294 | 1. Correct the inability to style clickable types in iOS and Safari. 295 | 2. Change font properties to `inherit` in Safari. 296 | */ 297 | 298 | ::-webkit-file-upload-button { 299 | -webkit-appearance: button; 300 | /* 1 */ 301 | font: inherit; 302 | /* 2 */ 303 | } 304 | 305 | /* 306 | Add the correct display in Chrome and Safari. 307 | */ 308 | 309 | summary { 310 | display: list-item; 311 | } 312 | 313 | /* 314 | Removes the default spacing and border for appropriate elements. 315 | */ 316 | 317 | blockquote, 318 | dl, 319 | dd, 320 | h1, 321 | h2, 322 | h3, 323 | h4, 324 | h5, 325 | h6, 326 | hr, 327 | figure, 328 | p, 329 | pre { 330 | margin: 0; 331 | } 332 | 333 | fieldset { 334 | margin: 0; 335 | padding: 0; 336 | } 337 | 338 | legend { 339 | padding: 0; 340 | } 341 | 342 | ol, 343 | ul, 344 | menu { 345 | list-style: none; 346 | margin: 0; 347 | padding: 0; 348 | } 349 | 350 | /* 351 | Reset default styling for dialogs. 352 | */ 353 | 354 | dialog { 355 | padding: 0; 356 | } 357 | 358 | /* 359 | Prevent resizing textareas horizontally by default. 360 | */ 361 | 362 | textarea { 363 | resize: vertical; 364 | } 365 | 366 | /* 367 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 368 | 2. Set the default placeholder color to the user's configured gray 400 color. 369 | */ 370 | 371 | input::-moz-placeholder, textarea::-moz-placeholder { 372 | opacity: 1; 373 | /* 1 */ 374 | color: #9ca3af; 375 | /* 2 */ 376 | } 377 | 378 | input::placeholder, 379 | textarea::placeholder { 380 | opacity: 1; 381 | /* 1 */ 382 | color: #9ca3af; 383 | /* 2 */ 384 | } 385 | 386 | /* 387 | Set the default cursor for buttons. 388 | */ 389 | 390 | button, 391 | [role="button"] { 392 | cursor: pointer; 393 | } 394 | 395 | /* 396 | Make sure disabled buttons don't get the pointer cursor. 397 | */ 398 | 399 | :disabled { 400 | cursor: default; 401 | } 402 | 403 | /* 404 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 405 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 406 | This can trigger a poorly considered lint error in some tools but is included by design. 407 | */ 408 | 409 | img, 410 | svg, 411 | video, 412 | canvas, 413 | audio, 414 | iframe, 415 | embed, 416 | object { 417 | display: block; 418 | /* 1 */ 419 | vertical-align: middle; 420 | /* 2 */ 421 | } 422 | 423 | /* 424 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 425 | */ 426 | 427 | img, 428 | video { 429 | max-width: 100%; 430 | height: auto; 431 | } 432 | 433 | /* Make elements with the HTML hidden attribute stay hidden by default */ 434 | 435 | [hidden] { 436 | display: none; 437 | } 438 | 439 | *, ::before, ::after{ 440 | --tw-border-spacing-x: 0; 441 | --tw-border-spacing-y: 0; 442 | --tw-translate-x: 0; 443 | --tw-translate-y: 0; 444 | --tw-rotate: 0; 445 | --tw-skew-x: 0; 446 | --tw-skew-y: 0; 447 | --tw-scale-x: 1; 448 | --tw-scale-y: 1; 449 | --tw-pan-x: ; 450 | --tw-pan-y: ; 451 | --tw-pinch-zoom: ; 452 | --tw-scroll-snap-strictness: proximity; 453 | --tw-gradient-from-position: ; 454 | --tw-gradient-via-position: ; 455 | --tw-gradient-to-position: ; 456 | --tw-ordinal: ; 457 | --tw-slashed-zero: ; 458 | --tw-numeric-figure: ; 459 | --tw-numeric-spacing: ; 460 | --tw-numeric-fraction: ; 461 | --tw-ring-inset: ; 462 | --tw-ring-offset-width: 0px; 463 | --tw-ring-offset-color: #fff; 464 | --tw-ring-color: rgb(59 130 246 / 0.5); 465 | --tw-ring-offset-shadow: 0 0 #0000; 466 | --tw-ring-shadow: 0 0 #0000; 467 | --tw-shadow: 0 0 #0000; 468 | --tw-shadow-colored: 0 0 #0000; 469 | --tw-blur: ; 470 | --tw-brightness: ; 471 | --tw-contrast: ; 472 | --tw-grayscale: ; 473 | --tw-hue-rotate: ; 474 | --tw-invert: ; 475 | --tw-saturate: ; 476 | --tw-sepia: ; 477 | --tw-drop-shadow: ; 478 | --tw-backdrop-blur: ; 479 | --tw-backdrop-brightness: ; 480 | --tw-backdrop-contrast: ; 481 | --tw-backdrop-grayscale: ; 482 | --tw-backdrop-hue-rotate: ; 483 | --tw-backdrop-invert: ; 484 | --tw-backdrop-opacity: ; 485 | --tw-backdrop-saturate: ; 486 | --tw-backdrop-sepia: ; 487 | } 488 | 489 | ::backdrop{ 490 | --tw-border-spacing-x: 0; 491 | --tw-border-spacing-y: 0; 492 | --tw-translate-x: 0; 493 | --tw-translate-y: 0; 494 | --tw-rotate: 0; 495 | --tw-skew-x: 0; 496 | --tw-skew-y: 0; 497 | --tw-scale-x: 1; 498 | --tw-scale-y: 1; 499 | --tw-pan-x: ; 500 | --tw-pan-y: ; 501 | --tw-pinch-zoom: ; 502 | --tw-scroll-snap-strictness: proximity; 503 | --tw-gradient-from-position: ; 504 | --tw-gradient-via-position: ; 505 | --tw-gradient-to-position: ; 506 | --tw-ordinal: ; 507 | --tw-slashed-zero: ; 508 | --tw-numeric-figure: ; 509 | --tw-numeric-spacing: ; 510 | --tw-numeric-fraction: ; 511 | --tw-ring-inset: ; 512 | --tw-ring-offset-width: 0px; 513 | --tw-ring-offset-color: #fff; 514 | --tw-ring-color: rgb(59 130 246 / 0.5); 515 | --tw-ring-offset-shadow: 0 0 #0000; 516 | --tw-ring-shadow: 0 0 #0000; 517 | --tw-shadow: 0 0 #0000; 518 | --tw-shadow-colored: 0 0 #0000; 519 | --tw-blur: ; 520 | --tw-brightness: ; 521 | --tw-contrast: ; 522 | --tw-grayscale: ; 523 | --tw-hue-rotate: ; 524 | --tw-invert: ; 525 | --tw-saturate: ; 526 | --tw-sepia: ; 527 | --tw-drop-shadow: ; 528 | --tw-backdrop-blur: ; 529 | --tw-backdrop-brightness: ; 530 | --tw-backdrop-contrast: ; 531 | --tw-backdrop-grayscale: ; 532 | --tw-backdrop-hue-rotate: ; 533 | --tw-backdrop-invert: ; 534 | --tw-backdrop-opacity: ; 535 | --tw-backdrop-saturate: ; 536 | --tw-backdrop-sepia: ; 537 | } 538 | 539 | .fixed{ 540 | position: fixed; 541 | } 542 | 543 | .left-0{ 544 | left: 0px; 545 | } 546 | 547 | .left-2\/3{ 548 | left: 66.666667%; 549 | } 550 | 551 | .top-0{ 552 | top: 0px; 553 | } 554 | 555 | .top-24{ 556 | top: 6rem; 557 | } 558 | 559 | .m-1{ 560 | margin: 0.25rem; 561 | } 562 | 563 | .m-2{ 564 | margin: 0.5rem; 565 | } 566 | 567 | .m-3{ 568 | margin: 0.75rem; 569 | } 570 | 571 | .m-4{ 572 | margin: 1rem; 573 | } 574 | 575 | .mx-2{ 576 | margin-left: 0.5rem; 577 | margin-right: 0.5rem; 578 | } 579 | 580 | .mx-3{ 581 | margin-left: 0.75rem; 582 | margin-right: 0.75rem; 583 | } 584 | 585 | .mx-6{ 586 | margin-left: 1.5rem; 587 | margin-right: 1.5rem; 588 | } 589 | 590 | .my-2{ 591 | margin-top: 0.5rem; 592 | margin-bottom: 0.5rem; 593 | } 594 | 595 | .mb-4{ 596 | margin-bottom: 1rem; 597 | } 598 | 599 | .ml-1{ 600 | margin-left: 0.25rem; 601 | } 602 | 603 | .mr-\[11px\]{ 604 | margin-right: 11px; 605 | } 606 | 607 | .mt-10{ 608 | margin-top: 2.5rem; 609 | } 610 | 611 | .mt-7{ 612 | margin-top: 1.75rem; 613 | } 614 | 615 | .flex{ 616 | display: flex; 617 | } 618 | 619 | .table{ 620 | display: table; 621 | } 622 | 623 | .grid{ 624 | display: grid; 625 | } 626 | 627 | .hidden{ 628 | display: none; 629 | } 630 | 631 | .h-0{ 632 | height: 0px; 633 | } 634 | 635 | .h-10{ 636 | height: 2.5rem; 637 | } 638 | 639 | .h-11{ 640 | height: 2.75rem; 641 | } 642 | 643 | .h-44{ 644 | height: 11rem; 645 | } 646 | 647 | .h-52{ 648 | height: 13rem; 649 | } 650 | 651 | .h-\[18vh\]{ 652 | height: 18vh; 653 | } 654 | 655 | .h-\[25vh\]{ 656 | height: 25vh; 657 | } 658 | 659 | .h-\[60vh\]{ 660 | height: 60vh; 661 | } 662 | 663 | .h-\[68vh\]{ 664 | height: 68vh; 665 | } 666 | 667 | .h-\[calc\(100vh-35px\)\]{ 668 | height: calc(100vh - 35px); 669 | } 670 | 671 | .h-full{ 672 | height: 100%; 673 | } 674 | 675 | .h-screen{ 676 | height: 100vh; 677 | } 678 | 679 | .max-h-\[60vh\]{ 680 | max-height: 60vh; 681 | } 682 | 683 | .w-16{ 684 | width: 4rem; 685 | } 686 | 687 | .w-64{ 688 | width: 16rem; 689 | } 690 | 691 | .w-\[15rem\]{ 692 | width: 15rem; 693 | } 694 | 695 | .w-\[93vw\]{ 696 | width: 93vw; 697 | } 698 | 699 | .w-full{ 700 | width: 100%; 701 | } 702 | 703 | .flex-1{ 704 | flex: 1 1 0%; 705 | } 706 | 707 | .flex-none{ 708 | flex: none; 709 | } 710 | 711 | .scale-0{ 712 | --tw-scale-x: 0; 713 | --tw-scale-y: 0; 714 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 715 | } 716 | 717 | .cursor-not-allowed{ 718 | cursor: not-allowed; 719 | } 720 | 721 | .grid-cols-1{ 722 | grid-template-columns: repeat(1, minmax(0, 1fr)); 723 | } 724 | 725 | .grid-cols-2{ 726 | grid-template-columns: repeat(2, minmax(0, 1fr)); 727 | } 728 | 729 | .grid-cols-5{ 730 | grid-template-columns: repeat(5, minmax(0, 1fr)); 731 | } 732 | 733 | .grid-rows-1{ 734 | grid-template-rows: repeat(1, minmax(0, 1fr)); 735 | } 736 | 737 | .grid-rows-2{ 738 | grid-template-rows: repeat(2, minmax(0, 1fr)); 739 | } 740 | 741 | .flex-col{ 742 | flex-direction: column; 743 | } 744 | 745 | .place-content-between{ 746 | place-content: space-between; 747 | } 748 | 749 | .items-center{ 750 | align-items: center; 751 | } 752 | 753 | .justify-end{ 754 | justify-content: flex-end; 755 | } 756 | 757 | .justify-center{ 758 | justify-content: center; 759 | } 760 | 761 | .justify-between{ 762 | justify-content: space-between; 763 | } 764 | 765 | .overflow-y-scroll{ 766 | overflow-y: scroll; 767 | } 768 | 769 | .rounded-2xl{ 770 | border-radius: 1rem; 771 | } 772 | 773 | .rounded-3xl{ 774 | border-radius: 1.5rem; 775 | } 776 | 777 | .rounded-lg{ 778 | border-radius: 0.5rem; 779 | } 780 | 781 | .rounded-xl{ 782 | border-radius: 0.75rem; 783 | } 784 | 785 | .border{ 786 | border-width: 1px; 787 | } 788 | 789 | .border-b-0{ 790 | border-bottom-width: 0px; 791 | } 792 | 793 | .border-custom-blue{ 794 | --tw-border-opacity: 1; 795 | border-color: rgb(0 159 253 / var(--tw-border-opacity)); 796 | } 797 | 798 | .bg-blue-100{ 799 | --tw-bg-opacity: 1; 800 | background-color: rgb(219 234 254 / var(--tw-bg-opacity)); 801 | } 802 | 803 | .bg-blue-200{ 804 | --tw-bg-opacity: 1; 805 | background-color: rgb(191 219 254 / var(--tw-bg-opacity)); 806 | } 807 | 808 | .bg-custom-blue{ 809 | --tw-bg-opacity: 1; 810 | background-color: rgb(0 159 253 / var(--tw-bg-opacity)); 811 | } 812 | 813 | .bg-custom-gray-2{ 814 | --tw-bg-opacity: 1; 815 | background-color: rgb(40 43 48 / var(--tw-bg-opacity)); 816 | } 817 | 818 | .bg-gray-800{ 819 | --tw-bg-opacity: 1; 820 | background-color: rgb(31 41 55 / var(--tw-bg-opacity)); 821 | } 822 | 823 | .bg-white{ 824 | --tw-bg-opacity: 1; 825 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 826 | } 827 | 828 | .p-0{ 829 | padding: 0px; 830 | } 831 | 832 | .p-0\.5{ 833 | padding: 0.125rem; 834 | } 835 | 836 | .p-2{ 837 | padding: 0.5rem; 838 | } 839 | 840 | .p-3{ 841 | padding: 0.75rem; 842 | } 843 | 844 | .p-4{ 845 | padding: 1rem; 846 | } 847 | 848 | .p-5{ 849 | padding: 1.25rem; 850 | } 851 | 852 | .px-0{ 853 | padding-left: 0px; 854 | padding-right: 0px; 855 | } 856 | 857 | .px-0\.5{ 858 | padding-left: 0.125rem; 859 | padding-right: 0.125rem; 860 | } 861 | 862 | .px-2{ 863 | padding-left: 0.5rem; 864 | padding-right: 0.5rem; 865 | } 866 | 867 | .py-0{ 868 | padding-top: 0px; 869 | padding-bottom: 0px; 870 | } 871 | 872 | .py-0\.5{ 873 | padding-top: 0.125rem; 874 | padding-bottom: 0.125rem; 875 | } 876 | 877 | .py-2{ 878 | padding-top: 0.5rem; 879 | padding-bottom: 0.5rem; 880 | } 881 | 882 | .pr-2{ 883 | padding-right: 0.5rem; 884 | } 885 | 886 | .text-center{ 887 | text-align: center; 888 | } 889 | 890 | .text-2xl{ 891 | font-size: 1.5rem; 892 | line-height: 2rem; 893 | } 894 | 895 | .text-3xl{ 896 | font-size: 1.875rem; 897 | line-height: 2.25rem; 898 | } 899 | 900 | .text-6xl{ 901 | font-size: 3.75rem; 902 | line-height: 1; 903 | } 904 | 905 | .text-8xl{ 906 | font-size: 6rem; 907 | line-height: 1; 908 | } 909 | 910 | .text-xl{ 911 | font-size: 1.25rem; 912 | line-height: 1.75rem; 913 | } 914 | 915 | .font-bold{ 916 | font-weight: 700; 917 | } 918 | 919 | .font-medium{ 920 | font-weight: 500; 921 | } 922 | 923 | .text-custom-blue{ 924 | --tw-text-opacity: 1; 925 | color: rgb(0 159 253 / var(--tw-text-opacity)); 926 | } 927 | 928 | .text-green-500{ 929 | --tw-text-opacity: 1; 930 | color: rgb(34 197 94 / var(--tw-text-opacity)); 931 | } 932 | 933 | .text-red-500{ 934 | --tw-text-opacity: 1; 935 | color: rgb(239 68 68 / var(--tw-text-opacity)); 936 | } 937 | 938 | .text-white{ 939 | --tw-text-opacity: 1; 940 | color: rgb(255 255 255 / var(--tw-text-opacity)); 941 | } 942 | 943 | .shadow-lg{ 944 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 945 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); 946 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 947 | } 948 | 949 | .shadow-custom-blue{ 950 | --tw-shadow-color: #009FFD; 951 | --tw-shadow: var(--tw-shadow-colored); 952 | } 953 | 954 | .drop-shadow-lg{ 955 | --tw-drop-shadow: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1)); 956 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 957 | } 958 | 959 | .filter{ 960 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 961 | } 962 | 963 | .transition-all{ 964 | transition-property: all; 965 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 966 | transition-duration: 150ms; 967 | } 968 | 969 | .duration-300{ 970 | transition-duration: 300ms; 971 | } 972 | 973 | .ease-linear{ 974 | transition-timing-function: linear; 975 | } 976 | 977 | *{ 978 | font-family: Montserrat, sans-serif; 979 | font-weight: 300; 980 | } 981 | 982 | .side-bar-item{ 983 | position: relative; 984 | margin-left: auto; 985 | margin-right: auto; 986 | margin-top: 0.5rem; 987 | margin-bottom: 0.5rem; 988 | display: flex; 989 | height: 3rem; 990 | width: 3rem; 991 | cursor: pointer; 992 | align-items: center; 993 | justify-content: center; 994 | border-radius: 1.5rem; 995 | --tw-bg-opacity: 1; 996 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 997 | --tw-text-opacity: 1; 998 | color: rgb(0 159 253 / var(--tw-text-opacity)); 999 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 1000 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); 1001 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1002 | transition-property: all; 1003 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1004 | transition-duration: 300ms; 1005 | transition-timing-function: linear; 1006 | } 1007 | 1008 | .side-bar-item:hover{ 1009 | border-radius: 0.75rem; 1010 | --tw-bg-opacity: 1; 1011 | background-color: rgb(0 159 253 / var(--tw-bg-opacity)); 1012 | } 1013 | 1014 | :is(.dark .side-bar-item){ 1015 | --tw-bg-opacity: 1; 1016 | background-color: rgb(66 69 73 / var(--tw-bg-opacity)); 1017 | } 1018 | 1019 | .side-bar-item-disabled{ 1020 | position: relative; 1021 | margin-left: auto; 1022 | margin-right: auto; 1023 | margin-top: 0.5rem; 1024 | margin-bottom: 0.5rem; 1025 | display: flex; 1026 | height: 3rem; 1027 | width: 3rem; 1028 | cursor: not-allowed; 1029 | align-items: center; 1030 | justify-content: center; 1031 | border-radius: 1.5rem; 1032 | --tw-bg-opacity: 1; 1033 | background-color: rgb(66 69 73 / var(--tw-bg-opacity)); 1034 | --tw-text-opacity: 1; 1035 | color: rgb(54 57 62 / var(--tw-text-opacity)); 1036 | } 1037 | 1038 | .side-bar-main{ 1039 | position: relative; 1040 | margin-left: auto; 1041 | margin-right: auto; 1042 | margin-top: 0.5rem; 1043 | margin-bottom: 0.5rem; 1044 | display: flex; 1045 | height: 3rem; 1046 | width: 3rem; 1047 | cursor: pointer; 1048 | align-items: center; 1049 | justify-content: center; 1050 | --tw-shadow: 0 0 #0000; 1051 | --tw-shadow-colored: 0 0 #0000; 1052 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1053 | } 1054 | 1055 | .side-bar-main:hover{ 1056 | --tw-scale-x: .9; 1057 | --tw-scale-y: .9; 1058 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1059 | } 1060 | 1061 | .current-item{ 1062 | border-radius: 0.75rem; 1063 | } 1064 | 1065 | .tableTr{ 1066 | text-align: center; 1067 | } 1068 | 1069 | .tableTd{ 1070 | border-width: 2px; 1071 | --tw-border-opacity: 1; 1072 | border-color: rgb(54 57 62 / var(--tw-border-opacity)); 1073 | text-align: center; 1074 | font-weight: 500; 1075 | } 1076 | 1077 | .tableTh{ 1078 | position: sticky; 1079 | top: 0px; 1080 | border-width: 2px; 1081 | --tw-border-opacity: 1; 1082 | border-color: rgb(54 57 62 / var(--tw-border-opacity)); 1083 | font-weight: 700; 1084 | } 1085 | 1086 | .grid-cell{ 1087 | border-width: 1px; 1088 | --tw-border-opacity: 1; 1089 | border-color: rgb(54 57 62 / var(--tw-border-opacity)); 1090 | padding: 0.25rem; 1091 | text-align: center; 1092 | font-weight: 700; 1093 | } 1094 | 1095 | :is(.dark .grid-cell){ 1096 | border-width: 2px; 1097 | font-weight: 500; 1098 | } 1099 | 1100 | ::-webkit-scrollbar { 1101 | width: 10px; 1102 | } 1103 | 1104 | /* Track */ 1105 | 1106 | ::-webkit-scrollbar-track{ 1107 | border-radius: 0.75rem; 1108 | --tw-bg-opacity: 1; 1109 | background-color: rgb(66 69 73 / var(--tw-bg-opacity)); 1110 | } 1111 | 1112 | /* Handle */ 1113 | 1114 | ::-webkit-scrollbar-thumb{ 1115 | border-radius: 0.75rem; 1116 | --tw-bg-opacity: 1; 1117 | background-color: rgb(0 159 253 / var(--tw-bg-opacity)); 1118 | } 1119 | 1120 | /* Handle on hover */ 1121 | 1122 | ::-webkit-scrollbar-thumb:hover{ 1123 | background-color: #108DD7FF; 1124 | } 1125 | 1126 | .hover\:rotate-1:hover{ 1127 | --tw-rotate: 1deg; 1128 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1129 | } 1130 | 1131 | .hover\:scale-105:hover{ 1132 | --tw-scale-x: 1.05; 1133 | --tw-scale-y: 1.05; 1134 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1135 | } 1136 | 1137 | .hover\:underline:hover{ 1138 | text-decoration-line: underline; 1139 | } 1140 | 1141 | .group:hover .group-hover\:scale-110{ 1142 | --tw-scale-x: 1.1; 1143 | --tw-scale-y: 1.1; 1144 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1145 | } 1146 | 1147 | .group:hover .group-hover\:text-white{ 1148 | --tw-text-opacity: 1; 1149 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1150 | } 1151 | 1152 | :is(.dark .dark\:bg-custom-gray-0){ 1153 | --tw-bg-opacity: 1; 1154 | background-color: rgb(66 69 73 / var(--tw-bg-opacity)); 1155 | } 1156 | 1157 | :is(.dark .dark\:bg-custom-gray-1){ 1158 | --tw-bg-opacity: 1; 1159 | background-color: rgb(54 57 62 / var(--tw-bg-opacity)); 1160 | } 1161 | 1162 | :is(.dark .dark\:bg-custom-gray-2){ 1163 | --tw-bg-opacity: 1; 1164 | background-color: rgb(40 43 48 / var(--tw-bg-opacity)); 1165 | } 1166 | 1167 | :is(.dark .dark\:bg-custom-gray-3){ 1168 | --tw-bg-opacity: 1; 1169 | background-color: rgb(30 33 36 / var(--tw-bg-opacity)); 1170 | } 1171 | 1172 | :is(.dark .dark\:text-white){ 1173 | --tw-text-opacity: 1; 1174 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1175 | } 1176 | 1177 | @media (min-width: 1280px){ 1178 | .xl\:text-9xl{ 1179 | font-size: 8rem; 1180 | line-height: 1; 1181 | } 1182 | } 1183 | 1184 | @media (min-height: 970px){ 1185 | .hXL\:col-span-2{ 1186 | grid-column: span 2 / span 2; 1187 | } 1188 | 1189 | .hXL\:h-min{ 1190 | height: -moz-min-content; 1191 | height: min-content; 1192 | } 1193 | 1194 | .hXL\:scale-100{ 1195 | --tw-scale-x: 1; 1196 | --tw-scale-y: 1; 1197 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1198 | } 1199 | } --------------------------------------------------------------------------------