mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Add sorting option for Size in LibraryItems component
Added Size Indicator UI to Library and More Items Section Added Episode count to Season View Added Traversal to Library Items Added Size view for Seasons and Shows Fixed Fetch Item Button for missing content
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
exports.up = async function (knex) {
|
||||
try {
|
||||
await knex.schema.raw(`
|
||||
DROP VIEW public.jf_library_items_with_playcount_playtime;
|
||||
CREATE OR REPLACE VIEW public.jf_library_items_with_playcount_playtime
|
||||
AS
|
||||
SELECT i."Id",
|
||||
i."Name",
|
||||
i."ServerId",
|
||||
i."PremiereDate",
|
||||
i."EndDate",
|
||||
i."CommunityRating",
|
||||
i."RunTimeTicks",
|
||||
i."ProductionYear",
|
||||
i."IsFolder",
|
||||
i."Type",
|
||||
i."Status",
|
||||
i."ImageTagsPrimary",
|
||||
i."ImageTagsBanner",
|
||||
i."ImageTagsLogo",
|
||||
i."ImageTagsThumb",
|
||||
i."BackdropImageTags",
|
||||
i."ParentId",
|
||||
i."PrimaryImageHash",
|
||||
i.archived,
|
||||
COALESCE(ii."Size",(SELECT SUM(im."Size") FROM jf_library_seasons s JOIN jf_library_episodes e on s."Id"=e."SeasonId" JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."SeriesId" = i."Id") )"Size",
|
||||
count(a."NowPlayingItemId") AS times_played,
|
||||
COALESCE(sum(a."PlaybackDuration"), 0::numeric) AS total_play_time
|
||||
FROM jf_library_items i
|
||||
LEFT JOIN jf_playback_activity a ON i."Id" = a."NowPlayingItemId"
|
||||
LEFT JOIN jf_item_info ii ON ii."Id" = i."Id"
|
||||
GROUP BY i."Id", "Size"
|
||||
ORDER BY (count(a."NowPlayingItemId")) DESC;`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
try {
|
||||
await knex.schema.raw(`
|
||||
DROP VIEW public.jf_library_items_with_playcount_playtime;
|
||||
CREATE OR REPLACE VIEW public.jf_library_items_with_playcount_playtime
|
||||
AS
|
||||
SELECT i."Id",
|
||||
i."Name",
|
||||
i."ServerId",
|
||||
i."PremiereDate",
|
||||
i."EndDate",
|
||||
i."CommunityRating",
|
||||
i."RunTimeTicks",
|
||||
i."ProductionYear",
|
||||
i."IsFolder",
|
||||
i."Type",
|
||||
i."Status",
|
||||
i."ImageTagsPrimary",
|
||||
i."ImageTagsBanner",
|
||||
i."ImageTagsLogo",
|
||||
i."ImageTagsThumb",
|
||||
i."BackdropImageTags",
|
||||
i."ParentId",
|
||||
i."PrimaryImageHash",
|
||||
i.archived,
|
||||
count(a."NowPlayingItemId") AS times_played,
|
||||
COALESCE(sum(a."PlaybackDuration"), 0::numeric) AS total_play_time
|
||||
FROM jf_library_items i
|
||||
LEFT JOIN jf_playback_activity a ON i."Id" = a."NowPlayingItemId"
|
||||
GROUP BY i."Id"
|
||||
ORDER BY (count(a."NowPlayingItemId")) DESC;`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@@ -343,7 +343,7 @@ router.post("/getSeasons", async (req, res) => {
|
||||
const { Id } = req.body;
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT s.*,i.archived, i."PrimaryImageHash" FROM jf_library_seasons s left join jf_library_items i on i."Id"=s."SeriesId" where "SeriesId"=$1`,
|
||||
`SELECT s.*,i.archived, i."PrimaryImageHash", (select count(e.*) "Episodes" from jf_library_episodes e where e."SeasonId"=s."Id") ,(select sum(ii."Size") "Size" from jf_library_episodes e join jf_item_info ii on ii."Id"=e."EpisodeId" where e."SeasonId"=s."Id") FROM jf_library_seasons s left join jf_library_items i on i."Id"=s."SeriesId" where "SeriesId"=$1`,
|
||||
[Id]
|
||||
);
|
||||
res.send(rows);
|
||||
@@ -369,17 +369,17 @@ router.post("/getItemDetails", async (req, res) => {
|
||||
try {
|
||||
const { Id } = req.body;
|
||||
// let query = `SELECT im."Name" "FileName",im.*,i.* FROM jf_library_items i left join jf_item_info im on i."Id" = im."Id" where i."Id"=$1`;
|
||||
let query = `SELECT im."Name" "FileName",im."Id",im."Path",im."Name",im."Bitrate",im."MediaStreams",im."Type", COALESCE(im."Size" ,(SELECT SUM(im."Size") FROM jf_library_seasons s JOIN jf_library_episodes e on s."Id"=e."SeasonId" JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."SeriesId" = i."Id")) "Size",i.* FROM jf_library_items i left join jf_item_info im on i."Id" = im."Id" where i."Id"=$1`;
|
||||
let query = `SELECT im."Name" "FileName",im."Id",im."Path",im."Name",im."Bitrate",im."MediaStreams",im."Type", COALESCE(im."Size" ,(SELECT SUM(im."Size") FROM jf_library_seasons s JOIN jf_library_episodes e on s."Id"=e."SeasonId" JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."SeriesId" = i."Id")) "Size",i.*, (select "Name" from jf_libraries l where l."Id"=i."ParentId") "LibraryName" FROM jf_library_items i left join jf_item_info im on i."Id" = im."Id" where i."Id"=$1`;
|
||||
|
||||
const { rows: items } = await db.query(query, [Id]);
|
||||
|
||||
if (items.length === 0) {
|
||||
// query = `SELECT im."Name" "FileName",im.*,s.*, s.archived, i."PrimaryImageHash" FROM jf_library_seasons s left join jf_item_info im on s."Id" = im."Id" left join jf_library_items i on i."Id"=s."SeriesId" where s."Id"=$1`;
|
||||
query = `SELECT s."Name", (SELECT SUM(im."Size") FROM jf_library_episodes e JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."Id" = e."SeasonId") AS "Size", s.*, i."PrimaryImageHash" FROM jf_library_seasons s LEFT JOIN jf_library_items i ON i."Id"=s."SeriesId" WHERE s."Id"=$1`;
|
||||
query = `SELECT s."Name", (SELECT SUM(im."Size") FROM jf_library_episodes e JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."Id" = e."SeasonId") AS "Size", s.*, i."PrimaryImageHash", i."ParentId",(select "Name" from jf_libraries l where l."Id"=i."ParentId") "LibraryName" FROM jf_library_seasons s LEFT JOIN jf_library_items i ON i."Id"=s."SeriesId" WHERE s."Id"=$1`;
|
||||
const { rows: seasons } = await db.query(query, [Id]);
|
||||
|
||||
if (seasons.length === 0) {
|
||||
query = `SELECT im."Name" "FileName",im.*,e.*, e.archived , i."PrimaryImageHash" FROM jf_library_episodes e join jf_item_info im on e."EpisodeId" = im."Id" left join jf_library_items i on i."Id"=e."SeriesId" where e."EpisodeId"=$1`;
|
||||
query = `SELECT im."Name" "FileName",im.*,e.*, e.archived , i."PrimaryImageHash", i."ParentId",(select "Name" from jf_libraries l where l."Id"=i."ParentId") "LibraryName" FROM jf_library_episodes e join jf_item_info im on e."EpisodeId" = im."Id" left join jf_library_items i on i."Id"=e."SeriesId" where e."EpisodeId"=$1`;
|
||||
const { rows: episodes } = await db.query(query, [Id]);
|
||||
|
||||
if (episodes.length !== 0) {
|
||||
|
||||
@@ -807,13 +807,34 @@ router.post("/fetchItem", async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const item_info = await Jellyfin.getItemInfo({ itemID: itemId });
|
||||
|
||||
let itemToInsert = await item.map(jf_library_items_mapping);
|
||||
let itemInfoToInsert = await item_info.map(jf_item_info_mapping);
|
||||
let insertTable = "jf_library_items";
|
||||
let itemToInsert = await item.map((item) => {
|
||||
if (item.Type === "Episode") {
|
||||
insertTable = "jf_library_episodes";
|
||||
return jf_library_episodes_mapping(item);
|
||||
} else if (item[0].Type === "Season") {
|
||||
insertTable = "jf_library_seasons";
|
||||
return jf_library_seasons_mapping(item);
|
||||
} else {
|
||||
return jf_library_items_mapping(item);
|
||||
}
|
||||
});
|
||||
let itemInfoToInsert = await item
|
||||
.map((item) =>
|
||||
item.MediaSources.map((iteminfo) => jf_item_info_mapping(iteminfo, item.Type == "Episode" ? "Episode" : "Item"))
|
||||
)
|
||||
.flat();
|
||||
|
||||
if (itemToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_library_items", itemToInsert, jf_library_items_columns);
|
||||
let result = await db.insertBulk(
|
||||
insertTable,
|
||||
itemToInsert,
|
||||
insertTable == "jf_library_items"
|
||||
? jf_library_items_columns
|
||||
: insertTable == "jf_library_seasons"
|
||||
? jf_library_seasons_columns
|
||||
: jf_library_episodes_columns
|
||||
);
|
||||
if (result.Result === "SUCCESS") {
|
||||
let result_info = await db.insertBulk("jf_item_info", itemInfoToInsert, jf_item_info_columns);
|
||||
if (result_info.Result === "SUCCESS") {
|
||||
@@ -831,7 +852,7 @@ router.post("/fetchItem", async (req, res) => {
|
||||
res.send("Unable to find Item");
|
||||
}
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
console.log(error);
|
||||
res.status(500);
|
||||
res.send(error);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Row, Col, Tabs, Tab, Button, ButtonGroup } from "react-bootstrap";
|
||||
|
||||
import ExternalLinkFillIcon from "remixicon-react/ExternalLinkFillIcon";
|
||||
import ArchiveDrawerFillIcon from "remixicon-react/ArchiveDrawerFillIcon";
|
||||
import ArrowLeftSLineIcon from "remixicon-react/ArrowLeftSLineIcon";
|
||||
|
||||
import GlobalStats from "./item-info/globalStats";
|
||||
import "../css/items/item-details.css";
|
||||
@@ -124,147 +125,189 @@ function ItemInfo() {
|
||||
<div>
|
||||
<div className="item-detail-container rounded-3" style={cardStyle}>
|
||||
<Row className="justify-content-center justify-content-md-start rounded-3 g-0 p-4" style={cardBgStyle}>
|
||||
<Col className="col-auto my-4 my-md-0 item-banner-image">
|
||||
{!data.archived && data.PrimaryImageHash && data.PrimaryImageHash != null && !loaded ? (
|
||||
<Blurhash
|
||||
hash={data.PrimaryImageHash}
|
||||
width={"200px"}
|
||||
height={"300px"}
|
||||
className="rounded-3 overflow-hidden"
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
) : null}
|
||||
{!data.archived ? (
|
||||
<img
|
||||
className="item-image"
|
||||
src={
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
(["Episode", "Season"].includes(data.Type) ? data.SeriesId : data.Id) +
|
||||
"&fillWidth=200&quality=90"
|
||||
}
|
||||
alt=""
|
||||
style={{
|
||||
display: loaded ? "block" : "none",
|
||||
}}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex flex-column justify-content-center align-items-center position-relative"
|
||||
style={{ height: "300px", width: "200px" }}
|
||||
>
|
||||
{data.PrimaryImageHash && data.PrimaryImageHash != null ? (
|
||||
<Blurhash
|
||||
hash={data.PrimaryImageHash}
|
||||
width={"200px"}
|
||||
height={"300px"}
|
||||
className="rounded-3 overflow-hidden position-absolute"
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
) : null}
|
||||
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
|
||||
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2" />
|
||||
<span>
|
||||
<Trans i18nKey="ARCHIVED" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<div className="item-details">
|
||||
<div className="d-flex">
|
||||
<h1 className="">
|
||||
{data.SeriesId ? (
|
||||
<Link to={`/libraries/item/${data.SeriesId}`}>{data.SeriesName || data.Name}</Link>
|
||||
) : (
|
||||
data.SeriesName || data.Name
|
||||
)}
|
||||
</h1>
|
||||
<Link
|
||||
className="px-2"
|
||||
to={config.hostUrl + "/web/index.html#!/details?id=" + (data.EpisodeId || data.Id)}
|
||||
title={i18next.t("ITEM_INFO.OPEN_IN_JELLYFIN")}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkFillIcon />
|
||||
{data.ParentId && (
|
||||
<Row className="mb-3">
|
||||
<Col className="col-auto pe-0">
|
||||
<Link to={`/libraries/${data.ParentId}`}>
|
||||
<Button className="d-inline-block" variant={"outline-primary"}>
|
||||
{data.LibraryName}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Col>
|
||||
{["Episode", "Season"].includes(data.Type) && (
|
||||
<>
|
||||
<Col className="col-auto px-0 d-flex justify-content-center align-items-center">
|
||||
<ArrowLeftSLineIcon />
|
||||
</Col>
|
||||
<Col className="col-auto px-0">
|
||||
<Link to={`/libraries/item/${data.SeriesId}`}>
|
||||
<Button className="d-inline-block" variant={"outline-primary"}>
|
||||
{data.SeriesName}
|
||||
</Button>
|
||||
</Link>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
{data.Type === "Episode" && (
|
||||
<>
|
||||
<Col className="col-auto px-0 d-flex justify-content-center align-items-center">
|
||||
<ArrowLeftSLineIcon />
|
||||
</Col>
|
||||
<Col className="col-auto px-0">
|
||||
<Link to={`/libraries/item/${data.SeasonId}`}>
|
||||
<Button className="d-inline-block" variant={"outline-primary"}>
|
||||
{data.SeasonName}
|
||||
</Button>
|
||||
</Link>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<div className="my-3">
|
||||
{data.Type === "Episode" ? (
|
||||
<p>
|
||||
<Link to={`/libraries/item/${data.SeasonId}`} className="fw-bold">
|
||||
{data.SeasonName}
|
||||
</Link>{" "}
|
||||
<Trans i18nKey="EPISODE" /> {data.IndexNumber} - {data.Name}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.Type === "Season" ? <p>{data.Name}</p> : <></>}
|
||||
{data.FileName ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
<Trans i18nKey="FILE_NAME" />: {data.FileName}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.Path ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
<Trans i18nKey="ITEM_INFO.FILE_PATH" />: {data.Path}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.RunTimeTicks ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
{data.Type === "Series" ? i18next.t("ITEM_INFO.AVERAGE_RUNTIME") : i18next.t("ITEM_INFO.RUNTIME")}:{" "}
|
||||
{ticksToTimeString(data.RunTimeTicks)}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.Size ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
<Trans i18nKey="ITEM_INFO.FILE_SIZE" />: {formatFileSize(data.Size)}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => setActiveTab("tabOverview")}
|
||||
active={activeTab === "tabOverview"}
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
<Row>
|
||||
<Col className="col-auto my-4 my-md-0 item-banner-image">
|
||||
{!data.archived && data.PrimaryImageHash && data.PrimaryImageHash != null && !loaded ? (
|
||||
<Blurhash
|
||||
hash={data.PrimaryImageHash}
|
||||
width={"200px"}
|
||||
height={"300px"}
|
||||
className="rounded-3 overflow-hidden"
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
) : null}
|
||||
{!data.archived ? (
|
||||
<img
|
||||
className="item-image"
|
||||
src={
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
(["Episode", "Season"].includes(data.Type) ? data.SeriesId : data.Id) +
|
||||
"&fillWidth=200&quality=90"
|
||||
}
|
||||
alt=""
|
||||
style={{
|
||||
display: loaded ? "block" : "none",
|
||||
}}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex flex-column justify-content-center align-items-center position-relative"
|
||||
style={{ height: "300px", width: "200px" }}
|
||||
>
|
||||
<Trans i18nKey="TAB_CONTROLS.OVERVIEW" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveTab("tabActivity")}
|
||||
active={activeTab === "tabActivity"}
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
>
|
||||
<Trans i18nKey="TAB_CONTROLS.ACTIVITY" />
|
||||
</Button>
|
||||
{data.PrimaryImageHash && data.PrimaryImageHash != null ? (
|
||||
<Blurhash
|
||||
hash={data.PrimaryImageHash}
|
||||
width={"200px"}
|
||||
height={"300px"}
|
||||
className="rounded-3 overflow-hidden position-absolute"
|
||||
style={{ display: "block" }}
|
||||
/>
|
||||
) : null}
|
||||
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
|
||||
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2" />
|
||||
<span>
|
||||
<Trans i18nKey="ARCHIVED" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{data.archived && (
|
||||
<Col>
|
||||
<div className="item-details">
|
||||
<div className="d-flex">
|
||||
<h1 className="">
|
||||
{data.SeriesId ? (
|
||||
<Link to={`/libraries/item/${data.SeriesId}`}>{data.SeriesName || data.Name}</Link>
|
||||
) : (
|
||||
data.SeriesName || data.Name
|
||||
)}
|
||||
</h1>
|
||||
<Link
|
||||
className="px-2"
|
||||
to={config.hostUrl + "/web/index.html#!/details?id=" + (data.EpisodeId || data.Id)}
|
||||
title={i18next.t("ITEM_INFO.OPEN_IN_JELLYFIN")}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkFillIcon />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="my-3">
|
||||
{data.Type === "Episode" ? (
|
||||
<p>
|
||||
<Link to={`/libraries/item/${data.SeasonId}`} className="fw-bold">
|
||||
{data.SeasonName}
|
||||
</Link>{" "}
|
||||
<Trans i18nKey="EPISODE" /> {data.IndexNumber} - {data.Name}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.Type === "Season" ? <p>{data.Name}</p> : <></>}
|
||||
{data.FileName ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
<Trans i18nKey="FILE_NAME" />: {data.FileName}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.Path ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
<Trans i18nKey="ITEM_INFO.FILE_PATH" />: {data.Path}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.RunTimeTicks ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
{data.Type === "Series" ? i18next.t("ITEM_INFO.AVERAGE_RUNTIME") : i18next.t("ITEM_INFO.RUNTIME")}:{" "}
|
||||
{ticksToTimeString(data.RunTimeTicks)}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.Size ? (
|
||||
<p style={{ color: "lightgrey" }} className="fst-italic fs-6">
|
||||
<Trans i18nKey="ITEM_INFO.FILE_SIZE" />: {formatFileSize(data.Size)}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => setActiveTab("tabOptions")}
|
||||
active={activeTab === "tabOptions"}
|
||||
onClick={() => setActiveTab("tabOverview")}
|
||||
active={activeTab === "tabOverview"}
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
>
|
||||
<Trans i18nKey="TAB_CONTROLS.OPTIONS" />
|
||||
<Trans i18nKey="TAB_CONTROLS.OVERVIEW" />
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Col>
|
||||
<Button
|
||||
onClick={() => setActiveTab("tabActivity")}
|
||||
active={activeTab === "tabActivity"}
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
>
|
||||
<Trans i18nKey="TAB_CONTROLS.ACTIVITY" />
|
||||
</Button>
|
||||
|
||||
{data.archived && (
|
||||
<Button
|
||||
onClick={() => setActiveTab("tabOptions")}
|
||||
active={activeTab === "tabOptions"}
|
||||
variant="outline-primary"
|
||||
type="button"
|
||||
>
|
||||
<Trans i18nKey="TAB_CONTROLS.OPTIONS" />
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,80 +1,107 @@
|
||||
import {useState} from "react";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { useState } from "react";
|
||||
import { Blurhash } from "react-blurhash";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ArchiveDrawerFillIcon from 'remixicon-react/ArchiveDrawerFillIcon';
|
||||
import { useParams } from "react-router-dom";
|
||||
import ArchiveDrawerFillIcon from "remixicon-react/ArchiveDrawerFillIcon";
|
||||
import "../../../css/lastplayed.css";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
function MoreItemCards(props) {
|
||||
const { Id } = useParams();
|
||||
const [loaded, setLoaded] = useState(props.data.archived);
|
||||
const [fallback, setFallback] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={props.data.Type==="Episode" ? "last-card episode-card" : "last-card"}>
|
||||
<Link 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"}>
|
||||
{((props.data.ImageBlurHashes && props.data.ImageBlurHashes!=null) || (props.data.PrimaryImageHash && props.data.PrimaryImageHash!=null) ) && !loaded ? <Blurhash hash={props.data.PrimaryImageHash || props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary] } width={'100%'} height={'100%'} className="rounded-top-3 overflow-hidden"/> : null}
|
||||
|
||||
|
||||
{!props.data.archived ?
|
||||
(fallback ?
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
"/proxy/Items/Images/Primary?id=" +
|
||||
Id +
|
||||
"&fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { 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"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => setFallback(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
)
|
||||
:
|
||||
<div className="d-flex flex-column justify-content-center align-items-center position-relative" style={{height: '100%'}}>
|
||||
{((props.data.ImageBlurHashes && props.data.ImageBlurHashes!=null) || (props.data.PrimaryImageHash && props.data.PrimaryImageHash!=null) )?
|
||||
<Blurhash hash={props.data.PrimaryImageHash || props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary] } width={'100%'} height={'100%'} className="rounded-top-3 overflow-hidden position-absolute"/>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
|
||||
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2"/>
|
||||
<span><Trans i18nKey="ARCHIVED"/></span>
|
||||
</div>
|
||||
</div>
|
||||
function formatFileSize(sizeInBytes) {
|
||||
const sizeInMB = sizeInBytes / 1048576; // 1 MB = 1048576 bytes
|
||||
if (sizeInMB < 1000) {
|
||||
return `${sizeInMB.toFixed(2)} MB`;
|
||||
} else {
|
||||
const sizeInGB = sizeInMB / 1024; // 1 GB = 1024 MB
|
||||
return `${sizeInGB.toFixed(2)} GB`;
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</Link>
|
||||
return (
|
||||
<div className={props.data.Type === "Episode" ? "last-card episode-card" : "last-card"}>
|
||||
<Link
|
||||
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"}>
|
||||
{((props.data.ImageBlurHashes && props.data.ImageBlurHashes != null) ||
|
||||
(props.data.PrimaryImageHash && props.data.PrimaryImageHash != null)) &&
|
||||
!loaded ? (
|
||||
<Blurhash
|
||||
hash={props.data.PrimaryImageHash || props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]}
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
className="rounded-top-3 overflow-hidden"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!props.data.archived ? (
|
||||
fallback ? (
|
||||
<img
|
||||
src={`${"/proxy/Items/Images/Primary?id=" + Id + "&fillHeight=320&fillWidth=213&quality=50"}`}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { 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"
|
||||
}`}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => setFallback(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: "none" }}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className="d-flex flex-column justify-content-center align-items-center position-relative"
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
{(props.data.ImageBlurHashes && props.data.ImageBlurHashes != null) ||
|
||||
(props.data.PrimaryImageHash && props.data.PrimaryImageHash != null) ? (
|
||||
<Blurhash
|
||||
hash={props.data.PrimaryImageHash || props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]}
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
className="rounded-top-3 overflow-hidden position-absolute"
|
||||
/>
|
||||
) : null}
|
||||
<div className="d-flex flex-column justify-content-center align-items-center position-absolute">
|
||||
<ArchiveDrawerFillIcon className="w-100 h-100 mb-2" />
|
||||
<span>
|
||||
<Trans i18nKey="ARCHIVED" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{props.data.Size && <div className="size-tag">{props.data.Size ? formatFileSize(props.data.Size) : ""}</div>}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="last-item-details">
|
||||
{props.data.Type === "Season" && (
|
||||
<div className="last-last-played">
|
||||
{props.data.Episodes} <Trans i18nKey="EPISODES" />
|
||||
</div>
|
||||
)}
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
{props.data.Type==="Episode"?
|
||||
<div className="last-item-name"> S{props.data.ParentIndexNumber || 0} - E{props.data.IndexNumber || 0}</div>
|
||||
:
|
||||
|
||||
{props.data.Type === "Episode" ? (
|
||||
<div className="last-item-name">
|
||||
S{props.data.ParentIndexNumber || 0} - E{props.data.IndexNumber || 0}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
}
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ function LibraryItems(props) {
|
||||
<option value="Title"><Trans i18nKey="TITLE"/></option>
|
||||
<option value="Views"><Trans i18nKey="VIEWS"/></option>
|
||||
<option value="WatchTime"><Trans i18nKey="WATCH_TIME"/></option>
|
||||
<option value="Size"><Trans i18nKey="SETTINGS_PAGE.SIZE"/></option>
|
||||
</FormSelect>
|
||||
|
||||
<Button className="my-md-3 rounded-0 rounded-end" onClick={()=>setSortAsc(!sortAsc)}>
|
||||
@@ -140,6 +141,14 @@ function LibraryItems(props) {
|
||||
}
|
||||
return b.times_played-a.times_played;
|
||||
}
|
||||
else if(sortOrder==='Size')
|
||||
{
|
||||
if(sortAsc)
|
||||
{
|
||||
return a.Size-b.Size;
|
||||
}
|
||||
return b.Size-a.Size;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(sortAsc)
|
||||
|
||||
@@ -1,127 +1,114 @@
|
||||
@import './variables.module.css';
|
||||
@import "./variables.module.css";
|
||||
.last-played-container {
|
||||
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
min-height: 300px;
|
||||
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.last-played-container::-webkit-scrollbar {
|
||||
width: 5px; /* set scrollbar width */
|
||||
}
|
||||
|
||||
.last-played-container::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* set track color */
|
||||
}
|
||||
|
||||
.last-played-container::-webkit-scrollbar-thumb {
|
||||
background-color: #8888884d; /* set thumb color */
|
||||
border-radius: 5px; /* round corners */
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last-played-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #88888883; /* set thumb color */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last-card
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 20px;
|
||||
|
||||
width: 150px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-color);
|
||||
|
||||
width: 5px; /* set scrollbar width */
|
||||
}
|
||||
|
||||
.last-played-container::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* set track color */
|
||||
}
|
||||
|
||||
.episode{
|
||||
.last-played-container::-webkit-scrollbar-thumb {
|
||||
background-color: #8888884d; /* set thumb color */
|
||||
border-radius: 5px; /* round corners */
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.last-played-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #88888883; /* set thumb color */
|
||||
}
|
||||
|
||||
.last-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 20px;
|
||||
|
||||
width: 150px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.episode {
|
||||
width: 220px !important;
|
||||
height: 128px !important;
|
||||
|
||||
}
|
||||
|
||||
.episode-card{
|
||||
.episode-card {
|
||||
width: 220px !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.last-card-banner {
|
||||
width: 150px;
|
||||
height: 220px;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
position: relative;
|
||||
}
|
||||
.size-tag {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(90, 45, 165, 0.8);
|
||||
padding: 2px;
|
||||
border-radius: 8px 0px 0px 0px;
|
||||
font-size: 0.9rem;
|
||||
/* opacity: 0.8; */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last-card-banner:hover {
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.last-card-banner img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
|
||||
}
|
||||
|
||||
.last-item-details {
|
||||
width: 90%;
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.last-item-details a{
|
||||
.last-item-details a {
|
||||
text-decoration: none !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.last-item-details a:hover{
|
||||
color: var(--secondary-color) !important;
|
||||
.last-item-details a:hover {
|
||||
color: var(--secondary-color) !important;
|
||||
}
|
||||
|
||||
|
||||
.last-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.last-item-episode {
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color:gray;
|
||||
font-size: 0.8em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: gray;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.number{
|
||||
margin-inline: 10px;
|
||||
padding-bottom: 10px;
|
||||
.number {
|
||||
margin-inline: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last-last-played{
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 5px;
|
||||
color: var(--secondary-color);
|
||||
.last-last-played {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 5px;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user