├── 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 |
--------------------------------------------------------------------------------
/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 | [](https://github.com/quentinformatique/Cuby/stargazers)
6 | [](https://github.com/quentinformatique/Cuby/network)
7 | [](https://github.com/quentinformatique/Cuby/issues)
8 | [](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 | 
36 | 
37 | 
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 |
50 |
Other informations
51 |
52 |
53 |
Github repository :
54 |
55 |
56 |
57 |
58 |
59 |
60 | created by :
61 |
Quentinformatique, all rights reserved
62 |
63 |
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 |
30 |
40 |
50 |
60 |
68 |
76 |
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 |
34 |
Rotate the cube
35 |
36 |
37 |
38 |
39 |
40 |
41 | Regenerate
42 |
43 |
44 |
45 | scramble
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | cube :
54 |
57 | 3x3
58 | 2x2
59 | 4x4
60 | 5x5
61 | 6x6
62 | 7x7
63 | square-one
64 | pyraminx
65 | skewb
66 | megaminx
67 |
68 |
69 |
70 |
75 |
76 |
77 |
Personal best :
78 |
Average :
79 |
solves :
80 |
81 |
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 | cube :
35 |
38 |
39 | 3x3
40 | 2x2
41 | 4x4
42 | 5x5
43 | 6x6
44 | 7x7
45 | square-one
46 | pyraminx
47 | skewb
48 | megaminx
49 |
50 |
51 |
52 |
53 |
54 | Table
55 |
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 |
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 number Time Gap to average Edit ";
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 | }
--------------------------------------------------------------------------------