fix(torrent): distinguish connected and listed peers

- label live connected peer telemetry separately from listed peer details
- report count mismatches and keep connected values accented
- synchronize bindings, locales, accessibility, and regression coverage
This commit is contained in:
NimBold
2026-08-21 23:04:34 +03:30
parent c385c38556
commit f724616cde
13 changed files with 103 additions and 51 deletions
+2 -2
View File
@@ -340,9 +340,9 @@ pub struct TorrentPeer {
#[ts(export, export_to = "../../src/bindings/")]
pub struct TorrentPeerDiagnostics {
#[ts(type = "number")]
pub total_peers: u32,
pub listed_peers: u32,
#[ts(type = "number")]
pub total_seeders: u32,
pub listed_seeders: u32,
pub peers: Vec<TorrentPeer>,
pub truncated: bool,
}
+8 -8
View File
@@ -6978,8 +6978,8 @@ pub(crate) fn parse_torrent_availability(
}
struct TorrentPeerCounts {
total_peers: u32,
total_seeders: u32,
listed_peers: u32,
listed_seeders: u32,
}
fn torrent_peer_counts_from_array(
@@ -7001,8 +7001,8 @@ fn torrent_peer_counts_from_array(
}
}
Ok(TorrentPeerCounts {
total_peers: u32::try_from(peers.len()).unwrap_or(u32::MAX),
total_seeders,
listed_peers: u32::try_from(peers.len()).unwrap_or(u32::MAX),
listed_seeders: total_seeders,
})
}
@@ -7032,8 +7032,8 @@ pub(crate) fn parse_torrent_peer_diagnostics(
}
Ok(crate::ipc::TorrentPeerDiagnostics {
total_peers: summary.total_peers,
total_seeders: summary.total_seeders,
listed_peers: summary.listed_peers,
listed_seeders: summary.listed_seeders,
peers: sanitized,
truncated: peers.len() > MAX_TORRENT_PEER_DIAGNOSTICS,
})
@@ -10035,8 +10035,8 @@ mod tests {
}));
let diagnostics = parse_torrent_peer_diagnostics(serde_json::Value::Array(result)).unwrap();
assert_eq!(diagnostics.total_peers, (MAX_TORRENT_PEER_DIAGNOSTICS + 2) as u32);
assert_eq!(diagnostics.total_seeders, 2);
assert_eq!(diagnostics.listed_peers, (MAX_TORRENT_PEER_DIAGNOSTICS + 2) as u32);
assert_eq!(diagnostics.listed_seeders, 2);
assert_eq!(diagnostics.peers.len(), MAX_TORRENT_PEER_DIAGNOSTICS);
assert!(diagnostics.truncated);
assert_eq!(diagnostics.peers[0].ip.as_deref(), Some("192.0.2.10"));
+1 -1
View File
@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TorrentPeer } from "./TorrentPeer";
export type TorrentPeerDiagnostics = { totalPeers: number, totalSeeders: number, peers: Array<TorrentPeer>, truncated: boolean, };
export type TorrentPeerDiagnostics = { listedPeers: number, listedSeeders: number, peers: Array<TorrentPeer>, truncated: boolean, };
+21 -7
View File
@@ -47,6 +47,7 @@ import {
formatPropertiesDiagnosticCount,
getPropertiesAvailabilityDiagnosticState,
getPropertiesPeerDiagnosticState,
hasTorrentPeerCountDifference,
hasLiveTorrentPeerWithoutDetails,
} from '../utils/propertiesDiagnostics';
import { shouldOfferPropertiesUrlExpansion, shouldResetPropertiesUrlExpansion } from '../utils/propertiesUrl';
@@ -513,7 +514,7 @@ export const PropertiesWindowApp = () => {
setPeers(peerResult.value);
setPeerDetailsUnavailable(hasLiveTorrentPeerWithoutDetails(
snapshotRef.current?.torrentConnectedPeers,
peerResult.value.totalPeers,
peerResult.value.listedPeers,
));
} else if (isExpectedPropertiesDiagnosticUnavailable(peerResult.reason)) {
setPeerDetailsUnavailable(hasLiveTorrentPeerWithoutDetails(
@@ -1174,9 +1175,9 @@ export const PropertiesWindowApp = () => {
seeders: seedersValue,
})}
>
<span className="properties-torrent-peer-count-primary">{peersValue}</span>
<span className="properties-torrent-peer-count-connected">{peersValue}</span>
<span aria-hidden="true"> / </span>
<span>{seedersValue}</span>
<span className="properties-torrent-peer-count-connected">{seedersValue}</span>
</strong>;
})()
: <strong>{connectionPresentation.value}</strong>;
@@ -1446,15 +1447,15 @@ export const PropertiesWindowApp = () => {
<div className="properties-diagnostic-heading">
<div className="min-w-0">
<span className="properties-diagnostic-label">{t($ => $.properties.torrentPeerDiagnostics)}</span>
<p className="properties-diagnostic-value" data-value-state={peerDetailsNotice ? 'unavailable' : peerDiagnosticState} role="status">
<p className="properties-diagnostic-value properties-diagnostic-value--secondary" data-value-state={peerDetailsNotice ? 'unavailable' : peerDiagnosticState} role="status">
{peerDetailsNotice
? t($ => $.properties.torrentPeerDetailsUnavailable, {
connected: formatPropertiesDiagnosticCount(snapshot.torrentConnectedPeers ?? 0, snapshot.appearance.locale),
})
: peers
? t($ => $.properties.torrentPeerCount, {
total: formatPropertiesDiagnosticCount(peers.totalPeers, snapshot.appearance.locale),
seeders: formatPropertiesDiagnosticCount(peers.totalSeeders, snapshot.appearance.locale),
listed: formatPropertiesDiagnosticCount(peers.listedPeers, snapshot.appearance.locale),
seeders: formatPropertiesDiagnosticCount(peers.listedSeeders, snapshot.appearance.locale),
})
: diagnosticsLoading
? t($ => $.properties.torrentPeerDiagnosticsLoading)
@@ -1464,8 +1465,21 @@ export const PropertiesWindowApp = () => {
<button type="button" className="app-button px-3 text-xs" aria-busy={diagnosticsLoading || diagnosticsRefreshing} onClick={() => downloadId && void refreshDiagnostics('peers', downloadId, true)}><RefreshCw size={14} className={diagnosticsLoading || diagnosticsRefreshing ? 'animate-spin motion-reduce:animate-none' : undefined} />{t($ => $.properties.torrentPeerDiagnosticsRefresh)}</button>
</div>
<p className="properties-diagnostic-hint">{t($ => $.properties.torrentPeerDiagnosticsHint)}</p>
{peers && hasTorrentPeerCountDifference(
snapshot.torrentConnectedPeers,
snapshot.torrentConnectedSeeders,
peers.listedPeers,
peers.listedSeeders,
) && <p className="properties-diagnostic-detail" aria-live="polite">
{t($ => $.properties.torrentPeerCountDifference, {
connectedPeers: formatPropertiesDiagnosticCount(snapshot.torrentConnectedPeers ?? Number.NaN, snapshot.appearance.locale),
connectedSeeders: formatPropertiesDiagnosticCount(snapshot.torrentConnectedSeeders ?? Number.NaN, snapshot.appearance.locale),
listedPeers: formatPropertiesDiagnosticCount(peers.listedPeers, snapshot.appearance.locale),
listedSeeders: formatPropertiesDiagnosticCount(peers.listedSeeders, snapshot.appearance.locale),
})}
</p>}
{peerDiagnosticPhase === 'stale' && <p className="properties-diagnostic-detail">{t($ => $.properties.torrentPeerDiagnosticsStale)}</p>}
{peers?.truncated && <p className="properties-diagnostic-detail">{t($ => $.properties.torrentPeerShowing, { shown: peers.peers.length, total: peers.totalPeers })}</p>}
{peers?.truncated && <p className="properties-diagnostic-detail">{t($ => $.properties.torrentPeerShowing, { shown: peers.peers.length, total: peers.listedPeers })}</p>}
</div>
<div className="properties-diagnostic-card" data-diagnostic-phase={availabilityDiagnosticPhase}>
<div className="min-w-0">
+6 -5
View File
@@ -303,13 +303,13 @@ const common = {
torrentPeerSpeedLimit: 'Peer speed threshold',
torrentMaxPeersInvalid: 'Torrent maximum peers must be an integer from 0 to 1000',
torrentPeerSpeedLimitInvalid: 'Torrent peer speed threshold must be greater than zero',
torrentPeerDiagnostics: 'Torrent peer diagnostics',
torrentPeerDiagnostics: 'Torrent peer details',
torrentPeerDiagnosticsRefresh: 'Refresh',
torrentPeerDiagnosticsLoading: 'Loading peer diagnostics…',
torrentPeerDiagnosticsStale: 'Showing the last validated result; refresh to check again.',
torrentPeerDiagnosticsUnavailable: 'Peer diagnostics are available while this Torrent is active or paused.',
torrentPeerDiagnosticsFailed: 'Could not read Torrent peer diagnostics.',
torrentPeerDiagnosticsHint: 'Validated peer addresses and ports are shown ephemerally; peer IDs and raw bitfields are never retained.',
torrentPeerDiagnosticsHint: 'Validated peer addresses and ports are shown ephemerally; peer IDs and raw bitfields are never retained. The connected count above is live Torrent status; this table is a separate peer-list response and may contain a different number.',
torrentPeerAddress: 'Peer address',
torrentPeerId: 'Peer ID',
torrentFileProgress: 'Torrent file progress',
@@ -346,22 +346,23 @@ const common = {
torrentWebSeedsAdd: 'Add web seed',
torrentWebSeedsRemove: 'Remove web seed',
torrentWebSeedsInvalid: 'Each web-seed row needs a valid Torrent file and an HTTP(S) base URI without credentials or fragments.',
torrentPeerCount: '{{total}} peers — {{seeders}} seeders',
torrentPeerCount: '{{listed}} listed peers — {{seeders}} listed seeders',
torrentPeerDownload: 'Download',
torrentPeerUpload: 'Upload',
torrentPeerSeeder: 'Seeder',
torrentPeerAmChoking: 'Firelink choking',
torrentPeerChoking: 'Peer choking',
torrentPeerShowing: 'Showing {{shown}} of {{total}} peers.',
torrentPeerShowing: 'Showing {{shown}} of {{total}} listed peers.',
torrentStatistics: 'Torrent statistics',
torrentUploaded: 'Uploaded',
torrentRatio: 'Ratio',
torrentSeededDuration: 'Seeded',
torrentSeedTimeHint: 'How long this Torrent may continue seeding after its files finish downloading. Leave blank to use the default.',
torrentConnectedPeers: 'Peers',
torrentPeersSeeders: 'Peers / Seeders',
torrentPeersSeeders: 'Connected peers / connected seeders',
torrentConnectedPeerMetric: '{{peers}} connected peers / {{seeders}} connected seeders',
torrentPeerDetailsUnavailable: '{{connected}} connected peers reported, but peer details are not available yet.',
torrentPeerCountDifference: 'Connected status: {{connectedPeers}} peers / {{connectedSeeders}} seeders. The peer-details response lists {{listedPeers}} peers / {{listedSeeders}} seeders.',
torrentSeeders: 'Seeders',
torrentUploadSpeed: 'Upload speed',
seconds: 'seconds',
+5 -4
View File
@@ -303,13 +303,13 @@ const fa = {
torrentPeerSpeedLimit: 'آستانه سرعت همتا',
torrentMaxPeersInvalid: 'حداکثر همتاهای تورنت باید عددی صحیح بین ۰ و ۱۰۰۰ باشد',
torrentPeerSpeedLimitInvalid: 'آستانه سرعت همتای تورنت باید بیشتر از صفر باشد',
torrentPeerDiagnostics: 'اطلاعات همتاهای تورنت',
torrentPeerDiagnostics: 'جزئیات همتاهای تورنت',
torrentPeerDiagnosticsRefresh: 'تازه‌سازی',
torrentPeerDiagnosticsLoading: 'در حال دریافت اطلاعات همتاها…',
torrentPeerDiagnosticsStale: 'آخرین نتیجهٔ معتبر نمایش داده می‌شود؛ برای بررسی دوباره تازه‌سازی کنید.',
torrentPeerDiagnosticsUnavailable: 'اطلاعات همتاها هنگام فعال یا متوقف بودن تورنت در دسترس است.',
torrentPeerDiagnosticsFailed: 'خواندن اطلاعات همتاهای تورنت ممکن نیست.',
torrentPeerDiagnosticsHint: 'آدرس و پورت معتبر همتاها فقط به‌صورت موقت نمایش داده می‌شوند؛ شناسه همتا و بیت‌فیلد خام هرگز نگه‌داری نمی‌شود.',
torrentPeerDiagnosticsHint: 'آدرس و پورت معتبر همتاها فقط به‌صورت موقت نمایش داده می‌شوند؛ شناسه همتا و بیت‌فیلد خام هرگز نگه‌داری نمی‌شود. تعداد متصلِ بالا وضعیت زندهٔ تورنت است؛ این جدول پاسخ جداگانه‌ای از فهرست همتاهاست و ممکن است تعداد متفاوتی داشته باشد.',
torrentPeerAddress: 'نشانی همتا',
torrentPeerId: 'شناسهٔ همتا',
torrentFileProgress: 'پیشرفت فایل‌های تورنت',
@@ -346,13 +346,13 @@ const fa = {
torrentWebSeedsAdd: 'افزودن وب‌سید',
torrentWebSeedsRemove: 'حذف وب‌سید',
torrentWebSeedsInvalid: 'هر ردیف وب‌سید باید فایل معتبر تورنت و نشانی پایهٔ HTTP(S) بدون اطلاعات ورود یا fragment داشته باشد.',
torrentPeerCount: '{{total}} همتا — {{seeders}} سید',
torrentPeerCount: '{{listed}} همتای فهرست‌شده — {{seeders}} سید فهرست‌شده',
torrentPeerDownload: 'دریافت',
torrentPeerUpload: 'آپلود',
torrentPeerSeeder: 'سید',
torrentPeerAmChoking: 'محدودسازی از طرف Firelink',
torrentPeerChoking: 'محدودسازی از طرف همتا',
torrentPeerShowing: 'نمایش {{shown}} همتا از {{total}} همتا.',
torrentPeerShowing: 'نمایش {{shown}} همتا از {{total}} همتای فهرست‌شده.',
torrentStatistics: 'آمار تورنت',
torrentUploaded: 'آپلودشده',
torrentRatio: 'نسبت',
@@ -362,6 +362,7 @@ const fa = {
torrentPeersSeeders: 'همتاهای متصل / سیدهای متصل',
torrentConnectedPeerMetric: '{{peers}} همتای متصل / {{seeders}} سید متصل',
torrentPeerDetailsUnavailable: '{{connected}} همتای متصل گزارش شده، اما جزئیات همتاها هنوز در دسترس نیست.',
torrentPeerCountDifference: 'وضعیت اتصال: {{connectedPeers}} همتا / {{connectedSeeders}} سید. پاسخ جزئیات همتاها {{listedPeers}} همتا / {{listedSeeders}} سید را فهرست کرده است.',
torrentSeeders: 'سیدها',
torrentUploadSpeed: 'سرعت آپلود',
seconds: 'ثانیه',
+6 -5
View File
@@ -303,13 +303,13 @@ const he = {
torrentPeerSpeedLimit: 'סף מהירות עמיתים',
torrentMaxPeersInvalid: 'מספר העמיתים המרבי חייב להיות מספר שלם בין 0 ל-1000',
torrentPeerSpeedLimitInvalid: 'סף מהירות העמיתים חייב להיות גדול מאפס',
torrentPeerDiagnostics: 'אבחון עמיתי טורנט',
torrentPeerDiagnostics: 'פרטי עמיתי טורנט',
torrentPeerDiagnosticsRefresh: 'רענון',
torrentPeerDiagnosticsLoading: 'טוען אבחון עמיתים…',
torrentPeerDiagnosticsStale: 'מוצגת התוצאה המאומתת האחרונה; רענן כדי לבדוק שוב.',
torrentPeerDiagnosticsUnavailable: 'אבחון עמיתים זמין כשהטורנט פעיל או מושהה.',
torrentPeerDiagnosticsFailed: 'לא ניתן לקרוא את אבחון עמיתי הטורנט.',
torrentPeerDiagnosticsHint: 'כתובות ויציאות מאומתות של עמיתים מוצגות באופן זמני בלבד; מזהי עמיתים ושדות סיביות גולמיים לעולם אינם נשמרים.',
torrentPeerDiagnosticsHint: 'כתובות ויציאות מאומתות של עמיתים מוצגות באופן זמני בלבד; מזהי עמיתים ושדות סיביות גולמיים לעולם אינם נשמרים. המספר המחובר למעלה הוא מצב הטורנט בזמן אמת; הטבלה הזו היא תגובה נפרדת של רשימת העמיתים וייתכן שתציג מספר אחר.',
torrentPeerAddress: 'כתובת עמית',
torrentPeerId: 'מזהה עמית',
torrentFileProgress: 'התקדמות קובצי הטורנט',
@@ -346,22 +346,23 @@ const he = {
torrentWebSeedsAdd: 'הוסף זריעת Web',
torrentWebSeedsRemove: 'הסר זריעת Web',
torrentWebSeedsInvalid: 'כל שורת זריעת Web צריכה קובץ טורנט תקין וכתובת בסיס HTTP(S) ללא פרטי התחברות או fragment.',
torrentPeerCount: '{{total}} עמיתים — {{seeders}} משתפים',
torrentPeerCount: '{{listed}} עמיתים ברשימה — {{seeders}} משתפים ברשימה',
torrentPeerDownload: 'הורדה',
torrentPeerUpload: 'העלאה',
torrentPeerSeeder: 'משתף',
torrentPeerAmChoking: 'Firelink מגביל',
torrentPeerChoking: 'העמית מגביל',
torrentPeerShowing: 'מוצגים {{shown}} מתוך {{total}} עמיתים.',
torrentPeerShowing: 'מוצגים {{shown}} מתוך {{total}} עמיתים ברשימה.',
torrentStatistics: 'סטטיסטיקות טורנט',
torrentUploaded: 'הועלה',
torrentRatio: 'יחס',
torrentSeededDuration: 'משך שיתוף',
torrentSeedTimeHint: 'משך הזמן שבו הטורנט ימשיך לשתף לאחר סיום ההורדה. השאר ריק כדי להשתמש בברירת המחדל.',
torrentConnectedPeers: 'עמיתים',
torrentPeersSeeders: 'עמיתים / משתפים',
torrentPeersSeeders: 'עמיתים מחוברים / משתפים מחוברים',
torrentConnectedPeerMetric: '{{peers}} עמיתים מחוברים / {{seeders}} משתפים מחוברים',
torrentPeerDetailsUnavailable: 'דווחו {{connected}} עמיתים מחוברים, אך פרטי העמיתים עדיין אינם זמינים.',
torrentPeerCountDifference: 'מחוברים: {{connectedPeers}} עמיתים / {{connectedSeeders}} משתפים. תגובת פרטי העמיתים מציגה {{listedPeers}} עמיתים / {{listedSeeders}} משתפים.',
torrentSeeders: 'משתפים',
torrentUploadSpeed: 'מהירות העלאה',
seconds: 'שניות',
+6 -5
View File
@@ -303,13 +303,13 @@ const ru = {
torrentPeerSpeedLimit: 'Порог скорости пиров',
torrentMaxPeersInvalid: 'Максимум пиров должен быть целым числом от 0 до 1000',
torrentPeerSpeedLimitInvalid: 'Порог скорости пиров должен быть больше нуля',
torrentPeerDiagnostics: 'Диагностика пиров торрента',
torrentPeerDiagnostics: 'Сведения о пирах торрента',
torrentPeerDiagnosticsRefresh: 'Обновить',
torrentPeerDiagnosticsLoading: 'Загрузка диагностики пиров…',
torrentPeerDiagnosticsStale: 'Показан последний проверенный результат; обновите, чтобы проверить снова.',
torrentPeerDiagnosticsUnavailable: 'Диагностика пиров доступна, пока торрент активен или приостановлен.',
torrentPeerDiagnosticsFailed: 'Не удалось получить диагностику пиров торрента.',
torrentPeerDiagnosticsHint: 'Проверенные адреса и порты пиров показываются только временно; идентификаторы пиров и исходные битовые поля никогда не сохраняются.',
torrentPeerDiagnosticsHint: 'Проверенные адреса и порты пиров показываются только временно; идентификаторы пиров и исходные битовые поля никогда не сохраняются. Число подключений выше — текущее состояние Torrent; эта таблица получена отдельным ответом со списком пиров и может содержать другое число.',
torrentPeerAddress: 'Адрес пира',
torrentPeerId: 'ID пира',
torrentFileProgress: 'Прогресс файлов торрента',
@@ -346,22 +346,23 @@ const ru = {
torrentWebSeedsAdd: 'Добавить веб-сид',
torrentWebSeedsRemove: 'Удалить веб-сид',
torrentWebSeedsInvalid: 'В каждой строке веб-сида нужны допустимый файл торрента и базовый HTTP(S)-адрес без учётных данных или фрагмента.',
torrentPeerCount: '{{total}} пиров — {{seeders}} сидеров',
torrentPeerCount: '{{listed}} пиров в списке — {{seeders}} сидов в списке',
torrentPeerDownload: 'Загрузка',
torrentPeerUpload: 'Отдача',
torrentPeerSeeder: 'Сидер',
torrentPeerAmChoking: 'Firelink ограничивает',
torrentPeerChoking: 'Пир ограничивает',
torrentPeerShowing: 'Показано {{shown}} из {{total}} пиров.',
torrentPeerShowing: 'Показано {{shown}} из {{total}} пиров в списке.',
torrentStatistics: 'Статистика торрента',
torrentUploaded: 'Отдано',
torrentRatio: 'Коэффициент',
torrentSeededDuration: 'Время раздачи',
torrentSeedTimeHint: 'Как долго Torrent продолжает раздачу после завершения загрузки. Оставьте пустым для значения по умолчанию.',
torrentConnectedPeers: 'Пиры',
torrentPeersSeeders: иры / Сиды',
torrentPeersSeeders: одключённые пиры / подключённые сиды',
torrentConnectedPeerMetric: '{{peers}} подключённых пиров / {{seeders}} подключённых сидов',
torrentPeerDetailsUnavailable: 'Подключённых пиров: {{connected}}, но сведения о них пока недоступны.',
torrentPeerCountDifference: 'Подключено: {{connectedPeers}} пиров / {{connectedSeeders}} сидов. В ответе со сведениями о пирах указано: {{listedPeers}} пиров / {{listedSeeders}} сидов.',
torrentSeeders: 'Сиды',
torrentUploadSpeed: 'Скорость отдачи',
seconds: 'секунд',
+6 -5
View File
@@ -303,13 +303,13 @@ const uk = {
torrentPeerSpeedLimit: 'Поріг швидкості пірів',
torrentMaxPeersInvalid: 'Максимум пірів має бути цілим числом від 0 до 1000',
torrentPeerSpeedLimitInvalid: 'Поріг швидкості пірів має бути більшим за нуль',
torrentPeerDiagnostics: 'Діагностика пірів торрента',
torrentPeerDiagnostics: 'Відомості про піри торрента',
torrentPeerDiagnosticsRefresh: 'Оновити',
torrentPeerDiagnosticsLoading: 'Завантаження діагностики пірів…',
torrentPeerDiagnosticsStale: 'Показано останній перевірений результат; оновіть, щоб перевірити ще раз.',
torrentPeerDiagnosticsUnavailable: 'Діагностика пірів доступна, поки торрент активний або призупинений.',
torrentPeerDiagnosticsFailed: 'Не вдалося отримати діагностику пірів торрента.',
torrentPeerDiagnosticsHint: 'Перевірені адреси й порти пірів показуються лише тимчасово; ідентифікатори пірів і сирі бітові поля ніколи не зберігаються.',
torrentPeerDiagnosticsHint: 'Перевірені адреси й порти пірів показуються лише тимчасово; ідентифікатори пірів і сирі бітові поля ніколи не зберігаються. Кількість підключень вище — це поточний стан торента; ця таблиця отримана окремою відповіддю зі списком пірів і може містити інше число.',
torrentPeerAddress: 'Адреса піра',
torrentPeerId: 'ID піра',
torrentFileProgress: 'Прогрес файлів торрента',
@@ -346,22 +346,23 @@ const uk = {
torrentWebSeedsAdd: 'Додати вебсід',
torrentWebSeedsRemove: 'Видалити вебсід',
torrentWebSeedsInvalid: 'Кожен рядок вебсіду має містити дійсний файл торента й базову HTTP(S)-адресу без облікових даних або фрагмента.',
torrentPeerCount: '{{total}} пірів — {{seeders}} сідів',
torrentPeerCount: '{{listed}} пірів у списку — {{seeders}} сідів у списку',
torrentPeerDownload: 'Завантаження',
torrentPeerUpload: 'Віддача',
torrentPeerSeeder: 'Сідер',
torrentPeerAmChoking: 'Firelink обмежує',
torrentPeerChoking: 'Пір обмежує',
torrentPeerShowing: 'Показано {{shown}} із {{total}} пірів.',
torrentPeerShowing: 'Показано {{shown}} із {{total}} пірів у списку.',
torrentStatistics: 'Статистика торента',
torrentUploaded: 'Віддано',
torrentRatio: 'Коефіцієнт',
torrentSeededDuration: 'Час роздачі',
torrentSeedTimeHint: 'Як довго Torrent продовжує роздачу після завершення завантаження. Залиште порожнім для значення за замовчуванням.',
torrentConnectedPeers: 'Піри',
torrentPeersSeeders: 'Піри / Сіди',
torrentPeersSeeders: 'Підключені піри / підключені сіди',
torrentConnectedPeerMetric: '{{peers}} підключених пірів / {{seeders}} підключених сідів',
torrentPeerDetailsUnavailable: 'Підключених пірів: {{connected}}, але відомості про них поки недоступні.',
torrentPeerCountDifference: 'Підключено: {{connectedPeers}} пірів / {{connectedSeeders}} сідів. У відповіді з відомостями про піри зазначено: {{listedPeers}} пірів / {{listedSeeders}} сідів.',
torrentSeeders: 'Сіди',
torrentUploadSpeed: 'Швидкість віддачі',
seconds: 'секунд',
+6 -5
View File
@@ -303,13 +303,13 @@ const zhCN = {
torrentPeerSpeedLimit: '对等节点速度阈值',
torrentMaxPeersInvalid: 'Torrent 最大对等节点数必须是 0 到 1000 之间的整数',
torrentPeerSpeedLimitInvalid: '对等节点速度阈值必须大于零',
torrentPeerDiagnostics: 'Torrent 对等节点诊断',
torrentPeerDiagnostics: 'Torrent 对等节点详情',
torrentPeerDiagnosticsRefresh: '刷新',
torrentPeerDiagnosticsLoading: '正在加载对等节点诊断…',
torrentPeerDiagnosticsStale: '当前显示最近一次验证的结果;刷新以再次检查。',
torrentPeerDiagnosticsUnavailable: 'Torrent 活跃或暂停时可查看对等节点诊断。',
torrentPeerDiagnosticsFailed: '无法读取 Torrent 对等节点诊断。',
torrentPeerDiagnosticsHint: '仅临时显示经过验证的对等端地址和端口;永不保留对等端 ID 或原始位域。',
torrentPeerDiagnosticsHint: '仅临时显示经过验证的对等端地址和端口;永不保留对等端 ID 或原始位域。上方的连接数是 Torrent 的实时状态;此表来自单独的对等节点列表响应,数量可能不同。',
torrentPeerAddress: '节点地址',
torrentPeerId: '节点 ID',
torrentFileProgress: 'Torrent 文件进度',
@@ -346,22 +346,23 @@ const zhCN = {
torrentWebSeedsAdd: '添加 Web 做种',
torrentWebSeedsRemove: '移除 Web 做种',
torrentWebSeedsInvalid: '每行 Web 做种都需要有效的 Torrent 文件和不含凭据或片段的 HTTP(S) 基础地址。',
torrentPeerCount: '{{total}} 个节点 — {{seeders}} 个做种节点',
torrentPeerCount: '{{listed}} 个列表节点 — {{seeders}} 个列表做种节点',
torrentPeerDownload: '下载',
torrentPeerUpload: '上传',
torrentPeerSeeder: '做种',
torrentPeerAmChoking: 'Firelink 限制中',
torrentPeerChoking: '对等节点限制中',
torrentPeerShowing: '显示 {{total}} 个节点中的 {{shown}} 个。',
torrentPeerShowing: '显示列表中的 {{shown}}/{{total}} 个节点。',
torrentStatistics: '种子统计',
torrentUploaded: '已上传',
torrentRatio: '分享率',
torrentSeededDuration: '做种时长',
torrentSeedTimeHint: '文件下载完成后继续做种的时长。留空以使用默认值。',
torrentConnectedPeers: '连接数',
torrentPeersSeeders: '节点 / 做种',
torrentPeersSeeders: '已连接节点 / 已连接做种节点',
torrentConnectedPeerMetric: '{{peers}} 个已连接节点 / {{seeders}} 个已连接做种节点',
torrentPeerDetailsUnavailable: '检测到 {{connected}} 个已连接节点,但其详细信息暂时不可用。',
torrentPeerCountDifference: '连接状态:{{connectedPeers}} 个节点 / {{connectedSeeders}} 个做种节点。对等节点详情响应列出 {{listedPeers}} 个节点 / {{listedSeeders}} 个做种节点。',
torrentSeeders: '种子数',
torrentUploadSpeed: '上传速度',
seconds: '秒',
+5 -1
View File
@@ -868,7 +868,7 @@ html[data-list-density="relaxed"] {
text-overflow: clip;
}
.properties-metric-card .properties-torrent-peer-count-primary {
.properties-metric-card .properties-torrent-peer-count-connected {
color: hsl(var(--accent-color));
}
@@ -1064,6 +1064,10 @@ html[data-list-density="relaxed"] {
line-height: 1.25;
}
.properties-diagnostic-value--secondary {
color: hsl(var(--text-primary));
}
.properties-diagnostic-hint {
max-width: 60ch;
margin-top: 6px;
+12 -2
View File
@@ -4,12 +4,13 @@ import {
formatPropertiesDiagnosticCount,
getPropertiesAvailabilityDiagnosticState,
getPropertiesPeerDiagnosticState,
hasTorrentPeerCountDifference,
hasLiveTorrentPeerWithoutDetails,
} from './propertiesDiagnostics';
const emptyPeerDiagnostics = {
totalPeers: 0,
totalSeeders: 0,
listedPeers: 0,
listedSeeders: 0,
peers: [],
truncated: false,
};
@@ -29,6 +30,15 @@ describe('Properties peer diagnostics presentation state', () => {
expect(formatPropertiesDiagnosticCount(Number.MAX_SAFE_INTEGER + 1, 'en-US')).toBe('—');
});
it('identifies when the connected telemetry differs from the listed peer response', () => {
expect(hasTorrentPeerCountDifference(38, 2, 5, 2)).toBe(true);
expect(hasTorrentPeerCountDifference(5, 2, 5, 2)).toBe(false);
expect(hasTorrentPeerCountDifference(undefined, 2, 5, 2)).toBe(false);
expect(hasTorrentPeerCountDifference(38, 2, 5, 1)).toBe(true);
expect(hasTorrentPeerCountDifference(Number.NaN, 2, 5, 2)).toBe(false);
expect(hasTorrentPeerCountDifference(38, 2, Number.POSITIVE_INFINITY, 2)).toBe(false);
});
it('keeps a genuine empty response live instead of treating it as unavailable', () => {
expect(getPropertiesPeerDiagnosticState(emptyPeerDiagnostics, false, 'idle')).toBe('live');
});
+19 -1
View File
@@ -5,11 +5,29 @@ import type { PropertiesDiagnosticPhase } from '../propertiesBridge';
export type PropertiesDiagnosticValueState = 'live' | 'loading' | 'stale' | 'error' | 'unavailable';
const isValidDiagnosticCount = (value: number | undefined): value is number =>
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
export const formatPropertiesDiagnosticCount = (value: number, locale: string): string => {
if (!Number.isSafeInteger(value) || value < 0) return '—';
if (!isValidDiagnosticCount(value)) return '—';
return new Intl.NumberFormat(resolveAppLocale(locale)).format(value);
};
export const hasTorrentPeerCountDifference = (
connectedPeers: number | undefined,
connectedSeeders: number | undefined,
listedPeers: number,
listedSeeders: number,
): boolean => (
isValidDiagnosticCount(connectedPeers)
&& isValidDiagnosticCount(listedPeers)
&& connectedPeers !== listedPeers
) || (
isValidDiagnosticCount(connectedSeeders)
&& isValidDiagnosticCount(listedSeeders)
&& connectedSeeders !== listedSeeders
);
export const hasLiveTorrentPeerWithoutDetails = (
connectedPeers: number | undefined,
detailedPeers: number,