From ff67dd0f9728b9df67a9c53d83d54779f116e800 Mon Sep 17 00:00:00 2001 From: Timmy Date: Fri, 30 Oct 2026 23:30:19 +0800 Subject: [PATCH] Add share ratio colouring and table row separators Colour the share ratio column based on its value. A ratio below 1 is shown in red, a ratio between 1 and the seed ratio limit in orange and a ratio at or above the limit in green. The limit is taken from the server side seed ratio limit, the ratio at which the daemon stops seeding, so it matches the requirement of the tracker site without extra configuration. A negative ratio means the ratio is unknown or infinite and is left uncoloured. Add an optional horizontal separator between the table rows. It makes wide tables easier to follow across. Both options are enabled by default and can be toggled from the layout menu. --- src/components/server.tsx | 170 ++++++++++++++----------- src/components/tables/common.tsx | 18 ++- src/components/tables/torrenttable.tsx | 25 +++- src/components/toolbar.tsx | 12 ++ src/config.ts | 4 + src/css/torrenttable.css | 5 + 6 files changed, 157 insertions(+), 77 deletions(-) diff --git a/src/components/server.tsx b/src/components/server.tsx index 4c83d6e8..524c2884 100644 --- a/src/components/server.tsx +++ b/src/components/server.tsx @@ -28,6 +28,7 @@ import type { TorrentFilter } from "./filters"; import { DefaultFilter, Filters } from "./filters"; import { Statusbar } from "./statusbar"; import { TorrentTable, useInitialTorrentRequiredFields } from "./tables/torrenttable"; +import { TableDisplayContext } from "./tables/common"; import { MemoizedToolbar } from "./toolbar"; import { useSession, useTorrentList } from "queries"; import type { TorrentFieldsType } from "rpc/transmission"; @@ -194,91 +195,114 @@ export function Server({ hostname, tabsRef, toolbarExtra, toggleTabStrip }: Serv const [showFiltersPanel, { toggle: toggleFiltersPanel }] = useDisclosure(config.values.interface.showFiltersPanel); const [showDetailsPanel, { toggle: toggleDetailsPanel }] = useDisclosure(config.values.interface.showDetailsPanel); + const [colorizeUploadRatio, setColorizeUploadRatio] = useState(config.values.interface.colorizeUploadRatio); + const [showTableRowSeparators, setShowTableRowSeparators] = useState(config.values.interface.showTableRowSeparators); const [mainSplit, toggleMainSplit] = useToggle([ config.values.interface.mainSplit, config.values.interface.mainSplit === "vertical" ? "horizontal" : "vertical"]); + // The config object is mutable and does not trigger re-renders, so the new + // value is written before the state update. That way the tables pick it up + // on the very same render. + const toggleColorizeUploadRatio = useCallback(() => { + const next = !config.values.interface.colorizeUploadRatio; + config.values.interface.colorizeUploadRatio = next; + setColorizeUploadRatio(next); + }, [config]); + + const toggleTableRowSeparators = useCallback(() => { + const next = !config.values.interface.showTableRowSeparators; + config.values.interface.showTableRowSeparators = next; + setShowTableRowSeparators(next); + }, [config]); + useEffect(() => { config.values.interface.showFiltersPanel = showFiltersPanel; config.values.interface.showDetailsPanel = showDetailsPanel; + config.values.interface.colorizeUploadRatio = colorizeUploadRatio; + config.values.interface.showTableRowSeparators = showTableRowSeparators; config.values.interface.mainSplit = mainSplit; - }, [config, showFiltersPanel, showDetailsPanel, mainSplit]); + }, [config, showFiltersPanel, showDetailsPanel, colorizeUploadRatio, showTableRowSeparators, mainSplit]); return - - - {overlayVisible && - - {sessionIsLoading - ? - : sessionIsError - ? <> - Failed to load session - {(sessionError as Error).message} - - : session?.["rpc-version"] === undefined - ? Server does not appear to be transmission daemon - : rpcVersion < 14 - ? Transmission version 2.40 or higher is required. - : <>} - - } - - - - - - - : undefined} - right={ - + + + {overlayVisible && + + {sessionIsLoading + ? + : sessionIsError + ? <> + Failed to load session + {(sessionError as Error).message} + + : session?.["rpc-version"] === undefined + ? Server does not appear to be transmission daemon + : rpcVersion < 14 + ? Transmission version 2.40 or higher is required. + : <>} + + } + + - } - bottom={showDetailsPanel - ? - : undefined} - /> - - + + + + : undefined} + right={ + + } + bottom={showDetailsPanel + ? + : undefined} /> - - + + + + + ; } diff --git a/src/components/tables/common.tsx b/src/components/tables/common.tsx index 9fa0e044..1caec1be 100644 --- a/src/components/tables/common.tsx +++ b/src/components/tables/common.tsx @@ -518,6 +518,20 @@ export interface TrguiTableRef { setExpanded: (state: boolean) => void, } +// Display options shared by all tables. They are carried in a context instead +// of being read straight from the config, because table rows are memoized: +// a context change re-renders the consumers even when the memoized parent +// bails out on unchanged props. +export interface TableDisplaySettings { + colorizeUploadRatio: boolean, + showRowSeparators: boolean, +} + +export const TableDisplayContext = React.createContext({ + colorizeUploadRatio: true, + showRowSeparators: true, +}); + export function TrguiTable(props: { tablename: TableName, tableRef?: React.MutableRefObject, @@ -544,6 +558,8 @@ export function TrguiTable(props: { const [lastIndex, onRowClick] = useSelectHandler( table, props.selectedReducer, props.getRowId, props.setCurrent); + const tableDisplay = useContext(TableDisplayContext); + const [parentRef, rowHeight, virtualizer] = useTableVirtualizer(table.getRowModel().rows.length); const scrollToId = useMemo(() => { @@ -563,7 +579,7 @@ export function TrguiTable(props: { const [parentRefProxy, headerRefProxy, syncHeaderScroll] = useSynchronizedScroll(parentRef, headerRef); return ( -
+
; + } + // The upper bound is the server side seed ratio limit, the ratio at which + // the daemon stops seeding. It defaults to 2 when the daemon does not + // report one. + const limit = Number(session?.seedRatioLimit ?? 2); + let color: string | undefined = undefined; + if (colorizeUploadRatio) { + if (num < 1) { + color = "light-dark(var(--mantine-color-red-7), var(--mantine-color-red-4))"; + } else if (num < limit) { + color = "light-dark(var(--mantine-color-orange-7), var(--mantine-color-orange-4))"; + } else { + color = "light-dark(var(--mantine-color-green-7), var(--mantine-color-green-4))"; + } + } return ( -
- {num < 0 ? "" : Number(num).toFixed(2)} +
+ {Number(num).toFixed(2)}
); } diff --git a/src/components/toolbar.tsx b/src/components/toolbar.tsx index 9a46ba7a..001cb130 100644 --- a/src/components/toolbar.tsx +++ b/src/components/toolbar.tsx @@ -68,6 +68,8 @@ interface ToolbarProps { toggleDetailsPanel: () => void, toggleMainSplit: () => void, toggleTabStrip: () => void, + toggleColorizeUploadRatio: () => void, + toggleTableRowSeparators: () => void, } function useButtonHandlers( @@ -422,6 +424,16 @@ function Toolbar(props: ToolbarProps) { > Toggle tab strip } + + Toggle share ratio colouring + + + Toggle table row separators + Interface settings diff --git a/src/config.ts b/src/config.ts index 978b3146..f7e93709 100644 --- a/src/config.ts +++ b/src/config.ts @@ -161,6 +161,8 @@ interface Settings { compactDirectories: boolean, recursiveDirectories: boolean, showSizesInFilters: boolean, + colorizeUploadRatio: boolean, + showTableRowSeparators: boolean, statusBarSections: SectionsVisibility, statusBarGlobalSpeeds: boolean, showFiltersPanel: boolean, @@ -291,6 +293,8 @@ const DefaultSettings: Settings = { compactDirectories: false, recursiveDirectories: true, showSizesInFilters: true, + colorizeUploadRatio: true, + showTableRowSeparators: true, statusBarSections: StatusbarSections.map((section) => ({ section, visible: true, diff --git a/src/css/torrenttable.css b/src/css/torrenttable.css index 5992ce07..3c9b1301 100644 --- a/src/css/torrenttable.css +++ b/src/css/torrenttable.css @@ -80,6 +80,11 @@ width: 100%; } +/* Optional horizontal separator between table rows. */ +.torrent-table-container.with-row-separators .tr { + border-bottom: 1px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.12)); +} + .torrent-table .tr:nth-child(odd)::before { content: ''; position: absolute;