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:
Thegan Govender
2024-02-27 12:16:36 +02:00
parent dca2060cc3
commit bb123c5dcd
7 changed files with 441 additions and 281 deletions

View File

@@ -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);
}
};

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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)

View File

@@ -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);
}