mirror of
https://github.com/neosubhamoy/pytubepp-helper.git
synced 2026-02-04 11:22:22 +05:30
(feat): added dynamic port config for msghost, new settings page and ui improvements
This commit is contained in:
592
package-lock.json
generated
592
package-lock.json
generated
@@ -8,8 +8,12 @@
|
|||||||
"name": "pytubepp-helper",
|
"name": "pytubepp-helper",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-toast": "^1.2.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.7",
|
||||||
"@tauri-apps/api": "^1",
|
"@tauri-apps/api": "^1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -17,8 +21,11 @@
|
|||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-router-dom": "^7.1.3",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1",
|
"@tauri-apps/cli": "^1",
|
||||||
@@ -742,6 +749,53 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||||
|
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||||
|
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -852,6 +906,61 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
@@ -867,6 +976,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-icons": {
|
"node_modules/@radix-ui/react-icons": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
|
||||||
@@ -876,6 +1027,150 @@
|
|||||||
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
|
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0",
|
||||||
|
"@radix-ui/rect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||||
@@ -894,6 +1189,205 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-ZzUsAaOx8NdXZZKcFNDhbSlbsCUy8qQWmzTdgrlrhhZAOx2ofLtKrBDW9fkqhFvXgmtv560Uj16pkLkqML7SHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.4",
|
||||||
|
"@radix-ui/react-portal": "1.1.3",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.4",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.3",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-slot": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.32.0",
|
"version": "4.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz",
|
||||||
@@ -1422,6 +1916,12 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
@@ -1461,7 +1961,7 @@
|
|||||||
"version": "18.3.5",
|
"version": "18.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
||||||
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
|
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
@@ -1767,6 +2267,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2646,6 +3155,22 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.54.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
|
||||||
|
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
@@ -2656,6 +3181,46 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-EezYymLY6Guk/zLQ2vRA8WvdUhWFEj5fcE3RfWihhxXBW7+cd1LsIiA3lmx+KCmneAGQuyBv820o44L2+TtkSA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0",
|
||||||
|
"turbo-stream": "2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-qQGTE+77hleBzv9SIUIkGRvuFBQGagW+TQKy53UTZAO/3+YFNBYvRsNIZ1GT17yHbc63FylMOdS+m3oUriF1GA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -2791,6 +3356,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -3058,6 +3629,12 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/turbo-stream": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
@@ -3300,6 +3877,15 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.24.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||||
|
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -10,8 +10,12 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-toast": "^1.2.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.7",
|
||||||
"@tauri-apps/api": "^1",
|
"@tauri-apps/api": "^1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -19,8 +23,11 @@
|
|||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-router-dom": "^7.1.3",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1",
|
"@tauri-apps/cli": "^1",
|
||||||
|
|||||||
30
src-tauri/Cargo.lock
generated
30
src-tauri/Cargo.lock
generated
@@ -667,6 +667,15 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "directories"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -677,6 +686,18 @@ dependencies = [
|
|||||||
"dirs-sys-next",
|
"dirs-sys-next",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys-next"
|
name = "dirs-sys-next"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -2104,6 +2125,12 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_info"
|
name = "os_info"
|
||||||
version = "3.8.2"
|
version = "3.8.2"
|
||||||
@@ -2475,6 +2502,7 @@ dependencies = [
|
|||||||
name = "pytubepp-helper"
|
name = "pytubepp-helper"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"directories",
|
||||||
"fix-path-env",
|
"fix-path-env",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2489,6 +2517,8 @@ dependencies = [
|
|||||||
name = "pytubepp-helper-msghost"
|
name = "pytubepp-helper-msghost"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"directories",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"websocket",
|
"websocket",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ tauri-build = { version = "1", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1", features = [ "os-all", "process-relaunch", "window-start-dragging", "window-close", "window-unmaximize", "process-exit", "window-show", "window-unminimize", "window-hide", "window-minimize", "window-maximize", "system-tray", "shell-all"] }
|
tauri = { version = "1", features = [ "os-all", "process-relaunch", "window-start-dragging", "window-close", "window-unmaximize", "process-exit", "window-show", "window-unminimize", "window-hide", "window-minimize", "window-maximize", "system-tray", "shell-all"] }
|
||||||
|
directories = "5.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1.39.2", features = ["full"] }
|
tokio = { version = "1.39.2", features = ["full"] }
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
websocket = "0.27.1"
|
websocket = "0.27.1"
|
||||||
|
directories = "5.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
37
src-tauri/msghost/src/config.rs
Normal file
37
src-tauri/msghost/src/config.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
port: 3030,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_dir() -> Option<PathBuf> {
|
||||||
|
ProjectDirs::from("com", "neosubhamoy", "pytubepp-helper")
|
||||||
|
.map(|proj_dirs| proj_dirs.config_dir().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_path() -> Option<PathBuf> {
|
||||||
|
get_config_dir().map(|dir| dir.join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> Config {
|
||||||
|
if let Some(config_path) = get_config_path() {
|
||||||
|
if let Ok(content) = fs::read_to_string(config_path) {
|
||||||
|
if let Ok(config) = serde_json::from_str(&content) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Config::default()
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
mod config;
|
||||||
|
use config::load_config;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use websocket::client::ClientBuilder;
|
use websocket::client::ClientBuilder;
|
||||||
use websocket::OwnedMessage;
|
use websocket::OwnedMessage;
|
||||||
@@ -5,6 +7,11 @@ use std::thread::sleep;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
fn get_websocket_url() -> String {
|
||||||
|
let config = load_config();
|
||||||
|
format!("ws://localhost:{}", config.port)
|
||||||
|
}
|
||||||
|
|
||||||
fn connect_with_retry(url: &str, max_attempts: u32) -> Result<websocket::sync::Client<std::net::TcpStream>, Box<dyn std::error::Error>> {
|
fn connect_with_retry(url: &str, max_attempts: u32) -> Result<websocket::sync::Client<std::net::TcpStream>, Box<dyn std::error::Error>> {
|
||||||
let mut attempts = 0;
|
let mut attempts = 0;
|
||||||
loop {
|
loop {
|
||||||
@@ -70,10 +77,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let parsed: Value = serde_json::from_str(&input)?;
|
let parsed: Value = serde_json::from_str(&input)?;
|
||||||
|
|
||||||
let websocket_url = "ws://localhost:3030";
|
let websocket_url = get_websocket_url();
|
||||||
eprintln!("Attempting to connect to {}", websocket_url);
|
eprintln!("Attempting to connect to {}", websocket_url);
|
||||||
|
|
||||||
let mut client = match connect_with_retry(websocket_url, 2) {
|
let mut client = match connect_with_retry(&websocket_url, 2) {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to connect after multiple attempts: {:?}", e);
|
eprintln!("Failed to connect after multiple attempts: {:?}", e);
|
||||||
|
|||||||
54
src-tauri/src/config.rs
Normal file
54
src-tauri/src/config.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
port: 3030,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_dir() -> Option<PathBuf> {
|
||||||
|
ProjectDirs::from("com", "neosubhamoy", "pytubepp-helper")
|
||||||
|
.map(|proj_dirs| proj_dirs.config_dir().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_path() -> Option<PathBuf> {
|
||||||
|
get_config_dir().map(|dir| dir.join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> Config {
|
||||||
|
if let Some(config_path) = get_config_path() {
|
||||||
|
if let Ok(content) = fs::read_to_string(config_path) {
|
||||||
|
if let Ok(config) = serde_json::from_str(&content) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Config::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(config: &Config) -> Result<(), String> {
|
||||||
|
let config_dir = get_config_dir()
|
||||||
|
.ok_or_else(|| "Could not determine config directory".to_string())?;
|
||||||
|
|
||||||
|
fs::create_dir_all(&config_dir)
|
||||||
|
.map_err(|e| format!("Failed to create config directory: {}", e))?;
|
||||||
|
|
||||||
|
let config_path = config_dir.join("config.json");
|
||||||
|
let content = serde_json::to_string_pretty(config)
|
||||||
|
.map_err(|e| format!("Failed to serialize config: {}", e))?;
|
||||||
|
|
||||||
|
fs::write(config_path, content)
|
||||||
|
.map_err(|e| format!("Failed to write config file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
use config::{Config, load_config, save_config, get_config_path};
|
||||||
use std::{process::Command, sync::Arc, env, time::Duration};
|
use std::{process::Command, sync::Arc, env, time::Duration};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu};
|
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu};
|
||||||
@@ -15,22 +17,134 @@ struct ResponseChannel {
|
|||||||
struct WebSocketState {
|
struct WebSocketState {
|
||||||
sender: Option<futures_util::stream::SplitSink<tokio_tungstenite::WebSocketStream<TcpStream>, tokio_tungstenite::tungstenite::Message>>,
|
sender: Option<futures_util::stream::SplitSink<tokio_tungstenite::WebSocketStream<TcpStream>, tokio_tungstenite::tungstenite::Message>>,
|
||||||
response_channel: ResponseChannel,
|
response_channel: ResponseChannel,
|
||||||
|
server_abort: Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
|
config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_another_instance_running() -> bool {
|
async fn is_another_instance_running(port: u16) -> bool {
|
||||||
match connect_async("ws://127.0.0.1:3030").await {
|
match connect_async(format!("ws://127.0.0.1:{}", port)).await {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(_) => false
|
Err(_) => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_bind_ws_port() -> Option<TcpListener> {
|
async fn try_bind_ws_port(port: u16) -> Option<TcpListener> {
|
||||||
match TcpListener::bind("127.0.0.1:3030").await {
|
match TcpListener::bind(format!("127.0.0.1:{}", port)).await {
|
||||||
Ok(listener) => Some(listener),
|
Ok(listener) => Some(listener),
|
||||||
Err(_) => None
|
Err(_) => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn start_websocket_server(app_handle: tauri::AppHandle, port: u16) -> Result<(), String> {
|
||||||
|
let addr = format!("127.0.0.1:{}", port);
|
||||||
|
|
||||||
|
// First ensure any existing server is stopped
|
||||||
|
{
|
||||||
|
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
|
||||||
|
let mut state = state.lock().await;
|
||||||
|
if let Some(old_abort) = state.server_abort.take() {
|
||||||
|
let _ = old_abort.send(());
|
||||||
|
// Give it a moment to shut down
|
||||||
|
sleep(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try to bind to the port
|
||||||
|
let listener = TcpListener::bind(&addr).await
|
||||||
|
.map_err(|e| format!("Failed to bind to port {}: {}", port, e))?;
|
||||||
|
|
||||||
|
let (abort_sender, mut abort_receiver) = tokio::sync::oneshot::channel();
|
||||||
|
|
||||||
|
// Store the new abort sender
|
||||||
|
{
|
||||||
|
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
|
||||||
|
let mut state = state.lock().await;
|
||||||
|
state.server_abort = Some(abort_sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the server task
|
||||||
|
tokio::spawn(async move {
|
||||||
|
println!("Starting WebSocket server on port {}", port);
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
accept_result = listener.accept() => {
|
||||||
|
match accept_result {
|
||||||
|
Ok((stream, _)) => {
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
tokio::spawn(handle_connection(stream, app_handle));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error accepting connection: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = &mut abort_receiver => {
|
||||||
|
println!("WebSocket server shutting down on port {}...", port);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a moment to ensure the server has started
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_config(state: tauri::State<'_, Arc<Mutex<WebSocketState>>>) -> Result<Config, String> {
|
||||||
|
let state = state.lock().await;
|
||||||
|
Ok(state.config.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_config_file_path() -> Result<String, String> {
|
||||||
|
match get_config_path() {
|
||||||
|
Some(path) => Ok(path.to_string_lossy().into_owned()),
|
||||||
|
None => Err("Could not determine config path".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn update_config(
|
||||||
|
new_config: Config,
|
||||||
|
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
) -> Result<Config, String> {
|
||||||
|
// Save the new config first
|
||||||
|
save_config(&new_config)?;
|
||||||
|
|
||||||
|
// Update the state with new config
|
||||||
|
{
|
||||||
|
let mut state = state.lock().await;
|
||||||
|
state.config = new_config.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the new server (this will also handle stopping the old one)
|
||||||
|
start_websocket_server(app_handle, new_config.port).await?;
|
||||||
|
|
||||||
|
Ok(new_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn reset_config(
|
||||||
|
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
) -> Result<Config, String> {
|
||||||
|
let config = Config::default();
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = state.lock().await;
|
||||||
|
state.config = config.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
start_websocket_server(app_handle, config.port).await?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn send_to_extension(
|
async fn send_to_extension(
|
||||||
message: String,
|
message: String,
|
||||||
@@ -184,39 +298,41 @@ fn download_stream(url: String, stream: String) {
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let _ = fix_path_env::fix();
|
let _ = fix_path_env::fix();
|
||||||
|
|
||||||
|
let config = load_config();
|
||||||
|
let port = config.port;
|
||||||
|
|
||||||
|
let websocket_state = Arc::new(Mutex::new(WebSocketState {
|
||||||
|
sender: None,
|
||||||
|
response_channel: ResponseChannel { sender: None },
|
||||||
|
server_abort: None,
|
||||||
|
config,
|
||||||
|
}));
|
||||||
|
|
||||||
// Check if another instance is running
|
// Check if another instance is running
|
||||||
if is_another_instance_running().await {
|
if is_another_instance_running(port).await {
|
||||||
println!("Another instance is already running. Exiting...");
|
println!("Another instance is already running. Exiting...");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to bind to the WebSocket port with a few retries
|
// Try to bind to the WebSocket port with a few retries
|
||||||
let mut listener = None;
|
let mut port_available = false;
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
if let Some(l) = try_bind_ws_port().await {
|
if let Some(_) = try_bind_ws_port(port).await {
|
||||||
listener = Some(l);
|
port_available = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
sleep(Duration::from_millis(100)).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't bind to the port after retries, assume another instance is running
|
// If we couldn't bind to the port after retries, assume another instance is running
|
||||||
let listener = match listener {
|
if !port_available {
|
||||||
Some(l) => l,
|
println!("Could not bind to WebSocket port. Another instance might be running. Exiting...");
|
||||||
None => {
|
std::process::exit(0);
|
||||||
println!("Could not bind to WebSocket port. Another instance might be running. Exiting...");
|
}
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
let start_hidden = args.contains(&"--hidden".to_string());
|
let start_hidden = args.contains(&"--hidden".to_string());
|
||||||
|
|
||||||
let websocket_state = Arc::new(Mutex::new(WebSocketState {
|
|
||||||
sender: None,
|
|
||||||
response_channel: ResponseChannel { sender: None },
|
|
||||||
}));
|
|
||||||
|
|
||||||
let tray_menu = SystemTrayMenu::new()
|
let tray_menu = SystemTrayMenu::new()
|
||||||
.add_item(CustomMenuItem::new("show".to_string(), "Show"))
|
.add_item(CustomMenuItem::new("show".to_string(), "Show"))
|
||||||
@@ -257,17 +373,14 @@ async fn main() {
|
|||||||
window.hide().unwrap();
|
window.hide().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the initial WebSocket server
|
||||||
let app_handle = app.handle();
|
let app_handle = app.handle();
|
||||||
let ws_state = websocket_state.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
println!("WebSocket server listening on ws://127.0.0.1:3030");
|
if let Err(e) = start_websocket_server(app_handle, port).await {
|
||||||
while let Ok((stream, _)) = listener.accept().await {
|
println!("Failed to start initial WebSocket server: {}", e);
|
||||||
let app_handle = app_handle.clone();
|
|
||||||
let ws_state = ws_state.clone();
|
|
||||||
tokio::spawn(handle_connection(stream, app_handle, ws_state));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
@@ -275,19 +388,24 @@ async fn main() {
|
|||||||
fetch_video_info,
|
fetch_video_info,
|
||||||
install_program,
|
install_program,
|
||||||
download_stream,
|
download_stream,
|
||||||
receive_frontend_response
|
receive_frontend_response,
|
||||||
|
get_config,
|
||||||
|
update_config,
|
||||||
|
reset_config,
|
||||||
|
get_config_file_path
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_state: Arc<Mutex<WebSocketState>>) {
|
async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle) {
|
||||||
let ws_stream = accept_async(stream).await.unwrap();
|
let ws_stream = accept_async(stream).await.unwrap();
|
||||||
let (ws_sender, mut ws_receiver) = ws_stream.split();
|
let (ws_sender, mut ws_receiver) = ws_stream.split();
|
||||||
|
|
||||||
// Store the sender in the shared state
|
// Store the sender in the shared state
|
||||||
{
|
{
|
||||||
let mut state = ws_state.lock().await;
|
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
|
||||||
|
let mut state = state.lock().await;
|
||||||
state.sender = Some(ws_sender);
|
state.sender = Some(ws_sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +421,8 @@ async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_s
|
|||||||
// Create a new channel for this request
|
// Create a new channel for this request
|
||||||
let (response_sender, response_receiver) = oneshot::channel();
|
let (response_sender, response_receiver) = oneshot::channel();
|
||||||
{
|
{
|
||||||
let mut state = ws_state.lock().await;
|
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
|
||||||
|
let mut state = state.lock().await;
|
||||||
state.response_channel.sender = Some(response_sender);
|
state.response_channel.sender = Some(response_sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +434,8 @@ async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_s
|
|||||||
.unwrap_or_else(|e| format!("Error receiving response: {:?}", e));
|
.unwrap_or_else(|e| format!("Error receiving response: {:?}", e));
|
||||||
|
|
||||||
// Send the response back through WebSocket
|
// Send the response back through WebSocket
|
||||||
let mut state = ws_state.lock().await;
|
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
|
||||||
|
let mut state = state.lock().await;
|
||||||
if let Some(sender) = &mut state.sender {
|
if let Some(sender) = &mut state.sender {
|
||||||
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(response)).await;
|
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(response)).await;
|
||||||
}
|
}
|
||||||
@@ -327,6 +447,7 @@ async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_s
|
|||||||
println!("WebSocket connection closed");
|
println!("WebSocket connection closed");
|
||||||
|
|
||||||
// Remove the sender from the shared state when the connection closes
|
// Remove the sender from the shared state when the connection closes
|
||||||
let mut state = ws_state.lock().await;
|
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
|
||||||
|
let mut state = state.lock().await;
|
||||||
state.sender = None;
|
state.sender = None;
|
||||||
}
|
}
|
||||||
417
src/App.tsx
417
src/App.tsx
@@ -1,18 +1,15 @@
|
|||||||
import clsx from "clsx";
|
import React from "react"
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import "./index.css";
|
|
||||||
import { invoke } from "@tauri-apps/api/tauri";
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { appWindow } from '@tauri-apps/api/window';
|
import { appWindow } from '@tauri-apps/api/window';
|
||||||
import { platform } from '@tauri-apps/api/os';
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { WebSocketMessage } from "@/types";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { sendStreamInfo } from "@/lib/utils";
|
||||||
import { InstalledPrograms, WebSocketMessage, } from "./types";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { compareVersions, extractVersion, isInstalled, sendStreamInfo, detectWindows, detectDistro, extractDistroId, detectMacOs, registerMacFiles, detectPackageManager, extractPkgMngrName } from "./lib/utils";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { CircleCheck, TriangleAlert, CircleAlert } from 'lucide-react';
|
|
||||||
|
|
||||||
function App() {
|
function App({ children }: { children: React.ReactNode }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCloseRequested = (event: any) => {
|
const handleCloseRequested = (event: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -22,59 +19,6 @@ function App() {
|
|||||||
appWindow.onCloseRequested(handleCloseRequested);
|
appWindow.onCloseRequested(handleCloseRequested);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [isWindows, setIsWindows] = useState<boolean>(false)
|
|
||||||
const [windowsVersion, setWindowsVersion] = useState<string | null>(null)
|
|
||||||
const [isMacOs, setIsMacOs] = useState<boolean>(false)
|
|
||||||
const [macOsVersion, setMacOsVersion] = useState<string | null>(null)
|
|
||||||
const [distroId, setDistroId] = useState<string | null>(null)
|
|
||||||
const [distroPkgMngr, setDistroPkgMngr] = useState<string | null>(null)
|
|
||||||
const [installedPrograms, setInstalledPrograms] = useState<InstalledPrograms>({
|
|
||||||
winget: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
apt: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
dnf: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
brew: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
python: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
pip: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
python3: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
pip3: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
ffmpeg: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
nodejs: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
pytubepp: {
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
|
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
|
||||||
if(event.payload.command === 'send-stream-info') {
|
if(event.payload.command === 'send-stream-info') {
|
||||||
@@ -103,349 +47,12 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function checkAllPrograms() {
|
|
||||||
isInstalled('winget', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
winget: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('apt', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
apt: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('dnf', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
dnf: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('homebrew', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
brew: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('python', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
python: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('pip', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
pip: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('python3', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
python3: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('pip3', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
pip3: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('ffmpeg', '-version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
ffmpeg: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('nodejs', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
nodejs: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
isInstalled('pytubepp', '--version').then((result) => {
|
|
||||||
setInstalledPrograms((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
pytubepp: {
|
|
||||||
installed: result.installed,
|
|
||||||
version: result.output ? extractVersion(result.output) : null,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkAllPrograms();
|
|
||||||
const runPlatformSpecificChecks = async () => {
|
|
||||||
const currentPlatform = await platform();
|
|
||||||
|
|
||||||
switch (currentPlatform) {
|
|
||||||
case 'win32':
|
|
||||||
const windowsResult = await detectWindows();
|
|
||||||
if (windowsResult) {
|
|
||||||
setIsWindows(true);
|
|
||||||
setWindowsVersion(extractVersion(windowsResult));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'darwin':
|
|
||||||
const macResult = await detectMacOs();
|
|
||||||
if (macResult) {
|
|
||||||
setIsMacOs(true);
|
|
||||||
setMacOsVersion(extractVersion(macResult));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'linux':
|
|
||||||
const distroResult = await detectDistro();
|
|
||||||
if (distroResult) {
|
|
||||||
setDistroId(extractDistroId(distroResult));
|
|
||||||
const distroPkgMngrResult = await detectPackageManager();
|
|
||||||
if (distroPkgMngrResult) {
|
|
||||||
setDistroPkgMngr(extractPkgMngrName(distroPkgMngrResult));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('Unsupported platform');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
runPlatformSpecificChecks().catch(console.error);
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<div className="container">
|
<TooltipProvider delayDuration={1000}>
|
||||||
<div className={clsx("topbar flex justify-between items-center mt-5", !isWindows && "mx-3")}>
|
{children}
|
||||||
<h1 className="text-xl font-bold">PytubePP Helper</h1>
|
<Toaster />
|
||||||
<div>
|
</TooltipProvider>
|
||||||
{ isMacOs && macOsVersion && compareVersions(macOsVersion, '10.13') > 0 ?
|
|
||||||
<Button size="sm" onClick={registerMacFiles}>Register</Button>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
<Button className="ml-3" size="sm" onClick={checkAllPrograms}>Refresh</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{ distroId && distroPkgMngr && distroPkgMngr === 'apt' ? /* Section for Debian */
|
|
||||||
<div className="programstats mt-5 mx-3">
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
|
|
||||||
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.apt.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install python3 -y'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
|
||||||
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.apt.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install ffmpeg -y'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
|
||||||
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.apt.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install nodejs -y'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
|
||||||
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
{(!installedPrograms.apt.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>APT Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
APT is required to install necessary debian packages. Please install it manually for your distro.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>PIP Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install python3-pip -y'})}}>install</Button>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5">
|
|
||||||
<CircleCheck className="h-5 w-5" />
|
|
||||||
<AlertTitle>Ready</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
: distroId && distroPkgMngr && distroPkgMngr === 'dnf' ? /* Section for RHEL */
|
|
||||||
<div className="programstats mt-5 mx-3">
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
|
|
||||||
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.dnf.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install python3 -y'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
|
||||||
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.dnf.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install ffmpeg -y'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
|
||||||
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.dnf.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install nodejs -y'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
|
||||||
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
{(!installedPrograms.dnf.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>DNF Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
DNF is required to install necessary rpm packages. Please install it manually for your distro.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>PIP Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install python3-pip -y'})}}>install</Button>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5">
|
|
||||||
<CircleCheck className="h-5 w-5" />
|
|
||||||
<AlertTitle>Ready</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
: isWindows && windowsVersion && parseInt(windowsVersion) >= 17134 ? /* Section for Windows */
|
|
||||||
<div className="programstats mt-5">
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Python:</b> {installedPrograms.python.installed ? 'installed' : 'not installed'} {installedPrograms.python.version ? `(${installedPrograms.python.version})` : ''}</p>
|
|
||||||
{installedPrograms.python.installed ? installedPrograms.python.version ? compareVersions(installedPrograms.python.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.winget.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'winget install Python.Python.3.12'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
|
||||||
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.winget.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'winget install ffmpeg'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
|
||||||
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.winget.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'winget install OpenJS.NodeJS.LTS'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
|
||||||
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip install pytubepp'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
{(!installedPrograms.winget.installed && (!installedPrograms.python.installed || !installedPrograms.ffmpeg.installed)) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>WinGet Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
WinGet is required to install necessary packages. Please install it manually from <a className="underline" href="https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget" target="_blank">here</a>.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(installedPrograms.python.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5">
|
|
||||||
<CircleCheck className="h-5 w-5" />
|
|
||||||
<AlertTitle>Ready</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
: isMacOs && macOsVersion && compareVersions(macOsVersion, '10.13') > 0 ? /* Section for macOS */
|
|
||||||
<div className="programstats mt-5 mx-3">
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
|
|
||||||
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.brew.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install python'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
|
||||||
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.brew.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install ffmpeg'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
|
||||||
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.brew.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install node'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
<div className="programitem flex items-center justify-between">
|
|
||||||
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
|
||||||
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
|
|
||||||
</div>
|
|
||||||
{(!installedPrograms.brew.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>Homebrew Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Homebrew is required to install necessary unix packages. Please install it manually for your mac.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>PIP Not Found</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install python3-pip -y'})}}>install</Button>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
|
||||||
<Alert className="mt-5">
|
|
||||||
<CircleCheck className="h-5 w-5" />
|
|
||||||
<AlertTitle>Ready</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
<div className="programstats mt-5 mx-3">
|
|
||||||
<Alert className="mt-5" variant="destructive">
|
|
||||||
<CircleAlert className="h-5 w-5" />
|
|
||||||
<AlertTitle>Unsupported OS</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Sorry, your os/distro is currently not supported. If you think this is just a mistake or you want to request us to add support for your os/distro you can create a github issue <a className="underline" href="https://github.com/neosubhamoy/pytubepp-helper/issues" target="_blank">here</a>.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
176
src/components/ui/form.tsx
Normal file
176
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
ControllerProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState, formState } = useFormContext()
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormItem.displayName = "FormItem"
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(error && "text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormLabel.displayName = "FormLabel"
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormControl.displayName = "FormControl"
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormDescription.displayName = "FormDescription"
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message) : children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
||||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
||||||
126
src/components/ui/toast.tsx
Normal file
126
src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Cross2Icon className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
33
src/components/ui/toaster.tsx
Normal file
33
src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
194
src/hooks/use-toast.ts
Normal file
194
src/hooks/use-toast.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
80
src/lib/platform-utils.ts
Normal file
80
src/lib/platform-utils.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { platform } from "@tauri-apps/api/os";
|
||||||
|
import { detectDistro, detectMacOs, detectPackageManager, detectWindows, extractDistroId, extractPkgMngrName, extractVersion } from "@/lib/utils";
|
||||||
|
import { PlatformInfo } from "@/types";
|
||||||
|
|
||||||
|
export async function getPlatformInfo(): Promise<PlatformInfo> {
|
||||||
|
const defaultInfo: PlatformInfo = {
|
||||||
|
isWindows: false,
|
||||||
|
windowsVersion: null,
|
||||||
|
isMacOs: false,
|
||||||
|
macOsVersion: null,
|
||||||
|
distroId: null,
|
||||||
|
distroPkgMngr: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentPlatform = await platform();
|
||||||
|
|
||||||
|
switch (currentPlatform) {
|
||||||
|
case 'win32': {
|
||||||
|
const windowsResult = await detectWindows();
|
||||||
|
if (windowsResult) {
|
||||||
|
return {
|
||||||
|
...defaultInfo,
|
||||||
|
isWindows: true,
|
||||||
|
windowsVersion: extractVersion(windowsResult),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'darwin': {
|
||||||
|
const macResult = await detectMacOs();
|
||||||
|
if (macResult) {
|
||||||
|
return {
|
||||||
|
...defaultInfo,
|
||||||
|
isMacOs: true,
|
||||||
|
macOsVersion: extractVersion(macResult),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'linux': {
|
||||||
|
const distroResult = await detectDistro();
|
||||||
|
if (distroResult) {
|
||||||
|
const distroPkgMngrResult = await detectPackageManager();
|
||||||
|
return {
|
||||||
|
...defaultInfo,
|
||||||
|
distroId: extractDistroId(distroResult),
|
||||||
|
distroPkgMngr: distroPkgMngrResult ? extractPkgMngrName(distroPkgMngrResult) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('Unsupported platform');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error detecting platform:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual getters for specific platforms
|
||||||
|
export async function isWindowsPlatform(): Promise<{ isWindows: boolean; version: string | null }> {
|
||||||
|
const info = await getPlatformInfo();
|
||||||
|
return { isWindows: info.isWindows, version: info.windowsVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isMacOsPlatform(): Promise<{ isMacOs: boolean; version: string | null }> {
|
||||||
|
const info = await getPlatformInfo();
|
||||||
|
return { isMacOs: info.isMacOs, version: info.macOsVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLinuxInfo(): Promise<{ distroId: string | null; packageManager: string | null }> {
|
||||||
|
const info = await getPlatformInfo();
|
||||||
|
return { distroId: info.distroId, packageManager: info.distroPkgMngr };
|
||||||
|
}
|
||||||
@@ -180,7 +180,9 @@ export async function registerMacFiles() {
|
|||||||
console.log(`File ${file.source} copied successfully to ${destinationPath}`);
|
console.log(`File ${file.source} copied successfully to ${destinationPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { success: true, message: 'Registered successfully' }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error copying files:', error);
|
console.error('Error copying files:', error);
|
||||||
|
return { success: false, message: 'Failed to register' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
src/main.tsx
17
src/main.tsx
@@ -1,9 +1,20 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import "@/index.css";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import App from "@/App";
|
||||||
|
import HomePage from "@/pages/home";
|
||||||
|
import SettingsPage from "@/pages/settings";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</App>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
441
src/pages/home.tsx
Normal file
441
src/pages/home.tsx
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
|
import { InstalledPrograms } from "@/types";
|
||||||
|
import { compareVersions, extractVersion, isInstalled, registerMacFiles } from "@/lib/utils";
|
||||||
|
import { CircleCheck, TriangleAlert, CircleAlert, Settings, RefreshCcw, Loader2, PackagePlus } from "lucide-react";
|
||||||
|
import { getPlatformInfo } from "@/lib/platform-utils";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isWindows, setIsWindows] = useState<boolean>(false)
|
||||||
|
const [windowsVersion, setWindowsVersion] = useState<string | null>(null)
|
||||||
|
const [isMacOs, setIsMacOs] = useState<boolean>(false)
|
||||||
|
const [macOsVersion, setMacOsVersion] = useState<string | null>(null)
|
||||||
|
const [distroId, setDistroId] = useState<string | null>(null)
|
||||||
|
const [distroPkgMngr, setDistroPkgMngr] = useState<string | null>(null)
|
||||||
|
const [installedPrograms, setInstalledPrograms] = useState<InstalledPrograms>({
|
||||||
|
winget: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
apt: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
dnf: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
brew: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
python: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
pip: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
python3: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
pip3: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
ffmpeg: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
nodejs: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
pytubepp: {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkAllPrograms() {
|
||||||
|
return Promise.all([
|
||||||
|
isInstalled('winget', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
winget: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('apt', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
apt: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('dnf', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
dnf: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('homebrew', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
brew: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('python', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
python: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('pip', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
pip: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('python3', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
python3: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('pip3', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
pip3: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('ffmpeg', '-version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
ffmpeg: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('nodejs', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
nodejs: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
isInstalled('pytubepp', '--version').then((result) => {
|
||||||
|
setInstalledPrograms((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
pytubepp: {
|
||||||
|
installed: result.installed,
|
||||||
|
version: result.output ? extractVersion(result.output) : null,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPlatformInfo = async () => {
|
||||||
|
const info = await getPlatformInfo();
|
||||||
|
setIsWindows(info.isWindows);
|
||||||
|
setWindowsVersion(info.windowsVersion);
|
||||||
|
setIsMacOs(info.isMacOs);
|
||||||
|
setMacOsVersion(info.macOsVersion);
|
||||||
|
setDistroId(info.distroId);
|
||||||
|
setDistroPkgMngr(info.distroPkgMngr);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await Promise.all([
|
||||||
|
checkAllPrograms(),
|
||||||
|
fetchPlatformInfo()
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className={clsx("topbar flex justify-between items-center mt-5", !isWindows && "mx-3")}>
|
||||||
|
<h1 className="text-xl font-bold">PytubePP Helper</h1>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="outline" size="icon" asChild>
|
||||||
|
<Link to="/settings">
|
||||||
|
<Settings className="w-5 h-5"/>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent><p>settings</p></TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{ isMacOs && macOsVersion && compareVersions(macOsVersion, '10.13') > 0 ?
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button className="ml-3" size="icon" onClick={async () => {
|
||||||
|
const result = await registerMacFiles();
|
||||||
|
toast({ title: result.message, variant: result.success ? 'default' : 'destructive' });
|
||||||
|
}}>
|
||||||
|
<PackagePlus className="w-5 h-5"/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent><p>register to mac</p></TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button className="ml-3" size="icon" disabled={isLoading} onClick={checkAllPrograms}>
|
||||||
|
<RefreshCcw className="w-5 h-5"/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent><p>refresh</p></TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ isLoading ?
|
||||||
|
<div className="mt-5 mx-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center justify-center py-[4.3rem]">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin"/>
|
||||||
|
<p className="ml-3 mt-2 text-muted-foreground">checking...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: distroId && distroPkgMngr && distroPkgMngr === 'apt' ? /* Section for Debian */
|
||||||
|
<div className="programstats mt-5 mx-3">
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
|
||||||
|
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.apt.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install python3 -y'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
||||||
|
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.apt.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install ffmpeg -y'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
||||||
|
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.apt.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install nodejs -y'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
||||||
|
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
{(!installedPrograms.apt.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>APT Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
APT is required to install necessary debian packages. Please install it manually for your distro.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>PIP Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo apt install python3-pip -y'})}}>install</Button>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5">
|
||||||
|
<CircleCheck className="h-5 w-5" />
|
||||||
|
<AlertTitle>Ready</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
: distroId && distroPkgMngr && distroPkgMngr === 'dnf' ? /* Section for RHEL */
|
||||||
|
<div className="programstats mt-5 mx-3">
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
|
||||||
|
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.dnf.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install python3 -y'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
||||||
|
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.dnf.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install ffmpeg -y'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
||||||
|
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.dnf.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install nodejs -y'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
||||||
|
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
{(!installedPrograms.dnf.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>DNF Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
DNF is required to install necessary rpm packages. Please install it manually for your distro.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>PIP Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo dnf install python3-pip -y'})}}>install</Button>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5">
|
||||||
|
<CircleCheck className="h-5 w-5" />
|
||||||
|
<AlertTitle>Ready</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
: isWindows && windowsVersion && parseInt(windowsVersion) >= 17134 ? /* Section for Windows */
|
||||||
|
<div className="programstats mt-5">
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Python:</b> {installedPrograms.python.installed ? 'installed' : 'not installed'} {installedPrograms.python.version ? `(${installedPrograms.python.version})` : ''}</p>
|
||||||
|
{installedPrograms.python.installed ? installedPrograms.python.version ? compareVersions(installedPrograms.python.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.winget.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'winget install Python.Python.3.12'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
||||||
|
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.winget.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'winget install ffmpeg'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
||||||
|
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.winget.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'winget install OpenJS.NodeJS.LTS'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
||||||
|
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip install pytubepp'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
{(!installedPrograms.winget.installed && (!installedPrograms.python.installed || !installedPrograms.ffmpeg.installed)) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>WinGet Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
WinGet is required to install necessary packages. Please install it manually from <a className="underline" href="https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget" target="_blank">here</a>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(installedPrograms.python.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5">
|
||||||
|
<CircleCheck className="h-5 w-5" />
|
||||||
|
<AlertTitle>Ready</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
: isMacOs && macOsVersion && compareVersions(macOsVersion, '10.13') > 0 ? /* Section for macOS */
|
||||||
|
<div className="programstats mt-5 mx-3">
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
|
||||||
|
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.brew.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install python'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
|
||||||
|
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.brew.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install ffmpeg'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
|
||||||
|
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.brew.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install node'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
<div className="programitem flex items-center justify-between">
|
||||||
|
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
|
||||||
|
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
|
||||||
|
</div>
|
||||||
|
{(!installedPrograms.brew.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>Homebrew Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Homebrew is required to install necessary unix packages. Please install it manually for your mac.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>PIP Not Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'brew install python3-pip -y'})}}>install</Button>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.nodejs.installed && installedPrograms.pytubepp.installed) ?
|
||||||
|
<Alert className="mt-5">
|
||||||
|
<CircleCheck className="h-5 w-5" />
|
||||||
|
<AlertTitle>Ready</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Everything looks ok! You can close this window now. Make sure it's always running in the background.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div className="programstats mt-5 mx-3">
|
||||||
|
<Alert className="mt-5" variant="destructive">
|
||||||
|
<CircleAlert className="h-5 w-5" />
|
||||||
|
<AlertTitle>Unsupported OS</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Sorry, your os/distro is currently not supported. If you think this is just a mistake or you want to request us to add support for your os/distro you can create a github issue <a className="underline" href="https://github.com/neosubhamoy/pytubepp-helper/issues" target="_blank">here</a>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
src/pages/settings.tsx
Normal file
183
src/pages/settings.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft, History, Save } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Config, PlatformInfo } from "@/types";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { getPlatformInfo } from "@/lib/platform-utils";
|
||||||
|
|
||||||
|
const DEFAULT_PORT = 3030;
|
||||||
|
const settingsFormSchema = z.object({
|
||||||
|
port: z.number().min(3000, { message: "Port must be greater than 3000" }).max(3999, { message: "Port must be less than 3999" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [platformInfo, setPlatformInfo] = useState<PlatformInfo | null>(null);
|
||||||
|
const [appConfig, setAppConfig] = useState<Config | null>(null);
|
||||||
|
const [isFormDirty, setIsFormDirty] = useState(false);
|
||||||
|
const saveButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const settingsForm = useForm<z.infer<typeof settingsFormSchema>>({
|
||||||
|
resolver: zodResolver(settingsFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
port: DEFAULT_PORT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = settingsForm.watch((value) => {
|
||||||
|
if (appConfig) {
|
||||||
|
setIsFormDirty(value.port !== appConfig.port);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [settingsForm, appConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getConfig = async () => {
|
||||||
|
const config: Config = await invoke("get_config");
|
||||||
|
if (config) {
|
||||||
|
setAppConfig(config);
|
||||||
|
settingsForm.reset({ port: config.port });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getConfig().catch(console.error);
|
||||||
|
}, [settingsForm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPlatformInfo().then(setPlatformInfo).catch(console.error);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateConfig = async () => {
|
||||||
|
try {
|
||||||
|
const updatedConfig: Config = await invoke("update_config", {
|
||||||
|
newConfig: {
|
||||||
|
port: Number(settingsForm.getValues().port)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setAppConfig(updatedConfig);
|
||||||
|
setIsFormDirty(false);
|
||||||
|
toast({
|
||||||
|
title: "Settings updated"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update config:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to update settings",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetConfig = async () => {
|
||||||
|
try {
|
||||||
|
const config: Config = await invoke("reset_config");
|
||||||
|
setAppConfig(config);
|
||||||
|
settingsForm.reset({ port: config.port });
|
||||||
|
setIsFormDirty(false);
|
||||||
|
toast({
|
||||||
|
title: "Using default settings"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reset config:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to reset settings",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUsingDefaultConfig = appConfig?.port === DEFAULT_PORT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className={clsx("topbar flex justify-between items-center mt-5", !platformInfo?.isWindows && "mx-3")}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link to="/">
|
||||||
|
<ArrowLeft className="w-5 h-5 mr-3"/>
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-xl font-bold">Settings</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
className="ml-3"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => resetConfig()}
|
||||||
|
disabled={isUsingDefaultConfig}
|
||||||
|
>
|
||||||
|
<History className="w-5 h-5"/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{isUsingDefaultConfig ? "using default settings" : "reset to default"}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
className="ml-3"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => saveButtonRef.current?.click()}
|
||||||
|
disabled={!isFormDirty}
|
||||||
|
>
|
||||||
|
<Save className="w-5 h-5"/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{isFormDirty ? "save changes" : "no changes to save"}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={clsx("mt-5", !platformInfo?.isWindows && "mx-3")}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Form {...settingsForm}>
|
||||||
|
<form onSubmit={settingsForm.handleSubmit(updateConfig)}>
|
||||||
|
<FormField
|
||||||
|
control={settingsForm.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
field.onChange(value ? Number(value) : DEFAULT_PORT);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The port to use for the websocket server
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button className="hidden" ref={saveButtonRef} type="submit">Save</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/types.ts
13
src/types.ts
@@ -1,3 +1,16 @@
|
|||||||
|
export interface Config {
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlatformInfo {
|
||||||
|
isWindows: boolean;
|
||||||
|
windowsVersion: string | null;
|
||||||
|
isMacOs: boolean;
|
||||||
|
macOsVersion: string | null;
|
||||||
|
distroId: string | null;
|
||||||
|
distroPkgMngr: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InstalledPrograms {
|
export interface InstalledPrograms {
|
||||||
winget: {
|
winget: {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user