mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
chore: added loading indicator to library items
chore: added default icons to library items/recently added cards when no images load chore: fixed css for audio cover images being stretched in library chore: added a testing subr route for testing conponents fix: fix session cards not pulling up IP info when ip lookup is enabled
This commit is contained in:
@@ -27,10 +27,9 @@ import ItemInfo from "./pages/components/item-info";
|
||||
import ErrorPage from "./pages/components/general/error";
|
||||
import About from "./pages/about";
|
||||
|
||||
import Testing from "./pages/testing";
|
||||
import TestingRoutes from "./pages/testing";
|
||||
import Activity from "./pages/activity";
|
||||
import Statistics from "./pages/statistics";
|
||||
import { t } from "i18next";
|
||||
|
||||
function App() {
|
||||
const [setupState, setSetupState] = useState(0);
|
||||
@@ -171,8 +170,8 @@ function App() {
|
||||
<Route path="/libraries/item/:Id" element={<ItemInfo />} />
|
||||
<Route path="/statistics" element={<Statistics />} />
|
||||
<Route path="/activity" element={<Activity />} />
|
||||
<Route path="/testing" element={<Testing />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/testing/*" element={<TestingRoutes />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +284,8 @@ export default function ActivityTable(props) {
|
||||
enableGlobalFilter: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowSelection: (row) => row.original.Id,
|
||||
enableSubRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
enableBatchRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
positionToolbarAlertBanner: "bottom",
|
||||
renderTopToolbarCustomActions: () => {
|
||||
|
||||
@@ -5,12 +5,30 @@ import { useParams } from "react-router-dom";
|
||||
import ArchiveDrawerFillIcon from "remixicon-react/ArchiveDrawerFillIcon";
|
||||
import "../../../css/lastplayed.css";
|
||||
import { Trans } from "react-i18next";
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
import FileMusicLineIcon from "remixicon-react/FileMusicLineIcon";
|
||||
import CheckboxMultipleBlankLineIcon from "remixicon-react/CheckboxMultipleBlankLineIcon";
|
||||
|
||||
function MoreItemCards(props) {
|
||||
const { Id } = useParams();
|
||||
const [loaded, setLoaded] = useState(props.data.archived);
|
||||
const [fallback, setFallback] = useState(false);
|
||||
|
||||
const SeriesIcon = <TvLineIcon size={"50%"} />;
|
||||
const MovieIcon = <FilmLineIcon size={"50%"} />;
|
||||
const MusicIcon = <FileMusicLineIcon size={"50%"} />;
|
||||
const MixedIcon = <CheckboxMultipleBlankLineIcon size={"50%"} />;
|
||||
|
||||
const currentLibraryDefaultIcon =
|
||||
props.data.Type === "Movie"
|
||||
? MovieIcon
|
||||
: props.data.Type === "Episode"
|
||||
? SeriesIcon
|
||||
: props.data.Type === "Audio"
|
||||
? MusicIcon
|
||||
: MixedIcon;
|
||||
|
||||
function formatFileSize(sizeInBytes) {
|
||||
const sizeInMB = sizeInBytes / 1048576; // 1 MB = 1048576 bytes
|
||||
if (sizeInMB < 1000) {
|
||||
@@ -27,7 +45,15 @@ function MoreItemCards(props) {
|
||||
to={`/libraries/item/${props.data.Type === "Episode" ? props.data.EpisodeId : props.data.Id}`}
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<div className={props.data.Type === "Episode" ? "last-card-banner episode" : "last-card-banner"}>
|
||||
<div
|
||||
className={
|
||||
(props.data.Type === "Episode"
|
||||
? "last-card-banner episode"
|
||||
: props.data.Type === "Audio"
|
||||
? "last-card-banner audio"
|
||||
: "last-card-banner") + " d-flex justify-content-center align-items-center"
|
||||
}
|
||||
>
|
||||
{((props.data.ImageBlurHashes && props.data.ImageBlurHashes != null) ||
|
||||
(props.data.PrimaryImageHash && props.data.PrimaryImageHash != null)) &&
|
||||
!loaded ? (
|
||||
@@ -41,18 +67,24 @@ function MoreItemCards(props) {
|
||||
|
||||
{!props.data.archived ? (
|
||||
fallback ? (
|
||||
<img
|
||||
src={`${"/proxy/Items/Images/Primary?id=" + Id + "&fillHeight=320&fillWidth=213&quality=50"}`}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { display: "block" } : { display: "none" }}
|
||||
/>
|
||||
Id == undefined ? (
|
||||
currentLibraryDefaultIcon
|
||||
) : (
|
||||
<img
|
||||
src={`${"/proxy/Items/Images/Primary?id=" + Id + "&fillHeight=320&fillWidth=213&quality=50"}`}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { display: "block" } : { display: "none" }}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<img
|
||||
src={`${
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
(props.data.Type === "Episode" ? props.data.EpisodeId : props.data.Id) +
|
||||
"&fillHeight=320&fillWidth=213&quality=50"
|
||||
(props.data.Type === "Audio"
|
||||
? "&fillHeight=300&fillWidth=300&quality=50"
|
||||
: "&fillHeight=320&fillWidth=213&quality=50")
|
||||
}`}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Blurhash } from "react-blurhash";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
import { Trans } from "react-i18next";
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
import FileMusicLineIcon from "remixicon-react/FileMusicLineIcon";
|
||||
import CheckboxMultipleBlankLineIcon from "remixicon-react/CheckboxMultipleBlankLineIcon";
|
||||
|
||||
function RecentlyAddedCard(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [fallback, setFallback] = useState(false);
|
||||
const twelve_hr = JSON.parse(localStorage.getItem("12hr"));
|
||||
const localization = localStorage.getItem("i18nextLng");
|
||||
|
||||
@@ -20,13 +25,34 @@ function RecentlyAddedCard(props) {
|
||||
hour12: twelve_hr,
|
||||
};
|
||||
|
||||
const SeriesIcon = <TvLineIcon size={"75%"} />;
|
||||
const MovieIcon = <FilmLineIcon size={"75%"} />;
|
||||
const MusicIcon = <FileMusicLineIcon size={"75%"} />;
|
||||
const MixedIcon = <CheckboxMultipleBlankLineIcon size={"75%"} />;
|
||||
|
||||
const currentLibraryDefaultIcon =
|
||||
props.data.Type === "Movie"
|
||||
? MovieIcon
|
||||
: props.data.Type === "Episode"
|
||||
? SeriesIcon
|
||||
: props.data.Type === "Audio"
|
||||
? MusicIcon
|
||||
: MixedIcon;
|
||||
|
||||
return (
|
||||
<div className="last-card">
|
||||
<Link to={`/libraries/item/${(props.data.NewEpisodeCount != undefined ? props.data.SeasonId : props.data.EpisodeId) ?? props.data.Id}`}>
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : props.data.PrimaryImageHash ? (
|
||||
<Blurhash hash={props.data.PrimaryImageHash} width={"100%"} height={"100%"} className="rounded-3 overflow-hidden" />
|
||||
) : null}
|
||||
<Link
|
||||
to={`/libraries/item/${
|
||||
(props.data.NewEpisodeCount != undefined ? props.data.SeasonId : props.data.EpisodeId) ?? props.data.Id
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
(props.data.Type === "Audio" ? "last-card-banner audio" : "last-card-banner") +
|
||||
" d-flex justify-content-center align-items-center"
|
||||
}
|
||||
>
|
||||
{fallback ? currentLibraryDefaultIcon : null}
|
||||
<img
|
||||
src={`${
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
@@ -35,6 +61,7 @@ function RecentlyAddedCard(props) {
|
||||
}`}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => setFallback(true)}
|
||||
style={loaded ? {} : { display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import "../../css/library/media-items.css";
|
||||
import "../../css/width_breakpoint_css.css";
|
||||
import "../../css/radius_breakpoint_css.css";
|
||||
import { Trans } from "react-i18next";
|
||||
import Loading from "../general/loading";
|
||||
|
||||
function LibraryItems(props) {
|
||||
const [data, setData] = useState();
|
||||
@@ -92,7 +93,7 @@ function LibraryItems(props) {
|
||||
}
|
||||
|
||||
if (!data || !config) {
|
||||
return <></>;
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, {useState,useEffect} from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import Card from 'react-bootstrap/Card';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import Card from "react-bootstrap/Card";
|
||||
import Row from "react-bootstrap/Row";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import Container from "react-bootstrap/Container";
|
||||
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
import PlayFillIcon from "remixicon-react/PlayFillIcon";
|
||||
import PauseFillIcon from "remixicon-react/PauseFillIcon";
|
||||
|
||||
import { clientData } from "../../../lib/devices";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IpInfoModal from '../ip-info';
|
||||
|
||||
import axios from 'axios';
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IpInfoModal from "../ip-info";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
function ticksToTimeString(ticks) {
|
||||
// Convert ticks to seconds
|
||||
@@ -24,57 +23,59 @@ function ticksToTimeString(ticks) {
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
// Format the time string as hh:MM:ss
|
||||
const timeString = `${hours.toString().padStart(2, "0")}:${minutes
|
||||
const timeString = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${remainingSeconds
|
||||
.toString()
|
||||
.padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
function getETAFromTicks(ticks) {
|
||||
|
||||
// Get current date
|
||||
const currentDate = Date.now();
|
||||
|
||||
// Calculate ETA
|
||||
const etaMillis = currentDate + ticks/10000;
|
||||
const etaMillis = currentDate + ticks / 10000;
|
||||
const eta = new Date(etaMillis);
|
||||
|
||||
// Return formated string in user locale
|
||||
return eta.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
return eta.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function convertBitrate(bitrate) {
|
||||
if(!bitrate)
|
||||
{
|
||||
return 'N/A';
|
||||
if (!bitrate) {
|
||||
return "N/A";
|
||||
}
|
||||
const kbps = (bitrate / 1000).toFixed(1);
|
||||
const mbps = (bitrate / 1000000).toFixed(1);
|
||||
|
||||
if (kbps >= 1000) {
|
||||
return mbps+' Mbps';
|
||||
return mbps + " Mbps";
|
||||
} else {
|
||||
return kbps+' Kbps';
|
||||
return kbps + " Kbps";
|
||||
}
|
||||
}
|
||||
|
||||
function SessionCard(props) {
|
||||
const cardStyle = {
|
||||
backgroundImage: `url(proxy/Items/Images/Backdrop?id=${(props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id)}&fillHeight=320&fillWidth=213&quality=80), linear-gradient(to right, #00A4DC, #AA5CC3)`,
|
||||
height:'100%',
|
||||
backgroundSize: 'cover',
|
||||
backgroundImage: `url(proxy/Items/Images/Backdrop?id=${
|
||||
props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id
|
||||
}&fillHeight=320&fillWidth=213&quality=80), linear-gradient(to right, #00A4DC, #AA5CC3)`,
|
||||
height: "100%",
|
||||
backgroundSize: "cover",
|
||||
};
|
||||
|
||||
const cardBgStyle = {
|
||||
backdropFilter: 'blur(5px)',
|
||||
backgroundColor: 'rgb(0, 0, 0, 0.6)',
|
||||
height:'100%',
|
||||
backdropFilter: "blur(5px)",
|
||||
backgroundColor: "rgb(0, 0, 0, 0.6)",
|
||||
height: "100%",
|
||||
};
|
||||
|
||||
|
||||
|
||||
const ipv4Regex = new RegExp(/\b(?!(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168))(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/);
|
||||
const ipv4Regex = new RegExp(
|
||||
/\b(?!(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168))(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/
|
||||
);
|
||||
|
||||
const [ipModalVisible, setIPModalVisible] = React.useState(false);
|
||||
const [ipAddressLookup, setIPAddressLookup] = React.useState();
|
||||
@@ -87,7 +88,6 @@ function SessionCard(props) {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
function showIPDataModal(ipAddress) {
|
||||
ipv4Regex.lastIndex = 0;
|
||||
setIPAddressLookup(ipAddress);
|
||||
@@ -98,188 +98,211 @@ function SessionCard(props) {
|
||||
setIPModalVisible(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card className="stat-card" style={cardStyle}>
|
||||
<IpInfoModal show={ipModalVisible} onHide={() => setIPModalVisible(false)} ipAddress={ipAddressLookup} />
|
||||
<div style={cardBgStyle} className="rounded-top">
|
||||
<Row className="h-100">
|
||||
<Col className="d-none d-lg-block stat-card-banner">
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className={props.data.session.NowPlayingItem.Type==='Audio' ? (
|
||||
"stat-card-image-audio rounded-0 rounded-start"
|
||||
) : (
|
||||
"stat-card-image rounded-0 rounded-start"
|
||||
)}
|
||||
src={"/proxy/Items/Images/Primary?id=" + (props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id) + "&fillHeight=320&fillWidth=213&quality=50"}
|
||||
/>
|
||||
|
||||
|
||||
</Col>
|
||||
<Col className="w-100 h-100">
|
||||
|
||||
<Card.Body className="w-100 h-100 p-1 pb-2" >
|
||||
<Container className="h-100 d-flex flex-column justify-content-between g-0">
|
||||
<Row className="d-flex justify-content-end" style={{fontSize: "smaller"}}>
|
||||
|
||||
<div style={cardBgStyle} className="rounded-top">
|
||||
<Row className="h-100">
|
||||
<Col className="d-none d-lg-block stat-card-banner">
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className={
|
||||
props.data.session.NowPlayingItem.Type === "Audio"
|
||||
? "stat-card-image-audio rounded-0 rounded-start"
|
||||
: "stat-card-image rounded-0 rounded-start"
|
||||
}
|
||||
src={
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id) +
|
||||
"&fillHeight=320&fillWidth=213&quality=50"
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col className="w-100 h-100">
|
||||
<Card.Body className="w-100 h-100 p-1 pb-2">
|
||||
<Container className="h-100 d-flex flex-column justify-content-between g-0">
|
||||
<Row className="d-flex justify-content-end" style={{ fontSize: "smaller" }}>
|
||||
<Col className="col-10">
|
||||
<Row>
|
||||
<Col className="col-auto ellipse">{props.data.session.DeviceName +" - "+props.data.session.Client + " " + props.data.session.ApplicationVersion}</Col>
|
||||
<Row>
|
||||
<Col className="col-auto ellipse">
|
||||
{props.data.session.DeviceName +
|
||||
" - " +
|
||||
props.data.session.Client +
|
||||
" " +
|
||||
props.data.session.ApplicationVersion}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="d-flex flex-column flex-md-row">
|
||||
<Col className="col-auto ellipse">{props.data.session.PlayState.PlayMethod +(props.data.session.NowPlayingItem.MediaStreams ? ' ( '+props.data.session.NowPlayingItem.MediaStreams.find(stream => stream.Type==='Video')?.Codec.toUpperCase()+(props.data.session.TranscodingInfo? ' - '+props.data.session.TranscodingInfo.VideoCodec.toUpperCase() : '')+' - '+convertBitrate(props.data.session.TranscodingInfo ? props.data.session.TranscodingInfo.Bitrate :props.data.session.NowPlayingItem.MediaStreams.find(stream => stream.Type==='Video')?.BitRate)+' )':'')}</Col>
|
||||
<Row className="d-flex flex-column flex-md-row">
|
||||
<Col className="col-auto ellipse">
|
||||
{props.data.session.PlayState.PlayMethod +
|
||||
(props.data.session.NowPlayingItem.MediaStreams
|
||||
? " ( " +
|
||||
props.data.session.NowPlayingItem.MediaStreams.find(
|
||||
(stream) => stream.Type === "Video"
|
||||
)?.Codec.toUpperCase() +
|
||||
(props.data.session.TranscodingInfo
|
||||
? " - " + props.data.session.TranscodingInfo.VideoCodec.toUpperCase()
|
||||
: "") +
|
||||
" - " +
|
||||
convertBitrate(
|
||||
props.data.session.TranscodingInfo
|
||||
? props.data.session.TranscodingInfo.Bitrate
|
||||
: props.data.session.NowPlayingItem.MediaStreams.find((stream) => stream.Type === "Video")
|
||||
?.BitRate
|
||||
) +
|
||||
" )"
|
||||
: "")}
|
||||
</Col>
|
||||
<Col className="col-auto ellipse">
|
||||
<Tooltip title={props.data.session.NowPlayingItem.SubtitleStream}>
|
||||
<span>
|
||||
{props.data.session.NowPlayingItem.SubtitleStream}
|
||||
</span>
|
||||
<span>{props.data.session.NowPlayingItem.SubtitleStream}</span>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className="col-auto ellipse">
|
||||
|
||||
{isRemoteSession(props.data.session.RemoteEndPoint) && (import.meta.env.JS_GEOLITE_ACCOUNT_ID && import.meta.env.JS_GEOLITE_LICENSE_KEY) ?
|
||||
<Card.Text>
|
||||
</Card.Text>
|
||||
:
|
||||
{isRemoteSession(props.data.session.RemoteEndPoint) &&
|
||||
import.meta.env.JS_GEOLITE_ACCOUNT_ID &&
|
||||
import.meta.env.JS_GEOLITE_LICENSE_KEY ? (
|
||||
<Link
|
||||
className="text-decoration-none text-white"
|
||||
onClick={() => showIPDataModal(props.data.session.RemoteEndPoint)}
|
||||
>
|
||||
<Trans i18nKey="ACTIVITY_TABLE.IP_ADDRESS" />: {props.data.session.RemoteEndPoint}
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
IP Address: {props.data.session.RemoteEndPoint}
|
||||
<Trans i18nKey="ACTIVITY_TABLE.IP_ADDRESS" />: {props.data.session.RemoteEndPoint}
|
||||
</span>
|
||||
}
|
||||
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
|
||||
<Col className="col-2 d-flex justify-content-center">
|
||||
<img
|
||||
className="card-device-image"
|
||||
src={
|
||||
"/proxy/web/assets/img/devices/?devicename=" +
|
||||
(props.data.session.Client.toLowerCase() === "jellyfin mobile (ios)" && props.data.session.DeviceName.toLowerCase() === "iphone" ?
|
||||
"apple"
|
||||
:
|
||||
props.data.session.Client.toLowerCase().includes("web") ?
|
||||
( clientData.find(item => props.data.session.DeviceName.toLowerCase().includes(item)) || "other")
|
||||
:
|
||||
( clientData.find(item => props.data.session.Client.toLowerCase().includes(item)) || "other")
|
||||
)
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
|
||||
|
||||
<Row className="p-0 m-0">
|
||||
{props.data.session.NowPlayingItem.Type==='Episode' ?
|
||||
<Row className="d-flex flex-row justify-content-between p-0">
|
||||
|
||||
<Card.Text>
|
||||
<Link to={`/libraries/item/${props.data.session.NowPlayingItem.Id}`} target="_blank" className="item-name">
|
||||
{props.data.session.NowPlayingItem.SeriesName ? (props.data.session.NowPlayingItem.SeriesName+" - "+ props.data.session.NowPlayingItem.Name) : (props.data.session.NowPlayingItem.Name)}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
|
||||
</Row>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
<Row className="d-flex flex-row justify-content-between p-0 m-0" >
|
||||
{props.data.session.NowPlayingItem.Type==='Episode' ?
|
||||
<Col className="col-auto p-0">
|
||||
<Card.Text >
|
||||
{'S'+props.data.session.NowPlayingItem.ParentIndexNumber +' - E'+ props.data.session.NowPlayingItem.IndexNumber}
|
||||
</Card.Text>
|
||||
</Col>
|
||||
|
||||
:
|
||||
<Col className="p-0">
|
||||
<Card.Text>
|
||||
<Link to={`/libraries/item/${props.data.session.NowPlayingItem.Id}`} target="_blank" className="item-name">
|
||||
{props.data.session.NowPlayingItem.SeriesName ? (props.data.session.NowPlayingItem.SeriesName+" - "+ props.data.session.NowPlayingItem.Name) : (props.data.session.NowPlayingItem.Name)}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
className="card-device-image"
|
||||
src={
|
||||
"/proxy/web/assets/img/devices/?devicename=" +
|
||||
(props.data.session.Client.toLowerCase() === "jellyfin mobile (ios)" &&
|
||||
props.data.session.DeviceName.toLowerCase() === "iphone"
|
||||
? "apple"
|
||||
: props.data.session.Client.toLowerCase().includes("web")
|
||||
? clientData.find((item) => props.data.session.DeviceName.toLowerCase().includes(item)) || "other"
|
||||
: clientData.find((item) => props.data.session.Client.toLowerCase().includes(item)) || "other")
|
||||
}
|
||||
|
||||
<Col className="d-flex flex-row justify-content-end text-end col-auto">
|
||||
|
||||
{props.data.session.UserPrimaryImageTag !== undefined ? (
|
||||
<img
|
||||
className="session-card-user-image"
|
||||
src={
|
||||
"/proxy/Users/Images/Primary?id=" +
|
||||
props.data.session.UserId +
|
||||
"&quality=50"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon className="session-card-user-image"/>
|
||||
)}
|
||||
<Card.Text >
|
||||
<Tooltip title={props.data.session.UserName} >
|
||||
<Link to={`/users/${props.data.session.UserId}`} className="item-name" style={{maxWidth:'15ch'}}>{props.data.session.UserName}</Link>
|
||||
</Tooltip>
|
||||
</Card.Text>
|
||||
|
||||
alt=""
|
||||
/>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
|
||||
<Row className="p-0 m-0">
|
||||
<Col className="col-auto p-0">
|
||||
{props.data.session.NowPlayingItem.Type === "Episode" ? (
|
||||
<Row className="d-flex flex-row justify-content-between p-0">
|
||||
<Card.Text>
|
||||
<Link
|
||||
to={`/libraries/item/${props.data.session.NowPlayingItem.Id}`}
|
||||
target="_blank"
|
||||
className="item-name"
|
||||
>
|
||||
{props.data.session.NowPlayingItem.SeriesName
|
||||
? props.data.session.NowPlayingItem.SeriesName + " - " + props.data.session.NowPlayingItem.Name
|
||||
: props.data.session.NowPlayingItem.Name}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
</Row>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Row className="d-flex flex-row justify-content-between p-0 m-0">
|
||||
{props.data.session.NowPlayingItem.Type === "Episode" ? (
|
||||
<Col className="col-auto p-0">
|
||||
<Card.Text>
|
||||
{"S" +
|
||||
props.data.session.NowPlayingItem.ParentIndexNumber +
|
||||
" - E" +
|
||||
props.data.session.NowPlayingItem.IndexNumber}
|
||||
</Card.Text>
|
||||
</Col>
|
||||
) : (
|
||||
<Col className="p-0">
|
||||
<Card.Text>
|
||||
<Link
|
||||
to={`/libraries/item/${props.data.session.NowPlayingItem.Id}`}
|
||||
target="_blank"
|
||||
className="item-name"
|
||||
>
|
||||
{props.data.session.NowPlayingItem.SeriesName
|
||||
? props.data.session.NowPlayingItem.SeriesName + " - " + props.data.session.NowPlayingItem.Name
|
||||
: props.data.session.NowPlayingItem.Name}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{props.data.session.PlayState.IsPaused ?
|
||||
<PauseFillIcon />
|
||||
:
|
||||
<PlayFillIcon />
|
||||
}
|
||||
<Col className="d-flex flex-row justify-content-end text-end col-auto">
|
||||
{props.data.session.UserPrimaryImageTag !== undefined ? (
|
||||
<img
|
||||
className="session-card-user-image"
|
||||
src={"/proxy/Users/Images/Primary?id=" + props.data.session.UserId + "&quality=50"}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon className="session-card-user-image" />
|
||||
)}
|
||||
<Card.Text>
|
||||
<Tooltip title={props.data.session.UserName}>
|
||||
<Link to={`/users/${props.data.session.UserId}`} className="item-name" style={{ maxWidth: "15ch" }}>
|
||||
{props.data.session.UserName}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Col>
|
||||
<Row className="p-0 m-0">
|
||||
<Col className="col-auto p-0">
|
||||
{props.data.session.PlayState.IsPaused ? <PauseFillIcon /> : <PlayFillIcon />}
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
|
||||
<Card.Text className="text-end">
|
||||
<Tooltip title={`Ends at ${getETAFromTicks(props.data.session.NowPlayingItem.RunTimeTicks - props.data.session.PlayState.PositionTicks)}`}>
|
||||
<span>
|
||||
{ticksToTimeString(props.data.session.PlayState.PositionTicks)}/
|
||||
{ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Card.Text>
|
||||
|
||||
</Col>
|
||||
<Col>
|
||||
<Card.Text className="text-end">
|
||||
<Tooltip
|
||||
title={`Ends at ${getETAFromTicks(
|
||||
props.data.session.NowPlayingItem.RunTimeTicks - props.data.session.PlayState.PositionTicks
|
||||
)}`}
|
||||
>
|
||||
<span>
|
||||
{ticksToTimeString(props.data.session.PlayState.PositionTicks)}/
|
||||
{ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-custom"
|
||||
style={{
|
||||
width: `${
|
||||
(props.data.session.PlayState.PositionTicks /
|
||||
props.data.session.NowPlayingItem.RunTimeTicks) *
|
||||
100
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</Card.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-custom"
|
||||
style={{
|
||||
width: `${
|
||||
(props.data.session.PlayState.PositionTicks / props.data.session.NowPlayingItem.RunTimeTicks) * 100
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
width: 220px !important;
|
||||
}
|
||||
|
||||
.audio {
|
||||
height: 150px !important;
|
||||
}
|
||||
|
||||
.last-card-banner {
|
||||
width: 150px;
|
||||
height: 220px;
|
||||
|
||||
317
src/pages/debugTools/session-card.jsx
Normal file
317
src/pages/debugTools/session-card.jsx
Normal file
@@ -0,0 +1,317 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import Card from "react-bootstrap/Card";
|
||||
import Row from "react-bootstrap/Row";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import Container from "react-bootstrap/Container";
|
||||
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
import PlayFillIcon from "remixicon-react/PlayFillIcon";
|
||||
import PauseFillIcon from "remixicon-react/PauseFillIcon";
|
||||
|
||||
import { clientData } from "../../lib/devices";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IpInfoModal from "../components/ip-info";
|
||||
import "./sessionCard.css";
|
||||
|
||||
function ticksToTimeString(ticks) {
|
||||
// Convert ticks to seconds
|
||||
const seconds = Math.floor(ticks / 10000000);
|
||||
// Calculate hours, minutes, and remaining seconds
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
// Format the time string as hh:MM:ss
|
||||
const timeString = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${remainingSeconds
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
function getETAFromTicks(ticks) {
|
||||
// Get current date
|
||||
const currentDate = Date.now();
|
||||
|
||||
// Calculate ETA
|
||||
const etaMillis = currentDate + ticks / 10000;
|
||||
const eta = new Date(etaMillis);
|
||||
|
||||
// Return formated string in user locale
|
||||
return eta.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function convertBitrate(bitrate) {
|
||||
if (!bitrate) {
|
||||
return "N/A";
|
||||
}
|
||||
const kbps = (bitrate / 1000).toFixed(1);
|
||||
const mbps = (bitrate / 1000000).toFixed(1);
|
||||
|
||||
if (kbps >= 1000) {
|
||||
return mbps + " Mbps";
|
||||
} else {
|
||||
return kbps + " Kbps";
|
||||
}
|
||||
}
|
||||
|
||||
function SessionCard(props) {
|
||||
const cardStyle = {
|
||||
backgroundImage: `url(proxy/Items/Images/Backdrop?id=${
|
||||
props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id
|
||||
}&fillHeight=320&fillWidth=213&quality=80), linear-gradient(to right, #00A4DC, #AA5CC3)`,
|
||||
height: "100%",
|
||||
backgroundSize: "cover",
|
||||
};
|
||||
|
||||
const cardBgStyle = {
|
||||
backdropFilter: "blur(5px)",
|
||||
backgroundColor: "rgb(0, 0, 0, 0.6)",
|
||||
height: "100%",
|
||||
};
|
||||
|
||||
const ipv4Regex = new RegExp(
|
||||
/\b(?!(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168))(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/
|
||||
);
|
||||
|
||||
const [ipModalVisible, setIPModalVisible] = React.useState(false);
|
||||
const [ipAddressLookup, setIPAddressLookup] = React.useState();
|
||||
const [isJsonVisible, setIsJsonVisible] = useState(false);
|
||||
|
||||
const toggleJsonVisibility = () => {
|
||||
setIsJsonVisible(!isJsonVisible);
|
||||
};
|
||||
|
||||
const isRemoteSession = (ipAddress) => {
|
||||
ipv4Regex.lastIndex = 0;
|
||||
if (ipv4Regex.test(ipAddress ?? ipAddressLookup)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function showIPDataModal(ipAddress) {
|
||||
ipv4Regex.lastIndex = 0;
|
||||
setIPAddressLookup(ipAddress);
|
||||
if (!isRemoteSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIPModalVisible(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="stat-card" style={cardStyle}>
|
||||
<IpInfoModal show={ipModalVisible} onHide={() => setIPModalVisible(false)} ipAddress={ipAddressLookup} />
|
||||
<div style={cardBgStyle} className="rounded-top">
|
||||
<Row className="h-100">
|
||||
<Col className="d-none d-lg-block stat-card-banner">
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className={
|
||||
props.data.session.NowPlayingItem.Type === "Audio"
|
||||
? "stat-card-image-audio rounded-0 rounded-start"
|
||||
: "stat-card-image rounded-0 rounded-start"
|
||||
}
|
||||
src={
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id) +
|
||||
"&fillHeight=320&fillWidth=213&quality=50"
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col className="w-100 h-100">
|
||||
<Card.Body className="w-100 h-100 p-1 pb-2">
|
||||
<Container className="h-100 d-flex flex-column justify-content-between g-0">
|
||||
<Row className="d-flex justify-content-end" style={{ fontSize: "smaller" }}>
|
||||
<Col className="col-10">
|
||||
<Row>
|
||||
<Col className="col-auto ellipse">
|
||||
{props.data.session.DeviceName +
|
||||
" - " +
|
||||
props.data.session.Client +
|
||||
" " +
|
||||
props.data.session.ApplicationVersion}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="d-flex flex-column flex-md-row">
|
||||
<Col className="col-auto ellipse">
|
||||
{props.data.session.PlayState.PlayMethod +
|
||||
(props.data.session.NowPlayingItem.MediaStreams
|
||||
? " ( " +
|
||||
props.data.session.NowPlayingItem.MediaStreams.find(
|
||||
(stream) => stream.Type === "Video"
|
||||
)?.Codec.toUpperCase() +
|
||||
(props.data.session.TranscodingInfo
|
||||
? " - " + props.data.session.TranscodingInfo.VideoCodec.toUpperCase()
|
||||
: "") +
|
||||
" - " +
|
||||
convertBitrate(
|
||||
props.data.session.TranscodingInfo
|
||||
? props.data.session.TranscodingInfo.Bitrate
|
||||
: props.data.session.NowPlayingItem.MediaStreams.find((stream) => stream.Type === "Video")
|
||||
?.BitRate
|
||||
) +
|
||||
" )"
|
||||
: "")}
|
||||
</Col>
|
||||
<Col className="col-auto ellipse">
|
||||
<Tooltip title={props.data.session.NowPlayingItem.SubtitleStream}>
|
||||
<span>{props.data.session.NowPlayingItem.SubtitleStream}</span>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className="col-auto ellipse">
|
||||
{isRemoteSession(props.data.session.RemoteEndPoint) &&
|
||||
import.meta.env.JS_GEOLITE_ACCOUNT_ID &&
|
||||
import.meta.env.JS_GEOLITE_LICENSE_KEY ? (
|
||||
<Card.Text></Card.Text>
|
||||
) : (
|
||||
<span>IP Address: {props.data.session.RemoteEndPoint}</span>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col className="col-2 d-flex justify-content-center">
|
||||
<img
|
||||
className="card-device-image"
|
||||
src={
|
||||
"/proxy/web/assets/img/devices/?devicename=" +
|
||||
(props.data.session.Client.toLowerCase() === "jellyfin mobile (ios)" &&
|
||||
props.data.session.DeviceName.toLowerCase() === "iphone"
|
||||
? "apple"
|
||||
: props.data.session.Client.toLowerCase().includes("web")
|
||||
? clientData.find((item) => props.data.session.DeviceName.toLowerCase().includes(item)) || "other"
|
||||
: clientData.find((item) => props.data.session.Client.toLowerCase().includes(item)) || "other")
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="p-0 m-0">
|
||||
{props.data.session.NowPlayingItem.Type === "Episode" ? (
|
||||
<Row className="d-flex flex-row justify-content-between p-0">
|
||||
<Card.Text>
|
||||
<Link
|
||||
to={`/libraries/item/${props.data.session.NowPlayingItem.Id}`}
|
||||
target="_blank"
|
||||
className="item-name"
|
||||
>
|
||||
{props.data.session.NowPlayingItem.SeriesName
|
||||
? props.data.session.NowPlayingItem.SeriesName + " - " + props.data.session.NowPlayingItem.Name
|
||||
: props.data.session.NowPlayingItem.Name}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
</Row>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Row className="d-flex flex-row justify-content-between p-0 m-0">
|
||||
{props.data.session.NowPlayingItem.Type === "Episode" ? (
|
||||
<Col className="col-auto p-0">
|
||||
<Card.Text>
|
||||
{"S" +
|
||||
props.data.session.NowPlayingItem.ParentIndexNumber +
|
||||
" - E" +
|
||||
props.data.session.NowPlayingItem.IndexNumber}
|
||||
</Card.Text>
|
||||
</Col>
|
||||
) : (
|
||||
<Col className="p-0">
|
||||
<Card.Text>
|
||||
<Link
|
||||
to={`/libraries/item/${props.data.session.NowPlayingItem.Id}`}
|
||||
target="_blank"
|
||||
className="item-name"
|
||||
>
|
||||
{props.data.session.NowPlayingItem.SeriesName
|
||||
? props.data.session.NowPlayingItem.SeriesName + " - " + props.data.session.NowPlayingItem.Name
|
||||
: props.data.session.NowPlayingItem.Name}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col className="d-flex flex-row justify-content-end text-end col-auto">
|
||||
{props.data.session.UserPrimaryImageTag !== undefined ? (
|
||||
<img
|
||||
className="session-card-user-image"
|
||||
src={"/proxy/Users/Images/Primary?id=" + props.data.session.UserId + "&quality=50"}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon className="session-card-user-image" />
|
||||
)}
|
||||
<Card.Text>
|
||||
<Tooltip title={props.data.session.UserName}>
|
||||
<Link to={`/users/${props.data.session.UserId}`} className="item-name" style={{ maxWidth: "15ch" }}>
|
||||
{props.data.session.UserName}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="p-0 m-0">
|
||||
<Col className="col-auto p-0">
|
||||
{props.data.session.PlayState.IsPaused ? <PauseFillIcon /> : <PlayFillIcon />}
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Card.Text className="text-end">
|
||||
<Tooltip
|
||||
title={`Ends at ${getETAFromTicks(
|
||||
props.data.session.NowPlayingItem.RunTimeTicks - props.data.session.PlayState.PositionTicks
|
||||
)}`}
|
||||
>
|
||||
<span>
|
||||
{ticksToTimeString(props.data.session.PlayState.PositionTicks)}/
|
||||
{ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-custom"
|
||||
style={{
|
||||
width: `${
|
||||
(props.data.session.PlayState.PositionTicks / props.data.session.NowPlayingItem.RunTimeTicks) * 100
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-primary w-100 p-0 m-0" onClick={toggleJsonVisibility}>
|
||||
Toggle JSON View
|
||||
</button>
|
||||
{isJsonVisible && (
|
||||
<div className="json-data-container">
|
||||
<pre>{JSON.stringify(props, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionCard;
|
||||
6
src/pages/debugTools/sessionCard.css
Normal file
6
src/pages/debugTools/sessionCard.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.json-data-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: black;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
}
|
||||
112
src/pages/debugTools/sessions.jsx
Normal file
112
src/pages/debugTools/sessions.jsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Config from "../../lib/config";
|
||||
|
||||
import "../css/sessions.css";
|
||||
import ErrorBoundary from "../components/general/ErrorBoundary";
|
||||
import SessionCard from "./session-card";
|
||||
|
||||
import Loading from "../components/general/loading";
|
||||
import { Trans } from "react-i18next";
|
||||
import socket from "../../socket";
|
||||
|
||||
function Sessions() {
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("sessions", (data) => {
|
||||
if (typeof data === "object" && Array.isArray(data)) {
|
||||
let toSet = data.filter((row) => row.NowPlayingItem !== undefined);
|
||||
toSet.forEach((s) => {
|
||||
handleLiveTV(s);
|
||||
s.NowPlayingItem.SubtitleStream = getSubtitleStream(s);
|
||||
});
|
||||
setData(toSet);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
socket.off("sessions");
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
const handleLiveTV = (row) => {
|
||||
let nowPlaying = row.NowPlayingItem;
|
||||
if (!nowPlaying.RunTimeTicks && nowPlaying?.CurrentProgram) {
|
||||
nowPlaying.RunTimeTicks = 0;
|
||||
nowPlaying.Name = `${nowPlaying.Name}: ${nowPlaying.CurrentProgram.Name}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getSubtitleStream = (row) => {
|
||||
let result = "";
|
||||
|
||||
if (!row.PlayState) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let subStreamIndex = row.PlayState.SubtitleStreamIndex;
|
||||
|
||||
if (subStreamIndex === undefined || subStreamIndex === -1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (row.NowPlayingItem.MediaStreams && row.NowPlayingItem.MediaStreams.length) {
|
||||
result = `Subtitles: ${row.NowPlayingItem.MediaStreams[subStreamIndex].DisplayTitle}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
if (!data && !config) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if ((!data && config) || data.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="my-3">
|
||||
<Trans i18nKey="HOME_PAGE.SESSIONS" />
|
||||
</h1>
|
||||
<div style={{ color: "grey", fontSize: "0.8em", fontStyle: "italic" }}>
|
||||
<Trans i18nKey="SESSIONS.NO_SESSIONS" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="my-3">
|
||||
<Trans i18nKey="HOME_PAGE.SESSIONS" />
|
||||
</h1>
|
||||
<div className="sessions-container">
|
||||
{data &&
|
||||
data.length > 0 &&
|
||||
data
|
||||
.sort((a, b) => a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0")))
|
||||
.map((session) => (
|
||||
<ErrorBoundary key={session.Id}>
|
||||
<SessionCard data={{ session: session, base_url: config.base_url }} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sessions;
|
||||
@@ -1,65 +1,12 @@
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import './css/library/libraries.css';
|
||||
|
||||
|
||||
|
||||
|
||||
// import LibraryOverView from './components/libraryOverview';
|
||||
// import HomeStatisticCards from './components/HomeStatisticCards';
|
||||
// import Sessions from './components/sessions/sessions';
|
||||
import LibrarySelector from './library_selector';
|
||||
import { Button } from '@mui/material';
|
||||
|
||||
|
||||
|
||||
function Testing() {
|
||||
|
||||
|
||||
|
||||
|
||||
// async function getToken(username,password) {
|
||||
// const response = await fetch('http://localhost:3003/login', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// username: username,
|
||||
// password: password,
|
||||
// }),
|
||||
// });
|
||||
|
||||
// const data = await response.json();
|
||||
// return data.token;
|
||||
// }
|
||||
|
||||
// // Make a GET request with JWT authentication
|
||||
// async function getDataWithAuth() {
|
||||
// try {
|
||||
// const token = await getToken('test','pass'); // a function to get the JWT token
|
||||
// // console.log(token);
|
||||
// localStorage.setItem('token', token);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
// }
|
||||
// getDataWithAuth();
|
||||
|
||||
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Sessions from "./debugTools/sessions";
|
||||
|
||||
const TestingRoutes = () => {
|
||||
return (
|
||||
<div className='Activity'>
|
||||
|
||||
<LibrarySelector/>
|
||||
<Button variant="contained" color="primary" onClick={()=>toast.info('Test Info', {autoClose: 15000,})}>Test Toast Info</Button>
|
||||
<Button variant="contained" color="success" onClick={()=>toast.success('Test Success', {autoClose: 15000,})}>Test Toast Success</Button>
|
||||
<Button variant="contained" color="error" onClick={()=>toast.error('Test Error', {autoClose: 15000,})}>Test Toast Error</Button>
|
||||
<Button variant="contained" color="warning" onClick={()=>toast.warn('Test Warn', {autoClose: 15000,})}>Test Toast Warn</Button>
|
||||
|
||||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route path="/sessions" element={<Sessions />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Testing;
|
||||
export default TestingRoutes;
|
||||
|
||||
Reference in New Issue
Block a user