mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Prototype implementation of activity timeline feature
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.raw(`
|
||||
CREATE OR REPLACE FUNCTION fs_get_user_activity(
|
||||
user_id text,
|
||||
library_ids text[]
|
||||
)
|
||||
RETURNS TABLE (
|
||||
"UserName" text,
|
||||
"Title" text,
|
||||
"EpisodeCount" bigint,
|
||||
"FirstActivityDate" timestamptz,
|
||||
"LastActivityDate" timestamptz,
|
||||
"TotalPlaybackDuration" bigint,
|
||||
"SeasonName" text,
|
||||
"MediaType" text,
|
||||
"NowPlayingItemId" text
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
jp."UserName",
|
||||
CASE
|
||||
WHEN jp."SeriesName" IS NOT NULL THEN jp."SeriesName"
|
||||
ELSE jp."NowPlayingItemName"
|
||||
END AS "Title",
|
||||
COUNT(DISTINCT jp."EpisodeId") AS "EpisodeCount",
|
||||
MIN(jp."ActivityDateInserted") AS "FirstActivityDate",
|
||||
MAX(jp."ActivityDateInserted") AS "LastActivityDate",
|
||||
SUM(jp."PlaybackDuration")::bigint AS "TotalPlaybackDuration",
|
||||
ls."Name" AS "SeasonName",
|
||||
CASE
|
||||
WHEN jp."SeriesName" IS NOT NULL THEN 'Show'
|
||||
ELSE 'Movie'
|
||||
END AS "MediaType",
|
||||
jp."NowPlayingItemId"
|
||||
FROM
|
||||
public.jf_playback_activity AS jp
|
||||
JOIN
|
||||
public.jf_library_items AS jli ON jp."NowPlayingItemId" = jli."Id"
|
||||
JOIN
|
||||
public.jf_libraries AS jl ON jli."ParentId" = jl."Id"
|
||||
LEFT JOIN
|
||||
public.jf_library_seasons AS ls ON jp."SeasonId" = ls."Id"
|
||||
WHERE
|
||||
jp."UserId" = user_id
|
||||
AND jl."Id" = ANY(library_ids)
|
||||
GROUP BY
|
||||
jp."UserName",
|
||||
CASE
|
||||
WHEN jp."SeriesName" IS NOT NULL THEN jp."SeriesName"
|
||||
ELSE jp."NowPlayingItemName"
|
||||
END,
|
||||
jp."SeriesName",
|
||||
jp."SeasonId",
|
||||
ls."Name",
|
||||
jp."NowPlayingItemId"
|
||||
HAVING
|
||||
NOT (MAX(jl."Name") = 'Shows' AND ls."Name" IS NULL)
|
||||
ORDER BY
|
||||
MAX(jp."ActivityDateInserted") DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.raw(`
|
||||
DROP FUNCTION IF EXISTS fs_get_user_activity;
|
||||
`);
|
||||
};
|
||||
@@ -1432,6 +1432,31 @@ router.post("/deletePlaybackActivity", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getActivityTimeLine", async (req, res) => {
|
||||
try {
|
||||
const { userId, libraries } = req.body;
|
||||
|
||||
if (libraries === undefined || !Array.isArray(libraries)) {
|
||||
res.status(400);
|
||||
res.send("A list of IDs is required. EG: [1,2,3]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (userId === undefined) {
|
||||
res.status(400);
|
||||
res.send("A userId is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const {rows} = await db.query(`SELECT * FROM fs_get_user_activity($1, $2);`, [userId, libraries]);
|
||||
res.send(rows);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(503);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle other routes
|
||||
router.use((req, res) => {
|
||||
res.status(404).send({ error: "Not Found" });
|
||||
|
||||
469
package-lock.json
generated
469
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^6.3.0",
|
||||
"@mui/lab": "^6.0.0-beta.22",
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/x-data-grid": "^7.23.3",
|
||||
"@mui/x-date-pickers": "^7.23.3",
|
||||
@@ -3257,6 +3258,44 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
@@ -4151,10 +4190,43 @@
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.68",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.68.tgz",
|
||||
"integrity": "sha512-F1JMNeLS9Qhjj3wN86JUQYBtJoXyQvknxlzwNl6eS0ZABo1MiohMONj3/WQzYPSXIKC2bS/ZbyBzdHhi2GnEpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@floating-ui/react-dom": "^2.1.1",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.0.tgz",
|
||||
"integrity": "sha512-/d8NwSuC3rMwCjswmGB3oXC4sdDuhIUJ8inVQAxGrADJhf0eq/kmy+foFKvpYhHl2siOZR+MLdFttw6/Bzqtqg==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.1.tgz",
|
||||
"integrity": "sha512-2OmnEyoHpj5//dJJpMuxOeLItCCHdf99pjMFfUFdBteCunAK9jW+PwEo4mtdGcLs7P+IgZ+85ypd52eY4AigoQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
@@ -4185,23 +4257,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.0.tgz",
|
||||
"integrity": "sha512-qhlTFyRMxfoVPxUtA5e8IvqxP0dWo2Ij7cvot7Orag+etUlZH+3UwD8gZGt+3irOoy7Ms3UNBflYjwEikUXtAQ==",
|
||||
"node_modules/@mui/lab": {
|
||||
"version": "6.0.0-beta.22",
|
||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.22.tgz",
|
||||
"integrity": "sha512-9nwUfBj+UzoQJOCbqV+JcCSJ74T+gGWrM1FMlXzkahtYUcMN+5Zmh2ArlttW3zv2dZyCzp7K5askcnKF0WzFQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/core-downloads-tracker": "^6.3.0",
|
||||
"@mui/system": "^6.3.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@mui/base": "5.0.0-beta.68",
|
||||
"@mui/system": "^6.3.1",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.0.0",
|
||||
"react-transition-group": "^4.4.5"
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -4213,7 +4281,8 @@
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^6.3.0",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/material-pigment-css": "^6.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -4233,13 +4302,69 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/private-theming": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.0.tgz",
|
||||
"integrity": "sha512-tdS8jvqMokltNTXg6ioRCCbVdDmZUJZa/T9VtTnX2Lwww3FTgCakst9tWLZSxm1fEE9Xp0m7onZJmgeUmWQYVw==",
|
||||
"node_modules/@mui/material": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.1.tgz",
|
||||
"integrity": "sha512-ynG9ayhxgCsHJ/dtDcT1v78/r2GwQyP3E0hPz3GdPRl0uFJz/uUTtI5KFYwadXmbC+Uv3bfB8laZ6+Cpzh03gA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@mui/core-downloads-tracker": "^6.3.1",
|
||||
"@mui/system": "^6.3.1",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.0.0",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^6.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"@mui/material-pigment-css": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/react-is": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.1.tgz",
|
||||
"integrity": "sha512-g0u7hIUkmXmmrmmf5gdDYv9zdAig0KoxhIQn1JN8IVqApzf/AyRhH3uDGx5mSvs8+a1zb4+0W6LC260SyTTtdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4259,10 +4384,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/styled-engine": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.0.tgz",
|
||||
"integrity": "sha512-iWA6eyiPkO07AlHxRUvI7dwVRSc+84zV54kLmjUms67GJeOWVuXlu8ZO+UhCnwJxHacghxnabsMEqet5PYQmHg==",
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.1.tgz",
|
||||
"integrity": "sha512-/7CC0d2fIeiUxN5kCCwYu4AWUDd9cCTxWCyo0v/Rnv6s8uk6hWgJC3VLZBoDENBHf/KjqDZuYJ2CR+7hD6QYww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
@@ -4292,16 +4418,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/system": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.0.tgz",
|
||||
"integrity": "sha512-L+8hUHMNlfReKSqcnVslFrVhoNfz/jw7Fe9NfDE85R3KarvZ4O3MU9daF/lZeqEAvnYxEilkkTfDwQ7qCgJdFg==",
|
||||
"node_modules/@mui/system": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.1.tgz",
|
||||
"integrity": "sha512-AwqQ3EAIT2np85ki+N15fF0lFXX1iFPqenCzVOSl3QXKy2eifZeGd9dGtt7pGMoFw5dzW4dRGGzRpLAq9rkl7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/private-theming": "^6.3.0",
|
||||
"@mui/styled-engine": "^6.3.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@mui/private-theming": "^6.3.1",
|
||||
"@mui/styled-engine": "^6.3.1",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -4331,13 +4458,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/@mui/utils": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz",
|
||||
"integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==",
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.21",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
|
||||
"integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.1.tgz",
|
||||
"integrity": "sha512-sjGjXAngoio6lniQZKJ5zGfjm+LD2wvLwco7FbKe1fu8A7VIFmz2SwkLb+MDPLNX1lE7IscvNNyh1pobtZg2tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -4360,150 +4502,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/react-is": {
|
||||
"node_modules/@mui/utils/node_modules/react-is": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "5.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz",
|
||||
"integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"@mui/utils": "^5.15.14",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "5.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
|
||||
"integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "5.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz",
|
||||
"integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"@mui/private-theming": "^5.15.14",
|
||||
"@mui/styled-engine": "^5.15.14",
|
||||
"@mui/types": "^7.2.14",
|
||||
"@mui/utils": "^5.15.14",
|
||||
"clsx": "^2.1.0",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz",
|
||||
"integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "5.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz",
|
||||
"integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"@types/prop-types": "^15.7.11",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^18.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "7.23.3",
|
||||
@@ -4541,40 +4544,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid/node_modules/@mui/utils": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz",
|
||||
"integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid/node_modules/react-is": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz",
|
||||
@@ -4640,40 +4609,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers/node_modules/@mui/utils": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz",
|
||||
"integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers/node_modules/react-is": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
|
||||
},
|
||||
"node_modules/@mui/x-internals": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.0.tgz",
|
||||
@@ -4693,40 +4628,6 @@
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-internals/node_modules/@mui/utils": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz",
|
||||
"integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-internals/node_modules/react-is": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^6.3.0",
|
||||
"@mui/lab": "^6.0.0-beta.22",
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/x-data-grid": "^7.23.3",
|
||||
"@mui/x-date-pickers": "^7.23.3",
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"STATISTICS": "Statistics",
|
||||
"SETTINGS": "Settings",
|
||||
"ABOUT": "About",
|
||||
"LOGOUT": "Logout"
|
||||
"LOGOUT": "Logout",
|
||||
"TIMELINE": "Timeline"
|
||||
},
|
||||
"HOME_PAGE": {
|
||||
"SESSIONS": "Sessions",
|
||||
@@ -230,6 +231,9 @@
|
||||
"GITHUB": "Github",
|
||||
"Backup": "Backup Jellystat"
|
||||
},
|
||||
"TIMELINE_PAGE": {
|
||||
"TIMELINE": "Timeline"
|
||||
},
|
||||
"SEARCH": "Search",
|
||||
"TOTAL": "Total",
|
||||
"LAST": "Last",
|
||||
@@ -305,4 +309,4 @@
|
||||
"POSTCODE": "Postcode",
|
||||
"X_ROWS_SELECTED": "{ROWS} Rows Selected",
|
||||
"SUBTITLES": "Subtitles"
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,14 @@ import SettingsFillIcon from 'remixicon-react/SettingsFillIcon';
|
||||
import GalleryFillIcon from 'remixicon-react/GalleryFillIcon';
|
||||
import UserFillIcon from 'remixicon-react/UserFillIcon';
|
||||
import InformationFillIcon from 'remixicon-react/InformationFillIcon';
|
||||
import TimeLineIcon from 'remixicon-react/TimeLineIcon';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
|
||||
export const navData = [
|
||||
{
|
||||
id: 0,
|
||||
icon: <HomeFillIcon/>,
|
||||
icon: <HomeFillIcon />,
|
||||
text: <Trans i18nKey="MENU_TABS.HOME" />,
|
||||
link: ""
|
||||
},
|
||||
@@ -35,6 +36,12 @@ export const navData = [
|
||||
text: <Trans i18nKey="MENU_TABS.ACTIVITY" />,
|
||||
link: "activity"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
icon: <TimeLineIcon />,
|
||||
text: <Trans i18nKey="MENU_TABS.TIMELINE" />,
|
||||
link: "timeline"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: <BarChartFillIcon />,
|
||||
|
||||
195
src/pages/activity_time_line.jsx
Normal file
195
src/pages/activity_time_line.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import "./css/stats.css";
|
||||
|
||||
import { Trans } from "react-i18next";
|
||||
import ActivityTimelineComponent from "./components/activity-timeline/activity-timeline";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import axios from "../lib/axios_instance.jsx";
|
||||
import Config from "../lib/config.jsx";
|
||||
import "./css/timeline/activity-timeline.css";
|
||||
import Loading from "./components/general/loading";
|
||||
import { Button, FormSelect, Modal } from "react-bootstrap";
|
||||
import LibraryFilterModal from "./components/library/library-filter-modal";
|
||||
|
||||
function ActivityTimeline() {
|
||||
const [users, setUsers] = useState();
|
||||
const [selectedUser, setSelectedUser] = useState(
|
||||
localStorage.getItem("PREF_ACTIVITY_TIMELINE_selectedUser") ?? ""
|
||||
);
|
||||
const [libraries, setLibraries] = useState();
|
||||
const [config, setConfig] = useState(null);
|
||||
const [showLibraryFilters, setShowLibraryFilters] = useState(false);
|
||||
const [selectedLibraries, setSelectedLibraries] = useState(
|
||||
localStorage.getItem("PREF_ACTIVITY_TIMELINE_selectedLibraries") !=
|
||||
undefined
|
||||
? JSON.parse(
|
||||
localStorage.getItem("PREF_ACTIVITY_TIMELINE_selectedLibraries")
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
const handleLibraryFilter = (selectedOptions) => {
|
||||
setSelectedLibraries(selectedOptions);
|
||||
localStorage.setItem(
|
||||
"PREF_ACTIVITY_TIMELINE_selectedLibraries",
|
||||
JSON.stringify(selectedOptions)
|
||||
);
|
||||
};
|
||||
const handleUserSelection = (selectedUser) => {
|
||||
console.log(selectedUser);
|
||||
|
||||
setSelectedUser(selectedUser);
|
||||
localStorage.setItem("PREF_ACTIVITY_TIMELINE_selectedUser", selectedUser);
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedLibraries.length > 0) {
|
||||
setSelectedLibraries([]);
|
||||
localStorage.setItem(
|
||||
"PREF_ACTIVITY_TIMELINE_selectedLibraries",
|
||||
JSON.stringify([])
|
||||
);
|
||||
} else {
|
||||
setSelectedLibraries(libraries.map((library) => library.Id));
|
||||
localStorage.setItem(
|
||||
"PREF_ACTIVITY_TIMELINE_selectedLibraries",
|
||||
JSON.stringify(libraries.map((library) => library.Id))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config.getConfig();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
const url = `/stats/getAllUserActivity`;
|
||||
axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((users) => {
|
||||
setUsers(users.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
const url = `/stats/getLibraryMetadata`;
|
||||
axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((libraries) => {
|
||||
setLibraries(libraries.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
return users?.length > 0 && libraries?.length > 0 ? (
|
||||
<div className="watch-stats">
|
||||
<div className="Heading">
|
||||
<h1>
|
||||
<Trans i18nKey={"TIMELINE_PAGE.TIMELINE"} />
|
||||
</h1>
|
||||
<div
|
||||
className="d-flex flex-column flex-md-row"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
<div className="user-selection">
|
||||
<div className="d-flex flex-row w-100 ms-md-3 w-sm-100 w-md-75 mb-3 my-md-3">
|
||||
<div className="d-flex flex-col rounded-0 rounded-start align-items-center px-2 bg-primary-1">
|
||||
<Trans i18nKey="USER" />
|
||||
</div>
|
||||
<FormSelect
|
||||
onChange={(e) => handleUserSelection(e.target.value)}
|
||||
value={selectedUser}
|
||||
className="w-md-75 rounded-0 rounded-end"
|
||||
>
|
||||
{users.map((user) => (
|
||||
<option key={user.UserId} value={user.UserId}>
|
||||
{user.UserName}
|
||||
</option>
|
||||
))}
|
||||
</FormSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div className="library-selection">
|
||||
<Button
|
||||
onClick={() => setShowLibraryFilters(true)}
|
||||
className="ms-md-3 mb-3 my-md-3"
|
||||
>
|
||||
<Trans i18nKey="MENU_TABS.LIBRARIES" />
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
show={showLibraryFilters}
|
||||
onHide={() => setShowLibraryFilters(false)}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<Trans i18nKey="MENU_TABS.LIBRARIES" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<LibraryFilterModal
|
||||
libraries={libraries}
|
||||
selectedLibraries={selectedLibraries}
|
||||
onSelectionChange={handleLibraryFilter}
|
||||
/>
|
||||
<Modal.Footer>
|
||||
<Button variant="outline-primary" onClick={toggleSelectAll}>
|
||||
<Trans i18nKey="ACTIVITY_TABLE.TOGGLE_SELECT_ALL" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => setShowLibraryFilters(false)}
|
||||
>
|
||||
<Trans i18nKey="CLOSE" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{selectedUser && selectedLibraries?.length > 0 && (
|
||||
<ActivityTimelineComponent
|
||||
userId={selectedUser}
|
||||
libraries={selectedLibraries}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
}
|
||||
|
||||
export default ActivityTimeline;
|
||||
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import { useState } from "react";
|
||||
|
||||
import TimelineItem from "@mui/lab/TimelineItem";
|
||||
import TimelineSeparator from "@mui/lab/TimelineSeparator";
|
||||
import TimelineConnector from "@mui/lab/TimelineConnector";
|
||||
import TimelineContent from "@mui/lab/TimelineContent";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Card from "react-bootstrap/Card";
|
||||
import baseUrl from "../../../lib/baseurl";
|
||||
|
||||
import "../../css/timeline/activity-timeline.css";
|
||||
|
||||
import moment from "moment";
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon.js";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon.js";
|
||||
import { MEDIA_TYPES } from "./helpers";
|
||||
|
||||
function formatEntryDates(entry) {
|
||||
const { FirstActivityDate, LastActivityDate, MediaType } = entry;
|
||||
const startDate = moment(FirstActivityDate);
|
||||
const endDate = moment(LastActivityDate);
|
||||
|
||||
if (startDate.isSame(endDate, "day") || MediaType === MEDIA_TYPES.Movies) {
|
||||
return startDate.format("L");
|
||||
} else {
|
||||
return `${startDate.format("L")} - ${endDate.format("L")}`;
|
||||
}
|
||||
}
|
||||
const DefaultImage = (props) => {
|
||||
const { MediaType } = props;
|
||||
return (
|
||||
<div className="default_library_image default_library_image_hover d-flex justify-content-center align-items-center">
|
||||
{MediaType === MEDIA_TYPES.Shows ? SeriesIcon : MovieIcon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const SeriesIcon = <TvLineIcon size={"50%"} color="white" />;
|
||||
const MovieIcon = <FilmLineIcon size={"50%"} color="white" />;
|
||||
|
||||
export default function ActivityTimelineItem(entry) {
|
||||
const { Title, SeasonName, NowPlayingItemId } = entry;
|
||||
const [useDefaultImage, setUseDefaultImage] = useState(false);
|
||||
return (
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineConnector />
|
||||
<div className="activity-card">
|
||||
{!useDefaultImage ? (
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className="activity-card-img"
|
||||
src={
|
||||
baseUrl +
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
NowPlayingItemId +
|
||||
"&fillWidth=800&quality=50"
|
||||
}
|
||||
onError={() => setUseDefaultImage(true)}
|
||||
/>
|
||||
) : (
|
||||
<DefaultImage {...entry} />
|
||||
)}
|
||||
</div>
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent>
|
||||
<Typography variant="h6" component="span">
|
||||
{Title}
|
||||
</Typography>
|
||||
{SeasonName && <Typography>{SeasonName}</Typography>}
|
||||
<Typography>{formatEntryDates(entry)}</Typography>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
);
|
||||
}
|
||||
80
src/pages/components/activity-timeline/activity-timeline.jsx
Normal file
80
src/pages/components/activity-timeline/activity-timeline.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "../../../lib/axios_instance";
|
||||
|
||||
import Timeline from "@mui/lab/Timeline";
|
||||
|
||||
import "../../css/timeline/activity-timeline.css";
|
||||
|
||||
import Config from "../../../lib/config.jsx";
|
||||
import Loading from "../../../pages/components/general/loading.jsx";
|
||||
|
||||
import ActivityTimelineItem from "./activity-timeline-item.jsx";
|
||||
import { groupAdjacentSeasons } from "./helpers.jsx";
|
||||
|
||||
export default function ActivityTimelineComponent(props) {
|
||||
const { userId, libraries } = props;
|
||||
|
||||
const [timelineEntries, setTimelineEntries] = useState();
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config.getConfig();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/api/getActivityTimeLine`;
|
||||
axios
|
||||
.post(
|
||||
url,
|
||||
{ userId: userId, libraries: libraries },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((timelineEntries) => {
|
||||
const groupedAdjacentSeasons = groupAdjacentSeasons([
|
||||
...timelineEntries.data,
|
||||
]);
|
||||
setTimelineEntries(groupedAdjacentSeasons);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
fetchLibraries();
|
||||
}, [userId, libraries, config]);
|
||||
|
||||
return timelineEntries?.length > 0 ? (
|
||||
<div>
|
||||
<Timeline position="alternate">
|
||||
{timelineEntries.map((entry) => (
|
||||
<ActivityTimelineItem
|
||||
key={`${entry.Title}-${entry.FirstActivityDate}-${entry.LastActivityDate}`}
|
||||
{...entry}
|
||||
/>
|
||||
))}
|
||||
</Timeline>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
}
|
||||
56
src/pages/components/activity-timeline/helpers.jsx
Normal file
56
src/pages/components/activity-timeline/helpers.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
export const MEDIA_TYPES = {
|
||||
Movies: "Movie",
|
||||
Shows: "Show",
|
||||
};
|
||||
|
||||
/**
|
||||
* groups subsequent seasons of shows into single entries with a combined label and timeframe
|
||||
* @param {*} timelineEntries List of entries as returned by /api/getActivityTimeLine
|
||||
* @returns Same list of entries, seasons of the same show that follow each other will be merged into one entry
|
||||
*/
|
||||
export function groupAdjacentSeasons(timelineEntries) {
|
||||
return timelineEntries
|
||||
.reverse()
|
||||
.map((entry, index, entryArray) => {
|
||||
if (entry?.MediaType === MEDIA_TYPES.Shows) {
|
||||
let potentialNextSeasonIndex = index + 1;
|
||||
//if the next entry is another season of the same show, merge them
|
||||
if (entry.Title === entryArray[potentialNextSeasonIndex]?.Title) {
|
||||
let highestSeasonName = entry.SeasonName;
|
||||
let lastSeasonInSession;
|
||||
//merge all further seasons as well
|
||||
while (entry.Title === entryArray[potentialNextSeasonIndex]?.Title) {
|
||||
const potentialNextSeason = entryArray[potentialNextSeasonIndex];
|
||||
if (entry.Title === potentialNextSeason?.Title) {
|
||||
lastSeasonInSession = potentialNextSeason;
|
||||
//remove season from list after usage
|
||||
entryArray[potentialNextSeasonIndex] = undefined;
|
||||
|
||||
//hack: in my db the seasons weren't always sorted correctly.
|
||||
if (
|
||||
highestSeasonName?.localeCompare(
|
||||
lastSeasonInSession.SeasonName
|
||||
) === -1
|
||||
) {
|
||||
highestSeasonName = lastSeasonInSession.SeasonName;
|
||||
}
|
||||
} else {
|
||||
//all subsequent seasons have been merged into one entry and were removed from the list
|
||||
break;
|
||||
}
|
||||
potentialNextSeasonIndex++;
|
||||
}
|
||||
const newSeasonName = `${entry.SeasonName} - ${highestSeasonName}`;
|
||||
const newLastActivityDate = lastSeasonInSession.LastActivityDate;
|
||||
return {
|
||||
...entry,
|
||||
SeasonName: newSeasonName,
|
||||
LastActivityDate: newLastActivityDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
})
|
||||
.filter((entry) => !!entry)
|
||||
.reverse();
|
||||
}
|
||||
25
src/pages/css/timeline/activity-timeline.css
Normal file
25
src/pages/css/timeline/activity-timeline.css
Normal file
@@ -0,0 +1,25 @@
|
||||
@import "../variables.module.css";
|
||||
|
||||
.Heading {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.activity-card {
|
||||
display: flex;
|
||||
width: 10rem;
|
||||
.activity-card-img {
|
||||
border-radius: var(--bs-border-radius-lg) !important;
|
||||
object-fit: cover;
|
||||
background-color: black;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiTimelineItem-root {
|
||||
height: 20rem;
|
||||
}
|
||||
|
||||
.MuiTimelineContent-root {
|
||||
align-self: center;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import About from "./pages/about";
|
||||
import TestingRoutes from "./pages/testing";
|
||||
import Activity from "./pages/activity";
|
||||
import Statistics from "./pages/statistics";
|
||||
import ActivityTimeline from "./pages/activity_time_line";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -58,6 +59,11 @@ const routes = [
|
||||
element: <Activity />,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: "/timeline",
|
||||
element: <ActivityTimeline />,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
element: <About />,
|
||||
|
||||
Reference in New Issue
Block a user