Skip to content
Closed
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
170 changes: 97 additions & 73 deletions src/components/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<SplitType>([
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 <ServerContext data={serverData} selected={selectedTorrents} rpc={rpcVersion}>
<Flex direction="column" className={classes.serverContainer}>
<MemoizedServerModals ref={modals} {...{ runUpdates, tabsRef }} serverName={serverConfig.name} />
{overlayVisible && <Overlay blur={10}>
<Flex
align="center"
justify="center"
h="100%"
direction="column"
gap="xl"
>
{sessionIsLoading
? <Loader size="xl" />
: sessionIsError
? <>
<Title c="red" order={1}>Failed to load session</Title>
<Title c="red" order={3}>{(sessionError as Error).message}</Title>
</>
: session?.["rpc-version"] === undefined
? <Title c="red" order={1}>Server does not appear to be transmission daemon</Title>
: rpcVersion < 14
? <Title c="red" order={1}>Transmission version 2.40 or higher is required.</Title>
: <></>}
</Flex>
</Overlay>}
<Box p="sm" className={classes.toolbarContainer}>
<MemoizedToolbar
setSearchTerms={setSearchTerms}
modals={modals}
altSpeedMode={session?.["alt-speed-enabled"] as boolean ?? false}
extra={toolbarExtra}
toggleFiltersPanel={toggleFiltersPanel}
toggleDetailsPanel={toggleDetailsPanel}
toggleMainSplit={toggleMainSplit}
toggleTabStrip={toggleTabStrip}
/>
</Box>
<SplitLayout
key={`split-${showFiltersPanel ? "1" : "0"}-${showDetailsPanel ? "1" : "0"}-${mainSplit}`}
mainSplit={mainSplit}
left={showFiltersPanel
? <div className="scrollable">
<Filters
torrents={torrents ?? []}
currentFilters={currentFilters}
setCurrentFilters={setCurrentFilters}
/>
</div>
: undefined}
right={
<TorrentTable
<TableDisplayContext.Provider value={{ colorizeUploadRatio, showRowSeparators: showTableRowSeparators }}>
<Flex direction="column" className={classes.serverContainer}>
<MemoizedServerModals ref={modals} {...{ runUpdates, tabsRef }} serverName={serverConfig.name} />
{overlayVisible && <Overlay blur={10}>
<Flex
align="center"
justify="center"
h="100%"
direction="column"
gap="xl"
>
{sessionIsLoading
? <Loader size="xl" />
: sessionIsError
? <>
<Title c="red" order={1}>Failed to load session</Title>
<Title c="red" order={3}>{(sessionError as Error).message}</Title>
</>
: session?.["rpc-version"] === undefined
? <Title c="red" order={1}>Server does not appear to be transmission daemon</Title>
: rpcVersion < 14
? <Title c="red" order={1}>Transmission version 2.40 or higher is required.</Title>
: <></>}
</Flex>
</Overlay>}
<Box p="sm" className={classes.toolbarContainer}>
<MemoizedToolbar
setSearchTerms={setSearchTerms}
modals={modals}
torrents={filteredTorrents}
setCurrentTorrent={setCurrentTorrent}
selectedTorrents={selectedTorrents}
selectedReducer={selectedReducer}
onColumnVisibilityChange={setTableRequiredFields}
scrollToRow={scrollToRow}
altSpeedMode={session?.["alt-speed-enabled"] as boolean ?? false}
extra={toolbarExtra}
toggleFiltersPanel={toggleFiltersPanel}
toggleDetailsPanel={toggleDetailsPanel}
toggleMainSplit={toggleMainSplit}
toggleTabStrip={toggleTabStrip}
toggleColorizeUploadRatio={toggleColorizeUploadRatio}
toggleTableRowSeparators={toggleTableRowSeparators}
/>
}
bottom={showDetailsPanel
? <MemoizedDetails torrentId={currentTorrent} updates={updates} />
: undefined}
/>
<Box px="xs" className={classes.statusbarContainer}>
<Statusbar {...{
session,
filteredTorrents,
selectedTorrents,
hostname,
torrents: torrents ?? [],
showMisc: Boolean(toolbarExtra),
}}
</Box>
<SplitLayout
key={`split-${showFiltersPanel ? "1" : "0"}-${showDetailsPanel ? "1" : "0"}-${mainSplit}`}
mainSplit={mainSplit}
left={showFiltersPanel
? <div className="scrollable">
<Filters
torrents={torrents ?? []}
currentFilters={currentFilters}
setCurrentFilters={setCurrentFilters}
/>
</div>
: undefined}
right={
<TorrentTable
modals={modals}
torrents={filteredTorrents}
setCurrentTorrent={setCurrentTorrent}
selectedTorrents={selectedTorrents}
selectedReducer={selectedReducer}
onColumnVisibilityChange={setTableRequiredFields}
scrollToRow={scrollToRow}
/>
}
bottom={showDetailsPanel
? <MemoizedDetails torrentId={currentTorrent} updates={updates} />
: undefined}
/>
</Box>
</Flex>
<Box px="xs" className={classes.statusbarContainer}>
<Statusbar {...{
session,
filteredTorrents,
selectedTorrents,
hostname,
torrents: torrents ?? [],
showMisc: Boolean(toolbarExtra),
}}
/>
</Box>
</Flex>
</TableDisplayContext.Provider>
</ServerContext>;
}
18 changes: 17 additions & 1 deletion src/components/tables/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TableDisplaySettings>({
colorizeUploadRatio: true,
showRowSeparators: true,
});

export function TrguiTable<TData>(props: {
tablename: TableName,
tableRef?: React.MutableRefObject<TrguiTableRef | undefined>,
Expand All @@ -544,6 +558,8 @@ export function TrguiTable<TData>(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(() => {
Expand All @@ -563,7 +579,7 @@ export function TrguiTable<TData>(props: {
const [parentRefProxy, headerRefProxy, syncHeaderScroll] = useSynchronizedScroll(parentRef, headerRef);

return (
<div className="torrent-table-container">
<div className={`torrent-table-container${tableDisplay.showRowSeparators ? " with-row-separators" : ""}`}>
<Box
ref={headerRefProxy}
className={classes.tableHeaderContainer}
Expand Down
25 changes: 22 additions & 3 deletions src/components/tables/torrenttable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { bytesToHumanReadableStr, fileSystemSafeName, modKeyString, pathMapFromS
import { ProgressBar } from "../progressbar";
import type { AccessorFn, CellContext } from "@tanstack/table-core";
import type { TableSelectReducer } from "./common";
import { EditableNameField, TrguiTable } from "./common";
import { EditableNameField, TrguiTable, TableDisplayContext } from "./common";
import { Badge, Box, Kbd, Menu, Text, useMantineTheme } from "@mantine/core";
import { ConfigContext, ServerConfigContext } from "config";
import { StatusIconMap, Error as StatusIconError, Magnetizing, CompletedStopped } from "components/statusicons";
Expand Down Expand Up @@ -251,10 +251,29 @@ function PositiveNumberField(props: TableFieldProps) {
}

function FixedDecimalField(props: TableFieldProps) {
const { data: session } = useSession(false);
const { colorizeUploadRatio } = useContext(TableDisplayContext);
const num = props.torrent[props.fieldName];
if (num < 0) {
return <div style={{ width: "100%", textAlign: "right" }} />;
}
// 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 (
<div style={{ width: "100%", textAlign: "right" }}>
{num < 0 ? "" : Number(num).toFixed(2)}
<div style={{ width: "100%", textAlign: "right", color: color }}>
{Number(num).toFixed(2)}
</div>
);
}
Expand Down
12 changes: 12 additions & 0 deletions src/components/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ interface ToolbarProps {
toggleDetailsPanel: () => void,
toggleMainSplit: () => void,
toggleTabStrip: () => void,
toggleColorizeUploadRatio: () => void,
toggleTableRowSeparators: () => void,
}

function useButtonHandlers(
Expand Down Expand Up @@ -422,6 +424,16 @@ function Toolbar(props: ToolbarProps) {
>
Toggle tab strip
</Menu.Item>}
<Menu.Item
onClick={props.toggleColorizeUploadRatio}
>
Toggle share ratio colouring
</Menu.Item>
<Menu.Item
onClick={props.toggleTableRowSeparators}
>
Toggle table row separators
</Menu.Item>
<Menu.Divider />
<Menu.Label>Interface settings</Menu.Label>
<Menu.Item onClick={onSettingsExport}>
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ interface Settings {
compactDirectories: boolean,
recursiveDirectories: boolean,
showSizesInFilters: boolean,
colorizeUploadRatio: boolean,
showTableRowSeparators: boolean,
statusBarSections: SectionsVisibility<StatusbarSectionName>,
statusBarGlobalSpeeds: boolean,
showFiltersPanel: boolean,
Expand Down Expand Up @@ -291,6 +293,8 @@ const DefaultSettings: Settings = {
compactDirectories: false,
recursiveDirectories: true,
showSizesInFilters: true,
colorizeUploadRatio: true,
showTableRowSeparators: true,
statusBarSections: StatusbarSections.map((section) => ({
section,
visible: true,
Expand Down
5 changes: 5 additions & 0 deletions src/css/torrenttable.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down