diff --git a/package-lock.json b/package-lock.json index 6d50896..1e9f401 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,12 @@ "name": "pytubepp-helper", "version": "0.6.0", "dependencies": { + "@hookform/resolvers": "^3.10.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", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -17,8 +21,11 @@ "lucide-react": "^0.436.0", "react": "^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", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.1" }, "devDependencies": { "@tauri-apps/cli": "^1", @@ -742,6 +749,53 @@ "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": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -852,6 +906,61 @@ "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": { "version": "1.1.1", "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": { "version": "1.3.2", "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" } }, + "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": { "version": "1.1.1", "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": { "version": "4.32.0", "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" } }, + "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1461,7 +1961,7 @@ "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -1767,6 +2267,15 @@ "dev": true, "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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2646,6 +3155,22 @@ "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": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -2656,6 +3181,46 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2791,6 +3356,12 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3058,6 +3629,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "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": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -3300,6 +3877,15 @@ "engines": { "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" + } } } } diff --git a/package.json b/package.json index 52e5f9b..b932da9 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,12 @@ "tauri": "tauri" }, "dependencies": { + "@hookform/resolvers": "^3.10.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", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -19,8 +23,11 @@ "lucide-react": "^0.436.0", "react": "^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", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.1" }, "devDependencies": { "@tauri-apps/cli": "^1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 01df240..69190e1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -667,6 +667,15 @@ dependencies = [ "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]] name = "dirs-next" version = "2.0.0" @@ -677,6 +686,18 @@ dependencies = [ "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]] name = "dirs-sys-next" version = "0.1.2" @@ -2104,6 +2125,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_info" version = "3.8.2" @@ -2475,6 +2502,7 @@ dependencies = [ name = "pytubepp-helper" version = "0.6.0" dependencies = [ + "directories", "fix-path-env", "futures-util", "serde", @@ -2489,6 +2517,8 @@ dependencies = [ name = "pytubepp-helper-msghost" version = "0.1.0" dependencies = [ + "directories", + "serde", "serde_json", "websocket", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7842d7d..0d18bb0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -12,6 +12,7 @@ tauri-build = { version = "1", features = [] } [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"] } +directories = "5.0" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1.39.2", features = ["full"] } diff --git a/src-tauri/msghost/Cargo.toml b/src-tauri/msghost/Cargo.toml index a1fc691..913ff68 100644 --- a/src-tauri/msghost/Cargo.toml +++ b/src-tauri/msghost/Cargo.toml @@ -9,4 +9,6 @@ edition = "2021" [dependencies] websocket = "0.27.1" +directories = "5.0" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" \ No newline at end of file diff --git a/src-tauri/msghost/src/config.rs b/src-tauri/msghost/src/config.rs new file mode 100644 index 0000000..33f31a5 --- /dev/null +++ b/src-tauri/msghost/src/config.rs @@ -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 { + ProjectDirs::from("com", "neosubhamoy", "pytubepp-helper") + .map(|proj_dirs| proj_dirs.config_dir().to_path_buf()) +} + +pub fn get_config_path() -> Option { + 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() +} \ No newline at end of file diff --git a/src-tauri/msghost/src/main.rs b/src-tauri/msghost/src/main.rs index c21a7b7..c8fbdcc 100644 --- a/src-tauri/msghost/src/main.rs +++ b/src-tauri/msghost/src/main.rs @@ -1,3 +1,5 @@ +mod config; +use config::load_config; use std::io::{self, Read, Write}; use websocket::client::ClientBuilder; use websocket::OwnedMessage; @@ -5,6 +7,11 @@ use std::thread::sleep; use std::time::Duration; 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, Box> { let mut attempts = 0; loop { @@ -70,10 +77,10 @@ fn main() -> Result<(), Box> { 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); - let mut client = match connect_with_retry(websocket_url, 2) { + let mut client = match connect_with_retry(&websocket_url, 2) { Ok(client) => client, Err(e) => { eprintln!("Failed to connect after multiple attempts: {:?}", e); diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs new file mode 100644 index 0000000..72dff5a --- /dev/null +++ b/src-tauri/src/config.rs @@ -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 { + ProjectDirs::from("com", "neosubhamoy", "pytubepp-helper") + .map(|proj_dirs| proj_dirs.config_dir().to_path_buf()) +} + +pub fn get_config_path() -> Option { + 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(()) +} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ebc3514..8325298 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![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 serde_json::Value; use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu}; @@ -15,22 +17,134 @@ struct ResponseChannel { struct WebSocketState { sender: Option, tokio_tungstenite::tungstenite::Message>>, response_channel: ResponseChannel, + server_abort: Option>, + config: Config, } -async fn is_another_instance_running() -> bool { - match connect_async("ws://127.0.0.1:3030").await { +async fn is_another_instance_running(port: u16) -> bool { + match connect_async(format!("ws://127.0.0.1:{}", port)).await { Ok(_) => true, Err(_) => false } } -async fn try_bind_ws_port() -> Option { - match TcpListener::bind("127.0.0.1:3030").await { +async fn try_bind_ws_port(port: u16) -> Option { + match TcpListener::bind(format!("127.0.0.1:{}", port)).await { Ok(listener) => Some(listener), 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::>>(); + 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::>>(); + 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>>) -> Result { + let state = state.lock().await; + Ok(state.config.clone()) +} + +#[tauri::command] +fn get_config_file_path() -> Result { + 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>>, + app_handle: tauri::AppHandle, +) -> Result { + // 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>>, + app_handle: tauri::AppHandle, +) -> Result { + 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] async fn send_to_extension( message: String, @@ -184,39 +298,41 @@ fn download_stream(url: String, stream: String) { #[tokio::main] async fn main() { 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 - if is_another_instance_running().await { + if is_another_instance_running(port).await { println!("Another instance is already running. Exiting..."); std::process::exit(0); } // Try to bind to the WebSocket port with a few retries - let mut listener = None; + let mut port_available = false; for _ in 0..3 { - if let Some(l) = try_bind_ws_port().await { - listener = Some(l); + if let Some(_) = try_bind_ws_port(port).await { + port_available = true; break; } sleep(Duration::from_millis(100)).await; } // If we couldn't bind to the port after retries, assume another instance is running - let listener = match listener { - Some(l) => l, - None => { - println!("Could not bind to WebSocket port. Another instance might be running. Exiting..."); - std::process::exit(0); - } - }; + if !port_available { + println!("Could not bind to WebSocket port. Another instance might be running. Exiting..."); + std::process::exit(0); + } let args: Vec = env::args().collect(); 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() .add_item(CustomMenuItem::new("show".to_string(), "Show")) @@ -257,17 +373,14 @@ async fn main() { window.hide().unwrap(); } + // Start the initial WebSocket server let app_handle = app.handle(); - let ws_state = websocket_state.clone(); - tokio::spawn(async move { - println!("WebSocket server listening on ws://127.0.0.1:3030"); - while let Ok((stream, _)) = listener.accept().await { - let app_handle = app_handle.clone(); - let ws_state = ws_state.clone(); - tokio::spawn(handle_connection(stream, app_handle, ws_state)); + if let Err(e) = start_websocket_server(app_handle, port).await { + println!("Failed to start initial WebSocket server: {}", e); } }); + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -275,19 +388,24 @@ async fn main() { fetch_video_info, install_program, download_stream, - receive_frontend_response + receive_frontend_response, + get_config, + update_config, + reset_config, + get_config_file_path ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } -async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_state: Arc>) { +async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle) { let ws_stream = accept_async(stream).await.unwrap(); let (ws_sender, mut ws_receiver) = ws_stream.split(); // Store the sender in the shared state { - let mut state = ws_state.lock().await; + let state = app_handle.state::>>(); + let mut state = state.lock().await; 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 let (response_sender, response_receiver) = oneshot::channel(); { - let mut state = ws_state.lock().await; + let state = app_handle.state::>>(); + let mut state = state.lock().await; 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)); // Send the response back through WebSocket - let mut state = ws_state.lock().await; + let state = app_handle.state::>>(); + let mut state = state.lock().await; if let Some(sender) = &mut state.sender { 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"); // Remove the sender from the shared state when the connection closes - let mut state = ws_state.lock().await; + let state = app_handle.state::>>(); + let mut state = state.lock().await; state.sender = None; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1ba10c7..2e05947 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,15 @@ -import clsx from "clsx"; -import { useState, useEffect } from "react"; -import "./index.css"; +import React from "react" +import { useEffect } from "react"; 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 { platform } from '@tauri-apps/api/os'; import { ThemeProvider } from "@/components/theme-provider"; -import { Button } from "@/components/ui/button"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { InstalledPrograms, WebSocketMessage, } from "./types"; -import { compareVersions, extractVersion, isInstalled, sendStreamInfo, detectWindows, detectDistro, extractDistroId, detectMacOs, registerMacFiles, detectPackageManager, extractPkgMngrName } from "./lib/utils"; -import { CircleCheck, TriangleAlert, CircleAlert } from 'lucide-react'; +import { WebSocketMessage } from "@/types"; +import { sendStreamInfo } from "@/lib/utils"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; -function App() { +function App({ children }: { children: React.ReactNode }) { useEffect(() => { const handleCloseRequested = (event: any) => { event.preventDefault(); @@ -22,59 +19,6 @@ function App() { appWindow.onCloseRequested(handleCloseRequested); }, []); - const [isWindows, setIsWindows] = useState(false) - const [windowsVersion, setWindowsVersion] = useState(null) - const [isMacOs, setIsMacOs] = useState(false) - const [macOsVersion, setMacOsVersion] = useState(null) - const [distroId, setDistroId] = useState(null) - const [distroPkgMngr, setDistroPkgMngr] = useState(null) - const [installedPrograms, setInstalledPrograms] = useState({ - 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(() => { const unlisten = listen('websocket-message', (event) => { 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 ( -
-
-

PytubePP Helper

-
- { isMacOs && macOsVersion && compareVersions(macOsVersion, '10.13') > 0 ? - - : - null - } - -
-
- { distroId && distroPkgMngr && distroPkgMngr === 'apt' ? /* Section for Debian */ -
-
-

Python: {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}

- {installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? : : installedPrograms.apt.installed ? : : null} -
-
-

FFmpeg: {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}

- {installedPrograms.ffmpeg.installed ? : installedPrograms.apt.installed ? : null} -
-
-

Node.js: {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}

- {installedPrograms.nodejs.installed ? : installedPrograms.apt.installed ? : null} -
-
-

PytubePP: {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}

- {installedPrograms.pytubepp.installed ? : installedPrograms.pip3.installed ? : null} -
- {(!installedPrograms.apt.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ? - - - APT Not Found - - APT is required to install necessary debian packages. Please install it manually for your distro. - - - : null} - {(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ? - - - PIP Not Found - - PIP is required to install necessary python packages. Please install it now to continue: - - - : null} - {(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ? - - - Ready - - Everything looks ok! You can close this window now. Make sure it's always running in the background. - - - : null} -
- : distroId && distroPkgMngr && distroPkgMngr === 'dnf' ? /* Section for RHEL */ -
-
-

Python: {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}

- {installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? : : installedPrograms.dnf.installed ? : : null} -
-
-

FFmpeg: {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}

- {installedPrograms.ffmpeg.installed ? : installedPrograms.dnf.installed ? : null} -
-
-

Node.js: {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}

- {installedPrograms.nodejs.installed ? : installedPrograms.dnf.installed ? : null} -
-
-

PytubePP: {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}

- {installedPrograms.pytubepp.installed ? : installedPrograms.pip3.installed ? : null} -
- {(!installedPrograms.dnf.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ? - - - DNF Not Found - - DNF is required to install necessary rpm packages. Please install it manually for your distro. - - - : null} - {(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ? - - - PIP Not Found - - PIP is required to install necessary python packages. Please install it now to continue: - - - : null} - {(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ? - - - Ready - - Everything looks ok! You can close this window now. Make sure it's always running in the background. - - - : null} -
- : isWindows && windowsVersion && parseInt(windowsVersion) >= 17134 ? /* Section for Windows */ -
-
-

Python: {installedPrograms.python.installed ? 'installed' : 'not installed'} {installedPrograms.python.version ? `(${installedPrograms.python.version})` : ''}

- {installedPrograms.python.installed ? installedPrograms.python.version ? compareVersions(installedPrograms.python.version, '3.8') < 0 ? : : installedPrograms.winget.installed ? : : null} -
-
-

FFmpeg: {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}

- {installedPrograms.ffmpeg.installed ? : installedPrograms.winget.installed ? : null} -
-
-

Node.js: {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}

- {installedPrograms.nodejs.installed ? : installedPrograms.winget.installed ? : null} -
-
-

PytubePP: {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}

- {installedPrograms.pytubepp.installed ? : installedPrograms.pip.installed ? : null} -
- {(!installedPrograms.winget.installed && (!installedPrograms.python.installed || !installedPrograms.ffmpeg.installed)) ? - - - WinGet Not Found - - WinGet is required to install necessary packages. Please install it manually from here. - - - : null} - {(installedPrograms.python.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ? - - - Ready - - Everything looks ok! You can close this window now. Make sure it's always running in the background. - - - : null} -
- : isMacOs && macOsVersion && compareVersions(macOsVersion, '10.13') > 0 ? /* Section for macOS */ -
-
-

Python: {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}

- {installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? : : installedPrograms.brew.installed ? : : null} -
-
-

FFmpeg: {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}

- {installedPrograms.ffmpeg.installed ? : installedPrograms.brew.installed ? : null} -
-
-

Node.js: {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}

- {installedPrograms.nodejs.installed ? : installedPrograms.brew.installed ? : null} -
-
-

PytubePP: {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}

- {installedPrograms.pytubepp.installed ? : installedPrograms.pip3.installed ? : null} -
- {(!installedPrograms.brew.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ? - - - Homebrew Not Found - - Homebrew is required to install necessary unix packages. Please install it manually for your mac. - - - : null} - {(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ? - - - PIP Not Found - - PIP is required to install necessary python packages. Please install it now to continue: - - - : null} - {(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ? - - - Ready - - Everything looks ok! You can close this window now. Make sure it's always running in the background. - - - : null} -
- : -
- - - Unsupported OS - - 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 here. - - -
- } -
+ + {children} + +
); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 0270f64..65d4fcd 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" 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: { variant: { diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..f6afdaf --- /dev/null +++ b/src/components/ui/form.tsx @@ -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 = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +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 ") + } + + 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( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +