diff --git a/.husky/pre-commit b/.husky/pre-commit
deleted file mode 100755
index 9b196259..00000000
--- a/.husky/pre-commit
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
-echo "๐ Running linter..."
-npm run lint
-if [ $? -ne 0 ]; then
- echo "โ Linting failed. Please fix the errors before committing."
- exit 1
-fi
-
-echo "๐ Running TypeScript type checking..."
-npm run lint:ts
-if [ $? -ne 0 ]; then
- echo "โ TypeScript type checking failed. Please fix the type errors before committing."
- exit 1
-fi
-
-echo "๐งช Running tests..."
-cd client && CI=true npm test -- --watchAll=false --passWithNoTests --silent
-TEST_RESULT=$?
-cd ..
-
-if [ $TEST_RESULT -ne 0 ]; then
- echo "โ Tests failed. Please fix the failing tests before committing."
- exit 1
-fi
-
-echo "โ
All checks passed!"
diff --git a/client/package-lock.json b/client/package-lock.json
index 4d86f4b3..477162d4 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -91,6 +91,7 @@
"version": "7.21.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz",
"integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.21.4",
@@ -962,6 +963,7 @@
"version": "7.21.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz",
"integrity": "sha512-l9xd3N+XG4fZRxEP3vXdK6RW7vN1Uf5dxzRC/09wV86wqZ/YYQooBIGNsiRdfNR3/q2/5pPzV4B54J/9ctX5jw==",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.20.2"
},
@@ -1523,6 +1525,7 @@
"version": "7.21.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.5.tgz",
"integrity": "sha512-ELdlq61FpoEkHO6gFRpfj0kUgSwQTGoaEU8eMRoS8Dv3v6e7BjEAj5WMtIBRdHUeAioMhKP5HyxNzNnP+heKbA==",
+ "peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.18.6",
"@babel/helper-module-imports": "^7.21.4",
@@ -2346,6 +2349,7 @@
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.0.tgz",
"integrity": "sha512-ZSK3ZJsNkwfjT3JpDAWJZlrGD81Z3ytNDsxw1LKq1o+xkmO5pnWfr6gmCC8gHEFf3nSSX/09YrG67jybNPxSUw==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
@@ -2386,6 +2390,7 @@
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
"integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
@@ -3028,6 +3033,7 @@
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.1.tgz",
"integrity": "sha512-qSnbJZer8lIuDYFDv19/t3s0AXYY9SxcOdhCnGvetRSfOG4gy3TkiFXNCdW5OLNveTieiMpOuv46eXUmE3ZA6A==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0",
"@mui/base": "5.0.0-beta.1",
@@ -3129,6 +3135,7 @@
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.1.tgz",
"integrity": "sha512-BsDUjhiO6ZVAvzKhnWBHLZ5AtPJcdT+62VjnRLyA4isboqDKLg4fmYIZXq51yndg/soDK9RkY5lYZwEDku13Ow==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0",
"@mui/private-theming": "^5.13.1",
@@ -3814,8 +3821,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@types/babel__core": {
"version": "7.20.0",
@@ -4080,6 +4086,7 @@
"version": "18.2.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz",
"integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4090,6 +4097,7 @@
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz",
"integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==",
+ "peer": true,
"dependencies": {
"@types/react": "*"
}
@@ -4207,6 +4215,7 @@
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz",
"integrity": "sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==",
+ "peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.59.6",
@@ -4258,6 +4267,7 @@
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz",
"integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.59.6",
"@typescript-eslint/types": "5.59.6",
@@ -4588,6 +4598,7 @@
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4674,6 +4685,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -5398,6 +5410,7 @@
"url": "https://tidelift.com/funding/github/npm/browserslist"
}
],
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001449",
"electron-to-chromium": "^1.4.284",
@@ -5515,9 +5528,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001488",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
- "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
+ "version": "1.0.30001766",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
+ "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
"funding": [
{
"type": "opencollective",
@@ -5531,7 +5544,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
@@ -6163,6 +6177,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -6470,6 +6485,7 @@
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
@@ -6741,8 +6757,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -7209,6 +7224,7 @@
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz",
"integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
@@ -7604,6 +7620,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -9694,6 +9711,7 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
+ "peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@@ -10880,7 +10898,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
- "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -11628,6 +11645,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -12426,6 +12444,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@@ -13492,6 +13511,7 @@
"version": "6.0.13",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -13839,6 +13859,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -13908,6 +13929,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@@ -13957,6 +13979,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14449,6 +14472,7 @@
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -15903,6 +15927,7 @@
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -15947,6 +15972,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16339,6 +16365,7 @@
"version": "5.82.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.0.tgz",
"integrity": "sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
@@ -16407,6 +16434,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -16456,6 +16484,7 @@
"version": "4.15.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz",
"integrity": "sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==",
+ "peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -16514,6 +16543,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -16859,6 +16889,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index a294f069..9e8f4059 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -36,6 +36,7 @@ import MenuIcon from '@mui/icons-material/Menu';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import SettingsIcon from '@mui/icons-material/Settings';
import SubscriptionsIcon from '@mui/icons-material/Subscriptions';
+import PlaylistPlayIcon from '@mui/icons-material/PlaylistPlay';
import DownloadIcon from '@mui/icons-material/Download';
import VideoLibraryIcon from '@mui/icons-material/VideoLibrary';
import LoginIcon from '@mui/icons-material/Login';
@@ -44,11 +45,13 @@ import ShieldIcon from '@mui/icons-material/Shield';
import NewReleasesIcon from '@mui/icons-material/NewReleases';
import Configuration from './components/Configuration';
import ChannelManager from './components/ChannelManager';
+import PlaylistManager from './components/PlaylistManager';
import DownloadManager from './components/DownloadManager';
import VideosPage from './components/VideosPage';
import LocalLogin from './components/LocalLogin';
import InitialSetup from './components/InitialSetup';
import ChannelPage from './components/ChannelPage';
+import PlaylistPage from './components/PlaylistPage';
import ChangelogPage from './components/ChangelogPage';
import StorageStatus from './components/StorageStatus';
import { useConfig } from './hooks/useConfig';
@@ -555,6 +558,31 @@ function AppContent() {
primary='Your Channels'
/>
+ `4px solid ${theme.palette.primary.main}` : 'none',
+ '&:hover': {
+ bgcolor: 'action.hover',
+ },
+ paddingX: isMobile ? '8px' : '16px'
+ }}
+ >
+
+
+
+
+
}
/>
+ }
+ />
+ }
+ />
}
diff --git a/client/src/components/DownloadManager/DownloadNew.tsx b/client/src/components/DownloadManager/DownloadNew.tsx
index 7a8bb43f..e7784fe4 100644
--- a/client/src/components/DownloadManager/DownloadNew.tsx
+++ b/client/src/components/DownloadManager/DownloadNew.tsx
@@ -32,6 +32,7 @@ const DownloadNew: React.FC = ({
}) => {
const [tabValue, setTabValue] = useState(0);
const [showChannelSettingsDialog, setShowChannelSettingsDialog] = useState(false);
+ const [showPlaylistSettingsDialog, setShowPlaylistSettingsDialog] = useState(false);
// Use config hook to get default resolution and video count
const { config } = useConfig(token);
@@ -42,6 +43,11 @@ const DownloadNew: React.FC = ({
setShowChannelSettingsDialog(true);
};
+ const handleOpenPlaylistSettings = () => {
+ console.log('Opening playlist settings dialog');
+ setShowPlaylistSettingsDialog(true);
+ };
+
const handleTriggerChannelDownloads = async (settings: DownloadSettings | null) => {
setShowChannelSettingsDialog(false);
downloadInitiatedRef.current = true;
@@ -68,6 +74,35 @@ const DownloadNew: React.FC = ({
setTimeout(fetchRunningJobs, 500);
};
+ const handleTriggerPlaylistDownloads = async (settings: DownloadSettings | null) => {
+ console.log('Triggering playlist downloads with settings:', settings);
+ setShowPlaylistSettingsDialog(false);
+ downloadInitiatedRef.current = true;
+
+ const body: any = {};
+ // Add settings to the request body if provided
+ if (settings) {
+ body.overrideSettings = settings;
+ }
+
+ console.log('Fetching /api/playlists/download-all with body:', body);
+ const result = await fetch('/api/playlists/download-all', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-access-token': token || '',
+ },
+ body: JSON.stringify(body),
+ });
+ console.log('Playlist download result status:', result.status);
+ // If the result is a 400 then we already have a running Playlist Download
+ // job and we should display an alert
+ if (result.status === 400) {
+ alert('Playlist Download already running');
+ }
+ setTimeout(fetchRunningJobs, 500);
+ };
+
const handleManualDownload = useCallback(async (urls: string[], settings?: DownloadSettings | null) => {
downloadInitiatedRef.current = true;
const strippedUrls = urls.map((url) =>
@@ -109,6 +144,7 @@ const DownloadNew: React.FC = ({
+
@@ -123,7 +159,7 @@ const DownloadNew: React.FC = ({
defaultResolution={defaultResolution}
/>
- ) : (
+ ) : tabValue === 1 ? (
setTabValue(1)}
@@ -145,6 +181,28 @@ const DownloadNew: React.FC = ({
+ ) : (
+ setTabValue(2)}
+ >
+
+
+
+
)}
@@ -158,6 +216,16 @@ const DownloadNew: React.FC = ({
mode="channel"
defaultResolutionSource="global"
/>
+
+ setShowPlaylistSettingsDialog(false)}
+ onConfirm={handleTriggerPlaylistDownloads}
+ defaultResolution={defaultResolution}
+ defaultVideoCount={defaultVideoCount}
+ mode="channel"
+ defaultResolutionSource="global"
+ />
);
};
diff --git a/client/src/components/PlaylistManager.tsx b/client/src/components/PlaylistManager.tsx
new file mode 100644
index 00000000..89cb8fee
--- /dev/null
+++ b/client/src/components/PlaylistManager.tsx
@@ -0,0 +1,367 @@
+import React, { useContext, useState, useEffect, useCallback } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardHeader,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ Fab,
+ IconButton,
+ List,
+ Pagination,
+ TextField,
+ Tooltip,
+ Typography,
+ Zoom,
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import SaveIcon from '@mui/icons-material/Save';
+import PlaylistPlayIcon from '@mui/icons-material/PlaylistPlay';
+import { useTheme } from '@mui/material/styles';
+import useMediaQuery from '@mui/material/useMediaQuery';
+import { useNavigate } from 'react-router-dom';
+import WebSocketContext from '../contexts/WebSocketContext';
+import { Playlist } from '../types/Playlist';
+import { usePlaylistList } from './PlaylistManager/hooks/usePlaylistList';
+import { usePlaylistMutations } from './PlaylistManager/hooks/usePlaylistMutations';
+import PlaylistListRow from './PlaylistManager/components/PlaylistListRow';
+
+interface PlaylistManagerProps {
+ token: string | null;
+}
+
+const PlaylistManager: React.FC = ({ token }) => {
+ const websocketContext = useContext(WebSocketContext);
+ if (!websocketContext) {
+ throw new Error('WebSocketContext not found');
+ }
+
+ const navigate = useNavigate();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+
+ const [newPlaylistUrl, setNewPlaylistUrl] = useState('');
+ const [page, setPage] = useState(1);
+ const [filterValue, setFilterValue] = useState('');
+ const [addDialogOpen, setAddDialogOpen] = useState(false);
+ const [pendingSave, setPendingSave] = useState(false);
+ const [addingPlaylist, setAddingPlaylist] = useState(false);
+ const [successMessage, setSuccessMessage] = useState(null);
+
+ const pageSize = isMobile ? 16 : 20;
+
+ const {
+ playlists,
+ total,
+ totalPages,
+ loading,
+ error,
+ refetch: refetchPlaylists,
+ } = usePlaylistList({
+ token,
+ page,
+ pageSize,
+ searchTerm: filterValue,
+ sortOrder: 'asc',
+ });
+
+ const {
+ addPlaylist,
+ deletePlaylist,
+ updatePlaylist,
+ fetchPlaylistVideos,
+ downloadPlaylist,
+ loading: mutationLoading,
+ error: mutationError,
+ } = usePlaylistMutations(token, refetchPlaylists);
+
+ const [modifiedPlaylists, setModifiedPlaylists] = useState