mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
near-complete feature parity with current frontend, another big push of features, some styling to dialogs / forms
This commit is contained in:
parent
04584d7b1e
commit
5e2125d5e7
48 changed files with 1853 additions and 1666 deletions
12
frontend/eslint.config.mjs
Normal file
12
frontend/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{files: ["**/*.{js,mjs,cjs,jsx}"]},
|
||||
{languageOptions: { globals: globals.browser }},
|
||||
pluginJs.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
];
|
||||
147
frontend/package-lock.json
generated
147
frontend/package-lock.json
generated
|
|
@ -14,9 +14,11 @@
|
|||
"@mui/icons-material": "^6.4.5",
|
||||
"@mui/material": "^6.4.5",
|
||||
"axios": "^1.7.9",
|
||||
"eslint": "^8.57.1",
|
||||
"formik": "^2.4.6",
|
||||
"material-react-table": "^3.2.0",
|
||||
"planby": "^1.1.7",
|
||||
"prettier": "^3.5.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
|
|
@ -111,6 +113,7 @@
|
|||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.26.8.tgz",
|
||||
"integrity": "sha512-3tBctaHRW6xSub26z7n8uyOTwwUsCdvIug/oxBH9n6yCO5hMj2vwDJAo7RbBMKrM7P+W2j61zLKviJQFGOYKMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
|
|
@ -128,6 +131,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
|
@ -136,6 +140,7 @@
|
|||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
|
|
@ -2393,6 +2398,7 @@
|
|||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
|
||||
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
|
|
@ -2414,12 +2420,14 @@
|
|||
"node_modules/@eslint/eslintrc/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
|
|
@ -2434,6 +2442,7 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
|
@ -2445,6 +2454,7 @@
|
|||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -2456,6 +2466,7 @@
|
|||
"version": "8.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
|
||||
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
|
|
@ -2470,6 +2481,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
|
||||
"deprecated": "Use @eslint/config-array instead",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^2.0.3",
|
||||
"debug": "^4.3.1",
|
||||
|
|
@ -2495,7 +2507,8 @@
|
|||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
|
||||
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
|
||||
"deprecated": "Use @eslint/object-schema instead"
|
||||
"deprecated": "Use @eslint/object-schema instead",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
|
|
@ -3296,6 +3309,7 @@
|
|||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-scope": "5.1.1"
|
||||
}
|
||||
|
|
@ -3304,6 +3318,7 @@
|
|||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^4.1.1"
|
||||
|
|
@ -3316,6 +3331,7 @@
|
|||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
|
|
@ -3494,12 +3510,14 @@
|
|||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="
|
||||
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rushstack/eslint-patch": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz",
|
||||
"integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A=="
|
||||
"integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.24.51",
|
||||
|
|
@ -3968,7 +3986,8 @@
|
|||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
|
|
@ -4053,7 +4072,8 @@
|
|||
"node_modules/@types/semver": {
|
||||
"version": "7.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
|
||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.4",
|
||||
|
|
@ -4125,6 +4145,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
|
|
@ -4158,6 +4179,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz",
|
||||
"integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "5.62.0"
|
||||
},
|
||||
|
|
@ -4176,6 +4198,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
|
|
@ -4202,6 +4225,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
|
||||
"integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
"@typescript-eslint/visitor-keys": "5.62.0"
|
||||
|
|
@ -4218,6 +4242,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
|
||||
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
||||
"@typescript-eslint/utils": "5.62.0",
|
||||
|
|
@ -4244,6 +4269,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
|
||||
"integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
|
|
@ -4256,6 +4282,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
|
||||
"integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
"@typescript-eslint/visitor-keys": "5.62.0",
|
||||
|
|
@ -4282,6 +4309,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
|
||||
"integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
|
|
@ -4307,6 +4335,7 @@
|
|||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^4.1.1"
|
||||
|
|
@ -4319,6 +4348,7 @@
|
|||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
|
|
@ -4327,6 +4357,7 @@
|
|||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
|
||||
"integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
|
|
@ -4342,7 +4373,8 @@
|
|||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
|
|
@ -4546,6 +4578,7 @@
|
|||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
|
|
@ -4740,6 +4773,7 @@
|
|||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
|
|
@ -4768,6 +4802,7 @@
|
|||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
|
||||
"integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -4795,6 +4830,7 @@
|
|||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
|
||||
"integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -4814,6 +4850,7 @@
|
|||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
|
||||
"integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -4833,6 +4870,7 @@
|
|||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
|
||||
"integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -4850,6 +4888,7 @@
|
|||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
|
||||
"integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -4887,6 +4926,7 @@
|
|||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
|
||||
"integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -4926,7 +4966,8 @@
|
|||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="
|
||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
|
|
@ -5008,6 +5049,7 @@
|
|||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz",
|
||||
"integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==",
|
||||
"license": "MPL-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
|
|
@ -5040,6 +5082,7 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
|
|
@ -5914,7 +5957,8 @@
|
|||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
||||
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="
|
||||
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/connect-history-api-fallback": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -6387,7 +6431,8 @@
|
|||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
|
||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -6667,6 +6712,7 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
|
|
@ -6988,6 +7034,7 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
|
||||
"integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"call-bound": "^1.0.3",
|
||||
|
|
@ -7044,6 +7091,7 @@
|
|||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
|
||||
"integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
|
|
@ -7125,6 +7173,7 @@
|
|||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
|
||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
|
|
@ -7179,6 +7228,7 @@
|
|||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
|
||||
"integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
|
|
@ -7206,6 +7256,7 @@
|
|||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
"integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7",
|
||||
"is-core-module": "^2.13.0",
|
||||
|
|
@ -7216,6 +7267,7 @@
|
|||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
|
|
@ -7224,6 +7276,7 @@
|
|||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
||||
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7"
|
||||
},
|
||||
|
|
@ -7240,6 +7293,7 @@
|
|||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
|
|
@ -7248,6 +7302,7 @@
|
|||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz",
|
||||
"integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"string-natural-compare": "^3.0.1"
|
||||
|
|
@ -7265,6 +7320,7 @@
|
|||
"version": "2.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
|
|
@ -7297,6 +7353,7 @@
|
|||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
|
|
@ -7305,6 +7362,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
|
|
@ -7316,6 +7374,7 @@
|
|||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
|
|
@ -7324,6 +7383,7 @@
|
|||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz",
|
||||
"integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/experimental-utils": "^5.0.0"
|
||||
},
|
||||
|
|
@ -7347,6 +7407,7 @@
|
|||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
|
||||
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aria-query": "^5.3.2",
|
||||
"array-includes": "^3.1.8",
|
||||
|
|
@ -7375,6 +7436,7 @@
|
|||
"version": "7.37.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",
|
||||
"integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.8",
|
||||
"array.prototype.findlast": "^1.2.5",
|
||||
|
|
@ -7406,6 +7468,7 @@
|
|||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
|
||||
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -7417,6 +7480,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
|
|
@ -7428,6 +7492,7 @@
|
|||
"version": "2.0.0-next.5",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||
"integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
"path-parse": "^1.0.7",
|
||||
|
|
@ -7444,6 +7509,7 @@
|
|||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
|
|
@ -7452,6 +7518,7 @@
|
|||
"version": "5.11.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz",
|
||||
"integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^5.58.0"
|
||||
},
|
||||
|
|
@ -7467,6 +7534,7 @@
|
|||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
|
|
@ -7493,6 +7561,7 @@
|
|||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
|
||||
"integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint": "^7.29.0 || ^8.4.1",
|
||||
"jest-worker": "^28.0.2",
|
||||
|
|
@ -7516,6 +7585,7 @@
|
|||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
|
||||
"integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"merge-stream": "^2.0.0",
|
||||
|
|
@ -7529,6 +7599,7 @@
|
|||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
|
|
@ -7542,7 +7613,8 @@
|
|||
"node_modules/eslint/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/eslint/node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
|
|
@ -7563,6 +7635,7 @@
|
|||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
|
|
@ -7577,6 +7650,7 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
|
@ -7630,6 +7704,7 @@
|
|||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -7641,6 +7716,7 @@
|
|||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.9.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
|
|
@ -7918,6 +7994,7 @@
|
|||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flat-cache": "^3.0.4"
|
||||
},
|
||||
|
|
@ -8074,6 +8151,7 @@
|
|||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
"integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatted": "^3.2.9",
|
||||
"keyv": "^4.5.3",
|
||||
|
|
@ -8086,7 +8164,8 @@
|
|||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
|
|
@ -9400,6 +9479,7 @@
|
|||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -9695,6 +9775,7 @@
|
|||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||
"integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-object-atoms": "^1.0.0",
|
||||
|
|
@ -10667,7 +10748,8 @@
|
|||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
|
|
@ -10745,6 +10827,7 @@
|
|||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
"integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.6",
|
||||
"array.prototype.flat": "^1.3.1",
|
||||
|
|
@ -10759,6 +10842,7 @@
|
|||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
|
|
@ -10790,12 +10874,14 @@
|
|||
"node_modules/language-subtag-registry": {
|
||||
"version": "0.3.23",
|
||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
|
||||
"integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="
|
||||
"integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/language-tags": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
|
||||
"integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"language-subtag-registry": "^0.3.20"
|
||||
},
|
||||
|
|
@ -11290,7 +11376,8 @@
|
|||
"node_modules/natural-compare-lite": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
|
||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="
|
||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
|
|
@ -11444,6 +11531,7 @@
|
|||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
|
||||
"integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -11457,6 +11545,7 @@
|
|||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
|
||||
"integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -11494,6 +11583,7 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
|
||||
"integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -13153,6 +13243,21 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
|
||||
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
|
|
@ -14875,7 +14980,8 @@
|
|||
"node_modules/string-natural-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw=="
|
||||
"integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
|
|
@ -14918,6 +15024,7 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||
"integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
|
|
@ -14957,6 +15064,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
|
||||
"integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.17.5"
|
||||
|
|
@ -15673,6 +15781,7 @@
|
|||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json5": "^0.0.29",
|
||||
"json5": "^1.0.2",
|
||||
|
|
@ -15684,6 +15793,7 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
|
|
@ -15695,6 +15805,7 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
|
|
@ -15708,6 +15819,7 @@
|
|||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
|
||||
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.8.1"
|
||||
},
|
||||
|
|
@ -15721,7 +15833,8 @@
|
|||
"node_modules/tsutils/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
"@mui/icons-material": "^6.4.5",
|
||||
"@mui/material": "^6.4.5",
|
||||
"axios": "^1.7.9",
|
||||
"eslint": "^8.57.1",
|
||||
"formik": "^2.4.6",
|
||||
"material-react-table": "^3.2.0",
|
||||
"planby": "^1.1.7",
|
||||
"prettier": "^3.5.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
|
|
@ -24,7 +26,8 @@
|
|||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
|
|||
10
frontend/prettier.config.js
Normal file
10
frontend/prettier.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// prettier.config.js or .prettierrc.js
|
||||
module.exports = {
|
||||
semi: true, // Add semicolons at the end of statements
|
||||
singleQuote: true, // Use single quotes instead of double
|
||||
tabWidth: 2, // Set the indentation width
|
||||
trailingComma: "es5", // Add trailing commas where valid in ES5
|
||||
printWidth: 80, // Wrap lines at 80 characters
|
||||
bracketSpacing: true, // Add spaces inside object braces
|
||||
arrowParens: "always", // Always include parentheses around arrow function parameters
|
||||
};
|
||||
|
|
@ -1,13 +1,19 @@
|
|||
// src/App.js
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Route,
|
||||
Routes,
|
||||
Navigate,
|
||||
} from 'react-router-dom';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Login from './pages/Login';
|
||||
import useAuthStore from './store/auth';
|
||||
import Channels from './pages/Channels';
|
||||
import M3U from './pages/M3U';
|
||||
import { ThemeProvider } from '@mui/material/styles'; // Import theme tools
|
||||
import { ThemeProvider } from '@mui/material/styles'; // Import theme tools
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Box,
|
||||
CssBaseline,
|
||||
Drawer,
|
||||
|
|
@ -17,32 +23,63 @@ import {
|
|||
ListItemText,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import theme from './theme'
|
||||
import theme from './theme';
|
||||
import EPG from './pages/EPG';
|
||||
import Guide from './pages/Guide';
|
||||
import StreamProfiles from './pages/StreamProfiles';
|
||||
import useAuthStore from './store/auth';
|
||||
import API from './api';
|
||||
|
||||
const drawerWidth = 240;
|
||||
const miniDrawerWidth = 60;
|
||||
|
||||
// Protected Route Component
|
||||
const ProtectedRoute = ({ element, ...rest }) => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
return isAuthenticated ? element : <Navigate to="/login" />;
|
||||
};
|
||||
const defaultRoute = '/channels';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const {
|
||||
isAuthenticated,
|
||||
setIsAuthenticated,
|
||||
logout,
|
||||
initData,
|
||||
initializeAuth,
|
||||
} = useAuthStore();
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const loggedIn = await initializeAuth();
|
||||
|
||||
if (loggedIn) {
|
||||
await initData();
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
await logout();
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [initializeAuth, initData, setIsAuthenticated, logout]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
{/* <AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
width: `calc(100% - ${open ? drawerWidth : miniDrawerWidth}px)`,
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
}}
|
||||
>
|
||||
<Toolbar variant="dense"></Toolbar>
|
||||
</AppBar> */}
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
open={open}
|
||||
|
|
@ -57,11 +94,20 @@ const App = () => {
|
|||
}}
|
||||
>
|
||||
{/* Drawer Toggle Button */}
|
||||
<List sx={{backgroundColor: '#495057', color: 'white'}}>
|
||||
<List sx={{ backgroundColor: '#495057', color: 'white' }}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={toggleDrawer}>
|
||||
<img src="/images/logo.png" width="35x" />
|
||||
{open && <ListItemText primary="Dispatcharr" sx={{paddingLeft: 3}}/>}
|
||||
<ListItemButton
|
||||
onClick={toggleDrawer}
|
||||
size="small"
|
||||
sx={{
|
||||
pt: 0,
|
||||
pb: 0,
|
||||
}}
|
||||
>
|
||||
<img src="/images/logo.png" width="33x" />
|
||||
{open && (
|
||||
<ListItemText primary="Dispatcharr" sx={{ paddingLeft: 3 }} />
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
|
@ -71,28 +117,57 @@ const App = () => {
|
|||
<Sidebar open />
|
||||
</Drawer>
|
||||
|
||||
<Box sx={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
backgroundColor: '#495057',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
backgroundColor: '#495057',
|
||||
// pt: '64px',
|
||||
}}
|
||||
>
|
||||
{/* Fixed Header */}
|
||||
{/* <Box sx={{ height: '67px', backgroundColor: '#495057', color: '#fff', display: 'flex', alignItems: 'center', padding: '0 16px' }}>
|
||||
|
||||
</Box> */}
|
||||
|
||||
{/* Main Content Area between Header and Footer */}
|
||||
<Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route exact path="/channels" element={<ProtectedRoute element={<Channels />}/>} />
|
||||
<Route exact path="/m3u" element={<ProtectedRoute element={<M3U />}/>} />
|
||||
<Route exact path="/epg" element={<ProtectedRoute element={<EPG />}/>} />
|
||||
<Route exact path="/stream-profiles" element={<ProtectedRoute element={<StreamProfiles />}/>} />
|
||||
<Route exact path="/guide" element={<ProtectedRoute element={<Guide />}/>} />
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Route exact path="/channels" element={<Channels />} />
|
||||
<Route exact path="/m3u" element={<M3U />} />
|
||||
<Route exact path="/epg" element={<EPG />} />
|
||||
<Route
|
||||
exact
|
||||
path="/stream-profiles"
|
||||
element={<StreamProfiles />}
|
||||
/>
|
||||
<Route exact path="/guide" element={<Guide />} />
|
||||
</>
|
||||
) : (
|
||||
<Route path="/login" element={<Login />} />
|
||||
)}
|
||||
{/* Redirect if no match */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Navigate
|
||||
to={isAuthenticated ? defaultRoute : '/login'}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Axios from 'axios'
|
||||
import Axios from 'axios';
|
||||
import useAuthStore from './store/auth';
|
||||
import useChannelsStore from './store/channels';
|
||||
import useUserAgentsStore from './store/userAgents';
|
||||
|
|
@ -7,11 +7,11 @@ import useEPGsStore from './store/epgs';
|
|||
import useStreamsStore from './store/streams';
|
||||
import useStreamProfilesStore from './store/streamProfiles';
|
||||
|
||||
const axios = Axios.create({
|
||||
withCredentials: true,
|
||||
})
|
||||
// const axios = Axios.create({
|
||||
// withCredentials: true,
|
||||
// });
|
||||
|
||||
const host = "http://192.168.1.151:9191"
|
||||
const host = 'http://192.168.1.151:9191';
|
||||
|
||||
const getAuthToken = async () => {
|
||||
const token = await useAuthStore.getState().getToken(); // Assuming token is stored in Zustand store
|
||||
|
|
@ -28,14 +28,14 @@ export default class API {
|
|||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
return await response.json()
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static async refreshToken(refreshToken) {
|
||||
const response = await fetch(`${host}/api/accounts/token/refresh/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh: refreshToken })
|
||||
body: JSON.stringify({ refresh: refreshToken }),
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
|
|
@ -45,9 +45,9 @@ export default class API {
|
|||
static async logout() {
|
||||
const response = await fetch(`${host}/api/accounts/auth/logout/`, {
|
||||
method: 'POST',
|
||||
})
|
||||
});
|
||||
|
||||
return response.data.data
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
static async getChannels() {
|
||||
|
|
@ -74,33 +74,72 @@ export default class API {
|
|||
return retval;
|
||||
}
|
||||
|
||||
static async addChannelGroup(values) {
|
||||
const response = await fetch(`${host}/api/channels/groups/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useChannelsStore.getState().addChannelGroup(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async updateChannelGroup(values) {
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/channels/groups/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useChannelsStore.getState().updateChannelGroup(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async addChannel(channel) {
|
||||
let body = null
|
||||
let body = null;
|
||||
if (channel.logo_file) {
|
||||
body = new FormData();
|
||||
for (const prop in channel) {
|
||||
body.append(prop, channel[prop])
|
||||
body.append(prop, channel[prop]);
|
||||
}
|
||||
} else {
|
||||
body = {...channel}
|
||||
delete body.logo_file
|
||||
body = JSON.stringify(body)
|
||||
body = { ...channel };
|
||||
delete body.logo_file;
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${host}/api/channels/channels/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
...(channel.logo_file ? {} :{
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
...(channel.logo_file
|
||||
? {}
|
||||
: {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useChannelsStore.getState().addChannel(retval)
|
||||
useChannelsStore.getState().addChannel(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -115,7 +154,7 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
useChannelsStore.getState().removeChannels([id])
|
||||
useChannelsStore.getState().removeChannels([id]);
|
||||
}
|
||||
|
||||
// @TODO: the bulk delete endpoint is currently broken
|
||||
|
|
@ -133,7 +172,7 @@ export default class API {
|
|||
// }
|
||||
|
||||
static async updateChannel(values) {
|
||||
const {id, ...payload} = values
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/channels/channels/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
|
@ -145,7 +184,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useChannelsStore.getState().updateChannel(retval)
|
||||
useChannelsStore.getState().updateChannel(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -163,7 +202,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useChannelsStore.getState().addChannel(retval)
|
||||
useChannelsStore.getState().addChannel(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -181,7 +220,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useChannelsStore.getState().addChannel(retval)
|
||||
useChannelsStore.getState().addChannel(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -211,14 +250,14 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useStreamsStore.getState().addStream(retval)
|
||||
useStreamsStore.getState().addStream(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async updateStream(values) {
|
||||
const {id, ...payload} = values
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/channels/streams/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
|
@ -230,7 +269,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useStreamsStore.getState().updateStream(retval)
|
||||
useStreamsStore.getState().updateStream(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -245,7 +284,7 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
useStreamsStore.getState().removeStreams([id])
|
||||
useStreamsStore.getState().removeStreams([id]);
|
||||
}
|
||||
|
||||
static async deleteStreams(ids) {
|
||||
|
|
@ -255,10 +294,10 @@ export default class API {
|
|||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ stream_ids: ids })
|
||||
body: JSON.stringify({ stream_ids: ids }),
|
||||
});
|
||||
|
||||
useStreamsStore.getState().removeStreams(ids)
|
||||
useStreamsStore.getState().removeStreams(ids);
|
||||
}
|
||||
|
||||
static async getUserAgents() {
|
||||
|
|
@ -285,14 +324,14 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useUserAgentsStore.getState().addUserAgent(retval)
|
||||
useUserAgentsStore.getState().addUserAgent(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async updateUserAgent(values) {
|
||||
const {id, ...payload} = values
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/core/useragents/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
|
@ -304,7 +343,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useUserAgentsStore.getState().updateUserAgent(retval)
|
||||
useUserAgentsStore.getState().updateUserAgent(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -319,7 +358,7 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
useUserAgentsStore.getState().removeUserAgents([id])
|
||||
useUserAgentsStore.getState().removeUserAgents([id]);
|
||||
}
|
||||
|
||||
static async getPlaylists() {
|
||||
|
|
@ -346,7 +385,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
usePlaylistsStore.getState().addPlaylist(retval)
|
||||
usePlaylistsStore.getState().addPlaylist(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -387,11 +426,11 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
usePlaylistsStore.getState().removePlaylists([id])
|
||||
usePlaylistsStore.getState().removePlaylists([id]);
|
||||
}
|
||||
|
||||
static async updatePlaylist(values) {
|
||||
const {id, ...payload} = values
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/m3u/accounts/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
|
@ -403,7 +442,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
usePlaylistsStore.getState().updatePlaylist(retval)
|
||||
usePlaylistsStore.getState().updatePlaylist(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -435,32 +474,34 @@ export default class API {
|
|||
}
|
||||
|
||||
static async addEPG(values) {
|
||||
let body = null
|
||||
let body = null;
|
||||
if (values.epg_file) {
|
||||
body = new FormData();
|
||||
for (const prop in values) {
|
||||
body.append(prop, values[prop])
|
||||
body.append(prop, values[prop]);
|
||||
}
|
||||
} else {
|
||||
body = {...values}
|
||||
delete body.epg_file
|
||||
body = JSON.stringify(body)
|
||||
body = { ...values };
|
||||
delete body.epg_file;
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${host}/api/epg/sources/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
...(values.epg_file ? {} :{
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
...(values.epg_file
|
||||
? {}
|
||||
: {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useEPGsStore.getState().addEPG(retval)
|
||||
useEPGsStore.getState().addEPG(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -475,7 +516,7 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
useEPGsStore.getState().removeEPGs([id])
|
||||
useEPGsStore.getState().removeEPGs([id]);
|
||||
}
|
||||
|
||||
static async refreshEPG(id) {
|
||||
|
|
@ -511,18 +552,18 @@ export default class API {
|
|||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(values)
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useStreamProfilesStore.getState().addStreamProfile(retval)
|
||||
useStreamProfilesStore.getState().addStreamProfile(retval);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async updateStreamProfile(values) {
|
||||
const {id, ...payload} = values
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/core/streamprofiles/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
|
@ -534,7 +575,7 @@ export default class API {
|
|||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useStreamProfilesStore.getState().updateStreamProfile(retval)
|
||||
useStreamProfilesStore.getState().updateStreamProfile(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
|
|
@ -549,7 +590,7 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
useStreamProfilesStore.getState().removeStreamProfiles([id])
|
||||
useStreamProfilesStore.getState().removeStreamProfiles([id]);
|
||||
}
|
||||
|
||||
static async getGrid() {
|
||||
|
|
@ -560,8 +601,8 @@ export default class API {
|
|||
},
|
||||
});
|
||||
|
||||
const retval = await response.json()
|
||||
console.log(retval)
|
||||
return retval
|
||||
const retval = await response.json();
|
||||
console.log(retval);
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const HeaderBar = () => {
|
||||
return (
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-lte-toggle="sidebar" href="#" role="button">
|
||||
<i class="bi bi-list"></i>
|
||||
</a>
|
||||
</li>
|
||||
{/* <li class="nav-item d-none d-md-block">
|
||||
<a href="{% url 'dashboard:dashboard' %}" class="nav-link">Home</a>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<Link to="/login" class="nav-link">Login</Link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
|
||||
id="themeToggleBtn" type="button" aria-expanded="false"
|
||||
data-bs-toggle="dropdown" data-bs-display="static">
|
||||
<span class="theme-icon-active"><i class="bi bi-sun-fill my-1"></i></span>
|
||||
<span class="d-lg-none ms-2" id="theme-toggle-text">Toggle theme</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="themeToggleBtn">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="light">
|
||||
<i class="bi bi-sun-fill me-2"></i> Light
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark">
|
||||
<i class="bi bi-moon-fill me-2"></i> Dark
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="auto">
|
||||
<i class="bi bi-circle-half me-2"></i> Auto
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderBar;
|
||||
|
|
@ -1,33 +1,30 @@
|
|||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import React from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Drawer,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Home as HomeIcon,
|
||||
Settings as SettingsIcon,
|
||||
Info as InfoIcon,
|
||||
Description as DescriptionIcon,
|
||||
Tv as TvIcon,
|
||||
CalendarMonth as CalendarMonthIcon,
|
||||
VideoFile as VideoFileIcon,
|
||||
LiveTv as LiveTvIcon,
|
||||
PlaylistPlay as PlaylistPlayIcon,
|
||||
} from '@mui/icons-material';
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const items = [
|
||||
{ text: 'Channels', icon: <TvIcon />, route: "/channels" },
|
||||
{ text: 'M3U', icon: <PlaylistPlayIcon />, route: "/m3u" },
|
||||
{ text: 'EPG', icon: <CalendarMonthIcon />, route: "/epg" },
|
||||
{ text: 'Stream Profiles', icon: <VideoFileIcon />, route: "/stream-profiles" },
|
||||
{ text: 'TV Guide', icon: <LiveTvIcon />, route: "/guide" },
|
||||
{ text: "Channels", icon: <TvIcon />, route: "/channels" },
|
||||
{ text: "M3U", icon: <PlaylistPlayIcon />, route: "/m3u" },
|
||||
{ text: "EPG", icon: <CalendarMonthIcon />, route: "/epg" },
|
||||
{
|
||||
text: "Stream Profiles",
|
||||
icon: <VideoFileIcon />,
|
||||
route: "/stream-profiles",
|
||||
},
|
||||
{ text: "TV Guide", icon: <LiveTvIcon />, route: "/guide" },
|
||||
];
|
||||
|
||||
const Sidebar = ({ open }) => {
|
||||
|
|
@ -37,10 +34,14 @@ const Sidebar = ({ open }) => {
|
|||
<List>
|
||||
{items.map((item) => (
|
||||
<ListItem key={item.text} disablePadding>
|
||||
<ListItemButton component={Link} to={item.route} selected={location.pathname == item.route}>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
{open && <ListItemText primary={item.text} />}
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={item.route}
|
||||
selected={location.pathname == item.route}
|
||||
>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
{open && <ListItemText primary={item.text} />}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Modal,
|
||||
Typography,
|
||||
Stack,
|
||||
TextField,
|
||||
|
|
@ -14,47 +12,53 @@ import {
|
|||
FormControl,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
FormHelperText,
|
||||
} from '@mui/material';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import useChannelsStore from "../../store/channels";
|
||||
import API from "../../api"
|
||||
import useStreamProfilesStore from "../../store/streamProfiles";
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import API from '../../api';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
import { Add as AddIcon, Remove as RemoveIcon } from '@mui/icons-material';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import useStreamsStore from "../../store/streams";
|
||||
import usePlaylistsStore from "../../store/playlists";
|
||||
import { MaterialReactTable, useMaterialReactTable } from "material-react-table";
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import ChannelGroupForm from './ChannelGroup';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
|
||||
const Channel = ({ channel = null, isOpen, onClose }) => {
|
||||
const channelGroups = useChannelsStore((state) => state.channelGroups);
|
||||
const streams = useStreamsStore(state => state.streams)
|
||||
const playlists = usePlaylistsStore(state => state.playlists)
|
||||
const streamProfiles = useStreamProfilesStore((state) => state.profiles);
|
||||
const streams = useStreamsStore((state) => state.streams);
|
||||
const { profiles: streamProfiles } = useStreamProfilesStore();
|
||||
const { playlists } = usePlaylistsStore();
|
||||
|
||||
const [logo, setLogo] = useState(null)
|
||||
const [logoPreview, setLogoPreview] = useState(null)
|
||||
|
||||
const [channelStreams, setChannelStreams] = useState([])
|
||||
const [logo, setLogo] = useState(null);
|
||||
const [logoPreview, setLogoPreview] = useState('/images/logo.png');
|
||||
const [channelStreams, setChannelStreams] = useState([]);
|
||||
const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false);
|
||||
|
||||
const addStream = (stream) => {
|
||||
const streamSet = new Set(channelStreams)
|
||||
streamSet.add(stream)
|
||||
setChannelStreams(Array.from(streamSet))
|
||||
}
|
||||
const streamSet = new Set(channelStreams);
|
||||
streamSet.add(stream);
|
||||
setChannelStreams(Array.from(streamSet));
|
||||
};
|
||||
|
||||
const removeStream = (stream) => {
|
||||
const streamSet = new Set(channelStreams)
|
||||
streamSet.delete(stream)
|
||||
setChannelStreams(Array.from(streamSet))
|
||||
}
|
||||
const streamSet = new Set(channelStreams);
|
||||
streamSet.delete(stream);
|
||||
setChannelStreams(Array.from(streamSet));
|
||||
};
|
||||
|
||||
const handleLogoChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setLogo(file)
|
||||
setLogo(file);
|
||||
setLogoPreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
|
@ -74,23 +78,29 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
channel_group_id: Yup.string().required('Channel group is required'),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
console.log(values);
|
||||
if (channel?.id) {
|
||||
await API.updateChannel({id: channel.id, ...values, logo_file: logo, streams: channelStreams.map(stream => stream.id)})
|
||||
await API.updateChannel({
|
||||
id: channel.id,
|
||||
...values,
|
||||
logo_file: logo,
|
||||
streams: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
} else {
|
||||
await API.addChannel({
|
||||
...values,
|
||||
logo_file: logo,
|
||||
streams: channelStreams.map(stream => stream.id),
|
||||
})
|
||||
streams: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setLogo(null)
|
||||
setLogoPreview(null)
|
||||
setLogo(null);
|
||||
setLogoPreview('/images/logo.png');
|
||||
setSubmitting(false);
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
|
|
@ -98,11 +108,22 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
channel_name: channel.channel_name,
|
||||
channel_number: channel.channel_number,
|
||||
channel_group_id: channel.channel_group?.id,
|
||||
stream_profile_id: channel.stream_profile_id,
|
||||
tvg_id: channel.tvg_id,
|
||||
tvg_name: channel.tvg_name,
|
||||
});
|
||||
|
||||
setChannelStreams(streams.filter(stream => channel.streams.includes(stream.id)))
|
||||
console.log('channel streams');
|
||||
console.log(channel.streams);
|
||||
const filteredStreams = streams
|
||||
.filter((stream) => channel.streams.includes(stream.id))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
channel.streams.indexOf(a.id) - channel.streams.indexOf(b.id)
|
||||
);
|
||||
console.log('filtered streams');
|
||||
console.log(filteredStreams);
|
||||
setChannelStreams(filteredStreams);
|
||||
} else {
|
||||
formik.resetForm();
|
||||
}
|
||||
|
|
@ -110,27 +131,32 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
|
||||
const activeStreamsTable = useMaterialReactTable({
|
||||
data: channelStreams,
|
||||
columns: useMemo(() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'M3U',
|
||||
accessorKey: 'group_name',
|
||||
},
|
||||
], []),
|
||||
columns: useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'M3U',
|
||||
accessorKey: 'group_name',
|
||||
},
|
||||
],
|
||||
[]
|
||||
),
|
||||
enableSorting: false,
|
||||
enableBottomToolbar: false,
|
||||
enableTopToolbar: false,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
enableRowOrdering: true,
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
},
|
||||
enableRowActions: true,
|
||||
positionActionsColumn: 'last',
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<IconButton
|
||||
|
|
@ -138,36 +164,53 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
color="error" // Red color for delete actions
|
||||
onClick={() => removeStream(row.original)}
|
||||
>
|
||||
<DeleteIcon fontSize="small" /> {/* Small icon size */}
|
||||
<RemoveIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</>
|
||||
),
|
||||
positionActionsColumn: 'last',
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: '200px',
|
||||
},
|
||||
},
|
||||
})
|
||||
muiRowDragHandleProps: ({ table }) => ({
|
||||
onDragEnd: () => {
|
||||
const { draggingRow, hoveredRow } = table.getState();
|
||||
|
||||
if (hoveredRow && draggingRow) {
|
||||
channelStreams.splice(
|
||||
hoveredRow.index,
|
||||
0,
|
||||
channelStreams.splice(draggingRow.index, 1)[0]
|
||||
);
|
||||
|
||||
setChannelStreams([...channelStreams]);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const availableStreamsTable = useMaterialReactTable({
|
||||
data: streams,
|
||||
columns: useMemo(() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'M3U',
|
||||
accessorKey: 'group_name',
|
||||
},
|
||||
], []),
|
||||
columns: useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'M3U',
|
||||
accessorFn: (row) =>
|
||||
playlists.find((playlist) => playlist.id === row.m3u_account)?.name,
|
||||
},
|
||||
],
|
||||
[]
|
||||
),
|
||||
enableBottomToolbar: false,
|
||||
enableTopToolbar: false,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
|
|
@ -190,196 +233,242 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
height: '200px',
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="form-modal-title" variant="h6" mb={2}>
|
||||
<>
|
||||
<Dialog open={isOpen} onClose={onClose} fullWidth maxWidth="lg">
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
}}
|
||||
>
|
||||
Channel
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={6}>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="channel_name"
|
||||
name="channel_name"
|
||||
label="Channel Name"
|
||||
value={formik.values.channel_name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.channel_name && Boolean(formik.errors.channel_name)}
|
||||
helperText={formik.touched.channel_name && formik.errors.channel_name}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="channel-group-label">Channel Group</InputLabel>
|
||||
<Select
|
||||
labelId="channel-group-label"
|
||||
id="channel_group_id"
|
||||
name="channel_group_id"
|
||||
label="Channel Group"
|
||||
value={formik.values.channel_group_id}
|
||||
<DialogContent>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="channel_name"
|
||||
name="channel_name"
|
||||
label="Channel Name"
|
||||
value={formik.values.channel_name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.channel_group_id && Boolean(formik.errors.channel_group_id)}
|
||||
// helperText={formik.touched.channel_group_id && formik.errors.channel_group_id}
|
||||
error={
|
||||
formik.touched.channel_name &&
|
||||
Boolean(formik.errors.channel_name)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.channel_name && formik.errors.channel_name
|
||||
}
|
||||
variant="standard"
|
||||
>
|
||||
{channelGroups.map((option, index) => (
|
||||
<MenuItem key={index} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
/>
|
||||
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="stream-profile-label">Stream Profile</InputLabel>
|
||||
<Select
|
||||
labelId="stream-profile-label"
|
||||
id="stream_profile_id"
|
||||
name="stream_profile_id"
|
||||
label="Stream Profile (optional)"
|
||||
value={formik.values.stream_profile_id}
|
||||
<Grid2
|
||||
container
|
||||
spacing={1}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Grid2 size={11}>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="channel-group-label">
|
||||
Channel Group
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="channel-group-label"
|
||||
id="channel_group_id"
|
||||
name="channel_group_id"
|
||||
label="Channel Group"
|
||||
value={formik.values.channel_group_id}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={
|
||||
formik.touched.channel_group_id &&
|
||||
Boolean(formik.errors.channel_group_id)
|
||||
}
|
||||
// helperText={formik.touched.channel_group_id && formik.errors.channel_group_id}
|
||||
variant="standard"
|
||||
>
|
||||
{channelGroups.map((option, index) => (
|
||||
<MenuItem key={index} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText sx={{ color: 'error.main' }}>
|
||||
{formik.touched.channel_group_id &&
|
||||
formik.errors.channel_group_id}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid2>
|
||||
<Grid2 size={1}>
|
||||
<IconButton
|
||||
color="success"
|
||||
onClick={() => setChannelGroupModalOpen(true)}
|
||||
title="Create new group"
|
||||
size="small"
|
||||
variant="filled"
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="stream-profile-label">
|
||||
Stream Profile
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="stream-profile-label"
|
||||
id="stream_profile_id"
|
||||
name="stream_profile_id"
|
||||
label="Stream Profile (optional)"
|
||||
value={formik.values.stream_profile_id}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={
|
||||
formik.touched.stream_profile_id &&
|
||||
Boolean(formik.errors.stream_profile_id)
|
||||
}
|
||||
// helperText={formik.touched.channel_group_id && formik.errors.stream_profile_id}
|
||||
variant="standard"
|
||||
>
|
||||
{streamProfiles.map((option, index) => (
|
||||
<MenuItem key={index} value={option.id}>
|
||||
{option.profile_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="channel_number"
|
||||
name="channel_number"
|
||||
label="Channel #"
|
||||
value={formik.values.channel_number}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.stream_profile_id && Boolean(formik.errors.stream_profile_id)}
|
||||
// helperText={formik.touched.channel_group_id && formik.errors.stream_profile_id}
|
||||
error={
|
||||
formik.touched.channel_number &&
|
||||
Boolean(formik.errors.channel_number)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.channel_number &&
|
||||
formik.errors.channel_number
|
||||
}
|
||||
variant="standard"
|
||||
>
|
||||
{streamProfiles.map((option, index) => (
|
||||
<MenuItem key={index} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
/>
|
||||
</Grid2>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="channel_number"
|
||||
name="channel_number"
|
||||
label="Channel #"
|
||||
value={formik.values.channel_number}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.channel_number && Boolean(formik.errors.channel_number)}
|
||||
helperText={formik.touched.channel_number && formik.errors.channel_number}
|
||||
variant="standard"
|
||||
/>
|
||||
<Grid2 size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="tvg_name"
|
||||
name="tvg_name"
|
||||
label="TVG Name"
|
||||
value={formik.values.tvg_name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={
|
||||
formik.touched.tvg_name && Boolean(formik.errors.tvg_name)
|
||||
}
|
||||
helperText={formik.touched.tvg_name && formik.errors.tvg_name}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
</Grid2>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="tvg_id"
|
||||
name="tvg_id"
|
||||
label="TVG ID"
|
||||
value={formik.values.tvg_id}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.tvg_id && Boolean(formik.errors.tvg_id)}
|
||||
helperText={formik.touched.tvg_id && formik.errors.tvg_id}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<Grid2 size={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="tvg_name"
|
||||
name="tvg_name"
|
||||
label="TVG Name"
|
||||
value={formik.values.tvg_name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.tvg_name && Boolean(formik.errors.tvg_name)}
|
||||
helperText={formik.touched.tvg_name && formik.errors.tvg_name}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="tvg_id"
|
||||
name="tvg_id"
|
||||
label="TVG ID"
|
||||
value={formik.values.tvg_id}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.tvg_id && Boolean(formik.errors.tvg_id)}
|
||||
helperText={formik.touched.tvg_id && formik.errors.tvg_id}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<Box mb={2}>
|
||||
{/* File upload input */}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Typography>Logo</Typography>
|
||||
{/* Display selected image */}
|
||||
<Box mb={2}>
|
||||
{logo && (
|
||||
<Box mt={2} mb={2}>
|
||||
{/* File upload input */}
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography>Logo</Typography>
|
||||
{/* Display selected image */}
|
||||
<Box mb={2}>
|
||||
<img
|
||||
src={logoPreview}
|
||||
alt="Selected"
|
||||
style={{ maxWidth: 50, height: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="logo"
|
||||
name="logo"
|
||||
accept="image/*"
|
||||
onChange={(event) => handleLogoChange(event)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<label htmlFor="logo">
|
||||
<Button variant="contained" component="span" size="small">
|
||||
Browse...
|
||||
</Button>
|
||||
</label>
|
||||
</Box>
|
||||
</Box>
|
||||
<input
|
||||
type="file"
|
||||
id="logo"
|
||||
name="logo"
|
||||
accept="image/*"
|
||||
onChange={(event) => handleLogoChange(event)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<label htmlFor="logo">
|
||||
<Button variant="contained" component="span" size="small">
|
||||
Browse...
|
||||
</Button>
|
||||
</label>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<Box mb={2}>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={6}>
|
||||
<Typography>Active Streams</Typography>
|
||||
<MaterialReactTable table={activeStreamsTable} />
|
||||
</Grid2>
|
||||
|
||||
<Grid2 size={6}>
|
||||
<Typography>Available Streams</Typography>
|
||||
<MaterialReactTable table={availableStreamsTable} />
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</form>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={6}>
|
||||
<Typography>Active Streams</Typography>
|
||||
<MaterialReactTable table={activeStreamsTable} />
|
||||
</Grid2>
|
||||
|
||||
<Grid2 size={6}>
|
||||
<Typography>Available Streams</Typography>
|
||||
<MaterialReactTable table={availableStreamsTable} />
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Dialog>
|
||||
<ChannelGroupForm
|
||||
isOpen={channelGroupModelOpen}
|
||||
onClose={() => setChannelGroupModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: "1200px",
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default Channel;
|
||||
|
|
|
|||
94
frontend/src/components/forms/ChannelGroup.js
Normal file
94
frontend/src/components/forms/ChannelGroup.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Modal.js
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import API from "../../api";
|
||||
|
||||
const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: "",
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required("Name is required"),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (channelGroup?.id) {
|
||||
await API.updateChannelGroup({ id: channelGroup.id, ...values });
|
||||
} else {
|
||||
await API.addChannelGroup(values);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (channelGroup) {
|
||||
formik.setValues({
|
||||
name: channelGroup.name,
|
||||
});
|
||||
} else {
|
||||
formik.resetForm();
|
||||
}
|
||||
}, [channelGroup]);
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={onClose}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
>
|
||||
Channel Group
|
||||
</DialogTitle>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.name && Boolean(formik.errors.name)}
|
||||
helperText={formik.touched.name && formik.errors.name}
|
||||
variant="standard"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : "Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelGroup;
|
||||
|
|
@ -1,50 +1,67 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress } from "@mui/material";
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import API from "../../api"
|
||||
import {
|
||||
Box,
|
||||
Modal,
|
||||
Typography,
|
||||
Stack,
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid2,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import API from "../../api";
|
||||
import useEPGsStore from "../../store/epgs";
|
||||
|
||||
const EPG = ({ epg = null, isOpen, onClose }) => {
|
||||
const epgs = useEPGsStore(state => state.epgs)
|
||||
const [file, setFile] = useState(null)
|
||||
const epgs = useEPGsStore((state) => state.epgs);
|
||||
const [file, setFile] = useState(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setFile(file)
|
||||
setFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
source_type: '',
|
||||
url: '',
|
||||
api_key: '',
|
||||
name: "",
|
||||
source_type: "",
|
||||
url: "",
|
||||
api_key: "",
|
||||
is_active: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('Name is required'),
|
||||
source_type: Yup.string().required('Source type is required'),
|
||||
name: Yup.string().required("Name is required"),
|
||||
source_type: Yup.string().required("Source type is required"),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (epg?.id) {
|
||||
await API.updateEPG({id: epg.id, ...values, epg_file: file})
|
||||
await API.updateEPG({ id: epg.id, ...values, epg_file: file });
|
||||
} else {
|
||||
await API.addEPG({
|
||||
...values,
|
||||
epg_file: file,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setFile(null)
|
||||
setFile(null);
|
||||
setSubmitting(false);
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (epg) {
|
||||
|
|
@ -61,20 +78,21 @@ const EPG = ({ epg = null, isOpen, onClose }) => {
|
|||
}, [epg]);
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="form-modal-title" variant="h6" mb={2}>
|
||||
EPG Source
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Dialog open={isOpen} onClose={onClose}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
>
|
||||
EPG Source
|
||||
</DialogTitle>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="name"
|
||||
|
|
@ -101,7 +119,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => {
|
|||
variant="standard"
|
||||
/>
|
||||
|
||||
< TextField
|
||||
<TextField
|
||||
fullWidth
|
||||
id="api_key"
|
||||
name="api_key"
|
||||
|
|
@ -124,8 +142,12 @@ const EPG = ({ epg = null, isOpen, onClose }) => {
|
|||
value={formik.values.source_type}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.source_type && Boolean(formik.errors.source_type)}
|
||||
helperText={formik.touched.source_type && formik.errors.source_type}
|
||||
error={
|
||||
formik.touched.source_type && Boolean(formik.errors.source_type)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.source_type && formik.errors.source_type
|
||||
}
|
||||
variant="standard"
|
||||
>
|
||||
<MenuItem key="0" value="xmltv">
|
||||
|
|
@ -136,35 +158,23 @@ const EPG = ({ epg = null, isOpen, onClose }) => {
|
|||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
|
||||
<Box mb={2}>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Modal>
|
||||
<DialogActions>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : "Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default EPG;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom'; // For redirection
|
||||
import useAuthStore from '../../store/auth'
|
||||
import { Box, TextField, Button, Typography, Grid2, Paper } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useAuthStore from '../../store/auth';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Grid2,
|
||||
Paper,
|
||||
} from '@mui/material';
|
||||
|
||||
const LoginForm = () => {
|
||||
const { login, isAuthenticated } = useAuthStore(); // Get login function from AuthContext
|
||||
const { login, isAuthenticated, initData } = useAuthStore(); // Get login function from AuthContext
|
||||
const navigate = useNavigate(); // Hook to navigate to other routes
|
||||
const [formData, setFormData] = useState({ username: '', password: '' });
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/channels')
|
||||
navigate('/channels');
|
||||
}
|
||||
}, [isAuthenticated, navigate])
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setFormData({
|
||||
|
|
@ -23,7 +30,8 @@ const LoginForm = () => {
|
|||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
await login(formData)
|
||||
await login(formData);
|
||||
initData();
|
||||
navigate('/channels'); // Or any other route you'd like
|
||||
};
|
||||
|
||||
|
|
@ -52,7 +60,12 @@ const LoginForm = () => {
|
|||
Login
|
||||
</Typography>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Grid2 container spacing={2} justifyContent="center" direction="column">
|
||||
<Grid2
|
||||
container
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
direction="column"
|
||||
>
|
||||
<Grid2 item xs={12}>
|
||||
<TextField
|
||||
label="Username"
|
||||
|
|
|
|||
|
|
@ -1,51 +1,71 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress, FormControlLabel, Checkbox } from "@mui/material";
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import API from "../../api"
|
||||
import usePlaylistsStore from "../../store/playlists";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
CircularProgress,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import API from "../../api";
|
||||
import useUserAgentsStore from "../../store/userAgents";
|
||||
|
||||
const M3U = ({ playlist = null, isOpen, onClose }) => {
|
||||
const userAgents = useUserAgentsStore(state => state.userAgents)
|
||||
const [file, setFile] = useState(null)
|
||||
const userAgents = useUserAgentsStore((state) => state.userAgents);
|
||||
const [file, setFile] = useState(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setFile(file)
|
||||
setFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
server_url: '',
|
||||
name: "",
|
||||
server_url: "",
|
||||
max_streams: 0,
|
||||
user_agent: '',
|
||||
user_agent: "",
|
||||
is_active: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('Name is required'),
|
||||
user_agent: Yup.string().required('User-Agent is required'),
|
||||
name: Yup.string().required("Name is required"),
|
||||
user_agent: Yup.string().required("User-Agent is required"),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (playlist?.id) {
|
||||
await API.updatePlaylist({id: playlist.id, ...values, uploaded_file: file})
|
||||
await API.updatePlaylist({
|
||||
id: playlist.id,
|
||||
...values,
|
||||
uploaded_file: file,
|
||||
});
|
||||
} else {
|
||||
await API.addPlaylist({
|
||||
...values,
|
||||
uploaded_file: file,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setFile(null)
|
||||
setFile(null);
|
||||
setSubmitting(false);
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (playlist) {
|
||||
|
|
@ -62,20 +82,22 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
|
|||
}, [playlist]);
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="form-modal-title" variant="h6" mb={2}>
|
||||
M3U Account
|
||||
</Typography>
|
||||
<Dialog open={isOpen} onClose={onClose}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
>
|
||||
M3U Account
|
||||
</DialogTitle>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="name"
|
||||
|
|
@ -97,30 +119,38 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
|
|||
value={formik.values.server_url}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.server_url && Boolean(formik.errors.server_url)}
|
||||
error={
|
||||
formik.touched.server_url && Boolean(formik.errors.server_url)
|
||||
}
|
||||
helperText={formik.touched.server_url && formik.errors.server_url}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<Box mb={2}>
|
||||
{/* File upload input */}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
pt: 2,
|
||||
}}
|
||||
>
|
||||
<Typography>File</Typography>
|
||||
</Stack>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="uploaded_file"
|
||||
name="uploaded_file"
|
||||
accept="image/*"
|
||||
onChange={(event) => handleFileChange(event)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<label htmlFor="uploaded_file">
|
||||
<Button variant="contained" component="span">
|
||||
Browse...
|
||||
</Button>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="uploaded_file"
|
||||
name="uploaded_file"
|
||||
accept="image/*"
|
||||
onChange={(event) => handleFileChange(event)}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<label htmlFor="uploaded_file">
|
||||
<Button variant="contained" component="span">
|
||||
Browse...
|
||||
</Button>
|
||||
</label>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
|
|
@ -131,7 +161,9 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
|
|||
value={formik.values.max_streams}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.max_streams && Boolean(formik.errors.max_streams)}
|
||||
error={
|
||||
formik.touched.max_streams && Boolean(formik.errors.max_streams)
|
||||
}
|
||||
helperText={formik.touched.max_streams && formik.errors.max_streams}
|
||||
variant="standard"
|
||||
/>
|
||||
|
|
@ -146,7 +178,9 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
|
|||
value={formik.values.user_agent}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.user_agent && Boolean(formik.errors.user_agent)}
|
||||
error={
|
||||
formik.touched.user_agent && Boolean(formik.errors.user_agent)
|
||||
}
|
||||
helperText={formik.touched.user_agent && formik.errors.user_agent}
|
||||
variant="standard"
|
||||
>
|
||||
|
|
@ -163,39 +197,31 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
|
|||
<Checkbox
|
||||
name="is_active"
|
||||
checked={formik.values.is_active}
|
||||
onChange={(e) => formik.setFieldValue('is_active', e.target.checked)}
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue("is_active", e.target.checked)
|
||||
}
|
||||
/>
|
||||
} label="Is Active"
|
||||
}
|
||||
label="Is Active"
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<Box mb={2}>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Modal>
|
||||
<DialogActions>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : "Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default M3U;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress, IconButton } from "@mui/material";
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid2,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import useChannelsStore from "../../store/channels";
|
||||
import API from "../../api"
|
||||
import useStreamProfilesStore from "../../store/streamProfiles";
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import useStreamsStore from "../../store/streams";
|
||||
import usePlaylistsStore from "../../store/playlists";
|
||||
import { MaterialReactTable, useMaterialReactTable } from "material-react-table";
|
||||
import API from '../../api';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
|
||||
const Stream = ({ stream = null, isOpen, onClose }) => {
|
||||
const streamProfiles = useStreamProfilesStore((state) => state.profiles);
|
||||
console.log(stream)
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
|
|
@ -31,16 +35,16 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
|
|||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (stream?.id) {
|
||||
await API.updateStream({id: stream.id, ...values})
|
||||
await API.updateStream({ id: stream.id, ...values });
|
||||
} else {
|
||||
await API.addStream(values)
|
||||
await API.addStream(values);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (stream) {
|
||||
|
|
@ -55,20 +59,22 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
|
|||
}, [stream]);
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="form-modal-title" variant="h6" mb={2}>
|
||||
Stream
|
||||
</Typography>
|
||||
<Dialog open={isOpen} onClose={onClose}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
}}
|
||||
>
|
||||
Stream
|
||||
</DialogTitle>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={12}>
|
||||
<TextField
|
||||
|
|
@ -98,7 +104,9 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
|
|||
/>
|
||||
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="stream-profile-label">Stream Profile</InputLabel>
|
||||
<InputLabel id="stream-profile-label">
|
||||
Stream Profile
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="stream-profile-label"
|
||||
id="stream_profile_id"
|
||||
|
|
@ -107,7 +115,10 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
|
|||
value={formik.values.stream_profile_id}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.stream_profile_id && Boolean(formik.errors.stream_profile_id)}
|
||||
error={
|
||||
formik.touched.stream_profile_id &&
|
||||
Boolean(formik.errors.stream_profile_id)
|
||||
}
|
||||
// helperText={formik.touched.channel_group_id && formik.errors.stream_profile_id}
|
||||
variant="standard"
|
||||
>
|
||||
|
|
@ -120,33 +131,21 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
|
|||
</FormControl>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<Box mb={2}>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: "500px",
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default Stream;
|
||||
|
|
|
|||
|
|
@ -1,39 +1,51 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress, Checkbox, FormControlLabel } from "@mui/material";
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import API from "../../api"
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import API from "../../api";
|
||||
import useUserAgentsStore from "../../store/userAgents";
|
||||
|
||||
const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
||||
const userAgents = useUserAgentsStore(state => state.userAgents)
|
||||
const userAgents = useUserAgentsStore((state) => state.userAgents);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
profile_name: '',
|
||||
command: '',
|
||||
parameters: '',
|
||||
profile_name: "",
|
||||
command: "",
|
||||
parameters: "",
|
||||
is_active: true,
|
||||
user_agent: '',
|
||||
user_agent: "",
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
profile_name: Yup.string().required('Name is required'),
|
||||
command: Yup.string().required('Command is required'),
|
||||
parameters: Yup.string().required('Parameters are is required'),
|
||||
profile_name: Yup.string().required("Name is required"),
|
||||
command: Yup.string().required("Command is required"),
|
||||
parameters: Yup.string().required("Parameters are is required"),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (profile?.id) {
|
||||
await API.updateStreamProfile({id: profile.id, ...values})
|
||||
await API.updateStreamProfile({ id: profile.id, ...values });
|
||||
} else {
|
||||
await API.addStreamProfile(values)
|
||||
await API.addStreamProfile(values);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
|
|
@ -50,20 +62,22 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
}, [profile]);
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="form-modal-title" variant="h6" mb={2}>
|
||||
Stream Profile
|
||||
</Typography>
|
||||
<Dialog open={isOpen} onClose={onClose}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
>
|
||||
Stream Profile
|
||||
</DialogTitle>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="profile_name"
|
||||
|
|
@ -72,8 +86,12 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
value={formik.values.profile_name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.profile_name && Boolean(formik.errors.profile_name)}
|
||||
helperText={formik.touched.profile_name && formik.errors.profile_name}
|
||||
error={
|
||||
formik.touched.profile_name && Boolean(formik.errors.profile_name)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.profile_name && formik.errors.profile_name
|
||||
}
|
||||
variant="standard"
|
||||
/>
|
||||
<TextField
|
||||
|
|
@ -96,7 +114,9 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
value={formik.values.parameters}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.parameters && Boolean(formik.errors.parameters)}
|
||||
error={
|
||||
formik.touched.parameters && Boolean(formik.errors.parameters)
|
||||
}
|
||||
helperText={formik.touched.parameters && formik.errors.parameters}
|
||||
variant="standard"
|
||||
/>
|
||||
|
|
@ -111,7 +131,9 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
value={formik.values.user_agent}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.user_agent && Boolean(formik.errors.user_agent)}
|
||||
error={
|
||||
formik.touched.user_agent && Boolean(formik.errors.user_agent)
|
||||
}
|
||||
// helperText={formik.touched.user_agent && formik.errors.user_agent}
|
||||
variant="standard"
|
||||
>
|
||||
|
|
@ -122,35 +144,22 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
|
||||
<Box mb={2}>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Modal>
|
||||
<DialogActions>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : "Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default StreamProfile;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,43 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress, Checkbox } from "@mui/material";
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import API from "../../api"
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import API from "../../api";
|
||||
|
||||
const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
user_agent_name: '',
|
||||
user_agent: '',
|
||||
description: '',
|
||||
user_agent_name: "",
|
||||
user_agent: "",
|
||||
description: "",
|
||||
is_active: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
user_agent_name: Yup.string().required('Name is required'),
|
||||
user_agent: Yup.string().required('User-Agent is required'),
|
||||
user_agent_name: Yup.string().required("Name is required"),
|
||||
user_agent: Yup.string().required("User-Agent is required"),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (userAgent?.id) {
|
||||
await API.updateUserAgent({id: userAgent.id, ...values})
|
||||
await API.updateUserAgent({ id: userAgent.id, ...values });
|
||||
} else {
|
||||
await API.addUserAgent(values)
|
||||
await API.addUserAgent(values);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (userAgent) {
|
||||
|
|
@ -44,20 +53,22 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
}, [userAgent]);
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="form-modal-title" variant="h6" mb={2}>
|
||||
User-Agent
|
||||
</Typography>
|
||||
<Dialog open={isOpen} onClose={onClose}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
>
|
||||
User-Agent
|
||||
</DialogTitle>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="user_agent_name"
|
||||
|
|
@ -66,8 +77,13 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
value={formik.values.user_agent_name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.user_agent_name && Boolean(formik.errors.user_agent_name)}
|
||||
helperText={formik.touched.user_agent_name && formik.errors.user_agent_name}
|
||||
error={
|
||||
formik.touched.user_agent_name &&
|
||||
Boolean(formik.errors.user_agent_name)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.user_agent_name && formik.errors.user_agent_name
|
||||
}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
|
|
@ -79,7 +95,9 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
value={formik.values.user_agent}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.user_agent && Boolean(formik.errors.user_agent)}
|
||||
error={
|
||||
formik.touched.user_agent && Boolean(formik.errors.user_agent)
|
||||
}
|
||||
helperText={formik.touched.user_agent && formik.errors.user_agent}
|
||||
variant="standard"
|
||||
/>
|
||||
|
|
@ -92,7 +110,9 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
value={formik.values.description}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.description && Boolean(formik.errors.description)}
|
||||
error={
|
||||
formik.touched.description && Boolean(formik.errors.description)
|
||||
}
|
||||
helperText={formik.touched.description && formik.errors.description}
|
||||
variant="standard"
|
||||
/>
|
||||
|
|
@ -102,34 +122,22 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
checked={formik.values.is_active}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Box mb={2}>
|
||||
{/* Submit button */}
|
||||
<Button
|
||||
size="small"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
size="small"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : "Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default UserAgent;
|
||||
|
|
|
|||
|
|
@ -1,36 +1,46 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
MaterialReactTable,
|
||||
MRT_ShowHideColumnsButton,
|
||||
MRT_ToggleFullScreenButton,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { Box, Grid2, Stack, Typography, Tooltip, IconButton, Button, ButtonGroup, Snackbar, Popover, TextField } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Grid2,
|
||||
Stack,
|
||||
Typography,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Snackbar,
|
||||
Popover,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
Add as AddIcon,
|
||||
SwapVert as SwapVertIcon,
|
||||
} from '@mui/icons-material'
|
||||
import API from '../../api'
|
||||
import ChannelForm from '../forms/Channel'
|
||||
import { TableHelper } from '../../helpers'
|
||||
} from '@mui/icons-material';
|
||||
import API from '../../api';
|
||||
import ChannelForm from '../forms/Channel';
|
||||
import { TableHelper } from '../../helpers';
|
||||
import utils from '../../utils';
|
||||
import { ContentCopy } from '@mui/icons-material';
|
||||
|
||||
const Example = () => {
|
||||
const [channel, setChannel] = useState(null)
|
||||
const [channel, setChannel] = useState(null);
|
||||
const [channelModelOpen, setChannelModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([])
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [textToCopy, setTextToCopy] = useState('');
|
||||
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("")
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
const channels = useChannelsStore((state) => state.channels);
|
||||
const { channels, isLoading: channelsLoading } = useChannelsStore();
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
|
|
@ -46,7 +56,7 @@ const Example = () => {
|
|||
},
|
||||
{
|
||||
header: 'Group',
|
||||
accessorFn: row => row.channel_group?.name || '',
|
||||
accessorFn: (row) => row.channel_group?.name || '',
|
||||
},
|
||||
{
|
||||
header: 'Logo',
|
||||
|
|
@ -57,11 +67,11 @@ const Example = () => {
|
|||
container
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<img src={info.getValue() || "/images/logo.png"} width="20"/>
|
||||
<img src={info.getValue() || '/images/logo.png'} width="20" />
|
||||
</Grid2>
|
||||
),
|
||||
meta: {
|
||||
|
|
@ -69,7 +79,7 @@ const Example = () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
//optionally access the underlying virtualizer instance
|
||||
|
|
@ -79,32 +89,39 @@ const Example = () => {
|
|||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
const closeSnackbar = () => {
|
||||
setSnackbarOpen(false)
|
||||
}
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const editChannel = async (channel = null) => {
|
||||
setChannel(channel)
|
||||
setChannelModalOpen(true)
|
||||
}
|
||||
setChannel(channel);
|
||||
setChannelModalOpen(true);
|
||||
};
|
||||
|
||||
const deleteChannel = async (id) => {
|
||||
await API.deleteChannel(id)
|
||||
}
|
||||
await API.deleteChannel(id);
|
||||
};
|
||||
|
||||
// @TODO: the bulk delete endpoint is currently broken
|
||||
const deleteChannels = async () => {
|
||||
setIsLoading(true)
|
||||
const selected = table.getRowModel().rows.filter(row => row.getIsSelected())
|
||||
await utils.Limiter(4, selected.map(chan => () => {
|
||||
return deleteChannel(chan.original.id)
|
||||
}))
|
||||
setIsLoading(false)
|
||||
}
|
||||
setIsLoading(true);
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
await utils.Limiter(
|
||||
4,
|
||||
selected.map((chan) => () => {
|
||||
return deleteChannel(chan.original.id);
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const assignChannels = async () => {
|
||||
const selected = table.getRowModel().rows.filter(row => row.getIsSelected())
|
||||
await API.assignChannelNumbers(selected.map(sel => sel.id))
|
||||
}
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
await API.assignChannelNumbers(selected.map((sel) => sel.id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -134,25 +151,25 @@ const Example = () => {
|
|||
setSnackbarMessage('Failed to copy');
|
||||
}
|
||||
|
||||
setSnackbarOpen(true)
|
||||
setSnackbarOpen(true);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const copyM3UUrl = async (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setTextToCopy('m3u url')
|
||||
}
|
||||
setTextToCopy('m3u url');
|
||||
};
|
||||
|
||||
const copyEPGUrl = async (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setTextToCopy('epg url')
|
||||
}
|
||||
setTextToCopy('epg url');
|
||||
};
|
||||
|
||||
const copyHDHRUrl = async (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setTextToCopy('hdhr url')
|
||||
}
|
||||
setTextToCopy('hdhr url');
|
||||
};
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
|
|
@ -166,7 +183,7 @@ const Example = () => {
|
|||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading,
|
||||
isLoading: isLoading || channelsLoading,
|
||||
sorting,
|
||||
rowSelection,
|
||||
},
|
||||
|
|
@ -182,7 +199,7 @@ const Example = () => {
|
|||
size="small" // Makes the button smaller
|
||||
color="warning" // Red color for delete actions
|
||||
onClick={() => {
|
||||
editChannel(row.original)
|
||||
editChannel(row.original);
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" /> {/* Small icon size */}
|
||||
|
|
@ -198,17 +215,20 @@ const Example = () => {
|
|||
),
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: 'calc(100vh - 100px)', // Subtract padding to avoid cutoff
|
||||
height: 'calc(100vh - 90px)', // Subtract padding to avoid cutoff
|
||||
overflowY: 'auto', // Internal scrolling for the table
|
||||
},
|
||||
},
|
||||
muiSearchTextFieldProps: {
|
||||
variant: "standard",
|
||||
variant: 'standard',
|
||||
},
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack direction="row" sx={{
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography>Channels</Typography>
|
||||
<Tooltip title="Add New Channel">
|
||||
<IconButton
|
||||
|
|
@ -241,21 +261,20 @@ const Example = () => {
|
|||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<ButtonGroup sx={{
|
||||
marginLeft: 1,
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={copyHDHRUrl}
|
||||
>HDHR URL</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={copyM3UUrl}
|
||||
>M3U URL</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={copyEPGUrl}
|
||||
>EPG</Button>
|
||||
<ButtonGroup
|
||||
sx={{
|
||||
marginLeft: 1,
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" onClick={copyHDHRUrl}>
|
||||
HDHR URL
|
||||
</Button>
|
||||
<Button variant="contained" onClick={copyM3UUrl}>
|
||||
M3U URL
|
||||
</Button>
|
||||
<Button variant="contained" onClick={copyEPGUrl}>
|
||||
EPG
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
),
|
||||
|
|
@ -282,7 +301,6 @@ const Example = () => {
|
|||
<div style={{ padding: '16px', display: 'flex', alignItems: 'center' }}>
|
||||
<TextField
|
||||
value={textToCopy}
|
||||
InputProps={{ readOnly: true }}
|
||||
variant="standard"
|
||||
disabled
|
||||
size="small"
|
||||
|
|
@ -296,7 +314,7 @@ const Example = () => {
|
|||
</Popover>
|
||||
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right"}}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={5000}
|
||||
onClose={closeSnackbar}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
MaterialReactTable,
|
||||
MRT_ShowHideColumnsButton,
|
||||
MRT_ToggleFullScreenButton,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Checkbox, Select, MenuItem, Snackbar } from '@mui/material';
|
||||
import API from '../../api'
|
||||
} from "material-react-table";
|
||||
import {
|
||||
Box,
|
||||
Grid2,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
Select,
|
||||
MenuItem,
|
||||
Snackbar,
|
||||
} from "@mui/material";
|
||||
import API from "../../api";
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
|
|
@ -15,35 +26,36 @@ import {
|
|||
Check as CheckIcon,
|
||||
Close as CloseIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material'
|
||||
import useEPGsStore from '../../store/epgs';
|
||||
import EPGForm from '../forms/EPG'
|
||||
} from "@mui/icons-material";
|
||||
import useEPGsStore from "../../store/epgs";
|
||||
import EPGForm from "../forms/EPG";
|
||||
import { TableHelper } from "../../helpers";
|
||||
|
||||
const EPGsTable = () => {
|
||||
const [epg, setEPG] = useState(null);
|
||||
const [epgModalOpen, setEPGModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([])
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("")
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false)
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
const epgs = useEPGsStore(state => state.epgs)
|
||||
const epgs = useEPGsStore((state) => state.epgs);
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
header: "Name",
|
||||
size: 10,
|
||||
accessorKey: 'name',
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: 'Source Type',
|
||||
accessorKey: 'source_type',
|
||||
header: "Source Type",
|
||||
accessorKey: "source_type",
|
||||
size: 50,
|
||||
},
|
||||
{
|
||||
header: 'URL / API Key',
|
||||
accessorKey: 'max_streams',
|
||||
header: "URL / API Key",
|
||||
accessorKey: "max_streams",
|
||||
},
|
||||
],
|
||||
[],
|
||||
|
|
@ -56,26 +68,26 @@ const EPGsTable = () => {
|
|||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
const closeSnackbar = () => {
|
||||
setSnackbarOpen(false)
|
||||
}
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const editEPG = async (epg = null) => {
|
||||
setEPG(epg)
|
||||
setEPGModalOpen(true)
|
||||
}
|
||||
setEPG(epg);
|
||||
setEPGModalOpen(true);
|
||||
};
|
||||
|
||||
const deleteEPG = async (id) => {
|
||||
await API.deleteEPG(id)
|
||||
}
|
||||
await API.deleteEPG(id);
|
||||
};
|
||||
|
||||
const refreshEPG = async (id) => {
|
||||
await API.refreshEPG(id)
|
||||
setSnackbarMessage("EPG refresh initiated")
|
||||
setSnackbarOpen(true)
|
||||
}
|
||||
await API.refreshEPG(id);
|
||||
setSnackbarMessage("EPG refresh initiated");
|
||||
setSnackbarOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -90,13 +102,10 @@ const EPGsTable = () => {
|
|||
}, [sorting]);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
data: epgs,
|
||||
enableBottomToolbar: false,
|
||||
// enableGlobalFilterModes: true,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
// enableRowNumbers: true,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
|
|
@ -109,7 +118,7 @@ const EPGsTable = () => {
|
|||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
density: "compact",
|
||||
},
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
|
|
@ -137,19 +146,18 @@ const EPGsTable = () => {
|
|||
</IconButton>
|
||||
</>
|
||||
),
|
||||
positionActionsColumn: 'last',
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: "calc(42vh - 0px)",
|
||||
},
|
||||
},
|
||||
renderTopToolbar: ({ table }) => (
|
||||
<Grid2 container direction="row" spacing={3} sx={{
|
||||
justifyContent: "left",
|
||||
alignItems: "center",
|
||||
// height: 30,
|
||||
ml: 2,
|
||||
}}>
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>EPGs</Typography>
|
||||
<Tooltip title="Add New EPG">
|
||||
<IconButton
|
||||
|
|
@ -161,19 +169,18 @@ const EPGsTable = () => {
|
|||
<AddIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<MRT_ShowHideColumnsButton table={table} />
|
||||
{/* <MRT_ToggleFullScreenButton table={table} /> */}
|
||||
</Grid2>
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
}}
|
||||
>
|
||||
<MaterialReactTable table={table} />
|
||||
|
||||
<EPGForm
|
||||
epg={epg}
|
||||
isOpen={epgModalOpen}
|
||||
|
|
@ -181,13 +188,13 @@ const EPGsTable = () => {
|
|||
/>
|
||||
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right"}}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={5000}
|
||||
onClose={closeSnackbar}
|
||||
message={snackbarMessage}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import React from 'react';
|
||||
import { TableCell, TextField } from '@mui/material'
|
||||
|
||||
const Filter = ({ column }) => {
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
const { filterVariant } = column.columnDef.meta ?? {}
|
||||
|
||||
if (filterVariant === null) {
|
||||
return <TableCell>{column.columnDef.header}</TableCell>
|
||||
}
|
||||
|
||||
return filterVariant === 'range' ? (
|
||||
<div>
|
||||
<div className="flex space-x-2">
|
||||
{/* See faceted column filters example for min max values functionality */}
|
||||
<DebouncedInput
|
||||
type="number"
|
||||
value={(columnFilterValue)?.[0] ?? ''}
|
||||
onChange={value =>
|
||||
column.setFilterValue((old) => [value, old?.[1]])
|
||||
}
|
||||
placeholder={`Min`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
<DebouncedInput
|
||||
type="number"
|
||||
value={(columnFilterValue)?.[1] ?? ''}
|
||||
onChange={value =>
|
||||
column.setFilterValue((old) => [old?.[0], value])
|
||||
}
|
||||
placeholder={`Max`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-1" />
|
||||
</div>
|
||||
) : filterVariant === 'select' ? (
|
||||
<select
|
||||
onChange={e => column.setFilterValue(e.target.value)}
|
||||
value={columnFilterValue?.toString()}
|
||||
>
|
||||
{/* See faceted column filters example for dynamic select options */}
|
||||
<option value="">All</option>
|
||||
<option value="complicated">complicated</option>
|
||||
<option value="relationship">relationship</option>
|
||||
<option value="single">single</option>
|
||||
</select>
|
||||
) : (
|
||||
<DebouncedInput
|
||||
className="w-36 border shadow rounded"
|
||||
onChange={value => column.setFilterValue(value)}
|
||||
placeholder={column.columnDef.header}
|
||||
type="text"
|
||||
value={(columnFilterValue ?? '')}
|
||||
/>
|
||||
// See faceted column filters example for datalist search suggestions
|
||||
)
|
||||
}
|
||||
|
||||
// A typical debounced input react component
|
||||
const DebouncedInput = ({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
debounce = 500,
|
||||
...props
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value)
|
||||
}, debounce)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<TextField size="small" variant="standard" {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Filter;
|
||||
|
|
@ -1,12 +1,22 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
MaterialReactTable,
|
||||
MRT_ShowHideColumnsButton,
|
||||
MRT_ToggleFullScreenButton,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Checkbox, Select, MenuItem } from '@mui/material';
|
||||
import API from '../../api'
|
||||
} from "material-react-table";
|
||||
import {
|
||||
Box,
|
||||
Grid2,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import API from "../../api";
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
|
|
@ -15,15 +25,16 @@ import {
|
|||
Check as CheckIcon,
|
||||
Close as CloseIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material'
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import M3UForm from '../forms/M3U'
|
||||
} from "@mui/icons-material";
|
||||
import usePlaylistsStore from "../../store/playlists";
|
||||
import M3UForm from "../forms/M3U";
|
||||
import { TableHelper } from "../../helpers";
|
||||
|
||||
const Example = () => {
|
||||
const [playlist, setPlaylist] = useState(null);
|
||||
const [playlistModalOpen, setPlaylistModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([])
|
||||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [activeFilterValue, setActiveFilterValue] = useState("all");
|
||||
|
||||
const playlists = usePlaylistsStore((state) => state.playlists);
|
||||
|
||||
|
|
@ -31,29 +42,33 @@ const Example = () => {
|
|||
//column definitions...
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: 'URL / File',
|
||||
accessorKey: 'server_url',
|
||||
header: "URL / File",
|
||||
accessorKey: "server_url",
|
||||
},
|
||||
{
|
||||
header: 'Max Streams',
|
||||
accessorKey: 'max_streams',
|
||||
header: "Max Streams",
|
||||
accessorKey: "max_streams",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
header: "Active",
|
||||
accessorKey: "is_active",
|
||||
size: 100,
|
||||
sortingFn: 'basic',
|
||||
sortingFn: "basic",
|
||||
muiTableBodyCellProps: {
|
||||
align: 'left',
|
||||
align: "left",
|
||||
},
|
||||
Cell: ({ cell }) => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{cell.getValue() ? <CheckIcon color="success" /> : <CloseIcon color="error" />}
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
{cell.getValue() ? (
|
||||
<CheckIcon color="success" />
|
||||
) : (
|
||||
<CloseIcon color="error" />
|
||||
)}
|
||||
</Box>
|
||||
),
|
||||
Filter: ({ column }) => (
|
||||
|
|
@ -77,7 +92,7 @@ const Example = () => {
|
|||
),
|
||||
filterFn: (row, _columnId, activeFilterValue) => {
|
||||
if (!activeFilterValue) return true; // Show all if no filter
|
||||
return String(row.getValue('is_active')) === activeFilterValue;
|
||||
return String(row.getValue("is_active")) === activeFilterValue;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -91,25 +106,27 @@ const Example = () => {
|
|||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
const editPlaylist = async (playlist = null) => {
|
||||
setPlaylist(playlist)
|
||||
setPlaylistModalOpen(true)
|
||||
}
|
||||
setPlaylist(playlist);
|
||||
setPlaylistModalOpen(true);
|
||||
};
|
||||
|
||||
const refreshPlaylist = async (id) => {
|
||||
await API.refreshPlaylist(id)
|
||||
}
|
||||
await API.refreshPlaylist(id);
|
||||
};
|
||||
|
||||
const deletePlaylist = async (id) => {
|
||||
await API.deletePlaylist(id)
|
||||
}
|
||||
await API.deletePlaylist(id);
|
||||
};
|
||||
|
||||
const deletePlaylists = async (ids) => {
|
||||
const selected = table.getRowModel().rows.filter(row => row.getIsSelected())
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
// await API.deleteStreams(selected.map(stream => stream.original.id))
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -124,13 +141,10 @@ const Example = () => {
|
|||
}, [sorting]);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
data: playlists,
|
||||
enableBottomToolbar: false,
|
||||
// enableGlobalFilterModes: true,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
// enableRowNumbers: true,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
|
|
@ -143,7 +157,7 @@ const Example = () => {
|
|||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
density: "compact",
|
||||
},
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
|
|
@ -152,7 +166,7 @@ const Example = () => {
|
|||
size="small" // Makes the button smaller
|
||||
color="warning" // Red color for delete actions
|
||||
onClick={() => {
|
||||
editPlaylist(row.original)
|
||||
editPlaylist(row.original);
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" /> {/* Small icon size */}
|
||||
|
|
@ -174,19 +188,18 @@ const Example = () => {
|
|||
</IconButton>
|
||||
</>
|
||||
),
|
||||
positionActionsColumn: 'last',
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: "calc(42vh - 0px)",
|
||||
},
|
||||
},
|
||||
renderTopToolbar: ({ table }) => (
|
||||
<Grid2 container direction="row" spacing={3} sx={{
|
||||
justifyContent: "left",
|
||||
alignItems: "center",
|
||||
// height: 30,
|
||||
ml: 2,
|
||||
}}>
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>M3U Accounts</Typography>
|
||||
<Tooltip title="Add New M3U Account">
|
||||
<IconButton
|
||||
|
|
@ -198,25 +211,23 @@ const Example = () => {
|
|||
<AddIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<MRT_ShowHideColumnsButton table={table} />
|
||||
{/* <MRT_ToggleFullScreenButton table={table} /> */}
|
||||
</Grid2>
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
}}
|
||||
>
|
||||
<MaterialReactTable table={table} />
|
||||
<M3UForm
|
||||
playlist={playlist}
|
||||
isOpen={playlistModalOpen}
|
||||
onClose={() => setPlaylistModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
MaterialReactTable,
|
||||
MRT_ShowHideColumnsButton,
|
||||
MRT_ToggleFullScreenButton,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Checkbox, Select, MenuItem } from '@mui/material';
|
||||
import API from '../../api'
|
||||
} from "material-react-table";
|
||||
import {
|
||||
Box,
|
||||
Grid2,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import API from "../../api";
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
|
|
@ -15,47 +25,52 @@ import {
|
|||
Check as CheckIcon,
|
||||
Close as CloseIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material'
|
||||
import useEPGsStore from '../../store/epgs';
|
||||
import StreamProfileForm from '../forms/StreamProfile'
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
} from "@mui/icons-material";
|
||||
import useEPGsStore from "../../store/epgs";
|
||||
import StreamProfileForm from "../forms/StreamProfile";
|
||||
import useStreamProfilesStore from "../../store/streamProfiles";
|
||||
import { TableHelper } from "../../helpers";
|
||||
|
||||
const StreamProfiles = () => {
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([])
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("")
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false)
|
||||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [activeFilterValue, setActiveFilterValue] = useState("all");
|
||||
|
||||
const streamProfiles = useStreamProfilesStore(state => state.profiles)
|
||||
const streamProfiles = useStreamProfilesStore((state) => state.profiles);
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'profile_name',
|
||||
header: "Name",
|
||||
accessorKey: "profile_name",
|
||||
},
|
||||
{
|
||||
header: 'Command',
|
||||
accessorKey: 'command',
|
||||
header: "Command",
|
||||
accessorKey: "command",
|
||||
},
|
||||
{
|
||||
header: 'Parameters',
|
||||
accessorKey: 'parameters',
|
||||
header: "Parameters",
|
||||
accessorKey: "parameters",
|
||||
},
|
||||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
header: "Active",
|
||||
accessorKey: "is_active",
|
||||
size: 100,
|
||||
sortingFn: 'basic',
|
||||
sortingFn: "basic",
|
||||
muiTableBodyCellProps: {
|
||||
align: 'left',
|
||||
align: "left",
|
||||
},
|
||||
Cell: ({ cell }) => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{cell.getValue() ? <CheckIcon color="success" /> : <CloseIcon color="error" />}
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
{cell.getValue() ? (
|
||||
<CheckIcon color="success" />
|
||||
) : (
|
||||
<CloseIcon color="error" />
|
||||
)}
|
||||
</Box>
|
||||
),
|
||||
Filter: ({ column }) => (
|
||||
|
|
@ -79,7 +94,7 @@ const StreamProfiles = () => {
|
|||
),
|
||||
filterFn: (row, _columnId, filterValue) => {
|
||||
if (filterValue == "all") return true; // Show all if no filter
|
||||
return String(row.getValue('is_active')) === filterValue;
|
||||
return String(row.getValue("is_active")) === filterValue;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -93,16 +108,16 @@ const StreamProfiles = () => {
|
|||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
const editStreamProfile = async (profile = null) => {
|
||||
setProfile(profile)
|
||||
setProfileModalOpen(true)
|
||||
}
|
||||
setProfile(profile);
|
||||
setProfileModalOpen(true);
|
||||
};
|
||||
|
||||
const deleteStreamProfile = async (ids) => {
|
||||
await API.deleteStreamProfile(ids)
|
||||
}
|
||||
await API.deleteStreamProfile(ids);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -117,13 +132,10 @@ const StreamProfiles = () => {
|
|||
}, [sorting]);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
data: streamProfiles,
|
||||
enableBottomToolbar: false,
|
||||
// enableGlobalFilterModes: true,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
// enableRowNumbers: true,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
|
|
@ -136,7 +148,7 @@ const StreamProfiles = () => {
|
|||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
density: "compact",
|
||||
},
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
|
|
@ -157,19 +169,19 @@ const StreamProfiles = () => {
|
|||
</IconButton>
|
||||
</>
|
||||
),
|
||||
positionActionsColumn: 'last',
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
// height: "calc(42vh - 0px)",
|
||||
height: "calc(100vh - 100px)", // Subtract padding to avoid cutoff
|
||||
overflowY: "auto", // Internal scrolling for the table
|
||||
},
|
||||
},
|
||||
renderTopToolbar: ({ table }) => (
|
||||
<Grid2 container direction="row" spacing={3} sx={{
|
||||
justifyContent: "left",
|
||||
alignItems: "center",
|
||||
// height: 30,
|
||||
ml: 2,
|
||||
}}>
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>Stream Profiles</Typography>
|
||||
<Tooltip title="Add New Stream Profile">
|
||||
<IconButton
|
||||
|
|
@ -181,26 +193,24 @@ const StreamProfiles = () => {
|
|||
<AddIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<MRT_ShowHideColumnsButton table={table} />
|
||||
{/* <MRT_ToggleFullScreenButton table={table} /> */}
|
||||
</Grid2>
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
}}
|
||||
>
|
||||
<MaterialReactTable table={table} />
|
||||
|
||||
<StreamProfileForm
|
||||
profile={profile}
|
||||
isOpen={profileModalOpen}
|
||||
onClose={() => setProfileModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,23 +3,32 @@ import {
|
|||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Button } from '@mui/material';
|
||||
import useStreamsStore from '../../store/streams'
|
||||
import API from '../../api'
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import API from '../../api';
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
Add as AddIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { TableHelper } from '../../helpers'
|
||||
} from '@mui/icons-material';
|
||||
import { TableHelper } from '../../helpers';
|
||||
import utils from '../../utils';
|
||||
import StreamForm from '../forms/Stream'
|
||||
import StreamForm from '../forms/Stream';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
|
||||
const Example = () => {
|
||||
const [rowSelection, setRowSelection] = useState([])
|
||||
const [stream, setStream] = useState(null)
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [stream, setStream] = useState(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const streams = useStreamsStore((state) => state.streams);
|
||||
const { streams, isLoading: streamsLoading } = useStreamsStore();
|
||||
const { playlists } = usePlaylistsStore();
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
|
|
@ -32,8 +41,14 @@ const Example = () => {
|
|||
header: 'Group',
|
||||
accessorKey: 'group_name',
|
||||
},
|
||||
{
|
||||
header: 'M3U',
|
||||
size: 100,
|
||||
accessorFn: (row) =>
|
||||
playlists.find((playlist) => playlist.id === row.m3u_account)?.name,
|
||||
},
|
||||
],
|
||||
[],
|
||||
[playlists]
|
||||
);
|
||||
|
||||
//optionally access the underlying virtualizer instance
|
||||
|
|
@ -47,32 +62,39 @@ const Example = () => {
|
|||
channel_name: stream.name,
|
||||
channel_number: 0,
|
||||
stream_id: stream.id,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// @TODO: bulk create is broken, returning a 404
|
||||
const createChannelsFromStreams = async () => {
|
||||
setIsLoading(true)
|
||||
const selected = table.getRowModel().rows.filter(row => row.getIsSelected())
|
||||
await utils.Limiter(4, selected.map(stream => () => {
|
||||
return createChannelFromStream(stream.original)
|
||||
}))
|
||||
setIsLoading(false)
|
||||
}
|
||||
setIsLoading(true);
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
await utils.Limiter(
|
||||
4,
|
||||
selected.map((stream) => () => {
|
||||
return createChannelFromStream(stream.original);
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const editStream = async (stream = null) => {
|
||||
setStream(stream)
|
||||
setModalOpen(true)
|
||||
}
|
||||
setStream(stream);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const deleteStream = async (id) => {
|
||||
await API.deleteStream(id)
|
||||
}
|
||||
await API.deleteStream(id);
|
||||
};
|
||||
|
||||
const deleteStreams = async () => {
|
||||
const selected = table.getRowModel().rows.filter(row => row.getIsSelected())
|
||||
await API.deleteStreams(selected.map(stream => stream.original.id))
|
||||
}
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
await API.deleteStreams(selected.map((stream) => stream.original.id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -101,7 +123,7 @@ const Example = () => {
|
|||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading,
|
||||
isLoading: isLoading || streamsLoading,
|
||||
sorting,
|
||||
rowSelection,
|
||||
},
|
||||
|
|
@ -110,13 +132,20 @@ const Example = () => {
|
|||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<IconButton
|
||||
size="small" // Makes the button smaller
|
||||
color="warning" // Red color for delete actions
|
||||
onClick={() => editStream(row.original)}
|
||||
<Tooltip
|
||||
title={
|
||||
row.original.m3u_account ? 'M3U streams locked' : 'Edit Stream'
|
||||
}
|
||||
>
|
||||
<EditIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small" // Makes the button smaller
|
||||
color="warning" // Red color for delete actions
|
||||
onClick={() => editStream(row.original)}
|
||||
disabled={row.original.m3u_account}
|
||||
>
|
||||
<EditIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size="small" // Makes the button smaller
|
||||
color="error" // Red color for delete actions
|
||||
|
|
@ -135,14 +164,17 @@ const Example = () => {
|
|||
),
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: 'calc(100vh - 100px)', // Subtract padding to avoid cutoff
|
||||
height: 'calc(100vh - 90px)', // Subtract padding to avoid cutoff
|
||||
overflowY: 'auto', // Internal scrolling for the table
|
||||
},
|
||||
},
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack direction="row" sx={{
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography>Streams</Typography>
|
||||
<Tooltip title="Add New Stream">
|
||||
<IconButton
|
||||
|
|
@ -171,18 +203,24 @@ const Example = () => {
|
|||
sx={{
|
||||
marginLeft: 1,
|
||||
}}
|
||||
>Create Channels</Button>
|
||||
>
|
||||
Create Channels
|
||||
</Button>
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
// paddingTop: 2,
|
||||
// paddingLeft: 1,
|
||||
// paddingRight: 2,
|
||||
// paddingBottom: 2,
|
||||
}}>
|
||||
<Box
|
||||
sx={
|
||||
{
|
||||
// paddingTop: 2,
|
||||
// paddingLeft: 1,
|
||||
// paddingRight: 2,
|
||||
// paddingBottom: 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
<MaterialReactTable table={table} />
|
||||
<StreamForm
|
||||
stream={stream}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,72 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
MaterialReactTable,
|
||||
MRT_ShowHideColumnsButton,
|
||||
MRT_ToggleFullScreenButton,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Select, MenuItem } from '@mui/material';
|
||||
import API from '../../api'
|
||||
} from "material-react-table";
|
||||
import {
|
||||
Box,
|
||||
Grid2,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import API from "../../api";
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Edit as EditIcon,
|
||||
Add as AddIcon,
|
||||
Check as CheckIcon,
|
||||
Close as CloseIcon,
|
||||
} from '@mui/icons-material'
|
||||
import useUserAgentsStore from '../../store/userAgents';
|
||||
import UserAgentForm from '../forms/UserAgent'
|
||||
} from "@mui/icons-material";
|
||||
import useUserAgentsStore from "../../store/userAgents";
|
||||
import UserAgentForm from "../forms/UserAgent";
|
||||
import { TableHelper } from "../../helpers";
|
||||
|
||||
const UserAgentsTable = () => {
|
||||
const [userAgent, setUserAgent] = useState(null);
|
||||
const [userAgentModalOpen, setUserAgentModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([])
|
||||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [activeFilterValue, setActiveFilterValue] = useState("all");
|
||||
|
||||
const userAgents = useUserAgentsStore(state => state.userAgents)
|
||||
const userAgents = useUserAgentsStore((state) => state.userAgents);
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
header: "Name",
|
||||
size: 10,
|
||||
accessorKey: 'user_agent_name',
|
||||
accessorKey: "user_agent_name",
|
||||
},
|
||||
{
|
||||
header: 'User-Agent',
|
||||
accessorKey: 'user_agent',
|
||||
header: "User-Agent",
|
||||
accessorKey: "user_agent",
|
||||
size: 50,
|
||||
},
|
||||
{
|
||||
header: 'Desecription',
|
||||
accessorKey: 'description',
|
||||
header: "Desecription",
|
||||
accessorKey: "description",
|
||||
},
|
||||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
header: "Active",
|
||||
accessorKey: "is_active",
|
||||
size: 100,
|
||||
sortingFn: 'basic',
|
||||
sortingFn: "basic",
|
||||
muiTableBodyCellProps: {
|
||||
align: 'left',
|
||||
align: "left",
|
||||
},
|
||||
Cell: ({ cell }) => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{cell.getValue() ? <CheckIcon color="success" /> : <CloseIcon color="error" />}
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
{cell.getValue() ? (
|
||||
<CheckIcon color="success" />
|
||||
) : (
|
||||
<CloseIcon color="error" />
|
||||
)}
|
||||
</Box>
|
||||
),
|
||||
Filter: ({ column }) => (
|
||||
|
|
@ -76,7 +90,7 @@ const UserAgentsTable = () => {
|
|||
),
|
||||
filterFn: (row, _columnId, activeFilterValue) => {
|
||||
if (activeFilterValue == "all") return true; // Show all if no filter
|
||||
return String(row.getValue('is_active')) === activeFilterValue;
|
||||
return String(row.getValue("is_active")) === activeFilterValue;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -90,20 +104,20 @@ const UserAgentsTable = () => {
|
|||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
const editUserAgent = async (userAgent = null) => {
|
||||
setUserAgent(userAgent)
|
||||
setUserAgentModalOpen(true)
|
||||
}
|
||||
setUserAgent(userAgent);
|
||||
setUserAgentModalOpen(true);
|
||||
};
|
||||
|
||||
const deleteUserAgent = async (ids) => {
|
||||
if (Array.isArray(ids)) {
|
||||
await API.deleteUserAgents(ids)
|
||||
await API.deleteUserAgents(ids);
|
||||
} else {
|
||||
await API.deleteUserAgent(ids)
|
||||
await API.deleteUserAgent(ids);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -118,13 +132,10 @@ const UserAgentsTable = () => {
|
|||
}, [sorting]);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
data: userAgents,
|
||||
enableBottomToolbar: false,
|
||||
// enableGlobalFilterModes: true,
|
||||
columnFilterDisplayMode: 'popover',
|
||||
enablePagination: false,
|
||||
// enableRowNumbers: true,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
|
|
@ -137,7 +148,7 @@ const UserAgentsTable = () => {
|
|||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
density: "compact",
|
||||
},
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
|
|
@ -146,7 +157,7 @@ const UserAgentsTable = () => {
|
|||
size="small" // Makes the button smaller
|
||||
color="warning" // Red color for delete actions
|
||||
onClick={() => {
|
||||
editUserAgent(row.original)
|
||||
editUserAgent(row.original);
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" /> {/* Small icon size */}
|
||||
|
|
@ -160,19 +171,18 @@ const UserAgentsTable = () => {
|
|||
</IconButton>
|
||||
</>
|
||||
),
|
||||
positionActionsColumn: 'last',
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
height: "calc(42vh - 0px)",
|
||||
height: "calc(42vh - 10px)",
|
||||
},
|
||||
},
|
||||
renderTopToolbar: ({ table }) => (
|
||||
<Grid2 container direction="row" spacing={3} sx={{
|
||||
justifyContent: "left",
|
||||
alignItems: "center",
|
||||
// height: 30,
|
||||
ml: 2,
|
||||
}}>
|
||||
renderTopToolbarCustomActions: ({ table }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>User-Agents</Typography>
|
||||
<Tooltip title="Add New User Agent">
|
||||
<IconButton
|
||||
|
|
@ -184,17 +194,17 @@ const UserAgentsTable = () => {
|
|||
<AddIcon fontSize="small" /> {/* Small icon size */}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<MRT_ShowHideColumnsButton table={table} />
|
||||
{/* <MRT_ToggleFullScreenButton table={table} /> */}
|
||||
</Grid2>
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{
|
||||
padding: 2,
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
<UserAgentForm
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
import table from "./table"
|
||||
import table from "./table";
|
||||
|
||||
export const TableHelper = table
|
||||
export const TableHelper = table;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ export default {
|
|||
enableDensityToggle: false,
|
||||
enableFullScreenToggle: false,
|
||||
positionToolbarAlertBanner: "none",
|
||||
columnFilterDisplayMode: 'popover',
|
||||
columnFilterDisplayMode: "popover",
|
||||
enableRowNumbers: false,
|
||||
positionActionsColumn: 'last',
|
||||
positionActionsColumn: "last",
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
density: "compact",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,41 +1,11 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client'; // Import the "react-dom/client" for React 18
|
||||
import './index.css'; // Optional styles
|
||||
import App from './App'; // Import your App component
|
||||
import useAuthStore from './store/auth';
|
||||
import useChannelsStore from './store/channels';
|
||||
import useStreamsStore from './store/streams';
|
||||
import useUserAgentsStore from './store/userAgents';
|
||||
import usePlaylistsStore from './store/playlists';
|
||||
import useEPGsStore from './store/epgs';
|
||||
import useStreamProfilesStore from './store/streamProfiles';
|
||||
import './index.css'; // Optional styles
|
||||
import App from './App'; // Import your App component
|
||||
|
||||
// Create a root element
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
const authStore = useAuthStore.getState();
|
||||
const channelsStore = useChannelsStore.getState();
|
||||
const streamsStore = useStreamsStore.getState();
|
||||
const userAgentsStore = useUserAgentsStore.getState();
|
||||
const playlistsStore = usePlaylistsStore.getState();
|
||||
const epgsStore = useEPGsStore.getState()
|
||||
const streamProfilesStore = useStreamProfilesStore.getState()
|
||||
|
||||
await authStore.initializeAuth();
|
||||
|
||||
// if (authStore.isAuthenticated) {
|
||||
await Promise.all([
|
||||
authStore.initializeAuth(),
|
||||
channelsStore.fetchChannels(),
|
||||
channelsStore.fetchChannelGroups(),
|
||||
streamsStore.fetchStreams(),
|
||||
userAgentsStore.fetchUserAgents(),
|
||||
playlistsStore.fetchPlaylists(),
|
||||
epgsStore.fetchEPGs(),
|
||||
streamProfilesStore.fetchProfiles(),
|
||||
])
|
||||
// }
|
||||
|
||||
// Render your app using the "root.render" method
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import ChannelsTable from '../components/tables/ChannelsTable';
|
||||
import StreamsTable from '../components/tables/StreamsTable';
|
||||
import { Grid2, Box } from '@mui/material';
|
||||
import React from "react";
|
||||
import ChannelsTable from "../components/tables/ChannelsTable";
|
||||
import StreamsTable from "../components/tables/StreamsTable";
|
||||
import { Grid2, Box } from "@mui/material";
|
||||
|
||||
const ChannelsPage = () => {
|
||||
return (
|
||||
|
|
@ -9,13 +9,13 @@ const ChannelsPage = () => {
|
|||
<Grid2 size={6}>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh', // Full viewport height
|
||||
paddingTop: '20px', // Top padding
|
||||
paddingBottom: '20px', // Bottom padding
|
||||
paddingRight: '10px',
|
||||
paddingLeft: '20px',
|
||||
boxSizing: 'border-box', // Include padding in height calculation
|
||||
overflow: 'hidden', // Prevent parent scrolling
|
||||
height: "100vh", // Full viewport height
|
||||
paddingTop: "20px", // Top padding
|
||||
paddingBottom: "20px", // Bottom padding
|
||||
paddingRight: "10px",
|
||||
paddingLeft: "20px",
|
||||
boxSizing: "border-box", // Include padding in height calculation
|
||||
overflow: "hidden", // Prevent parent scrolling
|
||||
}}
|
||||
>
|
||||
<ChannelsTable />
|
||||
|
|
@ -24,20 +24,20 @@ const ChannelsPage = () => {
|
|||
<Grid2 size={6}>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh', // Full viewport height
|
||||
paddingTop: '20px', // Top padding
|
||||
paddingBottom: '20px', // Bottom padding
|
||||
paddingRight: '20px',
|
||||
paddingLeft: '10px',
|
||||
boxSizing: 'border-box', // Include padding in height calculation
|
||||
overflow: 'hidden', // Prevent parent scrolling
|
||||
height: "100vh", // Full viewport height
|
||||
paddingTop: "20px", // Top padding
|
||||
paddingBottom: "20px", // Bottom padding
|
||||
paddingRight: "20px",
|
||||
paddingLeft: "10px",
|
||||
boxSizing: "border-box", // Include padding in height calculation
|
||||
overflow: "hidden", // Prevent parent scrolling
|
||||
}}
|
||||
>
|
||||
<StreamsTable />
|
||||
</Box>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelsPage;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// src/components/Dashboard.js
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState } from "react";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [newStream, setNewStream] = useState('');
|
||||
const [newStream, setNewStream] = useState("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Snackbar } from '@mui/material';
|
||||
import UserAgentsTable from '../components/tables/UserAgentsTable';
|
||||
import EPGsTable from '../components/tables/EPGsTable';
|
||||
import React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import UserAgentsTable from "../components/tables/UserAgentsTable";
|
||||
import EPGsTable from "../components/tables/EPGsTable";
|
||||
|
||||
const EPGPage = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 50%', overflow: 'hidden' }}>
|
||||
<Box sx={{ flex: "1 1 50%", overflow: "hidden" }}>
|
||||
<EPGsTable />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: '1 1 50%', overflow: 'hidden' }}>
|
||||
<Box sx={{ flex: "1 1 50%", overflow: "hidden" }}>
|
||||
<UserAgentsTable />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default EPGPage;
|
||||
|
|
|
|||
|
|
@ -1,63 +1,63 @@
|
|||
import React from "react";
|
||||
import React from 'react';
|
||||
import { useEpg, Epg, Layout } from 'planby';
|
||||
import API from '../api'
|
||||
import API from '../api';
|
||||
|
||||
function App() {
|
||||
const [channels, setChannels] = React.useState([])
|
||||
const [epg, setEpg] = React.useState([])
|
||||
const [channels, setChannels] = React.useState([]);
|
||||
const [epg, setEpg] = React.useState([]);
|
||||
|
||||
const fetchChannels = async () => {
|
||||
const channels = await API.getChannels()
|
||||
const retval = []
|
||||
const channels = await API.getChannels();
|
||||
const retval = [];
|
||||
for (const channel of channels) {
|
||||
if (!channel.channel_name.includes("Nickelod")) {
|
||||
continue
|
||||
if (!channel.tvg_id) {
|
||||
continue;
|
||||
}
|
||||
console.log(channel)
|
||||
console.log(channel);
|
||||
retval.push({
|
||||
uuid: "Nickelodeon (East).us",
|
||||
type: "channel",
|
||||
uuid: channel.tvg_id,
|
||||
type: 'channel',
|
||||
title: channel.channel_name,
|
||||
country: "USA",
|
||||
provider: channel.channel_group?.name || "Default",
|
||||
logo: channel.logo_url || "/images/logo.png",
|
||||
country: 'USA',
|
||||
provider: channel.channel_group?.name || 'Default',
|
||||
logo: channel.logo_url || '/images/logo.png',
|
||||
year: 2025,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setChannels(retval)
|
||||
setChannels(retval);
|
||||
return retval;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEpg = async () => {
|
||||
const programs = await API.getGrid();
|
||||
const retval = []
|
||||
console.log(programs)
|
||||
const retval = [];
|
||||
console.log(programs);
|
||||
for (const program of programs.data) {
|
||||
retval.push({
|
||||
id: program.id,
|
||||
channelUuid: "Nickelodeon (East).us",
|
||||
channelUuid: 'Nickelodeon (East).us',
|
||||
description: program.description,
|
||||
title: program.title,
|
||||
since: program.start_time,
|
||||
till: program.end_time,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setEpg(retval)
|
||||
setEpg(retval);
|
||||
return retval;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
const channels = await fetchChannels()
|
||||
const epg = await fetchEpg()
|
||||
const channels = await fetchChannels();
|
||||
const epg = await fetchEpg();
|
||||
|
||||
setChannels(channels)
|
||||
setEpg(epg)
|
||||
}
|
||||
setChannels(channels);
|
||||
setEpg(epg);
|
||||
};
|
||||
|
||||
if (channels.length === 0) {
|
||||
fetchData()
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const formatDate = (date) => date.toISOString().split('T')[0] + 'T00:00:00';
|
||||
|
|
@ -74,19 +74,17 @@ function App() {
|
|||
} = useEpg({
|
||||
epg,
|
||||
channels,
|
||||
startDate: '2025/02/24', // or 2022-02-02T00:00:00
|
||||
startDate: '2025-02-25T11:00:00', // or 2022-02-02T00:00:00
|
||||
width: '100%',
|
||||
height: 600,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Epg {...getEpgProps()}>
|
||||
<Layout
|
||||
{...getLayoutProps()}
|
||||
/>
|
||||
</Epg>
|
||||
</div>
|
||||
<Epg {...getEpgProps()}>
|
||||
<Layout {...getLayoutProps()} />
|
||||
</Epg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import { ChannelBox, ChannelLogo } from "planby";
|
||||
|
||||
export const ChannelItem = ({ channel }) => {
|
||||
const { position, logo } = channel;
|
||||
return (
|
||||
<ChannelBox {...position}>
|
||||
{/* Overwrite styles by add eg. style={{ maxHeight: 52, maxWidth: 52,... }} */}
|
||||
{/* Or stay with default styles */}
|
||||
<ChannelLogo
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
style={{ maxHeight: 52, maxWidth: 52 }}
|
||||
/>
|
||||
</ChannelBox>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import {
|
||||
ProgramBox,
|
||||
ProgramContent,
|
||||
ProgramFlex,
|
||||
ProgramStack,
|
||||
ProgramTitle,
|
||||
ProgramText,
|
||||
ProgramImage,
|
||||
useProgram
|
||||
} from "planby";
|
||||
|
||||
export const ProgramItem = ({ program, ...rest }) => {
|
||||
const {
|
||||
styles,
|
||||
formatTime,
|
||||
set12HoursTimeFormat,
|
||||
isLive,
|
||||
isMinWidth
|
||||
} = useProgram({
|
||||
program,
|
||||
...rest
|
||||
});
|
||||
|
||||
const { data } = program;
|
||||
const { image, title, since, till } = data;
|
||||
|
||||
const sinceTime = formatTime(since, set12HoursTimeFormat()).toLowerCase();
|
||||
const tillTime = formatTime(till, set12HoursTimeFormat()).toLowerCase();
|
||||
|
||||
return (
|
||||
<ProgramBox width={styles.width} style={styles.position}>
|
||||
<ProgramContent width={styles.width} isLive={isLive}>
|
||||
<ProgramFlex>
|
||||
{isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
|
||||
<ProgramStack>
|
||||
<ProgramTitle>{title}</ProgramTitle>
|
||||
<ProgramText>
|
||||
{sinceTime} - {tillTime}
|
||||
</ProgramText>
|
||||
</ProgramStack>
|
||||
</ProgramFlex>
|
||||
</ProgramContent>
|
||||
</ProgramBox>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import {
|
||||
TimelineWrapper,
|
||||
TimelineBox,
|
||||
TimelineTime,
|
||||
TimelineDivider,
|
||||
TimelineDividers,
|
||||
useTimeline
|
||||
} from "planby";
|
||||
|
||||
export function Timeline({
|
||||
isBaseTimeFormat,
|
||||
isSidebar,
|
||||
dayWidth,
|
||||
hourWidth,
|
||||
numberOfHoursInDay,
|
||||
offsetStartHoursRange,
|
||||
sidebarWidth
|
||||
}) {
|
||||
const { time, dividers, formatTime } = useTimeline(
|
||||
numberOfHoursInDay,
|
||||
isBaseTimeFormat
|
||||
);
|
||||
|
||||
const renderTime = (index) => (
|
||||
<TimelineBox key={index} width={hourWidth}>
|
||||
<TimelineTime>
|
||||
{formatTime(index + offsetStartHoursRange).toLowerCase()}
|
||||
</TimelineTime>
|
||||
<TimelineDividers>{renderDividers()}</TimelineDividers>
|
||||
</TimelineBox>
|
||||
);
|
||||
|
||||
const renderDividers = () =>
|
||||
dividers.map((_, index) => (
|
||||
<TimelineDivider key={index} width={hourWidth} />
|
||||
));
|
||||
|
||||
return (
|
||||
<TimelineWrapper
|
||||
dayWidth={dayWidth}
|
||||
sidebarWidth={sidebarWidth}
|
||||
isSidebar={isSidebar}
|
||||
>
|
||||
{time.map((_, index) => renderTime(index))}
|
||||
</TimelineWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./ChannelItem";
|
||||
export * from "./ProgramItem";
|
||||
export * from "./Timeline";
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
export const theme = {
|
||||
primary: {
|
||||
600: "#1a202c",
|
||||
900: "#171923"
|
||||
},
|
||||
grey: { 300: "#d1d1d1" },
|
||||
white: "#fff",
|
||||
green: {
|
||||
300: "#2c7a7b"
|
||||
},
|
||||
scrollbar: {
|
||||
border: "#ffffff",
|
||||
thumb: {
|
||||
bg: "#e1e1e1"
|
||||
}
|
||||
},
|
||||
loader: {
|
||||
teal: "#5DDADB",
|
||||
purple: "#3437A2",
|
||||
pink: "#F78EB6",
|
||||
bg: "#171923db"
|
||||
},
|
||||
gradient: {
|
||||
blue: {
|
||||
300: "#002eb3",
|
||||
600: "#002360",
|
||||
900: "#051937"
|
||||
}
|
||||
},
|
||||
|
||||
text: {
|
||||
grey: {
|
||||
300: "#a0aec0",
|
||||
500: "#718096"
|
||||
}
|
||||
},
|
||||
|
||||
timeline: {
|
||||
divider: {
|
||||
bg: "#718096"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import { useEpg } from "planby";
|
||||
|
||||
// Import theme
|
||||
import { theme } from "./theme";
|
||||
import API from "../../api";
|
||||
import { Description } from "@mui/icons-material";
|
||||
|
||||
const sampleChannel = {
|
||||
uuid: "16fdfefe-e466-4090-bc1a-57c43937f826",
|
||||
type: "channel",
|
||||
title: "r tv",
|
||||
country: "USA",
|
||||
provider: 7427,
|
||||
logo:
|
||||
"https://raw.githubusercontent.com/karolkozer/planby-demo-resources/master/resources/channel-logos/png/r-channel.png",
|
||||
year: 2002
|
||||
}
|
||||
|
||||
const fetchChannels = async () => {
|
||||
const channels = await API.getChannels()
|
||||
const retval = []
|
||||
for (const channel of channels) {
|
||||
if (!channel.channel_name.includes("Nickelod")) {
|
||||
continue
|
||||
}
|
||||
console.log(channel)
|
||||
retval.push({
|
||||
...sampleChannel,
|
||||
uuid: "Nickelodeon (East).us",
|
||||
type: "channel",
|
||||
title: channel.channel_name,
|
||||
country: "USA",
|
||||
provider: channel.channel_group?.name || "Default",
|
||||
logo: channel.logo_url || "/images/logo.png",
|
||||
year: 2025,
|
||||
})
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
const sample = {
|
||||
"id": "6f3caa7f-5b11-4edb-998e-80d4baa03373",
|
||||
"description": "Bounty hunter Boba Fett & mercenary Fennec Shand navigate the underworld when they return to Tatooine to claim Jabba the Hutt's old turf.",
|
||||
"title": "The Book of Boba Fett",
|
||||
"isYesterday": true,
|
||||
"since": "2022-10-17T23:50:00",
|
||||
"till": "2022-10-18T00:55:00",
|
||||
"channelUuid": "16fdfefe-e466-4090-bc1a-57c43937f826",
|
||||
"image": "https://www.themoviedb.org/t/p/w1066_and_h600_bestv2/sjx6zjQI2dLGtEL0HGWsnq6UyLU.jpg",
|
||||
"country": "Ghana",
|
||||
"Year": "2021–",
|
||||
"Rated": "TV-14",
|
||||
"Released": "29 Dec 2021",
|
||||
"Runtime": "N/A",
|
||||
"Genre": "Action, Adventure, Sci-Fi",
|
||||
"Director": "N/A",
|
||||
"Writer": "Jon Favreau",
|
||||
"Actors": "Temuera Morrison, Ming-Na Wen, Matt Berry",
|
||||
"Language": "English",
|
||||
"Country": "United States",
|
||||
"Awards": "N/A",
|
||||
"Metascore": "N/A",
|
||||
"imdbRating": "8.0",
|
||||
"imdbVotes": "20,147",
|
||||
"imdbID": "tt13668894",
|
||||
"Type": "series",
|
||||
"totalSeasons": "1",
|
||||
"Response": "True",
|
||||
"Ratings": [
|
||||
{
|
||||
"Source": "Internet Movie Database",
|
||||
"Value": "8.0/10"
|
||||
}
|
||||
],
|
||||
"rating": 3
|
||||
}
|
||||
|
||||
const fetchEpg = async () => {
|
||||
const programs = await API.getGrid();
|
||||
const retval = []
|
||||
console.log(programs)
|
||||
for (const program of programs.data) {
|
||||
retval.push({
|
||||
...sample,
|
||||
id: program.id,
|
||||
channelUuid: "Nickelodeon (East).us",
|
||||
description: program.description,
|
||||
title: program.title,
|
||||
since: program.start_time.replace('Z', ''),
|
||||
till: program.end_time.replace('Z', ''),
|
||||
})
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
export function useApp() {
|
||||
const [channels, setChannels] = React.useState([]);
|
||||
const [epg, setEpg] = React.useState([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const channelsData = React.useMemo(() => channels, [channels]);
|
||||
const epgData = React.useMemo(() => epg, [epg]);
|
||||
|
||||
const formatDate = (date) => date.toISOString().split('T')[0] + 'T00:00:00';
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
|
||||
const { getEpgProps, getLayoutProps } = useEpg({
|
||||
channels: channelsData,
|
||||
epg: epgData,
|
||||
dayWidth: 7200,
|
||||
sidebarWidth: 100,
|
||||
itemHeight: 80,
|
||||
isSidebar: true,
|
||||
isTimeline: true,
|
||||
isLine: true,
|
||||
startDate: today,
|
||||
endDate: tomorrow,
|
||||
isBaseTimeFormat: true,
|
||||
theme
|
||||
});
|
||||
|
||||
const handleFetchResources = React.useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const epg = await fetchEpg();
|
||||
const channels = await fetchChannels();
|
||||
setEpg(epg);
|
||||
setChannels(channels);
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
handleFetchResources();
|
||||
}, [handleFetchResources]);
|
||||
|
||||
return { getEpgProps, getLayoutProps, isLoading };
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
// src/components/Home.js
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState } from "react";
|
||||
|
||||
const Home = () => {
|
||||
const [newChannel, setNewChannel] = useState('');
|
||||
const [newChannel, setNewChannel] = useState("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import LoginForm from '../components/forms/LoginForm';
|
||||
import React from "react";
|
||||
import LoginForm from "../components/forms/LoginForm";
|
||||
|
||||
const Login = () => {
|
||||
return <LoginForm/>
|
||||
return <LoginForm />;
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import React, { useState } from 'react';
|
||||
import useUserAgentsStore from '../store/userAgents';
|
||||
import { Box } from '@mui/material';
|
||||
import M3UsTable from '../components/tables/M3UsTable';
|
||||
import UserAgentsTable from '../components/tables/UserAgentsTable';
|
||||
import usePlaylistsStore from '../store/playlists';
|
||||
import API from '../api'
|
||||
import M3UForm from '../components/forms/M3U'
|
||||
import React, { useState } from "react";
|
||||
import useUserAgentsStore from "../store/userAgents";
|
||||
import { Box } from "@mui/material";
|
||||
import M3UsTable from "../components/tables/M3UsTable";
|
||||
import UserAgentsTable from "../components/tables/UserAgentsTable";
|
||||
import usePlaylistsStore from "../store/playlists";
|
||||
import API from "../api";
|
||||
import M3UForm from "../components/forms/M3U";
|
||||
|
||||
const M3UPage = () => {
|
||||
const isLoading = useUserAgentsStore((state) => state.isLoading);
|
||||
const error = useUserAgentsStore((state) => state.error);
|
||||
const playlists = usePlaylistsStore(state => state.playlists)
|
||||
|
||||
const playlists = usePlaylistsStore((state) => state.playlists);
|
||||
|
||||
const [playlist, setPlaylist] = useState(null);
|
||||
const [playlistModalOpen, setPlaylistModalOpen] = useState(false);
|
||||
|
|
@ -20,26 +19,26 @@ const M3UPage = () => {
|
|||
const [userAgentModalOpen, setUserAgentModalOpen] = useState(false);
|
||||
|
||||
const editUserAgent = async (userAgent = null) => {
|
||||
setUserAgent(userAgent)
|
||||
setUserAgentModalOpen(true)
|
||||
}
|
||||
setUserAgent(userAgent);
|
||||
setUserAgentModalOpen(true);
|
||||
};
|
||||
|
||||
const editPlaylist = async (playlist = null) => {
|
||||
setPlaylist(playlist)
|
||||
setPlaylistModalOpen(true)
|
||||
}
|
||||
setPlaylist(playlist);
|
||||
setPlaylistModalOpen(true);
|
||||
};
|
||||
|
||||
const deleteUserAgent = async (ids) => {
|
||||
if (Array.isArray(ids)) {
|
||||
await API.deleteUserAgents(ids)
|
||||
await API.deleteUserAgents(ids);
|
||||
} else {
|
||||
await API.deleteUserAgent(ids)
|
||||
await API.deleteUserAgent(ids);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deletePlaylist = async (id) => {
|
||||
await API.deletePlaylist(id)
|
||||
}
|
||||
await API.deletePlaylist(id);
|
||||
};
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error}</div>;
|
||||
|
|
@ -47,17 +46,17 @@ const M3UPage = () => {
|
|||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 50%', overflow: 'hidden' }}>
|
||||
<Box sx={{ flex: "1 1 50%", overflow: "hidden" }}>
|
||||
<M3UsTable />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: '1 1 50%', overflow: 'hidden' }}>
|
||||
<Box sx={{ flex: "1 1 50%", overflow: "hidden" }}>
|
||||
<UserAgentsTable />
|
||||
</Box>
|
||||
|
||||
|
|
@ -66,10 +65,8 @@ const M3UPage = () => {
|
|||
isOpen={playlistModalOpen}
|
||||
onClose={() => setPlaylistModalOpen(false)}
|
||||
/>
|
||||
|
||||
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default M3UPage;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import StreamProfilesTable from '../components/tables/StreamProfilesTable';
|
||||
import React from "react";
|
||||
import StreamProfilesTable from "../components/tables/StreamProfilesTable";
|
||||
|
||||
const StreamProfilesPage = () => {
|
||||
return (
|
||||
<StreamProfilesTable />
|
||||
)
|
||||
return <StreamProfilesTable />;
|
||||
};
|
||||
|
||||
export default StreamProfilesPage;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// src/auth/authStore.js
|
||||
|
||||
import {create} from 'zustand';
|
||||
import API from '../api'
|
||||
import { create } from 'zustand';
|
||||
import API from '../api';
|
||||
import useChannelsStore from './channels';
|
||||
import useStreamsStore from './streams';
|
||||
import useUserAgentsStore from './userAgents';
|
||||
import usePlaylistsStore from './playlists';
|
||||
import useEPGsStore from './epgs';
|
||||
import useStreamProfilesStore from './streamProfiles';
|
||||
|
||||
const decodeToken = (token) => {
|
||||
if (!token) return null;
|
||||
|
|
@ -21,30 +25,45 @@ const useAuthStore = create((set, get) => ({
|
|||
tokenExpiration: localStorage.getItem('tokenExpiration') || null,
|
||||
isAuthenticated: false,
|
||||
|
||||
getToken: async () => {
|
||||
const expiration = localStorage.getItem('tokenExpiration')
|
||||
const tokenExpiration = localStorage.getItem('tokenExpiration');
|
||||
let accessToken = null;
|
||||
if (isTokenExpired(tokenExpiration)) {
|
||||
accessToken = await get().refreshToken();
|
||||
} else {
|
||||
accessToken = localStorage.getItem('accessToken');
|
||||
}
|
||||
setIsAuthenticated: (isAuthenticated) => set({ isAuthenticated }),
|
||||
|
||||
return accessToken;
|
||||
initData: async () => {
|
||||
console.log('fetching data');
|
||||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
useChannelsStore.getState().fetchChannelGroups(),
|
||||
useStreamsStore.getState().fetchStreams(),
|
||||
useUserAgentsStore.getState().fetchUserAgents(),
|
||||
usePlaylistsStore.getState().fetchPlaylists(),
|
||||
useEPGsStore.getState().fetchEPGs(),
|
||||
useStreamProfilesStore.getState().fetchProfiles(),
|
||||
]);
|
||||
},
|
||||
|
||||
getToken: async () => {
|
||||
const expiration = localStorage.getItem('tokenExpiration');
|
||||
const tokenExpiration = localStorage.getItem('tokenExpiration');
|
||||
let accessToken = null;
|
||||
if (isTokenExpired(tokenExpiration)) {
|
||||
accessToken = await get().refreshToken();
|
||||
} else {
|
||||
accessToken = localStorage.getItem('accessToken');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
},
|
||||
|
||||
// Action to login
|
||||
login: async ({username, password}) => {
|
||||
login: async ({ username, password }) => {
|
||||
try {
|
||||
const response = await API.login(username, password)
|
||||
const response = await API.login(username, password);
|
||||
if (response.access) {
|
||||
const expiration = decodeToken(response.access)
|
||||
const expiration = decodeToken(response.access);
|
||||
set({
|
||||
accessToken: response.access,
|
||||
refreshToken: response.refresh,
|
||||
tokenExpiration: expiration, // 1 hour from now
|
||||
isAuthenticated: true
|
||||
isAuthenticated: true,
|
||||
});
|
||||
// Store in localStorage
|
||||
localStorage.setItem('accessToken', response.access);
|
||||
|
|
@ -62,7 +81,7 @@ const useAuthStore = create((set, get) => ({
|
|||
if (!refreshToken) return;
|
||||
|
||||
try {
|
||||
const data = await API.refreshToken(refreshToken)
|
||||
const data = await API.refreshToken(refreshToken);
|
||||
if (data.access) {
|
||||
set({
|
||||
accessToken: data.access,
|
||||
|
|
@ -76,7 +95,7 @@ const useAuthStore = create((set, get) => ({
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
get().logout()
|
||||
get().logout();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -88,7 +107,7 @@ const useAuthStore = create((set, get) => ({
|
|||
accessToken: null,
|
||||
refreshToken: null,
|
||||
tokenExpiration: null,
|
||||
isAuthenticated: false
|
||||
isAuthenticated: false,
|
||||
});
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
|
|
@ -99,10 +118,13 @@ const useAuthStore = create((set, get) => ({
|
|||
const refreshToken = localStorage.getItem('refreshToken') || null;
|
||||
|
||||
if (refreshToken) {
|
||||
await get().refreshToken()
|
||||
} else {
|
||||
await get().logout()
|
||||
const loggedIn = await get().refreshToken();
|
||||
if (loggedIn) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
// src/stores/channelsStore.js
|
||||
|
||||
import { create } from 'zustand';
|
||||
import api from '../api'; // Your API helper that manages token & requests
|
||||
import { create } from "zustand";
|
||||
import api from "../api";
|
||||
|
||||
const useChannelsStore = create((set) => ({
|
||||
channels: [],
|
||||
|
|
@ -15,8 +13,8 @@ const useChannelsStore = create((set) => ({
|
|||
const channels = await api.getChannels();
|
||||
set({ channels: channels, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch channels:', error);
|
||||
set({ error: 'Failed to load channels.', isLoading: false });
|
||||
console.error("Failed to fetch channels:", error);
|
||||
set({ error: "Failed to load channels.", isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -26,22 +24,41 @@ const useChannelsStore = create((set) => ({
|
|||
const channelGroups = await api.getChannelGroups();
|
||||
set({ channelGroups: channelGroups, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch channel groups:', error);
|
||||
set({ error: 'Failed to load channel groups.', isLoading: false });
|
||||
console.error("Failed to fetch channel groups:", error);
|
||||
set({ error: "Failed to load channel groups.", isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addChannel: (newChannel) => set((state) => ({
|
||||
channels: [...state.channels, newChannel],
|
||||
})),
|
||||
addChannel: (newChannel) =>
|
||||
set((state) => ({
|
||||
channels: [...state.channels, newChannel],
|
||||
})),
|
||||
|
||||
updateChannel: (userAgent) => set((state) => ({
|
||||
channels: state.channels.map(chan => chan.id === userAgent.id ? userAgent : chan),
|
||||
})),
|
||||
updateChannel: (userAgent) =>
|
||||
set((state) => ({
|
||||
channels: state.channels.map((chan) =>
|
||||
chan.id === userAgent.id ? userAgent : chan,
|
||||
),
|
||||
})),
|
||||
|
||||
removeChannels: (channelIds) => set((state) => ({
|
||||
channels: state.channels.filter((channel) => !channelIds.includes(channel.id)),
|
||||
})),
|
||||
removeChannels: (channelIds) =>
|
||||
set((state) => ({
|
||||
channels: state.channels.filter(
|
||||
(channel) => !channelIds.includes(channel.id),
|
||||
),
|
||||
})),
|
||||
|
||||
addChannelGroup: (newChannelGroup) =>
|
||||
set((state) => ({
|
||||
channelGroups: [...state.channelGroups, newChannelGroup],
|
||||
})),
|
||||
|
||||
updateChannelGroup: (channelGroup) =>
|
||||
set((state) => ({
|
||||
channelGroups: state.channelGroups.map((group) =>
|
||||
group.id === channelGroup.id ? channelGroup : group,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useChannelsStore;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api'; // Your API helper that manages token & requests
|
||||
import { create } from "zustand";
|
||||
import api from "../api";
|
||||
|
||||
const useEPGsStore = create((set) => ({
|
||||
epgs: [],
|
||||
|
|
@ -12,18 +12,20 @@ const useEPGsStore = create((set) => ({
|
|||
const epgs = await api.getEPGs();
|
||||
set({ epgs: epgs, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch epgs:', error);
|
||||
set({ error: 'Failed to load epgs.', isLoading: false });
|
||||
console.error("Failed to fetch epgs:", error);
|
||||
set({ error: "Failed to load epgs.", isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addEPG: (newPlaylist) => set((state) => ({
|
||||
epgs: [...state.epgs, newPlaylist],
|
||||
})),
|
||||
addEPG: (newPlaylist) =>
|
||||
set((state) => ({
|
||||
epgs: [...state.epgs, newPlaylist],
|
||||
})),
|
||||
|
||||
removeEPGs: (epgIds) => set((state) => ({
|
||||
epgs: state.epgs.filter((epg) => !epgIds.includes(epg.id)),
|
||||
})),
|
||||
removeEPGs: (epgIds) =>
|
||||
set((state) => ({
|
||||
epgs: state.epgs.filter((epg) => !epgIds.includes(epg.id)),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useEPGsStore;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api'; // Your API helper that manages token & requests
|
||||
import { create } from "zustand";
|
||||
import api from "../api";
|
||||
|
||||
const usePlaylistsStore = create((set) => ({
|
||||
playlists: [],
|
||||
|
|
@ -12,22 +12,29 @@ const usePlaylistsStore = create((set) => ({
|
|||
const playlists = await api.getPlaylists();
|
||||
set({ playlists: playlists, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch playlists:', error);
|
||||
set({ error: 'Failed to load playlists.', isLoading: false });
|
||||
console.error("Failed to fetch playlists:", error);
|
||||
set({ error: "Failed to load playlists.", isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addPlaylist: (newPlaylist) => set((state) => ({
|
||||
playlists: [...state.playlists, newPlaylist],
|
||||
})),
|
||||
addPlaylist: (newPlaylist) =>
|
||||
set((state) => ({
|
||||
playlists: [...state.playlists, newPlaylist],
|
||||
})),
|
||||
|
||||
updatePlaylist: (playlist) => set((state) => ({
|
||||
playlists: state.playlists.map(pl => pl.id === playlist.id ? playlist : pl),
|
||||
})),
|
||||
updatePlaylist: (playlist) =>
|
||||
set((state) => ({
|
||||
playlists: state.playlists.map((pl) =>
|
||||
pl.id === playlist.id ? playlist : pl,
|
||||
),
|
||||
})),
|
||||
|
||||
removePlaylists: (playlistIds) => set((state) => ({
|
||||
playlists: state.playlists.filter((playlist) => !playlistIds.includes(playlist.id)),
|
||||
})),
|
||||
removePlaylists: (playlistIds) =>
|
||||
set((state) => ({
|
||||
playlists: state.playlists.filter(
|
||||
(playlist) => !playlistIds.includes(playlist.id),
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default usePlaylistsStore;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api'; // Your API helper that manages token & requests
|
||||
import api from '../api';
|
||||
|
||||
const useStreamProfilesStore = create((set) => ({
|
||||
profiles: [],
|
||||
|
|
@ -17,17 +17,24 @@ const useStreamProfilesStore = create((set) => ({
|
|||
}
|
||||
},
|
||||
|
||||
addStreamProfile: (profile) => set((state) => ({
|
||||
profiles: [...state.profiles, profile],
|
||||
})),
|
||||
addStreamProfile: (profile) =>
|
||||
set((state) => ({
|
||||
profiles: [...state.profiles, profile],
|
||||
})),
|
||||
|
||||
updateStreamProfile: (profile) => set((state) => ({
|
||||
profiles: state.profiles.map(prof => prof.id === profile.id ? profile : prof),
|
||||
})),
|
||||
updateStreamProfile: (profile) =>
|
||||
set((state) => ({
|
||||
profiles: state.profiles.map((prof) =>
|
||||
prof.id === profile.id ? profile : prof
|
||||
),
|
||||
})),
|
||||
|
||||
removeStreamProfiles: (propfileIds) => set((state) => ({
|
||||
profiles: state.profiles.filter((profile) => !propfileIds.includes(profile.id)),
|
||||
})),
|
||||
removeStreamProfiles: (propfileIds) =>
|
||||
set((state) => ({
|
||||
profiles: state.profiles.filter(
|
||||
(profile) => !propfileIds.includes(profile.id)
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useStreamProfilesStore;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api'; // Your API helper that manages token & requests
|
||||
import { create } from "zustand";
|
||||
import api from "../api";
|
||||
|
||||
const useStreamsStore = create((set) => ({
|
||||
streams: [],
|
||||
|
|
@ -12,22 +12,25 @@ const useStreamsStore = create((set) => ({
|
|||
const streams = await api.getStreams();
|
||||
set({ streams: streams, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch streams:', error);
|
||||
set({ error: 'Failed to load streams.', isLoading: false });
|
||||
console.error("Failed to fetch streams:", error);
|
||||
set({ error: "Failed to load streams.", isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addStream: (stream) => set((state) => ({
|
||||
streams: [...state.streams, stream],
|
||||
})),
|
||||
addStream: (stream) =>
|
||||
set((state) => ({
|
||||
streams: [...state.streams, stream],
|
||||
})),
|
||||
|
||||
updateStream: (stream) => set((state) => ({
|
||||
streams: state.streams.map(st => st.id === stream.id ? stream : st),
|
||||
})),
|
||||
updateStream: (stream) =>
|
||||
set((state) => ({
|
||||
streams: state.streams.map((st) => (st.id === stream.id ? stream : st)),
|
||||
})),
|
||||
|
||||
removeStreams: (streamIds) => set((state) => ({
|
||||
streams: state.streams.filter((stream) => !streamIds.includes(stream.id)),
|
||||
})),
|
||||
removeStreams: (streamIds) =>
|
||||
set((state) => ({
|
||||
streams: state.streams.filter((stream) => !streamIds.includes(stream.id)),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useStreamsStore;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api'; // Your API helper that manages token & requests
|
||||
import { create } from "zustand";
|
||||
import api from "../api";
|
||||
|
||||
const useUserAgentsStore = create((set) => ({
|
||||
userAgents: [],
|
||||
|
|
@ -12,22 +12,29 @@ const useUserAgentsStore = create((set) => ({
|
|||
const userAgents = await api.getUserAgents();
|
||||
set({ userAgents: userAgents, isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch userAgents:', error);
|
||||
set({ error: 'Failed to load userAgents.', isLoading: false });
|
||||
console.error("Failed to fetch userAgents:", error);
|
||||
set({ error: "Failed to load userAgents.", isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addUserAgent: (userAgent) => set((state) => ({
|
||||
userAgents: [...state.userAgents, userAgent],
|
||||
})),
|
||||
addUserAgent: (userAgent) =>
|
||||
set((state) => ({
|
||||
userAgents: [...state.userAgents, userAgent],
|
||||
})),
|
||||
|
||||
updateUserAgent: (userAgent) => set((state) => ({
|
||||
userAgents: state.userAgents.map(ua => ua.id === userAgent.id ? userAgent : ua),
|
||||
})),
|
||||
updateUserAgent: (userAgent) =>
|
||||
set((state) => ({
|
||||
userAgents: state.userAgents.map((ua) =>
|
||||
ua.id === userAgent.id ? userAgent : ua,
|
||||
),
|
||||
})),
|
||||
|
||||
removeUserAgents: (userAgentIds) => set((state) => ({
|
||||
userAgents: state.userAgents.filter((userAgent) => !userAgentIds.includes(userAgent.id)),
|
||||
})),
|
||||
removeUserAgents: (userAgentIds) =>
|
||||
set((state) => ({
|
||||
userAgents: state.userAgents.filter(
|
||||
(userAgent) => !userAgentIds.includes(userAgent.id),
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useUserAgentsStore;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue