Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .storybook/mocks/_fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {TorrentContentPriority} from '@shared/types/TorrentContent';
import type {TorrentPeer} from '@shared/types/TorrentPeer';
import type {TorrentTracker} from '@shared/types/TorrentTracker';
import {TorrentTrackerType} from '@shared/types/TorrentTracker';
import {calculateTorrentHealth} from '@shared/util/torrentHealth';

/**
* Time constants in milliseconds
Expand Down Expand Up @@ -123,6 +124,10 @@ export function generateMockTorrent(overrides: Partial<TorrentProperties> = {}):
const nameList = MOCK_TORRENT_NAMES[category];
const nameIndex = Math.floor(Math.random() * nameList.length);

// Health calculation based on seeds
const seedsConnected = overrides.seedsConnected ?? 5;
const seedsTotal = overrides.seedsTotal ?? 20;

const baseData: TorrentProperties = {
hash,
name: overrides.name || nameList[nameIndex],
Expand All @@ -137,6 +142,7 @@ export function generateMockTorrent(overrides: Partial<TorrentProperties> = {}):
downRate: SPEED.MB_PER_SEC,
downTotal: Math.floor(downTotal),
eta: percentComplete < 100 ? Math.floor((sizeBytes - bytesDone) / SPEED.MB_PER_SEC) : -1,
health: overrides.health ?? calculateTorrentHealth(seedsConnected, seedsTotal),
isPrivate: false,
isInitialSeeding: false,
isSequential: false,
Expand All @@ -146,8 +152,8 @@ export function generateMockTorrent(overrides: Partial<TorrentProperties> = {}):
percentComplete: Math.floor(percentComplete * 100) / 100,
priority: TorrentPriority.NORMAL,
ratio: Math.round(ratio * 1000) / 1000,
seedsConnected: 5,
seedsTotal: 20,
seedsConnected,
seedsTotal,
sizeBytes: Math.floor(sizeBytes),
selectedSizeBytes: Math.floor(sizeBytes),
tags: [],
Expand Down Expand Up @@ -376,6 +382,7 @@ export const MOCK_FLOOD_SETTINGS: FloodSettings = {
downRate: 100,
upRate: 100,
eta: 100,
health: 80,
ratio: 80,
sizeBytes: 100,
selectedSizeBytes: 100,
Expand Down Expand Up @@ -700,6 +707,8 @@ export function createNewTorrentTemplate(
},
): TorrentProperties {
const start = options?.start ?? false;
const seedsConnected = start ? 2 : 0;
const seedsTotal = 10;
return {
hash,
name: options?.name ?? `New Torrent ${new Date().toLocaleTimeString()}`,
Expand All @@ -714,6 +723,7 @@ export function createNewTorrentTemplate(
downRate: start ? MOCK_TORRENT_SPEEDS.DOWNLOADING : 0,
downTotal: 0,
eta: start ? 7200 : -1,
health: calculateTorrentHealth(seedsConnected, seedsTotal),
isPrivate: false,
isInitialSeeding: false,
isSequential: false,
Expand All @@ -723,8 +733,8 @@ export function createNewTorrentTemplate(
percentComplete: 0,
priority: TorrentPriority.NORMAL,
ratio: 0,
seedsConnected: start ? 2 : 0,
seedsTotal: 10,
seedsConnected,
seedsTotal,
sizeBytes: options?.sizeBytes ?? 5 * SIZE.GB,
selectedSizeBytes: options?.sizeBytes ?? 5 * SIZE.GB,
tags: options?.tags ?? [],
Expand Down
39 changes: 39 additions & 0 deletions client/src/javascript/components/general/HealthIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import classnames from 'classnames';
import {FC} from 'react';
import {observer} from 'mobx-react-lite';
import {useLingui} from '@lingui/react';

import {TorrentHealth} from '@shared/types/Torrent';

interface HealthIndicatorProps {
health: TorrentHealth;
}

const HEALTH_LABELS: Record<TorrentHealth, string> = {
[TorrentHealth.CRITICAL]: 'torrents.properties.health.critical',
[TorrentHealth.POOR]: 'torrents.properties.health.poor',
[TorrentHealth.FAIR]: 'torrents.properties.health.fair',
[TorrentHealth.GOOD]: 'torrents.properties.health.good',
[TorrentHealth.EXCELLENT]: 'torrents.properties.health.excellent',
};

const HEALTH_COLORS: Record<TorrentHealth, string> = {
[TorrentHealth.CRITICAL]: 'health-indicator--critical',
[TorrentHealth.POOR]: 'health-indicator--poor',
[TorrentHealth.FAIR]: 'health-indicator--fair',
[TorrentHealth.GOOD]: 'health-indicator--good',
[TorrentHealth.EXCELLENT]: 'health-indicator--excellent',
};

const HealthIndicator: FC<HealthIndicatorProps> = observer(({health}: HealthIndicatorProps) => {
const {i18n} = useLingui();
const label = i18n._(HEALTH_LABELS[health]);

return (
<div className={classnames('health-indicator', HEALTH_COLORS[health])}>
<span className="health-indicator__label">{label}</span>
</div>
);
});

export default HealthIndicator;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DownloadThick,
FolderClosedSolid,
Hash,
Health,
Lock,
TrackerMessage,
Peers,
Expand All @@ -25,6 +26,7 @@ import {
UploadThick,
} from '@client/ui/icons';
import Duration from '@client/components/general/Duration';
import HealthIndicator from '@client/components/general/HealthIndicator';
import ProgressBar from '@client/components/general/ProgressBar';
import Size from '@client/components/general/Size';
import {formatDate, formatNumber} from '@client/util/format';
Expand All @@ -42,6 +44,7 @@ const ICONS: Partial<Record<TorrentListColumn, JSX.Element>> = {
downRate: <DownloadThick />,
directory: <FolderClosedSolid />,
hash: <Hash />,
health: <Health />,
dateActive: <Clock />,
dateAdded: <Calendar />,
dateCreated: <CalendarCreated />,
Expand Down Expand Up @@ -157,6 +160,8 @@ const DefaultTorrentListCellContent: FC<TorrentListCellContentProps> = observer(
return <TrackersCell trackers={torrent[column]} />;
case 'eta':
return <ETACell eta={torrent[column]} />;
case 'health':
return <HealthIndicator health={torrent[column]} />;
case 'seeds':
return <PeerCell peersConnected={torrent.seedsConnected} totalPeers={torrent.seedsTotal} />;
case 'peers':
Expand Down
1 change: 1 addition & 0 deletions client/src/javascript/constants/TorrentListColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const TorrentListColumns = {
downRate: 'torrents.properties.download.speed',
downTotal: 'torrents.properties.download.total',
eta: 'torrents.properties.eta',
health: 'torrents.properties.health',
name: 'torrents.properties.name',
peers: 'torrents.properties.peers',
percentComplete: 'torrents.properties.percentage',
Expand Down
6 changes: 6 additions & 0 deletions client/src/javascript/i18n/strings/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,12 @@
"torrents.properties.eta": "ETA",
"torrents.properties.free.disk.space": "Free Disk Space",
"torrents.properties.hash": "Hash",
"torrents.properties.health": "Health",
"torrents.properties.health.critical": "Critical",
"torrents.properties.health.poor": "Poor",
"torrents.properties.health.fair": "Fair",
"torrents.properties.health.good": "Good",
"torrents.properties.health.excellent": "Excellent",
"torrents.properties.ignore.schedule": "Ignore Scheduler",
"torrents.properties.is.private": "Private",
"torrents.properties.name": "Name",
Expand Down
3 changes: 2 additions & 1 deletion client/src/javascript/routes/Overview.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import TorrentStore from '@client/stores/TorrentStore';
import AuthActions from '@client/actions/AuthActions';
import {createMockMouseEvent, TEST_TIMEOUTS} from '../test-utils/storybook-helpers';
import type {TorrentProperties} from '@shared/types/Torrent';
import {TorrentPriority} from '@shared/types/Torrent';
import {TorrentPriority, TorrentHealth} from '@shared/types/Torrent';
import {TorrentStatus} from '@shared/constants/torrentStatusMap';

import MockStateStore from '@client/storybook-mocks/MockStateStore';
Expand Down Expand Up @@ -107,6 +107,7 @@ const createTorrent = (hash: string, overrides: Partial<TorrentProperties>): Tor
downRate: 0,
downTotal: 0,
eta: -1,
health: TorrentHealth.CRITICAL,
isPrivate: false,
isInitialSeeding: false,
isSequential: false,
Expand Down
14 changes: 14 additions & 0 deletions client/src/javascript/ui/icons/Health.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import classnames from 'classnames';
import {FC, memo} from 'react';

interface HealthProps {
className?: string;
}

const Health: FC<HealthProps> = memo(({className}: HealthProps = {}) => (
<svg className={classnames('icon', 'icon--health', className)} viewBox="0 0 60 60">
<path d="M30,55.5c-1.1,0-2.2-0.4-3-1.2L7.3,34.5c-6.4-6.4-6.4-16.9,0-23.3c3.1-3.1,7.3-4.8,11.7-4.8s8.5,1.7,11.7,4.8 l2.3,2.3l2.3-2.3c3.1-3.1,7.3-4.8,11.7-4.8s8.5,1.7,11.7,4.8c6.4,6.4,6.4,16.9,0,23.3L33,54.3C32.2,55.1,31.1,55.5,30,55.5z" />
</svg>
));

export default Health;
1 change: 1 addition & 0 deletions client/src/javascript/ui/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export {default as FolderClosedSolid} from './FolderClosedSolid';
export {default as FolderOpenOutlined} from './FolderOpenOutlined';
export {default as FolderOpenSolid} from './FolderOpenSolid';
export {default as Hash} from './Hash';
export {default as Health} from './Health';
export {default as Inactive} from './Inactive';
export {default as InfinityIcon} from './InfinityIcon';
export {default as Information} from './Information';
Expand Down
67 changes: 67 additions & 0 deletions client/src/sass/components/_health-indicator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@use '../tools/colors';
@use '../tools/themes';

.health-indicator {
align-items: center;
display: flex;

&__label {
font-size: 0.8rem;
font-weight: 500;
}

// Critical - No seeders (red)
&--critical {
.health-indicator__label {
color: colors.$red;
}

.torrent--is-selected & .health-indicator__label {
color: rgba(colors.$white, 0.9);
}
}

// Poor - 1-2 seeders (orange)
&--poor {
.health-indicator__label {
color: #f59e0b;
}

.torrent--is-selected & .health-indicator__label {
color: rgba(colors.$white, 0.9);
}
}

// Fair - 3-5 seeders (yellow)
&--fair {
.health-indicator__label {
color: #eab308;
}

.torrent--is-selected & .health-indicator__label {
color: rgba(colors.$white, 0.9);
}
}

// Good - 6-10 seeders (light green)
&--good {
.health-indicator__label {
color: #84cc16;
}

.torrent--is-selected & .health-indicator__label {
color: rgba(colors.$white, 0.9);
}
}

// Excellent - 10+ seeders (green)
&--excellent {
.health-indicator__label {
color: colors.$green;
}

.torrent--is-selected & .health-indicator__label {
color: rgba(colors.$white, 0.9);
}
}
}
1 change: 1 addition & 0 deletions client/src/sass/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
@include meta.load-css('components/dropzone');
@include meta.load-css('components/duration');
@include meta.load-css('components/floating-action');
@include meta.load-css('components/health-indicator');
@include meta.load-css('components/icons');
@include meta.load-css('components/interactive-list');
@include meta.load-css('components/loading-indicator');
Expand Down
5 changes: 5 additions & 0 deletions server/services/Deluge/clientGatewayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {TorrentTracker} from '@shared/types/TorrentTracker';
import {TorrentTrackerType} from '@shared/types/TorrentTracker';
import type {TransferSummary} from '@shared/types/TransferData';

import {calculateTorrentHealth} from '../../../shared/util/torrentHealth';
import {fetchUrls} from '../../util/fetchUtil';
import BaseClientGatewayService, {type ClientGatewayService} from '../clientGatewayService';
import ClientRequestManager from './clientRequestManager';
Expand Down Expand Up @@ -344,6 +345,10 @@ class DelugeClientGatewayService extends BaseClientGatewayService implements Cli
downTotal: status.total_payload_download,
eta: status.eta === 0 ? -1 : status.eta,
hash: hash.toUpperCase(),
health: calculateTorrentHealth(
status.num_seeds,
status.total_seeds < 0 ? 0 : status.total_seeds,
),
isPrivate: status.private,
isInitialSeeding: status.super_seeding,
isSequential: status.sequential_download,
Expand Down
2 changes: 2 additions & 0 deletions server/services/Neptune/clientGatewayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {TransferSummary} from '@shared/types/TransferData';
import {NeptuneConnectionError, NeptuneHTTPError, TorrentState} from '@trim21/neptune';

import {TorrentContentPriority} from '../../../shared/types/TorrentContent';
import {calculateTorrentHealth} from '../../../shared/util/torrentHealth';
import {fetchUrls} from '../../util/fetchUtil';
import {getDomainsFromURLs} from '../../util/torrentPropertiesUtil';
import ClientGatewayService from '../clientGatewayService';
Expand Down Expand Up @@ -303,6 +304,7 @@ class NeptuneClientGatewayService extends ClientGatewayService {
? (torrent.selected_size - torrent.completed) / torrent.download_rate
: -1,
hash: torrent.hash.toUpperCase(),
health: calculateTorrentHealth(torrent.connected_seeding, torrent.total_seeding),
isPrivate: torrent.private,
isInitialSeeding: false,
isSequential: false,
Expand Down
2 changes: 2 additions & 0 deletions server/services/Transmission/clientGatewayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {TransferSummary} from '@shared/types/TransferData';
import {TorrentPriority} from '../../../shared/types/Torrent';
import {TorrentContentPriority} from '../../../shared/types/TorrentContent';
import {TorrentTrackerType} from '../../../shared/types/TorrentTracker';
import {calculateTorrentHealth} from '../../../shared/util/torrentHealth';
import {fetchUrls} from '../../util/fetchUtil';
import {getDomainsFromURLs} from '../../util/torrentPropertiesUtil';
import BaseClientGatewayService, {type ClientGatewayService} from '../clientGatewayService';
Expand Down Expand Up @@ -417,6 +418,7 @@ class TransmissionClientGatewayService extends BaseClientGatewayService implemen
upRate: torrent.rateUpload,
upTotal: torrent.uploadedEver,
eta: torrent.eta > 0 ? torrent.eta : -1,
health: calculateTorrentHealth(torrent.peersSendingToUs, torrent.peersSendingToUs),
isPrivate: torrent.isPrivate,
isInitialSeeding: false,
isSequential: false,
Expand Down
2 changes: 2 additions & 0 deletions server/services/qBittorrent/clientGatewayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import parseTorrent from 'parse-torrent';
import {TorrentPriority} from '../../../shared/types/Torrent';
import {TorrentContentPriority} from '../../../shared/types/TorrentContent';
import {TorrentTrackerType} from '../../../shared/types/TorrentTracker';
import {calculateTorrentHealth} from '../../../shared/util/torrentHealth';
import {fetchUrls} from '../../util/fetchUtil';
import {getDomainsFromURLs} from '../../util/torrentPropertiesUtil';
import BaseClientGatewayService, {type ClientGatewayService} from '../clientGatewayService';
Expand Down Expand Up @@ -444,6 +445,7 @@ class QBittorrentClientGatewayService extends BaseClientGatewayService implement
// For non-seeding states, hide ETA when dlspeed is 0 to avoid showing stale values.
eta: info.eta >= 8640000 || (!isSeeding && info.dlspeed === 0) ? -1 : info.eta,
hash: info.hash.toUpperCase(),
health: calculateTorrentHealth(info.num_seeds, info.num_complete),
isPrivate,
isInitialSeeding: info.super_seeding,
isSequential: info.seq_dl,
Expand Down
2 changes: 2 additions & 0 deletions server/services/rTorrent/clientGatewayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {TorrentContentPriority} from '@shared/types/TorrentContent';
import type {TorrentPeer} from '@shared/types/TorrentPeer';
import type {TorrentTracker} from '@shared/types/TorrentTracker';
import type {TransferSummary} from '@shared/types/TransferData';
import {calculateTorrentHealth} from '@shared/util/torrentHealth';
import {move} from 'fs-extra';
import sanitize from 'sanitize-filename';

Expand Down Expand Up @@ -865,6 +866,7 @@ class RTorrentClientGatewayService extends BaseClientGatewayService implements C
downTotal: response.downTotal,
eta: getTorrentETAFromProperties(effectiveSizeBytes, response.downRate, response.bytesDone),
hash: response.hash,
health: calculateTorrentHealth(response.seedsConnected, response.seedsTotal),
isPrivate: response.isPrivate,
isInitialSeeding: response.isInitialSeeding,
isSequential: response.isSequential,
Expand Down
2 changes: 2 additions & 0 deletions shared/schema/FloodSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const torrentListColumnsSchema = z.array(torrentListColumnItemSchema).default([
{id: 'upTotal', visible: true},
{id: 'upRate', visible: true},
{id: 'eta', visible: true},
{id: 'health', visible: true},
{id: 'ratio', visible: true},
{id: 'sizeBytes', visible: true},
{id: 'selectedSizeBytes', visible: false},
Expand Down Expand Up @@ -99,6 +100,7 @@ const torrentListColumnWidthsSchema = torrentListColumnWidthsSchemaBase.default(
upTotal: 100,
upRate: 100,
eta: 100,
health: 80,
ratio: 100,
sizeBytes: 100,
selectedSizeBytes: 100,
Expand Down
Loading
Loading