mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-21 01:05:30 +00:00
completely removed mantine-react-table, added empty / dummy output for VOD endpoints
This commit is contained in:
parent
0b0373f4ee
commit
9daa764fbb
17 changed files with 1224 additions and 1163 deletions
|
|
@ -489,6 +489,11 @@ def xc_player_api(request):
|
|||
if action == "get_live_streams":
|
||||
return xc_get_live_streams(request, user, request.GET.get("category_id"))
|
||||
|
||||
if action == "get_vod_categories":
|
||||
return JsonResponse([], safe=False)
|
||||
if action == "get_vod_streams":
|
||||
return JsonResponse([], safe=False)
|
||||
|
||||
|
||||
def xc_get(request):
|
||||
action = request.GET.get("action")
|
||||
|
|
|
|||
364
frontend/package-lock.json
generated
364
frontend/package-lock.json
generated
|
|
@ -12,13 +12,13 @@
|
|||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mantine/charts": "^7.17.2",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/dates": "^7.17.2",
|
||||
"@mantine/dropzone": "^7.17.2",
|
||||
"@mantine/form": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/notifications": "^7.17.2",
|
||||
"@mantine/charts": "~8.0.1",
|
||||
"@mantine/core": "~8.0.1",
|
||||
"@mantine/dates": "~8.0.1",
|
||||
"@mantine/dropzone": "~8.0.1",
|
||||
"@mantine/form": "~8.0.1",
|
||||
"@mantine/hooks": "~8.0.1",
|
||||
"@mantine/notifications": "~8.0.1",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"allotment": "^1.20.3",
|
||||
|
|
@ -27,8 +27,7 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"formik": "^2.4.6",
|
||||
"hls.js": "^1.5.20",
|
||||
"lucide-react": "^0.479.0",
|
||||
"mantine-react-table": "^2.0.0-beta.9",
|
||||
"lucide-react": "^0.511.0",
|
||||
"mpegts.js": "^1.8.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -753,72 +752,72 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@mantine/charts": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/charts/-/charts-7.17.2.tgz",
|
||||
"integrity": "sha512-ckB23pIqRjzysUz2EiWZD9AVyf7t0r7o7zfJbl01nzOezFgYq5RGeRoxvpcsfBC+YoSbB/43rjNcXtYhtA7QzA==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/charts/-/charts-8.0.1.tgz",
|
||||
"integrity": "sha512-yntk4siXpQGSj83tDwftJw6fHTOBS6c/VWinjvTW29ptEdjBCxbKFfyyDc9UGVVuO7ovbdtpfCZBpuN2I7HPCA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "7.17.2",
|
||||
"@mantine/hooks": "7.17.2",
|
||||
"@mantine/core": "8.0.1",
|
||||
"@mantine/hooks": "8.0.1",
|
||||
"react": "^18.x || ^19.x",
|
||||
"react-dom": "^18.x || ^19.x",
|
||||
"recharts": "^2.13.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/core": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.2.tgz",
|
||||
"integrity": "sha512-R6MYhitJ0JEgrhadd31Nw9FhRaQwDHjXUs5YIlitKH/fTOz9gKSxKjzmNng3bEBQCcbEDOkZj3FRcBgTUh/F0Q==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.0.1.tgz",
|
||||
"integrity": "sha512-4ezaxKjChSPtawamQ3KrJq+x506uTouXlL0Z5fP+t105KnyxMrAJUENhbh2ivD4pq9Zh1BFiD9IWzyu3IXFR8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"clsx": "^2.1.1",
|
||||
"react-number-format": "^5.4.3",
|
||||
"react-remove-scroll": "^2.6.2",
|
||||
"react-textarea-autosize": "8.5.6",
|
||||
"react-textarea-autosize": "8.5.9",
|
||||
"type-fest": "^4.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/hooks": "7.17.2",
|
||||
"@mantine/hooks": "8.0.1",
|
||||
"react": "^18.x || ^19.x",
|
||||
"react-dom": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/dates": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.17.2.tgz",
|
||||
"integrity": "sha512-7bB992j8f+uEi280jab0/8i5yfsN/3oSrMDFwatZ+7XSDUwiP0YFib/FVX0pNSSqdFpbXhUmsZEECX71QtHw+Q==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.0.1.tgz",
|
||||
"integrity": "sha512-YCmV5jiGE9Ts2uhNS217IA1Hd5kAa8oaEtfnU0bS1sL36zKEf2s6elmzY718XdF8tFil0jJWAj0jiCrA3/udMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "7.17.2",
|
||||
"@mantine/hooks": "7.17.2",
|
||||
"@mantine/core": "8.0.1",
|
||||
"@mantine/hooks": "8.0.1",
|
||||
"dayjs": ">=1.0.0",
|
||||
"react": "^18.x || ^19.x",
|
||||
"react-dom": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/dropzone": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.17.2.tgz",
|
||||
"integrity": "sha512-NMQ1SDmnW0sf3GO6p1r/VIcg/xWqlRmfnWCr00/bGRbBEGbyaUwL3LSn+KYBJdY+3/jNGvGa+xflWDvnby5tzw==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.0.1.tgz",
|
||||
"integrity": "sha512-8PH5yrtA/ebCIwjs0m4J9qOvEyS/P4XmNlHrw0E389/qq64Ol7+/ZH7Xtiq64IaY8kvsMW1XHaV0c+bdYrijiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-dropzone-esm": "15.2.0"
|
||||
"react-dropzone": "14.3.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "7.17.2",
|
||||
"@mantine/hooks": "7.17.2",
|
||||
"@mantine/core": "8.0.1",
|
||||
"@mantine/hooks": "8.0.1",
|
||||
"react": "^18.x || ^19.x",
|
||||
"react-dom": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/form": {
|
||||
"version": "7.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.17.3.tgz",
|
||||
"integrity": "sha512-ktERldD8f9lrjjz6wIbwMnNbAZq8XEWPx4K5WuFyjXaK0PI8D+gsXIGKMtA5rVrAUFHCWCdbK3yLgtjJNki8ew==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.0.1.tgz",
|
||||
"integrity": "sha512-lQ94gn/9p60C+tKEW7psQ1tZHod58Q0bXLbRDadRKMwnqBb2WFoIuaQWPDo7ox+PqyOv28dtflgS+Lm95EbBhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
|
|
@ -829,34 +828,34 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mantine/hooks": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.2.tgz",
|
||||
"integrity": "sha512-tbErVcGZu0E4dSmE6N0k6Tv1y9R3SQmmQgwqorcc+guEgKMdamc36lucZGlJnSGUmGj+WLUgELkEQ0asdfYBDA==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.1.tgz",
|
||||
"integrity": "sha512-GvLdM4Ro3QcDyIgqrdXsUZmeeKye2TNL/k3mEr9JhM5KacHQjr83JPp0u9eLobn7kiyBqpLTYmVYAbmjJdCxHw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/notifications": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.17.2.tgz",
|
||||
"integrity": "sha512-vg0L8cmihz0ODg4WJ9MAyK06WPt/6g67ksIUFxd4F8RfdJbIMLTsNG9yWoSfuhtXenUg717KaA917IWLjDSaqw==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.0.1.tgz",
|
||||
"integrity": "sha512-7TX9OyAmUcok3qffnheS7gTAMKDczETy8XEYDr38Sy/XIoXLjM+3CwO+a/vfd1F9oW2LvkahkHT0Ey+vBOVd0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mantine/store": "7.17.2",
|
||||
"@mantine/store": "8.0.1",
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "7.17.2",
|
||||
"@mantine/hooks": "7.17.2",
|
||||
"@mantine/core": "8.0.1",
|
||||
"@mantine/hooks": "8.0.1",
|
||||
"react": "^18.x || ^19.x",
|
||||
"react-dom": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/store": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.17.2.tgz",
|
||||
"integrity": "sha512-UoMUYQK/z58hMueCkpDIXc49gPgrVO/zcpb0k+B7MFU51EIUiFzHLxLFBmWrgCAM6rzJORqN8JjyCd/PB9j4aw==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.0.1.tgz",
|
||||
"integrity": "sha512-3wfUDeiERXJEI+MGgRAbh+9aY35D9oE4UzquLqZh8cIiH5i5g64Y/eJx3PfjHgO5+Zeu6lbgTgL6k4lg4a2SBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.x || ^19.x"
|
||||
|
|
@ -1016,22 +1015,6 @@
|
|||
"react": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/match-sorter-utils": {
|
||||
"version": "8.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
|
||||
"integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"remove-accents": "0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||
|
|
@ -1052,23 +1035,6 @@
|
|||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
|
||||
"integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.11.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
|
|
@ -1082,22 +1048,6 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
|
||||
"integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||
|
|
@ -1392,6 +1342,15 @@
|
|||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
|
|
@ -2126,6 +2085,21 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
|
|
@ -2139,6 +2113,18 @@
|
|||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
|
||||
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
|
|
@ -2243,6 +2229,21 @@
|
|||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -2682,9 +2683,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.479.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.479.0.tgz",
|
||||
"integrity": "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==",
|
||||
"version": "0.511.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz",
|
||||
"integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
|
|
@ -2701,67 +2702,6 @@
|
|||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mantine-react-table": {
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/mantine-react-table/-/mantine-react-table-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-ZdfcwebWaPERoDvAuk43VYcBCzamohARVclnbuepT0PHZ0wRcDPMBR+zgaocL+pFy8EXUGwvWTOKNh25ITpjNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/match-sorter-utils": "8.19.4",
|
||||
"@tanstack/react-table": "8.20.5",
|
||||
"@tanstack/react-virtual": "3.11.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kevinvandy"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/core": "^7.9",
|
||||
"@mantine/dates": "^7.9",
|
||||
"@mantine/hooks": "^7.9",
|
||||
"@tabler/icons-react": ">=2.23.0",
|
||||
"clsx": ">=2",
|
||||
"dayjs": ">=1.11",
|
||||
"react": ">=18.0",
|
||||
"react-dom": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mantine-react-table/node_modules/@tanstack/react-table": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
|
||||
"integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.20.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/mantine-react-table/node_modules/@tanstack/table-core": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -3023,6 +2963,19 @@
|
|||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkcs7": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||
|
|
@ -3181,12 +3134,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone-esm": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.2.0.tgz",
|
||||
"integrity": "sha512-pPwR8xWVL+tFLnbAb8KVH5f6Vtl397tck8dINkZ1cPMxHWH+l9dFmIgRWgbh7V7jbjIcuKXCsVrXbhQz68+dVA==",
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
|
||||
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.4",
|
||||
"file-selector": "^2.1.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -3288,15 +3243,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz",
|
||||
"integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
|
@ -3312,12 +3265,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.3.0.tgz",
|
||||
"integrity": "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
|
||||
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.3.0"
|
||||
"react-router": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
|
@ -3365,9 +3318,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-textarea-autosize": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.6.tgz",
|
||||
"integrity": "sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw==",
|
||||
"version": "8.5.9",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
|
||||
"integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
|
|
@ -3495,12 +3448,6 @@
|
|||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/remove-accents": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
|
||||
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
|
|
@ -3691,6 +3638,23 @@
|
|||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
|
|
@ -3703,12 +3667,6 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
@ -3780,9 +3738,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
|
||||
"integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
|
||||
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
|
|
@ -3906,15 +3864,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
|
||||
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.30.1"
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
|
@ -4008,21 +3969,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
||||
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@
|
|||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mantine/charts": "^7.17.2",
|
||||
"@mantine/core": "^7.17.2",
|
||||
"@mantine/dates": "^7.17.2",
|
||||
"@mantine/dropzone": "^7.17.2",
|
||||
"@mantine/form": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/notifications": "^7.17.2",
|
||||
"@mantine/charts": "~8.0.1",
|
||||
"@mantine/core": "~8.0.1",
|
||||
"@mantine/dates": "~8.0.1",
|
||||
"@mantine/dropzone": "~8.0.1",
|
||||
"@mantine/form": "~8.0.1",
|
||||
"@mantine/hooks": "~8.0.1",
|
||||
"@mantine/notifications": "~8.0.1",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"allotment": "^1.20.3",
|
||||
|
|
@ -29,8 +29,7 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"formik": "^2.4.6",
|
||||
"hls.js": "^1.5.20",
|
||||
"lucide-react": "^0.479.0",
|
||||
"mantine-react-table": "^2.0.0-beta.9",
|
||||
"lucide-react": "^0.511.0",
|
||||
"mpegts.js": "^1.8.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { WebsocketProvider } from './WebSocket';
|
|||
import { Box, AppShell, MantineProvider } from '@mantine/core';
|
||||
import '@mantine/core/styles.css'; // Ensure Mantine global styles load
|
||||
import '@mantine/notifications/styles.css';
|
||||
import 'mantine-react-table/styles.css';
|
||||
import '@mantine/dropzone/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import './index.css';
|
||||
|
|
|
|||
|
|
@ -18,60 +18,60 @@ import useWarningsStore from '../store/warnings';
|
|||
* @param {string} [props.size='md'] - Size of the modal
|
||||
*/
|
||||
const ConfirmationDialog = ({
|
||||
opened,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title = 'Confirm Action',
|
||||
message = 'Are you sure you want to proceed?',
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
actionKey,
|
||||
onSuppressChange,
|
||||
size = 'md', // Add default size parameter - md is a medium width
|
||||
opened,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title = 'Confirm Action',
|
||||
message = 'Are you sure you want to proceed?',
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
actionKey,
|
||||
onSuppressChange,
|
||||
size = 'md', // Add default size parameter - md is a medium width
|
||||
}) => {
|
||||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
||||
const [suppressChecked, setSuppressChecked] = useState(
|
||||
isWarningSuppressed(actionKey)
|
||||
);
|
||||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
||||
const [suppressChecked, setSuppressChecked] = useState(
|
||||
isWarningSuppressed(actionKey)
|
||||
);
|
||||
|
||||
const handleToggleSuppress = (e) => {
|
||||
setSuppressChecked(e.currentTarget.checked);
|
||||
if (onSuppressChange) {
|
||||
onSuppressChange(e.currentTarget.checked);
|
||||
}
|
||||
};
|
||||
const handleToggleSuppress = (e) => {
|
||||
setSuppressChecked(e.currentTarget.checked);
|
||||
if (onSuppressChange) {
|
||||
onSuppressChange(e.currentTarget.checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (suppressChecked) {
|
||||
suppressWarning(actionKey);
|
||||
}
|
||||
onConfirm();
|
||||
};
|
||||
const handleConfirm = () => {
|
||||
if (suppressChecked) {
|
||||
suppressWarning(actionKey);
|
||||
}
|
||||
onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={title} size={size} centered>
|
||||
<Box mb={20}>{message}</Box>
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={title} size={size} centered>
|
||||
<Box mb={20}>{message}</Box>
|
||||
|
||||
{actionKey && (
|
||||
<Checkbox
|
||||
label="Don't ask me again"
|
||||
checked={suppressChecked}
|
||||
onChange={handleToggleSuppress}
|
||||
mb={20}
|
||||
/>
|
||||
)}
|
||||
{actionKey && (
|
||||
<Checkbox
|
||||
label="Don't ask me again"
|
||||
checked={suppressChecked}
|
||||
onChange={handleToggleSuppress}
|
||||
mb={20}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button color="red" onClick={handleConfirm}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
<Group justify="flex-end">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button color="red" onClick={handleConfirm}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import useChannelsStore from '../../store/channels';
|
|||
import API from '../../api';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
import useStreamsStore from '../../store/streams';
|
||||
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
|
||||
import ChannelGroupForm from './ChannelGroup';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import logo from '../../images/logo.png';
|
||||
|
|
@ -224,134 +223,6 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// const activeStreamsTable = useMantineReactTable({
|
||||
// data: channelStreams,
|
||||
// columns: useMemo(
|
||||
// () => [
|
||||
// {
|
||||
// header: 'Name',
|
||||
// accessorKey: 'name',
|
||||
// Cell: ({ cell }) => (
|
||||
// <div
|
||||
// style={{
|
||||
// whiteSpace: 'nowrap',
|
||||
// overflow: 'hidden',
|
||||
// textOverflow: 'ellipsis',
|
||||
// }}
|
||||
// >
|
||||
// {cell.getValue()}
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// header: 'M3U',
|
||||
// accessorKey: 'group_name',
|
||||
// Cell: ({ cell }) => (
|
||||
// <div
|
||||
// style={{
|
||||
// whiteSpace: 'nowrap',
|
||||
// overflow: 'hidden',
|
||||
// textOverflow: 'ellipsis',
|
||||
// }}
|
||||
// >
|
||||
// {cell.getValue()}
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// ],
|
||||
// []
|
||||
// ),
|
||||
// enableSorting: false,
|
||||
// enableBottomToolbar: false,
|
||||
// enableTopToolbar: false,
|
||||
// columnFilterDisplayMode: 'popover',
|
||||
// enablePagination: false,
|
||||
// enableRowVirtualization: true,
|
||||
// enableRowOrdering: true,
|
||||
// rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
// initialState: {
|
||||
// density: 'compact',
|
||||
// },
|
||||
// enableRowActions: true,
|
||||
// positionActionsColumn: 'last',
|
||||
// renderRowActions: ({ row }) => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// size="small" // Makes the button smaller
|
||||
// color="error" // Red color for delete actions
|
||||
// onClick={() => removeStream(row.original)}
|
||||
// >
|
||||
// <RemoveIcon fontSize="small" /> {/* Small icon size */}
|
||||
// </IconButton>
|
||||
// </>
|
||||
// ),
|
||||
// mantineTableContainerProps: {
|
||||
// style: {
|
||||
// height: '200px',
|
||||
// },
|
||||
// },
|
||||
// mantineRowDragHandleProps: ({ 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 = useMantineReactTable({
|
||||
// data: streams,
|
||||
// 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,
|
||||
// rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
// initialState: {
|
||||
// density: 'compact',
|
||||
// },
|
||||
// enableRowActions: true,
|
||||
// renderRowActions: ({ row }) => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// size="small" // Makes the button smaller
|
||||
// color="success" // Red color for delete actions
|
||||
// onClick={() => addStream(row.original)}
|
||||
// >
|
||||
// <AddIcon fontSize="small" /> {/* Small icon size */}
|
||||
// </IconButton>
|
||||
// </>
|
||||
// ),
|
||||
// positionActionsColumn: 'last',
|
||||
// mantineTableContainerProps: {
|
||||
// style: {
|
||||
// height: '200px',
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// Update the handler for when channel group modal is closed
|
||||
const handleChannelGroupModalClose = (newGroup) => {
|
||||
setChannelGroupModalOpen(false);
|
||||
|
|
@ -839,18 +710,6 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
</Stack>
|
||||
</Group>
|
||||
|
||||
{/* <Grid gap={2}>
|
||||
<Grid.Col span={6}>
|
||||
<Typography>Active Streams</Typography>
|
||||
<MantineReactTable table={activeStreamsTable} />
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Typography>Available Streams</Typography>
|
||||
<MantineReactTable table={availableStreamsTable} />
|
||||
</Grid.Col>
|
||||
</Grid> */}
|
||||
|
||||
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -220,134 +220,6 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// const activeStreamsTable = useMantineReactTable({
|
||||
// data: channelStreams,
|
||||
// columns: useMemo(
|
||||
// () => [
|
||||
// {
|
||||
// header: 'Name',
|
||||
// accessorKey: 'name',
|
||||
// Cell: ({ cell }) => (
|
||||
// <div
|
||||
// style={{
|
||||
// whiteSpace: 'nowrap',
|
||||
// overflow: 'hidden',
|
||||
// textOverflow: 'ellipsis',
|
||||
// }}
|
||||
// >
|
||||
// {cell.getValue()}
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// header: 'M3U',
|
||||
// accessorKey: 'group_name',
|
||||
// Cell: ({ cell }) => (
|
||||
// <div
|
||||
// style={{
|
||||
// whiteSpace: 'nowrap',
|
||||
// overflow: 'hidden',
|
||||
// textOverflow: 'ellipsis',
|
||||
// }}
|
||||
// >
|
||||
// {cell.getValue()}
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
// ],
|
||||
// []
|
||||
// ),
|
||||
// enableSorting: false,
|
||||
// enableBottomToolbar: false,
|
||||
// enableTopToolbar: false,
|
||||
// columnFilterDisplayMode: 'popover',
|
||||
// enablePagination: false,
|
||||
// enableRowVirtualization: true,
|
||||
// enableRowOrdering: true,
|
||||
// rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
// initialState: {
|
||||
// density: 'compact',
|
||||
// },
|
||||
// enableRowActions: true,
|
||||
// positionActionsColumn: 'last',
|
||||
// renderRowActions: ({ row }) => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// size="small" // Makes the button smaller
|
||||
// color="error" // Red color for delete actions
|
||||
// onClick={() => removeStream(row.original)}
|
||||
// >
|
||||
// <RemoveIcon fontSize="small" /> {/* Small icon size */}
|
||||
// </IconButton>
|
||||
// </>
|
||||
// ),
|
||||
// mantineTableContainerProps: {
|
||||
// style: {
|
||||
// height: '200px',
|
||||
// },
|
||||
// },
|
||||
// mantineRowDragHandleProps: ({ 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 = useMantineReactTable({
|
||||
// data: streams,
|
||||
// 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,
|
||||
// rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
// initialState: {
|
||||
// density: 'compact',
|
||||
// },
|
||||
// enableRowActions: true,
|
||||
// renderRowActions: ({ row }) => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// size="small" // Makes the button smaller
|
||||
// color="success" // Red color for delete actions
|
||||
// onClick={() => addStream(row.original)}
|
||||
// >
|
||||
// <AddIcon fontSize="small" /> {/* Small icon size */}
|
||||
// </IconButton>
|
||||
// </>
|
||||
// ),
|
||||
// positionActionsColumn: 'last',
|
||||
// mantineTableContainerProps: {
|
||||
// style: {
|
||||
// height: '200px',
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// Update the handler for when channel group modal is closed
|
||||
const handleChannelGroupModalClose = (newGroup) => {
|
||||
setChannelGroupModalOpen(false);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
Center,
|
||||
ActionIcon,
|
||||
} from '@mantine/core';
|
||||
import { RotateCcw, X } from 'lucide-react';
|
||||
import { RotateCcwKey, X } from 'lucide-react';
|
||||
import { isNotEmpty, useForm } from '@mantine/form';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import { USER_LEVELS, USER_LEVEL_LABELS } from '../../constants';
|
||||
|
|
@ -32,6 +32,7 @@ import useAuthStore from '../../store/auth';
|
|||
const User = ({ user = null, isOpen, onClose }) => {
|
||||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
const authUser = useAuthStore((s) => s.user);
|
||||
const setUser = useAuthStore((s) => s.setUser);
|
||||
|
||||
const [enableXC, setEnableXC] = useState(false);
|
||||
const [selectedProfiles, setSelectedProfiles] = useState(new Set());
|
||||
|
|
@ -108,7 +109,11 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
delete values.password;
|
||||
}
|
||||
|
||||
await API.updateUser(user.id, values);
|
||||
const response = await API.updateUser(user.id, values);
|
||||
|
||||
if (user.id == authUser.id) {
|
||||
setUser(response);
|
||||
}
|
||||
}
|
||||
|
||||
form.reset();
|
||||
|
|
@ -123,7 +128,10 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
username: user.username,
|
||||
email: user.email,
|
||||
user_level: `${user.user_level}`,
|
||||
channel_profiles: user.channel_profiles.map((id) => `${id}`),
|
||||
channel_profiles:
|
||||
user.channel_profiles.length > 0
|
||||
? user.channel_profiles.map((id) => `${id}`)
|
||||
: ['0'],
|
||||
xc_password: customProps.xc_password || '',
|
||||
});
|
||||
|
||||
|
|
@ -197,7 +205,6 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
label="XC Password"
|
||||
description="Auto-generated - clear to disable XC API"
|
||||
{...form.getInputProps('xc_password')}
|
||||
onChange={onChannelProfilesChange}
|
||||
key={form.key('xc_password')}
|
||||
style={{ flex: 1 }}
|
||||
rightSectionWidth={30}
|
||||
|
|
@ -208,7 +215,7 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
color="white"
|
||||
onClick={generateXCPassword}
|
||||
>
|
||||
<RotateCcw />
|
||||
<RotateCcwKey />
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const CustomTable = ({ table }) => {
|
|||
table.onSelectAllChange ? table.onSelectAllChange : null
|
||||
}
|
||||
selectedTableIds={table.selectedTableIds}
|
||||
tableCellProps={table.tableCellProps}
|
||||
/>
|
||||
<CustomTableBody
|
||||
getRowModel={table.getRowModel}
|
||||
|
|
@ -36,6 +37,8 @@ const CustomTable = ({ table }) => {
|
|||
renderBodyCell={table.renderBodyCell}
|
||||
getExpandedRowHeight={table.getExpandedRowHeight}
|
||||
getRowStyles={table.getRowStyles} // Pass the getRowStyles function
|
||||
tableBodyProps={table.tableBodyProps}
|
||||
tableCellProps={table.tableCellProps}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Box, Flex } from '@mantine/core';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import table from '../../../helpers/table';
|
||||
|
||||
const CustomTableBody = ({
|
||||
getRowModel,
|
||||
|
|
@ -9,6 +10,8 @@ const CustomTableBody = ({
|
|||
renderBodyCell,
|
||||
getExpandedRowHeight,
|
||||
getRowStyles, // Add this prop to receive row styles
|
||||
tableBodyProps,
|
||||
tableCellProps,
|
||||
}) => {
|
||||
const renderExpandedRow = (row) => {
|
||||
if (expandedRowRenderer) {
|
||||
|
|
@ -25,7 +28,10 @@ const CustomTableBody = ({
|
|||
|
||||
if (virtualized) {
|
||||
return (
|
||||
<Box className="tbody" style={{ flex: 1 }}>
|
||||
<Box
|
||||
className="tbody"
|
||||
style={{ flex: 1, ...(tableBodyProps && tableBodyProps()) }}
|
||||
>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => {
|
||||
const getItemSize = (index) => {
|
||||
|
|
@ -105,6 +111,7 @@ const CustomTableBody = ({
|
|||
? cell.column.getSize()
|
||||
: undefined,
|
||||
minWidth: 0,
|
||||
...(tableCellProps && tableCellProps({ cell })),
|
||||
}}
|
||||
>
|
||||
<Flex align="center" style={{ height: '100%' }}>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const CustomTableHeader = ({
|
|||
selectedTableIds,
|
||||
headerCellRenderFns,
|
||||
onSelectAllChange,
|
||||
tableCellProps,
|
||||
}) => {
|
||||
const renderHeaderCell = (header) => {
|
||||
if (headerCellRenderFns[header.id]) {
|
||||
|
|
@ -66,6 +67,7 @@ const CustomTableHeader = ({
|
|||
? header.getSize()
|
||||
: undefined,
|
||||
minWidth: 0,
|
||||
// ...(tableCellProps && tableCellProps({ cell: header })),
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import API from '../../api';
|
||||
import useEPGsStore from '../../store/epgs';
|
||||
import EPGForm from '../forms/EPG';
|
||||
|
|
@ -17,15 +16,24 @@ import {
|
|||
Badge,
|
||||
Progress,
|
||||
Stack,
|
||||
Group,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconSquarePlus } from '@tabler/icons-react';
|
||||
import { RefreshCcw, SquareMinus, SquarePen } from 'lucide-react';
|
||||
import {
|
||||
ArrowDownWideNarrow,
|
||||
ArrowUpDown,
|
||||
ArrowUpNarrowWide,
|
||||
RefreshCcw,
|
||||
SquareMinus,
|
||||
SquarePen,
|
||||
} from 'lucide-react';
|
||||
import dayjs from 'dayjs';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
import useWarningsStore from '../../store/warnings';
|
||||
import { CustomTable, useTable } from './CustomTable';
|
||||
|
||||
// Helper function to format status text
|
||||
const formatStatusText = (status) => {
|
||||
|
|
@ -36,15 +44,59 @@ const formatStatusText = (status) => {
|
|||
// Helper function to get status text color
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'idle': return 'gray.5';
|
||||
case 'fetching': return 'blue.5';
|
||||
case 'parsing': return 'indigo.5';
|
||||
case 'error': return 'red.5';
|
||||
case 'success': return 'green.5';
|
||||
default: return 'gray.5';
|
||||
case 'idle':
|
||||
return 'gray.5';
|
||||
case 'fetching':
|
||||
return 'blue.5';
|
||||
case 'parsing':
|
||||
return 'indigo.5';
|
||||
case 'error':
|
||||
return 'red.5';
|
||||
case 'success':
|
||||
return 'green.5';
|
||||
default:
|
||||
return 'gray.5';
|
||||
}
|
||||
};
|
||||
|
||||
const RowActions = ({ tableSize, row, editEPG, deleteEPG, refreshEPG }) => {
|
||||
const iconSize =
|
||||
tableSize == 'default' ? 'sm' : tableSize == 'compact' ? 'xs' : 'md';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize} // Use standardized icon size
|
||||
color="yellow.5" // Red color for delete actions
|
||||
onClick={() => editEPG(row.original)}
|
||||
>
|
||||
<SquarePen size={tableSize === 'compact' ? 16 : 18} />{' '}
|
||||
{/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize} // Use standardized icon size
|
||||
color="red.9" // Red color for delete actions
|
||||
onClick={() => deleteEPG(row.original.id)}
|
||||
>
|
||||
<SquareMinus size={tableSize === 'compact' ? 16 : 18} />{' '}
|
||||
{/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize} // Use standardized icon size
|
||||
color="blue.5" // Red color for delete actions
|
||||
onClick={() => refreshEPG(row.original.id)}
|
||||
disabled={!row.original.is_active}
|
||||
>
|
||||
<RefreshCcw size={tableSize === 'compact' ? 16 : 18} />{' '}
|
||||
{/* Small icon size */}
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EPGsTable = () => {
|
||||
const [epg, setEPG] = useState(null);
|
||||
const [epgModalOpen, setEPGModalOpen] = useState(false);
|
||||
|
|
@ -52,6 +104,7 @@ const EPGsTable = () => {
|
|||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [epgToDelete, setEpgToDelete] = useState(null);
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const epgs = useEPGsStore((s) => s.epgs);
|
||||
const refreshProgress = useEPGsStore((s) => s.refreshProgress);
|
||||
|
|
@ -61,10 +114,12 @@ const EPGsTable = () => {
|
|||
const [tableSize] = useLocalStorage('table-size', 'default');
|
||||
|
||||
// Get proper size for action icons to match ChannelsTable
|
||||
const iconSize = tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm';
|
||||
const iconSize =
|
||||
tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm';
|
||||
|
||||
// Calculate density for Mantine Table
|
||||
const tableDensity = tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'xl' : 'md';
|
||||
const tableDensity =
|
||||
tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'xl' : 'md';
|
||||
|
||||
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
||||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
|
|
@ -72,10 +127,13 @@ const EPGsTable = () => {
|
|||
const toggleActive = async (epg) => {
|
||||
try {
|
||||
// Send only the is_active field to trigger our special handling
|
||||
await API.updateEPG({
|
||||
id: epg.id,
|
||||
is_active: !epg.is_active,
|
||||
}, true); // Add a new parameter to indicate this is just a toggle
|
||||
await API.updateEPG(
|
||||
{
|
||||
id: epg.id,
|
||||
is_active: !epg.is_active,
|
||||
},
|
||||
true
|
||||
); // Add a new parameter to indicate this is just a toggle
|
||||
} catch (error) {
|
||||
console.error('Error toggling active state:', error);
|
||||
}
|
||||
|
|
@ -103,36 +161,46 @@ const EPGsTable = () => {
|
|||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Text size="xs">{label}: {parseInt(progress.progress)}%</Text>
|
||||
<Progress value={parseInt(progress.progress)} size="xs" style={{ margin: '2px 0' }} />
|
||||
{progress.speed && <Text size="xs">Speed: {parseInt(progress.speed)} KB/s</Text>}
|
||||
<Text size="xs">
|
||||
{label}: {parseInt(progress.progress)}%
|
||||
</Text>
|
||||
<Progress
|
||||
value={parseInt(progress.progress)}
|
||||
size="xs"
|
||||
style={{ margin: '2px 0' }}
|
||||
/>
|
||||
{progress.speed && (
|
||||
<Text size="xs">Speed: {parseInt(progress.speed)} KB/s</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
console.log(epgs);
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
size: 150,
|
||||
minSize: 100,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
header: 'Source Type',
|
||||
accessorKey: 'source_type',
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: 'URL / API Key / File Path',
|
||||
accessorKey: 'url',
|
||||
size: 200,
|
||||
minSize: 120,
|
||||
enableSorting: false,
|
||||
Cell: ({ cell, row }) => {
|
||||
const value = cell.getValue() || row.original.api_key || row.original.file_path || '';
|
||||
cell: ({ cell, row }) => {
|
||||
const value =
|
||||
cell.getValue() ||
|
||||
row.original.api_key ||
|
||||
row.original.file_path ||
|
||||
'';
|
||||
return (
|
||||
<Tooltip label={value} disabled={!value}>
|
||||
<div
|
||||
|
|
@ -152,18 +220,13 @@ const EPGsTable = () => {
|
|||
{
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
Cell: ({ row }) => {
|
||||
size: 150,
|
||||
cell: ({ row }) => {
|
||||
const data = row.original;
|
||||
|
||||
// Always show status text, even when there's progress happening
|
||||
return (
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
c={getStatusColor(data.status)}
|
||||
>
|
||||
<Text size="sm" fw={500} c={getStatusColor(data.status)}>
|
||||
{formatStatusText(data.status)}
|
||||
</Text>
|
||||
);
|
||||
|
|
@ -172,14 +235,15 @@ const EPGsTable = () => {
|
|||
{
|
||||
header: 'Status Message',
|
||||
accessorKey: 'last_message',
|
||||
size: 250,
|
||||
minSize: 150,
|
||||
enableSorting: false,
|
||||
Cell: ({ row }) => {
|
||||
cell: ({ row }) => {
|
||||
const data = row.original;
|
||||
|
||||
// Check if there's an active progress for this EPG - show progress first if active
|
||||
if (refreshProgress[data.id] && refreshProgress[data.id].progress < 100) {
|
||||
if (
|
||||
refreshProgress[data.id] &&
|
||||
refreshProgress[data.id].progress < 100
|
||||
) {
|
||||
return buildProgressDisplay(data);
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +251,12 @@ const EPGsTable = () => {
|
|||
if (data.status === 'error' && data.last_message) {
|
||||
return (
|
||||
<Tooltip label={data.last_message} multiline width={300}>
|
||||
<Text c="dimmed" size="xs" lineClamp={2} style={{ color: theme.colors.red[6], lineHeight: 1.3 }}>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
lineClamp={2}
|
||||
style={{ color: theme.colors.red[6], lineHeight: 1.3 }}
|
||||
>
|
||||
{data.last_message}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
|
@ -197,7 +266,11 @@ const EPGsTable = () => {
|
|||
// Show success message for successful sources
|
||||
if (data.status === 'success') {
|
||||
return (
|
||||
<Text c="dimmed" size="xs" style={{ color: theme.colors.green[6], lineHeight: 1.3 }}>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
style={{ color: theme.colors.green[6], lineHeight: 1.3 }}
|
||||
>
|
||||
EPG data refreshed successfully
|
||||
</Text>
|
||||
);
|
||||
|
|
@ -210,24 +283,26 @@ const EPGsTable = () => {
|
|||
{
|
||||
header: 'Updated',
|
||||
accessorKey: 'updated_at',
|
||||
size: 180,
|
||||
minSize: 100,
|
||||
size: 150,
|
||||
enableSorting: false,
|
||||
Cell: ({ cell }) => {
|
||||
cell: ({ cell }) => {
|
||||
const value = cell.getValue();
|
||||
return value ? dayjs(value).format('MMMM D, YYYY h:mma') : 'Never';
|
||||
return value ? (
|
||||
<Text size="xs">{new Date(value).toLocaleString()}</Text>
|
||||
) : (
|
||||
<Text size="xs">Never</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
size: 50,
|
||||
sortingFn: 'basic',
|
||||
mantineTableBodyCellProps: {
|
||||
align: 'left',
|
||||
},
|
||||
Cell: ({ row, cell }) => (
|
||||
cell: ({ row, cell }) => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Switch
|
||||
size="xs"
|
||||
|
|
@ -237,13 +312,15 @@ const EPGsTable = () => {
|
|||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
size: tableSize == 'compact' ? 75 : 100,
|
||||
},
|
||||
],
|
||||
[refreshProgress]
|
||||
);
|
||||
|
||||
//optionally access the underlying virtualizer instance
|
||||
const rowVirtualizerInstanceRef = useRef(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
|
|
@ -286,118 +363,133 @@ const EPGsTable = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
//scroll to the top of the table when the sorting changes
|
||||
try {
|
||||
rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
const table = useMantineReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns,
|
||||
// Sort data before passing to table: active first, then by name
|
||||
data: Object.values(epgs)
|
||||
.sort((a, b) => {
|
||||
setData(
|
||||
Object.values(epgs).sort((a, b) => {
|
||||
// First sort by active status (active items first)
|
||||
if (a.is_active !== b.is_active) {
|
||||
return a.is_active ? -1 : 1;
|
||||
}
|
||||
// Then sort by name (case-insensitive)
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
}),
|
||||
})
|
||||
);
|
||||
}, [epgs]);
|
||||
|
||||
const renderBodyCell = ({ cell, row }) => {
|
||||
switch (cell.column.id) {
|
||||
case 'actions':
|
||||
return (
|
||||
<RowActions
|
||||
tableSize={tableSize}
|
||||
row={row}
|
||||
editEPG={editEPG}
|
||||
deleteEPG={deleteEPG}
|
||||
refreshEPG={refreshEPG}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderHeaderCell = (header) => {
|
||||
let sortingIcon = ArrowUpDown;
|
||||
if (sorting[0]?.id == header.id) {
|
||||
if (sorting[0].desc === false) {
|
||||
sortingIcon = ArrowUpNarrowWide;
|
||||
} else {
|
||||
sortingIcon = ArrowDownWideNarrow;
|
||||
}
|
||||
}
|
||||
|
||||
switch (header.id) {
|
||||
default:
|
||||
return (
|
||||
<Group>
|
||||
<Text size="sm" name={header.id}>
|
||||
{header.column.columnDef.header}
|
||||
</Text>
|
||||
{header.column.columnDef.sortable && (
|
||||
<Center>
|
||||
{React.createElement(sortingIcon, {
|
||||
onClick: () => onSortingChange(header.id),
|
||||
size: 14,
|
||||
})}
|
||||
</Center>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onSortingChange = (column) => {
|
||||
console.log(column);
|
||||
const sortField = sorting[0]?.id;
|
||||
const sortDirection = sorting[0]?.desc;
|
||||
|
||||
const newSorting = [];
|
||||
if (sortField == column) {
|
||||
if (sortDirection == false) {
|
||||
newSorting[0] = {
|
||||
id: column,
|
||||
desc: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
newSorting[0] = {
|
||||
id: column,
|
||||
desc: false,
|
||||
};
|
||||
}
|
||||
|
||||
setSorting(newSorting);
|
||||
if (newSorting.length > 0) {
|
||||
const compareColumn = newSorting[0].id;
|
||||
const compareDesc = newSorting[0].desc;
|
||||
|
||||
setData(
|
||||
epgs.sort((a, b) => {
|
||||
console.log(a);
|
||||
console.log(newSorting[0].id);
|
||||
if (a[compareColumn] !== b[compareColumn]) {
|
||||
return compareDesc ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useTable({
|
||||
columns,
|
||||
data,
|
||||
allRowIds: data.map((epg) => epg.id),
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: false,
|
||||
renderTopToolbar: false,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading,
|
||||
sorting,
|
||||
rowSelection,
|
||||
density: tableDensity,
|
||||
manualSorting: true,
|
||||
bodyCellRenderFns: {
|
||||
actions: renderBodyCell,
|
||||
},
|
||||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: tableDensity,
|
||||
},
|
||||
enableRowActions: true,
|
||||
positionActionsColumn: 'last',
|
||||
displayColumnDefOptions: {
|
||||
'mrt-row-actions': {
|
||||
size: 120, // Make action column wider
|
||||
minSize: 120, // Ensure minimum width for action buttons
|
||||
},
|
||||
},
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize} // Use standardized icon size
|
||||
color="yellow.5" // Red color for delete actions
|
||||
onClick={() => editEPG(row.original)}
|
||||
>
|
||||
<SquarePen size={tableSize === 'compact' ? 16 : 18} /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize} // Use standardized icon size
|
||||
color="red.9" // Red color for delete actions
|
||||
onClick={() => deleteEPG(row.original.id)}
|
||||
>
|
||||
<SquareMinus size={tableSize === 'compact' ? 16 : 18} /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize} // Use standardized icon size
|
||||
color="blue.5" // Red color for delete actions
|
||||
onClick={() => refreshEPG(row.original.id)}
|
||||
disabled={!row.original.is_active}
|
||||
>
|
||||
<RefreshCcw size={tableSize === 'compact' ? 16 : 18} /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
</>
|
||||
),
|
||||
mantineTableContainerProps: {
|
||||
style: {
|
||||
height: 'calc(40vh - 10px)',
|
||||
overflowX: 'auto', // Ensure horizontal scrolling works
|
||||
},
|
||||
},
|
||||
mantineTableProps: {
|
||||
...TableHelper.defaultProperties.mantineTableProps,
|
||||
className: `table-size-${tableSize}`,
|
||||
headerCellRenderFns: {
|
||||
name: renderHeaderCell,
|
||||
source_type: renderHeaderCell,
|
||||
url: renderHeaderCell,
|
||||
status: renderHeaderCell,
|
||||
last_message: renderHeaderCell,
|
||||
updated_at: renderHeaderCell,
|
||||
is_active: renderHeaderCell,
|
||||
actions: renderHeaderCell,
|
||||
},
|
||||
// Add custom cell styles to match CustomTable's sizing
|
||||
mantineTableBodyCellProps: ({ cell }) => {
|
||||
// Check if this is a status message cell with active progress
|
||||
const progressData = cell.column.id === 'last_message' &&
|
||||
refreshProgress[cell.row.original.id] &&
|
||||
refreshProgress[cell.row.original.id].progress < 100 ?
|
||||
refreshProgress[cell.row.original.id] : null;
|
||||
|
||||
// Only expand height for certain actions that need more space
|
||||
const needsExpandedHeight = progressData &&
|
||||
['downloading', 'parsing_channels', 'parsing_programs'].includes(progressData.action);
|
||||
|
||||
tableCellProps: ({ cell }) => {
|
||||
return {
|
||||
style: {
|
||||
// Apply taller height for progress cells (except initializing), otherwise use standard height
|
||||
height: needsExpandedHeight ? '80px' : (
|
||||
tableSize === 'compact' ? '28px' : tableSize === 'large' ? '48px' : '40px'
|
||||
),
|
||||
fontSize: tableSize === 'compact' ? 'var(--mantine-font-size-xs)' : 'var(--mantine-font-size-sm)',
|
||||
padding: tableSize === 'compact' ? '2px 8px' : '4px 10px'
|
||||
}
|
||||
// Apply taller height for progress cells (except initializing), otherwise use standard height
|
||||
fontSize:
|
||||
tableSize === 'compact'
|
||||
? 'var(--mantine-font-size-xs)'
|
||||
: 'var(--mantine-font-size-sm)',
|
||||
padding: tableSize === 'compact' ? '2px 8px' : '4px 10px',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -467,7 +559,26 @@ const EPGsTable = () => {
|
|||
</Box>
|
||||
</Paper>
|
||||
|
||||
<MantineReactTable table={table} />
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(40vh - 10px)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
border: 'solid 1px rgb(68,68,68)',
|
||||
borderRadius: 'var(--mantine-radius-default)',
|
||||
}}
|
||||
>
|
||||
<CustomTable table={table} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<EPGForm epg={epg} isOpen={epgModalOpen} onClose={closeEPGForm} />
|
||||
|
||||
<ConfirmationDialog
|
||||
|
|
@ -482,9 +593,15 @@ const EPGsTable = () => {
|
|||
|
||||
Name: ${epgToDelete.name}
|
||||
Source Type: ${epgToDelete.source_type}
|
||||
${epgToDelete.url ? `URL: ${epgToDelete.url}` :
|
||||
epgToDelete.api_key ? `API Key: ${epgToDelete.api_key}` :
|
||||
epgToDelete.file_path ? `File Path: ${epgToDelete.file_path}` : ''}
|
||||
${
|
||||
epgToDelete.url
|
||||
? `URL: ${epgToDelete.url}`
|
||||
: epgToDelete.api_key
|
||||
? `API Key: ${epgToDelete.api_key}`
|
||||
: epgToDelete.file_path
|
||||
? `File Path: ${epgToDelete.file_path}`
|
||||
: ''
|
||||
}
|
||||
|
||||
This will remove all related program information and channel associations.
|
||||
This action cannot be undone.`}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
|
||||
import React, {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import API from '../../api';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import M3UForm from '../forms/M3U';
|
||||
|
|
@ -18,41 +23,111 @@ import {
|
|||
Stack,
|
||||
Badge,
|
||||
Group,
|
||||
Center,
|
||||
} from '@mantine/core';
|
||||
import { SquareMinus, SquarePen, RefreshCcw, Check, X } from 'lucide-react';
|
||||
import {
|
||||
SquareMinus,
|
||||
SquarePen,
|
||||
RefreshCcw,
|
||||
Check,
|
||||
X,
|
||||
ArrowUpDown,
|
||||
ArrowUpNarrowWide,
|
||||
ArrowDownWideNarrow,
|
||||
} from 'lucide-react';
|
||||
import { IconSquarePlus } from '@tabler/icons-react'; // Import custom icons
|
||||
import dayjs from 'dayjs';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
import useWarningsStore from '../../store/warnings';
|
||||
import { CustomTable, useTable } from './CustomTable';
|
||||
|
||||
// Helper function to format status text
|
||||
const formatStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'idle': return 'Idle';
|
||||
case 'fetching': return 'Fetching';
|
||||
case 'parsing': return 'Parsing';
|
||||
case 'error': return 'Error';
|
||||
case 'success': return 'Success';
|
||||
case 'pending_setup': return 'Pending Setup';
|
||||
default: return status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Unknown';
|
||||
case 'idle':
|
||||
return 'Idle';
|
||||
case 'fetching':
|
||||
return 'Fetching';
|
||||
case 'parsing':
|
||||
return 'Parsing';
|
||||
case 'error':
|
||||
return 'Error';
|
||||
case 'success':
|
||||
return 'Success';
|
||||
case 'pending_setup':
|
||||
return 'Pending Setup';
|
||||
default:
|
||||
return status
|
||||
? status.charAt(0).toUpperCase() + status.slice(1)
|
||||
: 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get status text color
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'idle': return 'gray.5';
|
||||
case 'fetching': return 'blue.5';
|
||||
case 'parsing': return 'indigo.5';
|
||||
case 'error': return 'red.5';
|
||||
case 'success': return 'green.5';
|
||||
case 'pending_setup': return 'orange.5'; // Orange to indicate action needed
|
||||
default: return 'gray.5';
|
||||
case 'idle':
|
||||
return 'gray.5';
|
||||
case 'fetching':
|
||||
return 'blue.5';
|
||||
case 'parsing':
|
||||
return 'indigo.5';
|
||||
case 'error':
|
||||
return 'red.5';
|
||||
case 'success':
|
||||
return 'green.5';
|
||||
case 'pending_setup':
|
||||
return 'orange.5'; // Orange to indicate action needed
|
||||
default:
|
||||
return 'gray.5';
|
||||
}
|
||||
};
|
||||
|
||||
const RowActions = ({
|
||||
tableSize,
|
||||
editPlaylist,
|
||||
deletePlaylist,
|
||||
row,
|
||||
refreshPlaylist,
|
||||
}) => {
|
||||
const iconSize =
|
||||
tableSize == 'default' ? 'sm' : tableSize == 'compact' ? 'xs' : 'md';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize}
|
||||
color="yellow.5"
|
||||
onClick={() => {
|
||||
editPlaylist(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen size={tableSize === 'compact' ? 16 : 18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize}
|
||||
color="red.9"
|
||||
onClick={() => deletePlaylist(row.original.id)}
|
||||
>
|
||||
<SquareMinus size={tableSize === 'compact' ? 16 : 18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={iconSize}
|
||||
color="blue.5"
|
||||
onClick={() => refreshPlaylist(row.original.id)}
|
||||
disabled={!row.original.is_active}
|
||||
>
|
||||
<RefreshCcw size={tableSize === 'compact' ? 16 : 18} />
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const M3UTable = () => {
|
||||
const [playlist, setPlaylist] = useState(null);
|
||||
const [playlistModalOpen, setPlaylistModalOpen] = useState(false);
|
||||
|
|
@ -63,6 +138,8 @@ const M3UTable = () => {
|
|||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [playlistToDelete, setPlaylistToDelete] = useState(null);
|
||||
const [data, setData] = useState([]);
|
||||
const [sorting, setSorting] = useState([{ id: 'name', desc: '' }]);
|
||||
|
||||
const playlists = usePlaylistsStore((s) => s.playlists);
|
||||
const refreshProgress = usePlaylistsStore((s) => s.refreshProgress);
|
||||
|
|
@ -94,7 +171,9 @@ const M3UTable = () => {
|
|||
return buildParsingStats(data);
|
||||
|
||||
default:
|
||||
return data.status === 'error' ? buildErrorStats(data) : `${data.action || 'Processing'}...`;
|
||||
return data.status === 'error'
|
||||
? buildErrorStats(data)
|
||||
: `${data.action || 'Processing'}...`;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -108,28 +187,35 @@ const M3UTable = () => {
|
|||
}
|
||||
|
||||
// Format time remaining in minutes:seconds
|
||||
const timeRemaining = data.time_remaining ?
|
||||
`${Math.floor(data.time_remaining / 60)}:${String(Math.floor(data.time_remaining % 60)).padStart(2, '0')}` :
|
||||
'calculating...';
|
||||
const timeRemaining = data.time_remaining
|
||||
? `${Math.floor(data.time_remaining / 60)}:${String(Math.floor(data.time_remaining % 60)).padStart(2, '0')}`
|
||||
: 'calculating...';
|
||||
|
||||
// Format speed with appropriate unit (KB/s or MB/s)
|
||||
const speed = data.speed >= 1024 ?
|
||||
`${(data.speed / 1024).toFixed(2)} MB/s` :
|
||||
`${Math.round(data.speed)} KB/s`;
|
||||
const speed =
|
||||
data.speed >= 1024
|
||||
? `${(data.speed / 1024).toFixed(2)} MB/s`
|
||||
: `${Math.round(data.speed)} KB/s`;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Downloading:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Downloading:
|
||||
</Text>
|
||||
<Text size="xs">{parseInt(data.progress)}%</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Speed:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Speed:
|
||||
</Text>
|
||||
<Text size="xs">{speed}</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Time left:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Time left:
|
||||
</Text>
|
||||
<Text size="xs">{timeRemaining}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
|
@ -147,26 +233,32 @@ const M3UTable = () => {
|
|||
}
|
||||
|
||||
// Format time displays if available
|
||||
const elapsedTime = data.elapsed_time ?
|
||||
`${Math.floor(data.elapsed_time / 60)}:${String(Math.floor(data.elapsed_time % 60)).padStart(2, '0')}` :
|
||||
null;
|
||||
const elapsedTime = data.elapsed_time
|
||||
? `${Math.floor(data.elapsed_time / 60)}:${String(Math.floor(data.elapsed_time % 60)).padStart(2, '0')}`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Processing groups:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Processing groups:
|
||||
</Text>
|
||||
<Text size="xs">{parseInt(data.progress)}%</Text>
|
||||
</Flex>
|
||||
{elapsedTime && (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Elapsed:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Elapsed:
|
||||
</Text>
|
||||
<Text size="xs">{elapsedTime}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{data.groups_processed && (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Groups:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Groups:
|
||||
</Text>
|
||||
<Text size="xs">{data.groups_processed}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
|
@ -180,9 +272,13 @@ const M3UTable = () => {
|
|||
<Box>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex align="center">
|
||||
<Text size="xs" fw={500} color="red">Error:</Text>
|
||||
<Text size="xs" fw={500} color="red">
|
||||
Error:
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text size="xs" color="red" style={{ lineHeight: 1.3 }}>{data.error || "Unknown error occurred"}</Text>
|
||||
<Text size="xs" color="red" style={{ lineHeight: 1.3 }}>
|
||||
{data.error || 'Unknown error occurred'}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -198,36 +294,44 @@ const M3UTable = () => {
|
|||
}
|
||||
|
||||
// Format time displays
|
||||
const timeRemaining = data.time_remaining ?
|
||||
`${Math.floor(data.time_remaining / 60)}:${String(Math.floor(data.time_remaining % 60)).padStart(2, '0')}` :
|
||||
'calculating...';
|
||||
const timeRemaining = data.time_remaining
|
||||
? `${Math.floor(data.time_remaining / 60)}:${String(Math.floor(data.time_remaining % 60)).padStart(2, '0')}`
|
||||
: 'calculating...';
|
||||
|
||||
const elapsedTime = data.elapsed_time ?
|
||||
`${Math.floor(data.elapsed_time / 60)}:${String(Math.floor(data.elapsed_time % 60)).padStart(2, '0')}` :
|
||||
'0:00';
|
||||
const elapsedTime = data.elapsed_time
|
||||
? `${Math.floor(data.elapsed_time / 60)}:${String(Math.floor(data.elapsed_time % 60)).padStart(2, '0')}`
|
||||
: '0:00';
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Parsing:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Parsing:
|
||||
</Text>
|
||||
<Text size="xs">{parseInt(data.progress)}%</Text>
|
||||
</Flex>
|
||||
{data.elapsed_time && (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Elapsed:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Elapsed:
|
||||
</Text>
|
||||
<Text size="xs">{elapsedTime}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{data.time_remaining && (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Remaining:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Remaining:
|
||||
</Text>
|
||||
<Text size="xs">{timeRemaining}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{data.streams_processed && (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="xs" fw={500}>Streams:</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Streams:
|
||||
</Text>
|
||||
<Text size="xs">{data.streams_processed}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
|
@ -241,7 +345,9 @@ const M3UTable = () => {
|
|||
<Box>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Flex align="center">
|
||||
<Text size="xs" fw={500}>Initializing refresh...</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
Initializing refresh...
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
|
@ -261,7 +367,7 @@ const M3UTable = () => {
|
|||
action: 'initializing',
|
||||
progress: 0,
|
||||
account: id,
|
||||
type: 'm3u_refresh'
|
||||
type: 'm3u_refresh',
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -275,14 +381,14 @@ const M3UTable = () => {
|
|||
account: id,
|
||||
type: 'm3u_refresh',
|
||||
error: 'Failed to start refresh task',
|
||||
status: 'error'
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deletePlaylist = async (id) => {
|
||||
// Get playlist details for the confirmation dialog
|
||||
const playlist = playlists.find(p => p.id === id);
|
||||
const playlist = playlists.find((p) => p.id === id);
|
||||
setPlaylistToDelete(playlist);
|
||||
setDeleteTarget(id);
|
||||
|
||||
|
|
@ -304,10 +410,13 @@ const M3UTable = () => {
|
|||
const toggleActive = async (playlist) => {
|
||||
try {
|
||||
// Send only the is_active field to trigger our special handling
|
||||
await API.updatePlaylist({
|
||||
id: playlist.id,
|
||||
is_active: !playlist.is_active,
|
||||
}, true); // Add a new parameter to indicate this is just a toggle
|
||||
await API.updatePlaylist(
|
||||
{
|
||||
id: playlist.id,
|
||||
is_active: !playlist.is_active,
|
||||
},
|
||||
true
|
||||
); // Add a new parameter to indicate this is just a toggle
|
||||
} catch (error) {
|
||||
console.error('Error toggling active state:', error);
|
||||
}
|
||||
|
|
@ -318,14 +427,15 @@ const M3UTable = () => {
|
|||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
size: 150,
|
||||
minSize: 100, // Minimum width
|
||||
size: 200,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
header: 'Account Type',
|
||||
accessorKey: 'account_type',
|
||||
size: 100,
|
||||
Cell: ({ cell }) => {
|
||||
sortable: true,
|
||||
size: 150,
|
||||
cell: ({ cell }) => {
|
||||
const value = cell.getValue();
|
||||
return value === 'XC' ? 'XC' : 'M3U';
|
||||
},
|
||||
|
|
@ -333,9 +443,7 @@ const M3UTable = () => {
|
|||
{
|
||||
header: 'URL / File',
|
||||
accessorKey: 'server_url',
|
||||
size: 200,
|
||||
minSize: 120,
|
||||
Cell: ({ cell, row }) => {
|
||||
cell: ({ cell, row }) => {
|
||||
const value = cell.getValue() || row.original.file_path || '';
|
||||
return (
|
||||
<Tooltip label={value} disabled={!value}>
|
||||
|
|
@ -356,13 +464,14 @@ const M3UTable = () => {
|
|||
{
|
||||
header: 'Max Streams',
|
||||
accessorKey: 'max_streams',
|
||||
size: 100,
|
||||
sortable: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
size: 100,
|
||||
Cell: ({ cell }) => {
|
||||
size: 150,
|
||||
cell: ({ cell }) => {
|
||||
const value = cell.getValue();
|
||||
if (!value) return null;
|
||||
|
||||
|
|
@ -377,10 +486,7 @@ const M3UTable = () => {
|
|||
{
|
||||
header: 'Status Message',
|
||||
accessorKey: 'last_message',
|
||||
size: 250, // Increase default size
|
||||
minSize: 200, // Set minimum size
|
||||
maxSize: 400, // Allow expansion up to this size
|
||||
Cell: ({ cell, row }) => {
|
||||
cell: ({ cell, row }) => {
|
||||
const value = cell.getValue();
|
||||
const data = row.original;
|
||||
|
||||
|
|
@ -391,16 +497,18 @@ const M3UTable = () => {
|
|||
// If we have active progress data for this account, show that instead
|
||||
if (progressData && progressData.progress < 100) {
|
||||
return (
|
||||
<Box style={{
|
||||
// Use full height of the cell with proper spacing
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
// Add some padding to give content room to breathe
|
||||
padding: '4px 0'
|
||||
}}>
|
||||
<Box
|
||||
style={{
|
||||
// Use full height of the cell with proper spacing
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
// Add some padding to give content room to breathe
|
||||
padding: '4px 0',
|
||||
}}
|
||||
>
|
||||
{generateStatusString(progressData)}
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -413,7 +521,12 @@ const M3UTable = () => {
|
|||
if (data.status === 'error') {
|
||||
return (
|
||||
<Tooltip label={value} multiline width={300}>
|
||||
<Text c="dimmed" size="xs" lineClamp={2} style={{ color: theme.colors.red[6], lineHeight: 1.3 }}>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
lineClamp={2}
|
||||
style={{ color: theme.colors.red[6], lineHeight: 1.3 }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
|
@ -424,7 +537,17 @@ const M3UTable = () => {
|
|||
if (data.status === 'success') {
|
||||
return (
|
||||
<Tooltip label={value} multiline width={300}>
|
||||
<Text c="dimmed" size="xs" style={{ color: theme.colors.green[6], lineHeight: 1.3 }}>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
style={{
|
||||
color: theme.colors.green[6],
|
||||
lineHeight: 1.3,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
|
@ -434,7 +557,12 @@ const M3UTable = () => {
|
|||
// For all other status values, just use dimmed text
|
||||
return (
|
||||
<Tooltip label={value} multiline width={300}>
|
||||
<Text c="dimmed" size="xs" lineClamp={2} style={{ lineHeight: 1.3 }}>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
lineClamp={2}
|
||||
style={{ lineHeight: 1.3 }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
|
@ -444,22 +572,21 @@ const M3UTable = () => {
|
|||
{
|
||||
header: 'Updated',
|
||||
accessorKey: 'updated_at',
|
||||
size: 120,
|
||||
Cell: ({ cell }) => {
|
||||
size: 150,
|
||||
cell: ({ cell }) => {
|
||||
const value = cell.getValue();
|
||||
return value ? <Text size="xs">{new Date(value).toLocaleString()}</Text> : <Text size="xs">Never</Text>;
|
||||
return value ? (
|
||||
<Text size="xs">{new Date(value).toLocaleString()}</Text>
|
||||
) : (
|
||||
<Text size="xs">Never</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
sortingFn: 'basic',
|
||||
mantineTableBodyCellProps: {
|
||||
align: 'left',
|
||||
},
|
||||
Cell: ({ cell, row }) => {
|
||||
size: 50,
|
||||
cell: ({ cell, row }) => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Switch
|
||||
|
|
@ -471,7 +598,11 @@ const M3UTable = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
// Remove the custom Actions column here
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
size: tableSize == 'compact' ? 75 : 100,
|
||||
},
|
||||
],
|
||||
[refreshPlaylist, editPlaylist, deletePlaylist, toggleActive]
|
||||
);
|
||||
|
|
@ -480,7 +611,6 @@ const M3UTable = () => {
|
|||
const rowVirtualizerInstanceRef = useRef(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
const closeModal = (newPlaylist = null) => {
|
||||
if (newPlaylist) {
|
||||
|
|
@ -493,19 +623,6 @@ const M3UTable = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const deletePlaylists = async (ids) => {
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
// await API.deleteStreams(selected.map(stream => stream.original.id))
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
//scroll to the top of the table when the sorting changes
|
||||
try {
|
||||
|
|
@ -517,8 +634,21 @@ const M3UTable = () => {
|
|||
|
||||
// Listen for edit playlist requests from notifications
|
||||
useEffect(() => {
|
||||
setData(
|
||||
playlists
|
||||
.filter((playlist) => playlist.locked === false)
|
||||
.sort((a, b) => {
|
||||
// First sort by active status (active items first)
|
||||
if (a.is_active !== b.is_active) {
|
||||
return a.is_active ? -1 : 1;
|
||||
}
|
||||
// Then sort by name (case-insensitive)
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
})
|
||||
);
|
||||
|
||||
if (editPlaylistId) {
|
||||
const playlistToEdit = playlists.find(p => p.id === editPlaylistId);
|
||||
const playlistToEdit = playlists.find((p) => p.id === editPlaylistId);
|
||||
if (playlistToEdit) {
|
||||
editPlaylist(playlistToEdit);
|
||||
// Reset the ID after handling
|
||||
|
|
@ -527,80 +657,120 @@ const M3UTable = () => {
|
|||
}
|
||||
}, [editPlaylistId, playlists]);
|
||||
|
||||
const tableDensity = tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'xl' : 'md';
|
||||
const onSortingChange = (column) => {
|
||||
console.log(column);
|
||||
const sortField = sorting[0]?.id;
|
||||
const sortDirection = sorting[0]?.desc;
|
||||
|
||||
const table = useMantineReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
const newSorting = [];
|
||||
if (sortField == column) {
|
||||
if (sortDirection == false) {
|
||||
newSorting[0] = {
|
||||
id: column,
|
||||
desc: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
newSorting[0] = {
|
||||
id: column,
|
||||
desc: false,
|
||||
};
|
||||
}
|
||||
|
||||
setSorting(newSorting);
|
||||
if (newSorting.length > 0) {
|
||||
const compareColumn = newSorting[0].id;
|
||||
const compareDesc = newSorting[0].desc;
|
||||
|
||||
setData(
|
||||
playlists
|
||||
.filter((playlist) => playlist.locked === false)
|
||||
.sort((a, b) => {
|
||||
console.log(a);
|
||||
console.log(newSorting[0].id);
|
||||
if (a[compareColumn] !== b[compareColumn]) {
|
||||
return compareDesc ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderHeaderCell = (header) => {
|
||||
let sortingIcon = ArrowUpDown;
|
||||
if (sorting[0]?.id == header.id) {
|
||||
if (sorting[0].desc === false) {
|
||||
sortingIcon = ArrowUpNarrowWide;
|
||||
} else {
|
||||
sortingIcon = ArrowDownWideNarrow;
|
||||
}
|
||||
}
|
||||
|
||||
switch (header.id) {
|
||||
default:
|
||||
return (
|
||||
<Group>
|
||||
<Text size="sm" name={header.id}>
|
||||
{header.column.columnDef.header}
|
||||
</Text>
|
||||
{header.column.columnDef.sortable && (
|
||||
<Center>
|
||||
{React.createElement(sortingIcon, {
|
||||
onClick: () => onSortingChange(header.id),
|
||||
size: 14,
|
||||
})}
|
||||
</Center>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderBodyCell = useCallback(({ cell, row }) => {
|
||||
switch (cell.column.id) {
|
||||
case 'actions':
|
||||
return (
|
||||
<RowActions
|
||||
tableSize={tableSize}
|
||||
editPlaylist={editPlaylist}
|
||||
deletePlaylist={deletePlaylist}
|
||||
row={row}
|
||||
refreshPlaylist={refreshPlaylist}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const table = useTable({
|
||||
columns,
|
||||
// Sort data before passing to table: active first, then by name
|
||||
data: playlists
|
||||
.filter((playlist) => playlist.locked === false)
|
||||
.sort((a, b) => {
|
||||
// First sort by active status (active items first)
|
||||
if (a.is_active !== b.is_active) {
|
||||
return a.is_active ? -1 : 1;
|
||||
}
|
||||
// Then sort by name (case-insensitive)
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
}),
|
||||
data,
|
||||
allRowIds: data.map((playlist) => playlist.id),
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
enableRowSelection: false,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
renderTopToolbar: false,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading,
|
||||
sorting,
|
||||
rowSelection,
|
||||
// Use density directly from tableSize
|
||||
density: tableDensity,
|
||||
},
|
||||
sorting,
|
||||
manualSorting: true,
|
||||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
// Use density directly from tableSize
|
||||
density: tableDensity,
|
||||
bodyCellRenderFns: {
|
||||
actions: renderBodyCell,
|
||||
},
|
||||
enableRowActions: true, // Enable row actions
|
||||
positionActionsColumn: 'last',
|
||||
displayColumnDefOptions: {
|
||||
'mrt-row-actions': {
|
||||
size: 120, // Make action column wider
|
||||
minSize: 120, // Ensure minimum width for action buttons
|
||||
},
|
||||
headerCellRenderFns: {
|
||||
name: renderHeaderCell,
|
||||
account_type: renderHeaderCell,
|
||||
server_url: renderHeaderCell,
|
||||
max_streams: renderHeaderCell,
|
||||
status: renderHeaderCell,
|
||||
last_message: renderHeaderCell,
|
||||
updated_at: renderHeaderCell,
|
||||
is_active: renderHeaderCell,
|
||||
actions: renderHeaderCell,
|
||||
},
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm'} // Use standardized icon size
|
||||
color="yellow.5"
|
||||
onClick={() => {
|
||||
editPlaylist(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen size={tableSize === 'compact' ? 16 : 18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm'} // Use standardized icon size
|
||||
color="red.9"
|
||||
onClick={() => deletePlaylist(row.original.id)}
|
||||
>
|
||||
<SquareMinus size={tableSize === 'compact' ? 16 : 18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size={tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm'} // Use standardized icon size
|
||||
color="blue.5"
|
||||
onClick={() => refreshPlaylist(row.original.id)}
|
||||
disabled={!row.original.is_active}
|
||||
>
|
||||
<RefreshCcw size={tableSize === 'compact' ? 16 : 18} />
|
||||
</ActionIcon>
|
||||
</>
|
||||
),
|
||||
mantineTableContainerProps: {
|
||||
style: {
|
||||
height: 'calc(40vh - 10px)',
|
||||
|
|
@ -612,34 +782,22 @@ const M3UTable = () => {
|
|||
className: `table-size-${tableSize}`,
|
||||
},
|
||||
// Add custom cell styles to match CustomTable's sizing
|
||||
mantineTableBodyCellProps: ({ cell }) => {
|
||||
// Check if this is a status message cell with active progress
|
||||
const progressData = cell.column.id === 'last_message' &&
|
||||
refreshProgress[cell.row.original.id] &&
|
||||
refreshProgress[cell.row.original.id].progress < 100 ?
|
||||
refreshProgress[cell.row.original.id] : null;
|
||||
|
||||
// Only expand height for certain actions that need more space
|
||||
const needsExpandedHeight = progressData &&
|
||||
['downloading', 'parsing', 'processing_groups'].includes(progressData.action);
|
||||
|
||||
tableCellProps: ({ cell }) => {
|
||||
return {
|
||||
style: {
|
||||
// Apply taller height for progress cells (except initializing), otherwise use standard height
|
||||
height: needsExpandedHeight ? '80px' : (
|
||||
tableSize === 'compact' ? '28px' : tableSize === 'large' ? '48px' : '40px'
|
||||
),
|
||||
fontSize: tableSize === 'compact' ? 'var(--mantine-font-size-xs)' : 'var(--mantine-font-size-sm)',
|
||||
padding: tableSize === 'compact' ? '2px 8px' : '4px 10px'
|
||||
}
|
||||
fontSize:
|
||||
tableSize === 'compact'
|
||||
? 'var(--mantine-font-size-xs)'
|
||||
: 'var(--mantine-font-size-sm)',
|
||||
padding: tableSize === 'compact' ? '2px 8px' : '4px 10px',
|
||||
};
|
||||
},
|
||||
// Additional text styling to match ChannelsTable
|
||||
mantineTableBodyProps: {
|
||||
style: {
|
||||
fontSize: tableSize === 'compact' ? 'var(--mantine-font-size-xs)' : 'var(--mantine-font-size-sm)',
|
||||
}
|
||||
},
|
||||
tableBodyProps: () => ({
|
||||
fontSize:
|
||||
tableSize === 'compact'
|
||||
? 'var(--mantine-font-size-xs)'
|
||||
: 'var(--mantine-font-size-sm)',
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -702,7 +860,26 @@ const M3UTable = () => {
|
|||
</Flex>
|
||||
</Box>
|
||||
</Paper>
|
||||
<MantineReactTable table={table} />
|
||||
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(40vh - 10px)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
border: 'solid 1px rgb(68,68,68)',
|
||||
borderRadius: 'var(--mantine-radius-default)',
|
||||
}}
|
||||
>
|
||||
<CustomTable table={table} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<M3UForm
|
||||
m3uAccount={playlist}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
|
||||
import API from '../../api';
|
||||
import StreamProfileForm from '../forms/StreamProfile';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
|
|
@ -21,57 +20,101 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { IconSquarePlus } from '@tabler/icons-react';
|
||||
import { SquareMinus, SquarePen, Check, X, Eye, EyeOff } from 'lucide-react';
|
||||
import { CustomTable, useTable } from './CustomTable';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
|
||||
const RowActions = ({ row, editStreamProfile, deleteStreamProfile }) => {
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
color="yellow.5"
|
||||
size="sm"
|
||||
disabled={row.original.locked}
|
||||
onClick={() => editStreamProfile(row.original)}
|
||||
>
|
||||
<SquarePen size="18" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
color="red.9"
|
||||
disabled={row.original.locked}
|
||||
onClick={() => deleteStreamProfile(row.original.id)}
|
||||
>
|
||||
<SquareMinus fontSize="small" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StreamProfiles = () => {
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
const [hideInactive, setHideInactive] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const streamProfiles = useStreamProfilesStore((state) => state.profiles);
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
const [tableSize] = useLocalStorage('table-size', 'default');
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
size: 50,
|
||||
size: 150,
|
||||
cell: ({ cell }) => (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{cell.getValue()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Command',
|
||||
accessorKey: 'command',
|
||||
size: 100,
|
||||
size: 150,
|
||||
cell: ({ cell }) => (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{cell.getValue()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Parameters',
|
||||
accessorKey: 'parameters',
|
||||
enableSorting: false,
|
||||
mantineTableBodyCellProps: {
|
||||
style: {
|
||||
whiteSpace: 'nowrap',
|
||||
// maxWidth: 400,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
},
|
||||
// size: 200,
|
||||
cell: ({ cell }) => (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{cell.getValue()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
size: 10,
|
||||
enableSorting: false,
|
||||
mantineTableHeadCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
mantineTableBodyCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
Cell: ({ row, cell }) => (
|
||||
size: 50,
|
||||
cell: ({ row, cell }) => (
|
||||
<Center>
|
||||
<Switch
|
||||
size="xs"
|
||||
|
|
@ -81,24 +124,11 @@ const StreamProfiles = () => {
|
|||
/>
|
||||
</Center>
|
||||
),
|
||||
Filter: ({ column }) => (
|
||||
<Box>
|
||||
<Select
|
||||
size="small"
|
||||
value={activeFilterValue}
|
||||
onChange={(e) => {
|
||||
setActiveFilterValue(e.target.value);
|
||||
column.setFilterValue(e.target.value);
|
||||
}}
|
||||
displayEmpty
|
||||
data={['All', 'Active', 'Inactive']}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
filterFn: (row, _columnId, filterValue) => {
|
||||
if (filterValue == 'all') return true;
|
||||
return String(row.getValue('is_active')) === filterValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
size: tableSize == 'compact' ? 75 : 100,
|
||||
},
|
||||
],
|
||||
[]
|
||||
|
|
@ -159,63 +189,48 @@ const StreamProfiles = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const filteredData = streamProfiles.filter((profile) =>
|
||||
hideInactive && !profile.is_active ? false : true
|
||||
);
|
||||
useEffect(() => {
|
||||
setData(
|
||||
streamProfiles.filter((profile) =>
|
||||
hideInactive && !profile.is_active ? false : true
|
||||
)
|
||||
);
|
||||
}, [streamProfiles, hideInactive]);
|
||||
|
||||
const table = useMantineReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
const renderHeaderCell = (header) => {
|
||||
return (
|
||||
<Text size="sm" name={header.id}>
|
||||
{header.column.columnDef.header}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBodyCell = ({ cell, row }) => {
|
||||
switch (cell.column.id) {
|
||||
case 'actions':
|
||||
return (
|
||||
<RowActions
|
||||
row={row}
|
||||
editUserAgent={editStreamProfile}
|
||||
deleteUserAgent={deleteStreamProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useTable({
|
||||
columns,
|
||||
data: filteredData,
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
// enableRowSelection: true,
|
||||
renderTopToolbar: false,
|
||||
// onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading,
|
||||
sorting,
|
||||
// rowSelection,
|
||||
data,
|
||||
allRowIds: data.map((d) => d.id),
|
||||
bodyCellRenderFns: {
|
||||
actions: renderBodyCell,
|
||||
},
|
||||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
},
|
||||
displayColumnDefOptions: {
|
||||
'mrt-row-actions': {
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
color="yellow.5"
|
||||
size="sm"
|
||||
disabled={row.original.locked}
|
||||
onClick={() => editStreamProfile(row.original)}
|
||||
>
|
||||
<SquarePen size="18" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
color="red.9"
|
||||
disabled={row.original.locked}
|
||||
onClick={() => deleteStreamProfile(row.original.id)}
|
||||
>
|
||||
<SquareMinus fontSize="small" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
</>
|
||||
),
|
||||
mantineTableContainerProps: {
|
||||
style: {
|
||||
// height: 'calc(60vh - 100px)',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
headerCellRenderFns: {
|
||||
name: renderHeaderCell,
|
||||
command: renderHeaderCell,
|
||||
parameters: renderHeaderCell,
|
||||
is_active: renderHeaderCell,
|
||||
actions: renderHeaderCell,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -275,7 +290,25 @@ const StreamProfiles = () => {
|
|||
</Box>
|
||||
</Paper>
|
||||
|
||||
<MantineReactTable table={table} />
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: 300,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
border: 'solid 1px rgb(68,68,68)',
|
||||
borderRadius: 'var(--mantine-radius-default)',
|
||||
}}
|
||||
>
|
||||
<CustomTable table={table} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<StreamProfileForm
|
||||
profile={profile}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
|
||||
import API from '../../api';
|
||||
import useUserAgentsStore from '../../store/userAgents';
|
||||
import UserAgentForm from '../forms/UserAgent';
|
||||
|
|
@ -20,15 +19,42 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { IconSquarePlus } from '@tabler/icons-react';
|
||||
import { SquareMinus, SquarePen, Check, X } from 'lucide-react';
|
||||
import { CustomTable, useTable } from './CustomTable';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
|
||||
const RowActions = ({ row, editUserAgent, deleteUserAgent }) => {
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm" // Makes the button smaller
|
||||
color="yellow.5" // Red color for delete actions
|
||||
onClick={() => {
|
||||
editUserAgent(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen size="18" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
color="red.9" // Red color for delete actions
|
||||
onClick={() => deleteUserAgent(row.original.id)}
|
||||
>
|
||||
<SquareMinus size="18" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAgentsTable = () => {
|
||||
const [userAgent, setUserAgent] = useState(null);
|
||||
const [userAgentModalOpen, setUserAgentModalOpen] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState([]);
|
||||
const [activeFilterValue, setActiveFilterValue] = useState('all');
|
||||
|
||||
const userAgents = useUserAgentsStore((state) => state.userAgents);
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
const [tableSize] = useLocalStorage('table-size', 'default');
|
||||
|
||||
const columns = useMemo(
|
||||
//column definitions...
|
||||
|
|
@ -36,13 +62,12 @@ const UserAgentsTable = () => {
|
|||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: 'User-Agent',
|
||||
accessorKey: 'user_agent',
|
||||
enableSorting: false,
|
||||
Cell: ({ cell }) => (
|
||||
cell: ({ cell }) => (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
|
|
@ -58,7 +83,7 @@ const UserAgentsTable = () => {
|
|||
header: 'Description',
|
||||
accessorKey: 'description',
|
||||
enableSorting: false,
|
||||
Cell: ({ cell }) => (
|
||||
cell: ({ cell }) => (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
|
|
@ -73,57 +98,23 @@ const UserAgentsTable = () => {
|
|||
{
|
||||
header: 'Active',
|
||||
accessorKey: 'is_active',
|
||||
size: 10,
|
||||
sortingFn: 'basic',
|
||||
enableSorting: false,
|
||||
mantineTableHeadCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
mantineTableBodyCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
Cell: ({ cell }) => (
|
||||
cell: ({ cell }) => (
|
||||
<Center>
|
||||
{cell.getValue() ? <Check color="green" /> : <X color="red" />}
|
||||
</Center>
|
||||
),
|
||||
Filter: ({ column }) => (
|
||||
<Select
|
||||
size="small"
|
||||
value={activeFilterValue}
|
||||
onChange={(e) => {
|
||||
setActiveFilterValue(e.target.value);
|
||||
column.setFilterValue(e.target.value);
|
||||
}}
|
||||
displayEmpty
|
||||
data={[
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
label: 'Active',
|
||||
},
|
||||
{
|
||||
value: 'inactive',
|
||||
label: 'Inactive',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
filterFn: (row, _columnId, activeFilterValue) => {
|
||||
if (activeFilterValue == 'all') return true; // Show all if no filter
|
||||
return String(row.getValue('is_active')) === activeFilterValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
size: tableSize == 'compact' ? 75 : 100,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
//optionally access the underlying virtualizer instance
|
||||
const rowVirtualizerInstanceRef = useRef(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [sorting, setSorting] = useState([]);
|
||||
|
||||
|
|
@ -167,95 +158,54 @@ const UserAgentsTable = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
//scroll to the top of the table when the sorting changes
|
||||
try {
|
||||
rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const renderHeaderCell = (header) => {
|
||||
switch (header.id) {
|
||||
default:
|
||||
return (
|
||||
<Text size="sm" name={header.id}>
|
||||
{header.column.columnDef.header}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}, [sorting]);
|
||||
};
|
||||
|
||||
const table = useMantineReactTable({
|
||||
...TableHelper.defaultProperties,
|
||||
const renderBodyCell = ({ cell, row }) => {
|
||||
switch (cell.column.id) {
|
||||
case 'actions':
|
||||
return (
|
||||
<RowActions
|
||||
row={row}
|
||||
editUserAgent={editUserAgent}
|
||||
deleteUserAgent={deleteUserAgent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useTable({
|
||||
columns,
|
||||
data: userAgents,
|
||||
enablePagination: false,
|
||||
enableRowVirtualization: true,
|
||||
// enableRowSelection: true,
|
||||
renderTopToolbar: false,
|
||||
// onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
isLoading,
|
||||
sorting,
|
||||
// rowSelection,
|
||||
allRowIds: userAgents.map((ua) => ua.id),
|
||||
bodyCellRenderFns: {
|
||||
actions: renderBodyCell,
|
||||
},
|
||||
rowVirtualizerInstanceRef, //optional
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
initialState: {
|
||||
density: 'compact',
|
||||
},
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm" // Makes the button smaller
|
||||
color="yellow.5" // Red color for delete actions
|
||||
onClick={() => {
|
||||
editUserAgent(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen size="18" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
color="red.9" // Red color for delete actions
|
||||
onClick={() => deleteUserAgent(row.original.id)}
|
||||
>
|
||||
<SquareMinus size="18" /> {/* Small icon size */}
|
||||
</ActionIcon>
|
||||
</>
|
||||
),
|
||||
mantineTableContainerProps: {
|
||||
style: {
|
||||
maxHeight: 300,
|
||||
overflowY: 'auto',
|
||||
// margin: 5,
|
||||
},
|
||||
},
|
||||
displayColumnDefOptions: {
|
||||
'mrt-row-actions': {
|
||||
size: 10,
|
||||
},
|
||||
headerCellRenderFns: {
|
||||
name: renderHeaderCell,
|
||||
user_agent: renderHeaderCell,
|
||||
description: renderHeaderCell,
|
||||
is_active: renderHeaderCell,
|
||||
actions: renderHeaderCell,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={0} style={{ padding: 0 }}>
|
||||
<Paper
|
||||
style={
|
||||
{
|
||||
// bgcolor: theme.palette.background.paper,
|
||||
// borderRadius: 2,
|
||||
// overflow: 'hidden',
|
||||
// height: 'calc(100vh - 75px)',
|
||||
// display: 'flex',
|
||||
// flexDirection: 'column',
|
||||
}
|
||||
}
|
||||
>
|
||||
{/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
|
||||
<Paper>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
// alignItems: 'center',
|
||||
// backgroundColor: theme.palette.background.paper,
|
||||
justifyContent: 'flex-end',
|
||||
padding: 10,
|
||||
// gap: 1,
|
||||
}}
|
||||
>
|
||||
<Flex gap={6}>
|
||||
|
|
@ -280,7 +230,26 @@ const UserAgentsTable = () => {
|
|||
</Box>
|
||||
</Paper>
|
||||
|
||||
<MantineReactTable table={table} />
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: 300,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
border: 'solid 1px rgb(68,68,68)',
|
||||
borderRadius: 'var(--mantine-radius-default)',
|
||||
}}
|
||||
>
|
||||
<CustomTable table={table} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<UserAgentForm
|
||||
userAgent={userAgent}
|
||||
isOpen={userAgentModalOpen}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
useMantineTheme,
|
||||
Select,
|
||||
} from '@mantine/core';
|
||||
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
|
||||
import { TableHelper } from '../helpers';
|
||||
import API from '../api';
|
||||
import useChannelsStore from '../store/channels';
|
||||
|
|
@ -37,6 +36,7 @@ import useStreamProfilesStore from '../store/streamProfiles';
|
|||
import usePlaylistsStore from '../store/playlists'; // Add this import
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { CustomTable, useTable } from '../components/tables/CustomTable';
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
|
|
@ -78,12 +78,19 @@ const getStartDate = (uptime) => {
|
|||
};
|
||||
|
||||
// Create a separate component for each channel card to properly handle the hook
|
||||
const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channelsByUUID }) => {
|
||||
const ChannelCard = ({
|
||||
channel,
|
||||
clients,
|
||||
stopClient,
|
||||
stopChannel,
|
||||
logos,
|
||||
channelsByUUID,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const [availableStreams, setAvailableStreams] = useState([]);
|
||||
const [isLoadingStreams, setIsLoadingStreams] = useState(false);
|
||||
const [activeStreamId, setActiveStreamId] = useState(null);
|
||||
const [currentM3UProfile, setCurrentM3UProfile] = useState(null); // Add state for current M3U profile
|
||||
const [currentM3UProfile, setCurrentM3UProfile] = useState(null); // Add state for current M3U profile
|
||||
|
||||
// Get M3U account data from the playlists store
|
||||
const m3uAccounts = usePlaylistsStore((s) => s.playlists);
|
||||
|
|
@ -92,7 +99,7 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
const m3uAccountsMap = useMemo(() => {
|
||||
const map = {};
|
||||
if (m3uAccounts && Array.isArray(m3uAccounts)) {
|
||||
m3uAccounts.forEach(account => {
|
||||
m3uAccounts.forEach((account) => {
|
||||
if (account.id) {
|
||||
map[account.id] = account.name;
|
||||
}
|
||||
|
|
@ -111,7 +118,10 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
// If the channel data includes M3U profile information, update our state
|
||||
if (channel.m3u_profile || channel.m3u_profile_name) {
|
||||
setCurrentM3UProfile({
|
||||
name: channel.m3u_profile?.name || channel.m3u_profile_name || 'Default M3U'
|
||||
name:
|
||||
channel.m3u_profile?.name ||
|
||||
channel.m3u_profile_name ||
|
||||
'Default M3U',
|
||||
});
|
||||
}
|
||||
}, [channel.m3u_profile, channel.m3u_profile_name, channel.stream_id]);
|
||||
|
|
@ -132,8 +142,10 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
// If we have a channel URL, try to find the matching stream
|
||||
if (channel.url && streamData.length > 0) {
|
||||
// Try to find matching stream based on URL
|
||||
const matchingStream = streamData.find(stream =>
|
||||
channel.url.includes(stream.url) || stream.url.includes(channel.url)
|
||||
const matchingStream = streamData.find(
|
||||
(stream) =>
|
||||
channel.url.includes(stream.url) ||
|
||||
stream.url.includes(channel.url)
|
||||
);
|
||||
|
||||
if (matchingStream) {
|
||||
|
|
@ -147,7 +159,7 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching streams:", error);
|
||||
console.error('Error fetching streams:', error);
|
||||
} finally {
|
||||
setIsLoadingStreams(false);
|
||||
}
|
||||
|
|
@ -159,14 +171,16 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
// Handle stream switching
|
||||
const handleStreamChange = async (streamId) => {
|
||||
try {
|
||||
console.log("Switching to stream ID:", streamId);
|
||||
console.log('Switching to stream ID:', streamId);
|
||||
// Find the selected stream in availableStreams for debugging
|
||||
const selectedStream = availableStreams.find(s => s.id.toString() === streamId);
|
||||
console.log("Selected stream details:", selectedStream);
|
||||
const selectedStream = availableStreams.find(
|
||||
(s) => s.id.toString() === streamId
|
||||
);
|
||||
console.log('Selected stream details:', selectedStream);
|
||||
|
||||
// Make sure we're passing the correct ID to the API
|
||||
const response = await API.switchStream(channel.channel_id, streamId);
|
||||
console.log("Stream switch API response:", response);
|
||||
console.log('Stream switch API response:', response);
|
||||
|
||||
// Update the local active stream ID immediately
|
||||
setActiveStreamId(streamId);
|
||||
|
|
@ -192,21 +206,22 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
const channelId = channelsByUUID[channel.channel_id];
|
||||
if (channelId) {
|
||||
const updatedStreamData = await API.getChannelStreams(channelId);
|
||||
console.log("Channel streams after switch:", updatedStreamData);
|
||||
console.log('Channel streams after switch:', updatedStreamData);
|
||||
|
||||
// Update current stream information with fresh data
|
||||
const updatedStream = updatedStreamData.find(s => s.id.toString() === streamId);
|
||||
const updatedStream = updatedStreamData.find(
|
||||
(s) => s.id.toString() === streamId
|
||||
);
|
||||
if (updatedStream && updatedStream.m3u_profile) {
|
||||
setCurrentM3UProfile(updatedStream.m3u_profile);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking streams after switch:", error);
|
||||
console.error('Error checking streams after switch:', error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Stream switch error:", error);
|
||||
console.error('Stream switch error:', error);
|
||||
notifications.show({
|
||||
title: 'Error switching stream',
|
||||
message: error.toString(),
|
||||
|
|
@ -230,7 +245,10 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
if (row.connected_since) {
|
||||
// Calculate the actual connection time by subtracting the seconds from current time
|
||||
const currentTime = dayjs();
|
||||
const connectedTime = currentTime.subtract(row.connected_since, 'second');
|
||||
const connectedTime = currentTime.subtract(
|
||||
row.connected_since,
|
||||
'second'
|
||||
);
|
||||
return connectedTime.format('MM/DD HH:mm:ss');
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +261,13 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
return 'Unknown';
|
||||
},
|
||||
Cell: ({ cell }) => (
|
||||
<Tooltip label={cell.getValue() !== 'Unknown' ? `Connected at ${cell.getValue()}` : 'Unknown connection time'}>
|
||||
<Tooltip
|
||||
label={
|
||||
cell.getValue() !== 'Unknown'
|
||||
? `Connected at ${cell.getValue()}`
|
||||
: 'Unknown connection time'
|
||||
}
|
||||
>
|
||||
<Text size="xs">{cell.getValue()}</Text>
|
||||
</Tooltip>
|
||||
),
|
||||
|
|
@ -258,27 +282,36 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
}
|
||||
|
||||
if (row.connection_duration) {
|
||||
return dayjs.duration(row.connection_duration, 'seconds').humanize();
|
||||
return dayjs
|
||||
.duration(row.connection_duration, 'seconds')
|
||||
.humanize();
|
||||
}
|
||||
|
||||
return '-';
|
||||
},
|
||||
Cell: ({ cell, row }) => {
|
||||
const exactDuration = row.original.connected_since || row.original.connection_duration;
|
||||
const exactDuration =
|
||||
row.original.connected_since || row.original.connection_duration;
|
||||
return (
|
||||
<Tooltip label={exactDuration ? `${exactDuration.toFixed(1)} seconds` : 'Unknown duration'}>
|
||||
<Tooltip
|
||||
label={
|
||||
exactDuration
|
||||
? `${exactDuration.toFixed(1)} seconds`
|
||||
: 'Unknown duration'
|
||||
}
|
||||
>
|
||||
<Text size="xs">{cell.getValue()}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
size: 50,
|
||||
}
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// This hook is now at the top level of this component
|
||||
const channelClientsTable = useMantineReactTable({
|
||||
const channelClientsTable = useTable({
|
||||
...TableHelper.defaultProperties,
|
||||
columns: clientsColumns,
|
||||
data: clients.filter(
|
||||
|
|
@ -319,8 +352,10 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
renderDetailPanel: ({ row }) => (
|
||||
<Box p="xs">
|
||||
<Group spacing="xs" align="flex-start">
|
||||
<Text size="xs" fw={500} color="dimmed">User Agent:</Text>
|
||||
<Text size="xs">{row.original.user_agent || "Unknown"}</Text>
|
||||
<Text size="xs" fw={500} color="dimmed">
|
||||
User Agent:
|
||||
</Text>
|
||||
<Text size="xs">{row.original.user_agent || 'Unknown'}</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
),
|
||||
|
|
@ -347,8 +382,10 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
}
|
||||
|
||||
// Get logo URL from the logos object if available
|
||||
const logoUrl = channel.logo_id && logos && logos[channel.logo_id] ?
|
||||
logos[channel.logo_id].cache_url : null;
|
||||
const logoUrl =
|
||||
channel.logo_id && logos && logos[channel.logo_id]
|
||||
? logos[channel.logo_id].cache_url
|
||||
: null;
|
||||
|
||||
// Ensure these values exist to prevent errors
|
||||
const channelName = channel.name || 'Unnamed Channel';
|
||||
|
|
@ -360,19 +397,21 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
const streamProfileName = channel.stream_profile?.name || 'Unknown Profile';
|
||||
|
||||
// Use currentM3UProfile if available, otherwise fall back to channel data
|
||||
const m3uProfileName = currentM3UProfile?.name ||
|
||||
const m3uProfileName =
|
||||
currentM3UProfile?.name ||
|
||||
channel.m3u_profile?.name ||
|
||||
channel.m3u_profile_name ||
|
||||
'Unknown M3U Profile';
|
||||
|
||||
// Create select options for available streams
|
||||
const streamOptions = availableStreams.map(stream => {
|
||||
const streamOptions = availableStreams.map((stream) => {
|
||||
// Get account name from our mapping if it exists
|
||||
const accountName = stream.m3u_account && m3uAccountsMap[stream.m3u_account]
|
||||
? m3uAccountsMap[stream.m3u_account]
|
||||
: stream.m3u_account
|
||||
? `M3U #${stream.m3u_account}`
|
||||
: 'Unknown M3U';
|
||||
const accountName =
|
||||
stream.m3u_account && m3uAccountsMap[stream.m3u_account]
|
||||
? m3uAccountsMap[stream.m3u_account]
|
||||
: stream.m3u_account
|
||||
? `M3U #${stream.m3u_account}`
|
||||
: 'Unknown M3U';
|
||||
|
||||
return {
|
||||
value: stream.id.toString(),
|
||||
|
|
@ -394,19 +433,21 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
>
|
||||
<Stack style={{ position: 'relative' }}>
|
||||
<Group justify="space-between">
|
||||
<Box style={{
|
||||
width: '100px',
|
||||
height: '50px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Box
|
||||
style={{
|
||||
width: '100px',
|
||||
height: '50px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={logoUrl || logo}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain'
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
alt="channel logo"
|
||||
/>
|
||||
|
|
@ -464,7 +505,9 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
<Select
|
||||
size="xs"
|
||||
label="Active Stream"
|
||||
placeholder={isLoadingStreams ? "Loading streams..." : "Select stream"}
|
||||
placeholder={
|
||||
isLoadingStreams ? 'Loading streams...' : 'Select stream'
|
||||
}
|
||||
data={streamOptions}
|
||||
value={activeStreamId || channel.stream_id?.toString() || null}
|
||||
onChange={handleStreamChange}
|
||||
|
|
@ -476,7 +519,9 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
|
||||
<Group justify="space-between">
|
||||
<Group gap={4}>
|
||||
<Tooltip label={`Current bitrate: ${formatSpeed(bitrates.at(-1) || 0)}`}>
|
||||
<Tooltip
|
||||
label={`Current bitrate: ${formatSpeed(bitrates.at(-1) || 0)}`}
|
||||
>
|
||||
<Group gap={4} style={{ cursor: 'help' }}>
|
||||
<Gauge style={{ paddingRight: 5 }} size="22" />
|
||||
<Text size="sm">{formatSpeed(bitrates.at(-1) || 0)}</Text>
|
||||
|
|
@ -485,7 +530,9 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
</Group>
|
||||
|
||||
<Tooltip label={`Average bitrate: ${avgBitrate}`}>
|
||||
<Text size="sm" style={{ cursor: 'help' }}>Avg: {avgBitrate}</Text>
|
||||
<Text size="sm" style={{ cursor: 'help' }}>
|
||||
Avg: {avgBitrate}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
||||
<Group gap={4}>
|
||||
|
|
@ -498,7 +545,9 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
</Group>
|
||||
|
||||
<Group gap={5}>
|
||||
<Tooltip label={`${clientCount} active client${clientCount !== 1 ? 's' : ''}`}>
|
||||
<Tooltip
|
||||
label={`${clientCount} active client${clientCount !== 1 ? 's' : ''}`}
|
||||
>
|
||||
<Group gap={4} style={{ cursor: 'help' }}>
|
||||
<Users size="18" />
|
||||
<Text size="sm">{clientCount}</Text>
|
||||
|
|
@ -507,7 +556,7 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel
|
|||
</Group>
|
||||
</Group>
|
||||
|
||||
<MantineReactTable table={channelClientsTable} />
|
||||
<CustomTable table={channelClientsTable} />
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -616,8 +665,13 @@ const ChannelsPage = () => {
|
|||
// The main clientsTable is no longer needed since each channel card has its own table
|
||||
|
||||
useEffect(() => {
|
||||
if (!channelStats || !channelStats.channels || !Array.isArray(channelStats.channels) || channelStats.channels.length === 0) {
|
||||
console.log("No channel stats available:", channelStats);
|
||||
if (
|
||||
!channelStats ||
|
||||
!channelStats.channels ||
|
||||
!Array.isArray(channelStats.channels) ||
|
||||
channelStats.channels.length === 0
|
||||
) {
|
||||
console.log('No channel stats available:', channelStats);
|
||||
// Clear active channels when there are no stats
|
||||
if (Object.keys(activeChannels).length > 0) {
|
||||
setActiveChannels({});
|
||||
|
|
@ -631,13 +685,13 @@ const ChannelsPage = () => {
|
|||
|
||||
// Track which channels are currently active according to channelStats
|
||||
const currentActiveChannelIds = new Set(
|
||||
channelStats.channels.map(ch => ch.channel_id).filter(Boolean)
|
||||
channelStats.channels.map((ch) => ch.channel_id).filter(Boolean)
|
||||
);
|
||||
|
||||
channelStats.channels.forEach(ch => {
|
||||
channelStats.channels.forEach((ch) => {
|
||||
// Make sure we have a valid channel_id
|
||||
if (!ch.channel_id) {
|
||||
console.warn("Found channel without channel_id:", ch);
|
||||
console.warn('Found channel without channel_id:', ch);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -656,12 +710,14 @@ const ChannelsPage = () => {
|
|||
}
|
||||
|
||||
// Find corresponding channel data
|
||||
const channelData = channelsByUUID && ch.channel_id ?
|
||||
channels[channelsByUUID[ch.channel_id]] : null;
|
||||
const channelData =
|
||||
channelsByUUID && ch.channel_id
|
||||
? channels[channelsByUUID[ch.channel_id]]
|
||||
: null;
|
||||
|
||||
// Find stream profile
|
||||
const streamProfile = streamProfiles.find(
|
||||
profile => profile.id == parseInt(ch.stream_profile)
|
||||
(profile) => profile.id == parseInt(ch.stream_profile)
|
||||
);
|
||||
|
||||
stats[ch.channel_id] = {
|
||||
|
|
@ -674,7 +730,7 @@ const ChannelsPage = () => {
|
|||
};
|
||||
});
|
||||
|
||||
console.log("Processed active channels:", stats);
|
||||
console.log('Processed active channels:', stats);
|
||||
setActiveChannels(stats);
|
||||
|
||||
const clientStats = Object.values(stats).reduce((acc, ch) => {
|
||||
|
|
@ -704,8 +760,16 @@ const ChannelsPage = () => {
|
|||
verticalSpacing="lg"
|
||||
>
|
||||
{Object.keys(activeChannels).length === 0 ? (
|
||||
<Box style={{ gridColumn: '1 / -1', textAlign: 'center', padding: '40px' }}>
|
||||
<Text size="xl" color="dimmed">No active channels currently streaming</Text>
|
||||
<Box
|
||||
style={{
|
||||
gridColumn: '1 / -1',
|
||||
textAlign: 'center',
|
||||
padding: '40px',
|
||||
}}
|
||||
>
|
||||
<Text size="xl" color="dimmed">
|
||||
No active channels currently streaming
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
Object.values(activeChannels).map((channel) => (
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ const useAuthStore = create((set, get) => ({
|
|||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
setUser: (user) => set({ user }),
|
||||
|
||||
initData: async () => {
|
||||
const user = await API.me();
|
||||
if (user.user_level <= USER_LEVELS.STREAMER) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue