Merge pull request #391 from Sanidhya30/main

Enhancement: Support for Visualizing Total Duration/Total Playback Time in Statistics Page
This commit is contained in:
Thegan Govender
2025-06-14 18:41:04 +02:00
committed by GitHub
11 changed files with 500 additions and 34 deletions

View File

@@ -0,0 +1,121 @@
exports.up = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_over_time(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_over_time(
days integer)
RETURNS TABLE("Date" date, "Count" bigint, "Duration" bigint, "Library" text, "LibraryID" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
dates."Date",
COALESCE(counts."Count", 0) AS "Count",
COALESCE(counts."Duration", 0) AS "Duration",
l."Name" as "Library",
l."Id" as "LibraryID"
FROM
(SELECT generate_series(
DATE_TRUNC('day', NOW() - CAST(days || ' days' as INTERVAL)),
DATE_TRUNC('day', NOW()),
'1 day')::DATE AS "Date"
) dates
CROSS JOIN jf_libraries l
LEFT JOIN
(SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
(SUM(a."PlaybackDuration") / 60)::bigint AS "Duration",
l."Name" as "Library"
FROM
jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
JOIN jf_libraries l ON i."ParentId" = l."Id"
WHERE
a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
GROUP BY
l."Name", DATE_TRUNC('day', a."ActivityDateInserted")
) counts
ON counts."Date" = dates."Date" AND counts."Library" = l."Name"
where l.archived=false
ORDER BY
"Date", "Library";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_over_time(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};
exports.down = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_over_time(integer);
CREATE OR REPLACE FUNCTION fs_watch_stats_over_time(
days integer
)
RETURNS TABLE(
"Date" date,
"Count" bigint,
"Library" text
)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
dates."Date",
COALESCE(counts."Count", 0) AS "Count",
l."Name" as "Library"
FROM
(SELECT generate_series(
DATE_TRUNC('day', NOW() - CAST(days || ' days' as INTERVAL)),
DATE_TRUNC('day', NOW()),
'1 day')::DATE AS "Date"
) dates
CROSS JOIN jf_libraries l
LEFT JOIN
(SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
l."Name" as "Library"
FROM
jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
JOIN jf_libraries l ON i."ParentId" = l."Id"
WHERE
a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
GROUP BY
l."Name", DATE_TRUNC('day', a."ActivityDateInserted")
) counts
ON counts."Date" = dates."Date" AND counts."Library" = l."Name"
ORDER BY
"Date", "Library";
END;
$BODY$;
ALTER FUNCTION fs_watch_stats_over_time(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";`);
} catch (error) {
console.error(error);
}
};

View File

@@ -0,0 +1,143 @@
exports.up = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_days_of_week(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_days_of_week(
days integer)
RETURNS TABLE("Day" text, "Count" bigint, "Duration" bigint, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
WITH library_days AS (
SELECT
l."Name" AS "Library",
d.day_of_week,
d.day_name
FROM
jf_libraries l,
(SELECT 0 AS "day_of_week", 'Sunday' AS "day_name" UNION ALL
SELECT 1 AS "day_of_week", 'Monday' AS "day_name" UNION ALL
SELECT 2 AS "day_of_week", 'Tuesday' AS "day_name" UNION ALL
SELECT 3 AS "day_of_week", 'Wednesday' AS "day_name" UNION ALL
SELECT 4 AS "day_of_week", 'Thursday' AS "day_name" UNION ALL
SELECT 5 AS "day_of_week", 'Friday' AS "day_name" UNION ALL
SELECT 6 AS "day_of_week", 'Saturday' AS "day_name"
) d
where l.archived=false
)
SELECT
library_days.day_name AS "Day",
COALESCE(SUM(counts."Count"), 0)::bigint AS "Count",
COALESCE(SUM(counts."Duration"), 0)::bigint AS "Duration",
library_days."Library" AS "Library"
FROM
library_days
LEFT JOIN
(SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
(SUM(a."PlaybackDuration") / 60)::bigint AS "Duration",
EXTRACT(DOW FROM a."ActivityDateInserted") AS "DOW",
l."Name" AS "Library"
FROM
jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
JOIN jf_libraries l ON i."ParentId" = l."Id" and l.archived=false
WHERE
a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
GROUP BY
l."Name", EXTRACT(DOW FROM a."ActivityDateInserted"), DATE_TRUNC('day', a."ActivityDateInserted")
) counts
ON counts."DOW" = library_days.day_of_week AND counts."Library" = library_days."Library"
GROUP BY
library_days.day_name, library_days.day_of_week, library_days."Library"
ORDER BY
library_days.day_of_week, library_days."Library";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_popular_days_of_week(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};
exports.down = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_days_of_week(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_days_of_week(
days integer)
RETURNS TABLE("Day" text, "Count" bigint, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
WITH library_days AS (
SELECT
l."Name" AS "Library",
d.day_of_week,
d.day_name
FROM
jf_libraries l,
(SELECT 0 AS "day_of_week", 'Sunday' AS "day_name" UNION ALL
SELECT 1 AS "day_of_week", 'Monday' AS "day_name" UNION ALL
SELECT 2 AS "day_of_week", 'Tuesday' AS "day_name" UNION ALL
SELECT 3 AS "day_of_week", 'Wednesday' AS "day_name" UNION ALL
SELECT 4 AS "day_of_week", 'Thursday' AS "day_name" UNION ALL
SELECT 5 AS "day_of_week", 'Friday' AS "day_name" UNION ALL
SELECT 6 AS "day_of_week", 'Saturday' AS "day_name"
) d
where l.archived=false
)
SELECT
library_days.day_name AS "Day",
COALESCE(SUM(counts."Count"), 0)::bigint AS "Count",
library_days."Library" AS "Library"
FROM
library_days
LEFT JOIN
(SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
EXTRACT(DOW FROM a."ActivityDateInserted") AS "DOW",
l."Name" AS "Library"
FROM
jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
JOIN jf_libraries l ON i."ParentId" = l."Id" and l.archived=false
WHERE
a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
GROUP BY
l."Name", EXTRACT(DOW FROM a."ActivityDateInserted"), DATE_TRUNC('day', a."ActivityDateInserted")
) counts
ON counts."DOW" = library_days.day_of_week AND counts."Library" = library_days."Library"
GROUP BY
library_days.day_name, library_days.day_of_week, library_days."Library"
ORDER BY
library_days.day_of_week, library_days."Library";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_popular_days_of_week(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};

View File

@@ -0,0 +1,117 @@
exports.up = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_hour_of_day(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_hour_of_day(
days integer)
RETURNS TABLE("Hour" integer, "Count" integer, "Duration" integer, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
h."Hour",
COUNT(a."Id")::integer AS "Count",
COALESCE(SUM(a."PlaybackDuration") / 60, 0)::integer AS "Duration",
l."Name" AS "Library"
FROM
(
SELECT
generate_series(0, 23) AS "Hour"
) h
CROSS JOIN jf_libraries l
LEFT JOIN jf_library_items i ON i."ParentId" = l."Id"
LEFT JOIN (
SELECT
"NowPlayingItemId",
DATE_PART('hour', "ActivityDateInserted") AS "Hour",
"Id",
"PlaybackDuration"
FROM
jf_playback_activity
WHERE
"ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' AS INTERVAL) AND NOW()
) a ON a."NowPlayingItemId" = i."Id" AND a."Hour"::integer = h."Hour"
WHERE
l.archived=false
and l."Id" IN (SELECT "Id" FROM jf_libraries)
GROUP BY
h."Hour",
l."Name"
ORDER BY
l."Name",
h."Hour";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_popular_hour_of_day(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};
exports.down = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_hour_of_day(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_hour_of_day(
days integer)
RETURNS TABLE("Hour" integer, "Count" integer, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
h."Hour",
COUNT(a."Id")::integer AS "Count",
l."Name" AS "Library"
FROM
(
SELECT
generate_series(0, 23) AS "Hour"
) h
CROSS JOIN jf_libraries l
LEFT JOIN jf_library_items i ON i."ParentId" = l."Id"
LEFT JOIN (
SELECT
"NowPlayingItemId",
DATE_PART('hour', "ActivityDateInserted") AS "Hour",
"Id"
FROM
jf_playback_activity
WHERE
"ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' AS INTERVAL) AND NOW()
) a ON a."NowPlayingItemId" = i."Id" AND a."Hour"::integer = h."Hour"
WHERE
l.archived=false
and l."Id" IN (SELECT "Id" FROM jf_libraries)
GROUP BY
h."Hour",
l."Name"
ORDER BY
l."Name",
h."Hour";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_popular_hour_of_day(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};

View File

@@ -423,6 +423,7 @@ router.get("/getViewsOverTime", async (req, res) => {
stats.forEach((item) => {
const library = item.Library;
const count = item.Count;
const duration = item.Duration;
const date = new Date(item.Date).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
@@ -435,7 +436,7 @@ router.get("/getViewsOverTime", async (req, res) => {
};
}
reorganizedData[date] = { ...reorganizedData[date], [library]: count };
reorganizedData[date] = { ...reorganizedData[date], [library]: { count, duration } };
});
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
res.send(finalData);
@@ -462,6 +463,7 @@ router.get("/getViewsByDays", async (req, res) => {
stats.forEach((item) => {
const library = item.Library;
const count = item.Count;
const duration = item.Duration;
const day = item.Day;
if (!reorganizedData[day]) {
@@ -470,7 +472,7 @@ router.get("/getViewsByDays", async (req, res) => {
};
}
reorganizedData[day] = { ...reorganizedData[day], [library]: count };
reorganizedData[day] = { ...reorganizedData[day], [library]: { count, duration } };
});
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
res.send(finalData);
@@ -497,6 +499,7 @@ router.get("/getViewsByHour", async (req, res) => {
stats.forEach((item) => {
const library = item.Library;
const count = item.Count;
const duration = item.Duration;
const hour = item.Hour;
if (!reorganizedData[hour]) {
@@ -505,7 +508,7 @@ router.get("/getViewsByHour", async (req, res) => {
};
}
reorganizedData[hour] = { ...reorganizedData[hour], [library]: count };
reorganizedData[hour] = { ...reorganizedData[hour], [library]: { count, duration } };
});
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
res.send(finalData);

View File

@@ -167,7 +167,11 @@
"STAT_PAGE": {
"STATISTICS": "Statistics",
"DAILY_PLAY_PER_LIBRARY": "Daily Play Count Per Library",
"PLAY_COUNT_BY": "Play Count By"
"DAILY_DURATION_PER_LIBRARY": "Daily Play Duration Per Library",
"PLAY_COUNT_BY": "Play Count By",
"PLAY_DURATION_BY": "Play Duration By",
"COUNT_VIEW": "Count",
"DURATION_VIEW": "Duration"
},
"SETTINGS_PAGE": {
"SETTINGS": "Settings",

View File

@@ -1,6 +1,6 @@
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, Legend } from "recharts";
function Chart({ stats, libraries }) {
function Chart({ stats, libraries, viewName }) {
const colors = [
"rgb(54, 162, 235)", // blue
"rgb(255, 99, 132)", // pink
@@ -24,13 +24,25 @@ function Chart({ stats, libraries }) {
"rgb(147, 112, 219)", // medium purple
];
const flattenedStats = stats.map(item => {
const flatItem = { Key: item.Key };
for (const [libraryName, data] of Object.entries(item)) {
if (libraryName === "Key") continue;
flatItem[libraryName] = data[viewName] ?? 0;
}
return flatItem;
});
const CustomTooltip = ({ payload, label, active }) => {
if (active) {
return (
<div style={{ backgroundColor: "rgba(0,0,0,0.8)", color: "white" }} className="p-2 rounded-2 border-0">
<p className="text-center fs-5">{label}</p>
{libraries.map((library, index) => (
<p key={library.Id} style={{ color: `${colors[index]}` }}>{`${library.Name} : ${payload[index].value} Views`}</p>
// <p key={library.Id} style={{ color: `${colors[index]}` }}>{`${library.Name} : ${payload[index].value} Views`}</p>
<p key={library.Id} style={{ color: `${colors[index]}` }}>
{`${library.Name} : ${payload?.find(p => p.dataKey === library.Name).value} ${viewName === "count" ? "Views" : "Minutes"}`}
</p>
))}
</div>
);
@@ -41,16 +53,14 @@ function Chart({ stats, libraries }) {
const getMaxValue = () => {
let max = 0;
if (stats) {
stats.forEach((datum) => {
Object.keys(datum).forEach((key) => {
if (key !== "Key") {
max = Math.max(max, parseInt(datum[key]));
}
});
flattenedStats.forEach(datum => {
libraries.forEach(library => {
const value = parseFloat(datum[library.Name]);
if (!isNaN(value)) {
max = Math.max(max, value);
}
});
}
});
return max;
};
@@ -58,7 +68,7 @@ function Chart({ stats, libraries }) {
return (
<ResponsiveContainer width="100%">
<AreaChart data={stats} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<AreaChart data={flattenedStats} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
{libraries.map((library, index) => (
<linearGradient key={library.Id} id={library.Id} x1="0" y1="0" x2="0" y2="1">

View File

@@ -10,6 +10,7 @@ function DailyPlayStats(props) {
const [stats, setStats] = useState();
const [libraries, setLibraries] = useState();
const [days, setDays] = useState(20);
const [viewName, setViewName] = useState("count");
const token = localStorage.getItem("token");
@@ -45,19 +46,24 @@ function DailyPlayStats(props) {
setDays(props.days);
fetchLibraries();
}
if (props.viewName !== viewName) {
setViewName(props.viewName);
}
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [stats,libraries, days, props.days, token]);
}, [stats,libraries, days, props.days, props.viewName, token]);
if (!stats) {
return <></>;
}
const titleKey = viewName === "count" ? "STAT_PAGE.DAILY_PLAY_PER_LIBRARY" : "STAT_PAGE.DAILY_DURATION_PER_LIBRARY";
if (stats.length === 0) {
return (
<div className="main-widget">
<h1><Trans i18nKey={"STAT_PAGE.DAILY_PLAY_PER_LIBRARY"}/> - {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h1><Trans i18nKey={titleKey}/> - {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
</div>
@@ -65,10 +71,10 @@ function DailyPlayStats(props) {
}
return (
<div className="main-widget">
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.DAILY_PLAY_PER_LIBRARY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<div className="graph">
<Chart libraries={libraries} stats={stats} />
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
</div>
</div>
);

View File

@@ -9,6 +9,7 @@ function PlayStatsByDay(props) {
const [stats, setStats] = useState();
const [libraries, setLibraries] = useState();
const [days, setDays] = useState(20);
const [viewName, setViewName] = useState("count");
const token = localStorage.getItem("token");
useEffect(() => {
@@ -41,19 +42,24 @@ function PlayStatsByDay(props) {
setDays(props.days);
fetchLibraries();
}
if (props.viewName !== viewName) {
setViewName(props.viewName);
}
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [stats, libraries, days, props.days, token]);
}, [stats, libraries, days, props.days, props.viewName, token]);
if (!stats) {
return <></>;
}
const titleKey = viewName === "count" ? "STAT_PAGE.PLAY_COUNT_BY" : "STAT_PAGE.PLAY_DURATION_BY";
if (stats.length === 0) {
return (
<div className="statistics-widget small">
<h1><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h1><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
</div>
@@ -62,9 +68,9 @@ function PlayStatsByDay(props) {
return (
<div className="statistics-widget">
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<div className="graph small">
<Chart libraries={libraries} stats={stats} />
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
</div>
</div>
);

View File

@@ -8,6 +8,7 @@ function PlayStatsByHour(props) {
const [stats, setStats] = useState();
const [libraries, setLibraries] = useState();
const [days, setDays] = useState(20);
const [viewName, setViewName] = useState("count");
const token = localStorage.getItem("token");
useEffect(() => {
@@ -40,19 +41,23 @@ function PlayStatsByHour(props) {
setDays(props.days);
fetchLibraries();
}
if (props.viewName !== viewName) {
setViewName(props.viewName);
}
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [stats, libraries, days, props.days, token]);
}, [stats, libraries, days, props.days, props.viewName, token]);
if (!stats) {
return <></>;
}
const titleKey = viewName === "count" ? "STAT_PAGE.PLAY_COUNT_BY" : "STAT_PAGE.PLAY_DURATION_BY";
if (stats.length === 0) {
return (
<div className="statistics-widget small">
<h1><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h1><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
</div>
@@ -62,9 +67,9 @@ function PlayStatsByHour(props) {
return (
<div className="statistics-widget">
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<div className="graph small">
<Chart libraries={libraries} stats={stats} />
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
</div>
</div>
);

View File

@@ -47,6 +47,17 @@
margin-bottom: 10px !important;
}
.stats-tab-nav {
background-color: var(--secondary-background-color);
border-radius: 8px;
align-self: flex-end;
}
.nav-item {
display: flex;
justify-content: center;
}
.chart-canvas {
width: 100%;
height: 400px;

View File

@@ -1,3 +1,4 @@
import { Tabs, Tab } from "react-bootstrap";
import { useState } from "react";
import "./css/stats.css";
@@ -20,6 +21,13 @@ function Statistics() {
localStorage.setItem("PREF_STATISTICS_STAT_DAYS_INPUT", event.target.value);
};
const [activeTab, setActiveTab] = useState(localStorage.getItem(`PREF_STATISTICS_LAST_SELECTED_TAB`) ?? "tabCount");
function setTab(tabName) {
setActiveTab(tabName);
localStorage.setItem(`PREF_STATISTICS_LAST_SELECTED_TAB`, tabName);
}
const handleKeyDown = (event) => {
if (event.key === "Enter") {
if (input < 1) {
@@ -43,6 +51,26 @@ function Statistics() {
<h1>
<Trans i18nKey={"STAT_PAGE.STATISTICS"} />
</h1>
<div className="stats-tab-nav">
<Tabs
defaultActiveKey={activeTab}
activeKey={activeTab}
onSelect={setTab}
variant="pills"
>
<Tab
eventKey="tabCount"
className="bg-transparent"
title={<Trans i18nKey="STAT_PAGE.COUNT_VIEW" />}
/>
<Tab
eventKey="tabDuration"
className="bg-transparent"
title={<Trans i18nKey="STAT_PAGE.DURATION_VIEW" />}
/>
</Tabs>
</div>
<div className="date-range">
<div className="header">
<Trans i18nKey={"LAST"} />
@@ -55,14 +83,26 @@ function Statistics() {
</div>
</div>
</div>
<div>
<DailyPlayStats days={days} />
<div className="statistics-graphs">
<PlayStatsByDay days={days} />
<PlayStatsByHour days={days} />
{activeTab === "tabCount" && (
<div>
<DailyPlayStats days={days} viewName="count" />
<div className="statistics-graphs">
<PlayStatsByDay days={days} viewName="count" />
<PlayStatsByHour days={days} viewName="count" />
</div>
</div>
</div>
)}
{activeTab === "tabDuration" && (
<div>
<DailyPlayStats days={days} viewName="duration" />
<div className="statistics-graphs">
<PlayStatsByDay days={days} viewName="duration" />
<PlayStatsByHour days={days} viewName="duration" />
</div>
</div>
)}
</div>
);
}