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

@ -211,7 +211,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
}
queryBuilder := repo.db.squirrel.
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp",
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.category", "r.season", "r.episode", "r.year", "r.resolution", "r.source", "r.codec", "r.container", "r.release_group", "r.timestamp",
"ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp").
Column(sq.Alias(countQuery, "page_total")).
From("release r").
@ -245,17 +245,22 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
var rls domain.Release
var ras domain.ReleaseActionStatus
var rlsindexer, rlsfilter, infoUrl, downloadUrl sql.NullString
var rlsindexer, rlsfilter, infoUrl, downloadUrl, codec sql.NullString
var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
var rasRejections []sql.NullString
var rasTimestamp sql.NullTime
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Category, &rls.Season, &rls.Episode, &rls.Year, &rls.Resolution, &rls.Source, &codec, &rls.Container, &rls.Group, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
return res, 0, 0, errors.Wrap(err, "error scanning row")
}
//for _, codec := range codecs {
// rls.Codec = append(rls.Codec, codec.String)
//
//}
ras.ID = rasId.Int64
ras.Status = domain.ReleasePushStatus(rasStatus.String)
ras.Action = rasAction.String
@ -291,6 +296,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
rls.ActionStatus = make([]domain.ReleaseActionStatus, 0)
rls.InfoURL = infoUrl.String
rls.DownloadURL = downloadUrl.String
rls.Codec = strings.Split(codec.String, ",")
// only add ActionStatus if it's not empty
if ras.ID > 0 {

View file

@ -59,7 +59,7 @@ type Release struct {
TorrentTmpFile string `json:"-"`
TorrentDataRawBytes []byte `json:"-"`
TorrentHash string `json:"-"`
TorrentName string `json:"torrent_name"` // full release name
TorrentName string `json:"name"` // full release name
Size uint64 `json:"size"`
Title string `json:"title"` // Parsed title
Description string `json:"-"`
@ -538,32 +538,6 @@ func (r *Release) HasMagnetUri() bool {
return r.MagnetURI != ""
}
type magnetRoundTripper struct{}
func (rt *magnetRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
if r.URL.Scheme == "magnet" {
responseBody := r.URL.String()
respReader := io.NopCloser(strings.NewReader(responseBody))
resp := &http.Response{
Status: http.StatusText(http.StatusOK),
StatusCode: http.StatusOK,
Body: respReader,
ContentLength: int64(len(responseBody)),
Header: map[string][]string{
"Content-Type": {"text/plain"},
"Location": {responseBody},
},
Proto: "HTTP/2.0",
ProtoMajor: 2,
}
return resp, nil
}
return http.DefaultTransport.RoundTrip(r)
}
func (r *Release) ResolveMagnetUri(ctx context.Context) error {
if r.MagnetURI == "" {
return nil

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",