(props: {
return (
(props: {
onVisibilityChange?: React.Dispatch,
scrollToRow?: { id: string },
}) {
- const [table, columnVisibility, setColumnVisibility, columnOrder, setColumnOrder, columnSizing, sorting] =
+ const [table, columnVisibility, setColumnVisibility, columnOrder, setColumnOrder, columnSizing, sorting, rowsWithSelectedDescendants] =
useTable(props.tablename, props.columns, props.data, props.selected, props.getRowId, props.getSubRows, props.onVisibilityChange);
if (props.tableRef !== undefined) {
@@ -543,6 +574,7 @@ export function TrguiTable(props: {
return key={props.getRowId(row.original)} {...{
row,
selected: row.getIsSelected(),
+ descendantSelected: rowsWithSelectedDescendants.has(row.id),
expanded: row.getIsExpanded(),
index: virtualRow.index,
lastIndex,
diff --git a/src/components/tables/filetreetable.tsx b/src/components/tables/filetreetable.tsx
index 0211ae7..9d4d8a3 100644
--- a/src/components/tables/filetreetable.tsx
+++ b/src/components/tables/filetreetable.tsx
@@ -164,6 +164,11 @@ interface FileTreeTableProps {
brief?: boolean,
}
+function entryMatchesSearchTerms(entry: FileDirEntryView, searchTerms: string[]) {
+ const path = entry.fullpath.toLowerCase().substring(entry.fullpath.indexOf("/") + 1);
+ return searchTerms.every(term => path.includes(term));
+}
+
export function useUnwantedFiles(ft: CachedFileTree, setUpdating: boolean): EntryWantedChangeHandler {
const changeHandler = useCallback((entryPath: string, state: boolean) => {
ft.setWanted(entryPath, state, setUpdating);
@@ -172,7 +177,7 @@ export function useUnwantedFiles(ft: CachedFileTree, setUpdating: boolean): Entr
return changeHandler;
}
-function useSelected(props: FileTreeTableProps) {
+function useSelected(data: FileDirEntryView[], fileTree: CachedFileTree, searchTerms: string[]) {
const [selected, setSelected] = useReducer((prev: string[], next: string[]) => {
if (prev.length === next.length) {
for (let i = 0; i < prev.length; i++) {
@@ -183,26 +188,49 @@ function useSelected(props: FileTreeTableProps) {
return next;
}, []);
+ const deriveNewSelection = useRef((_: boolean) => [] as string[]);
+ deriveNewSelection.current = useCallback((selectAll: boolean) => {
+ const result: string[] = [];
+ const recurse = (entry: FileDirEntryView) => {
+ if (
+ (selectAll || fileTree.findEntry(entry.fullpath)?.isSelected === true) &&
+ (entry.subrows.length === 0 || entryMatchesSearchTerms(entry, searchTerms))
+ ) {
+ result.push(entry.fullpath);
+ return;
+ }
+ entry.subrows.forEach(recurse);
+ };
+ data.forEach(recurse);
+ return result;
+ }, [data, fileTree, searchTerms]);
+
+ useEffect(() => {
+ if (searchTerms.length === 0) return;
+
+ fileTree.selectAction({ verb: "set", ids: deriveNewSelection.current(false) });
+ setSelected(fileTree.getSelected());
+ }, [fileTree, searchTerms]);
+
const selectAll = useRef(() => { });
const hk = useHotkeysContext();
selectAll.current = useCallback(() => {
- const ids = props.data.map((entry) => entry.fullpath);
- props.fileTree.selectAction({ verb: "set", ids });
- setSelected(props.fileTree.getSelected());
- }, [props.data, props.fileTree]);
+ fileTree.selectAction({ verb: "set", ids: deriveNewSelection.current(true) });
+ setSelected(fileTree.getSelected());
+ }, [fileTree]);
useEffect(() => {
return () => { hk.handlers.selectAll = () => { }; };
}, [hk]);
const selectedReducer = useCallback((action: { verb: "add" | "set" | "toggle", ids: string[], isReset?: boolean }) => {
- props.fileTree.selectAction(action);
- setSelected(props.fileTree.getSelected());
+ fileTree.selectAction(action);
+ setSelected(fileTree.getSelected());
if (action.isReset !== true) {
hk.handlers.selectAll = () => { selectAll.current?.(); };
}
- }, [props.fileTree, hk]);
+ }, [fileTree, hk]);
return { selected, selectedReducer };
}
@@ -290,12 +318,6 @@ export function FileTreeTable(props: FileTreeTableProps) {
const getRowId = useCallback((row: FileDirEntryView) => row.fullpath, []);
const getSubRows = useCallback((row: FileDirEntryView) => row.subrows, []);
- const { selected, selectedReducer } = useSelected(props);
-
- useEffect(() => {
- selectedReducer({ verb: "set", ids: [], isReset: true });
- }, [props.fileTree.torrenthash, selectedReducer]);
-
const onEntryOpen = useCallback((rowPath: string, reveal: boolean = false) => {
if (TAURI) {
if (props.downloadDir === undefined || props.downloadDir === "") return;
@@ -329,24 +351,16 @@ export function FileTreeTable(props: FileTreeTableProps) {
const data = useMemo(() => {
if (searchTerms.length === 0) return props.data;
- const matches = (entry: FileDirEntryView) => {
- let matchesAll = true;
- const path = entry.fullpath.toLowerCase().substring(entry.fullpath.indexOf("/") + 1);
- searchTerms.forEach((term) => {
- if (!path.includes(term)) matchesAll = false;
- });
- return matchesAll;
- };
const filter = (entries: FileDirEntryView[]) => {
const result: FileDirEntryView[] = [];
entries.forEach((entry) => {
if (entry.subrows.length > 0) {
const copy = { ...entry };
copy.subrows = filter(copy.subrows);
- if (matches(copy) || copy.subrows.length > 0) {
+ if (copy.subrows.length > 0 || entryMatchesSearchTerms(copy, searchTerms)) {
result.push(copy);
}
- } else if (matches(entry)) {
+ } else if (entryMatchesSearchTerms(entry, searchTerms)) {
result.push(entry);
}
});
@@ -361,6 +375,12 @@ export function FileTreeTable(props: FileTreeTableProps) {
else tableRef.current?.setExpanded(false);
}, [searchTerms]);
+ const { selected, selectedReducer } = useSelected(data, props.fileTree, searchTerms);
+
+ useEffect(() => {
+ selectedReducer({ verb: "set", ids: [], isReset: true });
+ }, [props.fileTree.torrenthash, selectedReducer]);
+
const [showFileSearchBox, toggleFileSearchBox] = useReducer((shown: boolean) => {
const show = !shown;
if (!show) setSearchTerms([]);
diff --git a/src/css/torrenttable.css b/src/css/torrenttable.css
index 675f9ce..b422353 100644
--- a/src/css/torrenttable.css
+++ b/src/css/torrenttable.css
@@ -84,6 +84,16 @@
box-shadow: inset 0 0 0 9999px rgba(133, 133, 133, 0.1);
}
+.torrent-table .tr.descendant-selected {
+ background-color: rgb(50, 80, 170);
+ color: white;
+ box-shadow: inset 0 0 0 9999px rgba(133, 133, 133, 0.7);
+}
+
+.torrent-table .tr.descendant-selected:nth-child(odd) {
+ box-shadow: inset 0 0 0 9999px rgba(133, 133, 133, 0.8);
+}
+
.resizer {
display: block;
background: rgba(124, 124, 124, 0.4);
diff --git a/src/trutil.ts b/src/trutil.ts
index 565d161..028cdd1 100644
--- a/src/trutil.ts
+++ b/src/trutil.ts
@@ -227,3 +227,7 @@ const badChars = /[<>:"\\/|?*]/g;
export function fileSystemSafeName(name: string) {
return name.replace(badChars, "_");
}
+
+export function * chainedIterables(...iterables: Array>) {
+ for (const iterable of iterables) yield * iterable;
+}