feat(releases): show details in list view (#1337)

* feat(releases): show details in list view

* fix(releases): activitytable columns type

* fix(releases): incognito mode

* feat(releases): move details button

* do we wanna truncate?

* fix(web): release column width at full size

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
This commit is contained in:
ze0s 2024-01-02 21:53:38 +01:00 committed by GitHub
parent 7eaf499d66
commit 9992675971
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 91 deletions

View file

@ -7,33 +7,110 @@ import * as React from "react";
import { toast } from "react-hot-toast";
import { formatDistanceToNowStrict } from "date-fns";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CellProps } from "react-table";
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
import { ArrowDownTrayIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { ExternalLink } from "../ExternalLink";
import { ClockIcon, XMarkIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
import {
ClockIcon,
XMarkIcon,
NoSymbolIcon,
ArrowDownTrayIcon,
ArrowTopRightOnSquareIcon, DocumentTextIcon
} from "@heroicons/react/24/outline";
import { APIClient } from "@api/APIClient";
import { classNames, simplifyDate } from "@utils";
import {classNames, humanFileSize, simplifyDate} from "@utils";
import { filterKeys } from "@screens/filters/List";
import Toast from "@components/notifications/Toast";
import { RingResizeSpinner } from "@components/Icons";
import { Tooltip } from "@components/tooltips/Tooltip";
interface CellProps {
value: string;
}
interface LinksCellProps {
value: Release;
}
export const AgeCell = ({ value }: CellProps) => (
<div className="text-sm text-gray-500" title={simplifyDate(value)}>
{formatDistanceToNowStrict(new Date(value), { addSuffix: false })}
export const NameCell = (props: CellProps<Release>) => (
<div
className={classNames(
"flex justify-between items-center py-2 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
"max-w-[82px] sm:max-w-[160px] md:max-w-[290px] lg:max-w-[535px] xl:max-w-[775px]"
)}
>
<div className="flex flex-col truncate">
<span className="truncate">
{String(props.cell.value)}
</span>
<div className="text-xs truncate">
<span className="text-xs text-gray-500 dark:text-gray-400">Category:</span> {props.row.original.category}
<span
className="text-xs text-gray-500 dark:text-gray-400"> Size:</span> {humanFileSize(props.row.original.size)}
<span
className="text-xs text-gray-500 dark:text-gray-400"> Misc:</span> {`${props.row.original.resolution} ${props.row.original.source} ${props.row.original.codec ?? ""} ${props.row.original.container}`}
</div>
</div>
</div>
);
export const IndexerCell = ({ value }: CellProps) => (
export const LinksCell = (props: CellProps<Release>) => {
return (
<div className="flex space-x-2 text-blue-400 dark:text-blue-500">
<div>
<Tooltip
requiresClick
label={<DocumentTextIcon
className="h-5 w-5 cursor-pointer text-blue-400 hover:text-blue-500 dark:text-blue-500 dark:hover:text-blue-600"
aria-hidden={true}/>}
title="Details"
>
<div className="mb-1">
<CellLine title="Release">{props.row.original.name}</CellLine>
<CellLine title="Indexer">{props.row.original.indexer}</CellLine>
<CellLine title="Protocol">{props.row.original.protocol}</CellLine>
<CellLine title="Implementation">{props.row.original.implementation}</CellLine>
<CellLine title="Category">{props.row.original.category}</CellLine>
<CellLine title="Uploader">{props.row.original.uploader}</CellLine>
<CellLine title="Size">{humanFileSize(props.row.original.size)}</CellLine>
<CellLine title="Title">{props.row.original.title}</CellLine>
{props.row.original.year > 0 && <CellLine title="Year">{props.row.original.year.toString()}</CellLine>}
{props.row.original.season > 0 &&
<CellLine title="Season">{props.row.original.season.toString()}</CellLine>}
{props.row.original.episode > 0 &&
<CellLine title="Episode">{props.row.original.episode.toString()}</CellLine>}
<CellLine title="Resolution">{props.row.original.resolution}</CellLine>
<CellLine title="Source">{props.row.original.source}</CellLine>
<CellLine title="Codec">{props.row.original.codec}</CellLine>
<CellLine title="HDR">{props.row.original.hdr}</CellLine>
<CellLine title="Group">{props.row.original.group}</CellLine>
<CellLine title="Container">{props.row.original.container}</CellLine>
<CellLine title="Origin">{props.row.original.origin}</CellLine>
</div>
</Tooltip>
</div>
{props.row.original.download_url && (
<ExternalLink href={props.row.original.download_url}>
<ArrowDownTrayIcon
title="Download torrent file"
className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600"
aria-hidden="true"
/>
</ExternalLink>
)}
{props.row.original.info_url && (
<ExternalLink href={props.row.original.info_url}>
<ArrowTopRightOnSquareIcon
title="Visit torrentinfo url"
className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600"
aria-hidden="true"
/>
</ExternalLink>
)}
</div>
);
};
export const AgeCell = ({value}: CellProps<Release>) => (
<div className="text-sm text-gray-500" title={simplifyDate(value)}>
{formatDistanceToNowStrict(new Date(value), {addSuffix: false})}
</div>
);
export const IndexerCell = ({value}: CellProps<Release>) => (
<div
className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
@ -52,7 +129,7 @@ export const IndexerCell = ({ value }: CellProps) => (
</div>
);
export const TitleCell = ({ value }: CellProps) => (
export const TitleCell = ({value}: CellProps<Release>) => (
<div
className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
@ -250,19 +327,3 @@ export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
</div>
);
export const LinksCell = ({ value }: LinksCellProps) => {
return (
<div className="flex space-x-2 text-blue-400 dark:text-blue-500">
{value.download_url && (
<ExternalLink href={value.download_url}>
<ArrowDownTrayIcon title="Download torrent file" className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600" aria-hidden="true" />
</ExternalLink>
)}
{value.info_url && (
<ExternalLink href={value.info_url}>
<ArrowTopRightOnSquareIcon title="Visit torrentinfo url" className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600" aria-hidden="true" />
</ExternalLink>
)}
</div>
);
};

View file

@ -4,4 +4,4 @@
*/
export { Button, PageButton } from "./Buttons";
export { AgeCell, IndexerCell, TitleCell, ReleaseStatusCell, LinksCell } from "./Cells";
export { AgeCell, IndexerCell, NameCell, TitleCell, ReleaseStatusCell, LinksCell } from "./Cells";

View file

@ -168,7 +168,7 @@ export const ActivityTable = () => {
},
{
Header: "Release",
accessor: "torrent_name",
accessor: "name",
Cell: DataTable.TitleCell
},
{
@ -183,7 +183,7 @@ export const ActivityTable = () => {
Filter: SelectColumnFilter,
filter: "includes"
}
], []);
] as Column[], []);
const { isLoading, data } = useSuspenseQuery({
queryKey: ["dash_recent_releases"],
@ -213,7 +213,7 @@ export const ActivityTable = () => {
const randomNames = RandomLinuxIsos(data.data.length);
const newData: Release[] = data.data.map((item, index) => ({
...item,
torrent_name: `${randomNames[index]}.iso`,
name: `${randomNames[index]}.iso`,
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
}));
setModifiedData(newData);

View file

@ -6,7 +6,7 @@
import React, { useState } from "react";
import { useLocation } from "react-router-dom";
import { useSuspenseQuery } from "@tanstack/react-query";
import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
import { Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
import {
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
@ -23,8 +23,6 @@ import * as Icons from "@components/Icons";
import * as DataTable from "@components/data-table";
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters";
import { classNames } from "@utils";
import { Tooltip } from "@components/tooltips/Tooltip";
export const releaseKeys = {
all: ["releases"] as const,
@ -94,27 +92,8 @@ export const ReleaseTable = () => {
},
{
Header: "Release",
accessor: "torrent_name",
Cell: (props: CellProps<Release>) => {
return (
<div
className={classNames(
"flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
"max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"
)}
>
<Tooltip
requiresClick
label={props.cell.value}
maxWidth="max-w-[90vw]"
>
<span className="whitespace-pre-wrap break-words">
{String(props.cell.value)}
</span>
</Tooltip>
</div>
);
},
accessor: "name",
Cell: DataTable.NameCell,
Filter: SearchColumnFilter
},
{
@ -156,7 +135,7 @@ export const ReleaseTable = () => {
const randomNames = RandomLinuxIsos(data.data.length);
const newData: Release[] = data.data.map((item, index) => ({
...item,
torrent_name: `${randomNames[index]}.iso`,
name: `${randomNames[index]}.iso`,
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
}));
setModifiedData(newData);

View file

@ -10,11 +10,27 @@ interface Release {
indexer: string;
filter: string;
protocol: string;
implementation: string;
name: string;
title: string;
size: number;
raw: string;
info_url: string;
download_url: string;
category: string;
group: string;
season: number;
episode: number;
year: number;
resolution: string;
codec: string;
source: string;
container: string;
hdr: string;
uploader: string;
origin: string;
// freeleech: boolean;
// freeleech_percent:number;
timestamp: Date
action_status: ReleaseActionStatus[]
}

View file

@ -84,6 +84,34 @@ export const get = <T> (obj: T, path: string|Array<any>, defValue?: string) => {
return result === undefined ? defValue : result;
};
const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte']
const BYTES_PER_KB = 1000
/**
* Format bytes as human-readable text.
*
* @param sizeBytes Number of bytes.
*
* @return Formatted string.
*/
export function humanFileSize(sizeBytes: number | bigint): string {
let size = Math.abs(Number(sizeBytes))
let u = 0
while (size >= BYTES_PER_KB && u < UNITS.length - 1) {
size /= BYTES_PER_KB
++u
}
return new Intl.NumberFormat([], {
style: 'unit',
unit: UNITS[u],
unitDisplay: 'short',
maximumFractionDigits: 1,
}).format(size)
}
export const RandomLinuxIsos = (count: number) => {
const linuxIsos = [
"ubuntu-20.04.4-lts-focal-fossa-desktop-amd64-secure-boot",