Skip to content

Commit

Permalink
Added filter by filesystem loc+overflow filt ttip
Browse files Browse the repository at this point in the history
  • Loading branch information
FinalDoom authored and FinalDoom committed Dec 21, 2023
1 parent e20a2f0 commit bc6b386
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 22 deletions.
68 changes: 68 additions & 0 deletions client/src/javascript/components/sidebar/LocationFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {FC, KeyboardEvent, MouseEvent, ReactNode, TouchEvent} from 'react';
import {observer} from 'mobx-react';
import {useLingui} from '@lingui/react';
import {LocationTreeNode} from '@shared/types/Taxonomy';

import SidebarFilter from './SidebarFilter';
import TorrentFilterStore from '../../stores/TorrentFilterStore';

const LocationFilters: FC = observer(() => {
const {i18n} = useLingui();

const locations = Object.keys(TorrentFilterStore.taxonomy.locationCounts);

if (locations.length === 1 && locations[0] === '') {
return null;
}

const buildLocationFilterTree = (location: LocationTreeNode): ReactNode => {
if (
location.children.length === 1 &&
TorrentFilterStore.taxonomy.locationCounts[location.fullPath] ===
TorrentFilterStore.taxonomy.locationCounts[location.children[0].fullPath]
) {
const onlyChild = location.children[0];
const separator = onlyChild.fullPath.includes('/') ? '/' : '\\';
return buildLocationFilterTree({
...onlyChild,
directoryName: location.directoryName + separator + onlyChild.directoryName,
});
}

const children = location.children.map(buildLocationFilterTree);

return (
<SidebarFilter
handleClick={(filter: string | '', event: KeyboardEvent | MouseEvent | TouchEvent) =>
TorrentFilterStore.setLocationFilters(filter, event)
}
count={TorrentFilterStore.taxonomy.locationCounts[location.fullPath] || 0}
key={location.fullPath}
isActive={
(location.fullPath === '' && !TorrentFilterStore.locationFilter.length) ||
TorrentFilterStore.locationFilter.includes(location.fullPath)
}
name={location.directoryName}
slug={location.fullPath}
size={TorrentFilterStore.taxonomy.locationSizes[location.fullPath]}
>
{(children.length && children) || undefined}
</SidebarFilter>
);
};

const filterElements = TorrentFilterStore.taxonomy.locationTree.map(buildLocationFilterTree);

const title = i18n._('filter.location.title');

return (
<ul aria-label={title} className="sidebar-filter sidebar__item" role="menu">
<li className="sidebar-filter__item sidebar-filter__item--heading" role="none">
{title}
</li>
{filterElements}
</ul>
);
});

export default LocationFilters;
2 changes: 2 additions & 0 deletions client/src/javascript/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {OverlayScrollbarsComponent} from 'overlayscrollbars-react';
import DiskUsage from './DiskUsage';
import FeedsButton from './FeedsButton';
import LogoutButton from './LogoutButton';
import LocationFilters from './LocationFilters';
import NotificationsButton from './NotificationsButton';
import SearchBox from './SearchBox';
import SettingsButton from './SettingsButton';
Expand Down Expand Up @@ -44,6 +45,7 @@ const Sidebar: FC = () => (
<StatusFilters />
<TagFilters />
<TrackerFilters />
<LocationFilters />
<DiskUsage />
<div style={{flexGrow: 1}} />
<SidebarActions>
Expand Down
80 changes: 62 additions & 18 deletions client/src/javascript/components/sidebar/SidebarFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import classnames from 'classnames';
import {FC, ReactNode, KeyboardEvent, MouseEvent, TouchEvent} from 'react';
import {FC, ReactNode, KeyboardEvent, MouseEvent, TouchEvent, useState} from 'react';
import {useLingui} from '@lingui/react';
import {Start} from '@client/ui/icons';

import Badge from '../general/Badge';
import Size from '../general/Size';

interface SidebarFilterProps {
children?: ReactNode[];
name: string;
icon?: ReactNode;
isActive: boolean;
Expand All @@ -16,6 +18,7 @@ interface SidebarFilterProps {
}

const SidebarFilter: FC<SidebarFilterProps> = ({
children,
name: _name,
icon,
isActive,
Expand All @@ -26,9 +29,27 @@ const SidebarFilter: FC<SidebarFilterProps> = ({
}: SidebarFilterProps) => {
const {i18n} = useLingui();

const [expanded, setExpanded] = useState(false);

const classNames = classnames('sidebar-filter__item', {
'is-active': isActive,
});
const expanderClassNames = classnames('sidebar-filter__expander', {
'is-active': isActive,
expanded: expanded,
});

const flexCss = children
? {
display: 'flex',
}
: {};
const focusCss = {
':focus': {
outline: 'none',
WebkitTapHighlightColor: 'transparent',
},
};

let name = _name;
if (name === '') {
Expand All @@ -46,25 +67,48 @@ const SidebarFilter: FC<SidebarFilterProps> = ({
}
}

const setTitleForOverflowedName = (event: MouseEvent) => {
const target = event.target as HTMLSpanElement;
const overflowed = target.scrollWidth > target.clientWidth;
target.title = overflowed ? target.textContent || '' : '';
};

const unsetTitle = (event: MouseEvent) => {
const target = event.target as HTMLSpanElement;
target.title = '';
};

return (
<li>
<button
className={classNames}
css={{
':focus': {
outline: 'none',
WebkitTapHighlightColor: 'transparent',
},
}}
type="button"
onClick={(event) => handleClick(slug, event)}
role="menuitem"
>
{icon}
<span className="name">{name}</span>
<Badge>{count}</Badge>
{size != null && <Size value={size} className="size" />}
</button>
<div css={flexCss}>
{children && (
<button
className={expanderClassNames}
css={focusCss}
type="button"
onClick={() => setExpanded(!expanded)}
role="switch"
aria-checked={expanded}
>
<Start />
</button>
)}
<button
className={classNames}
css={focusCss}
type="button"
onClick={(event) => handleClick(slug, event)}
role="menuitem"
>
{icon}
<span className="name" onMouseOver={setTitleForOverflowedName} onMouseOut={unsetTitle}>
{name}
</span>
<Badge>{count}</Badge>
{size != null && <Size value={size} className="size" />}
</button>
</div>
{children && expanded && <ul className="sidebar-filter__nested">{children}</ul>}
</li>
);
};
Expand Down
1 change: 1 addition & 0 deletions client/src/javascript/i18n/strings/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"filter.status.seeding": "Seeding",
"filter.status.stopped": "Stopped",
"filter.status.title": "Filter by Status",
"filter.location.title": "Filter by Location",
"filter.tag.title": "Filter by Tag",
"filter.tracker.title": "Filter by Tracker",
"filter.untagged": "Untagged",
Expand Down
20 changes: 19 additions & 1 deletion client/src/javascript/stores/TorrentFilterStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {Taxonomy} from '@shared/types/Taxonomy';
import torrentStatusMap, {TorrentStatus} from '@shared/constants/torrentStatusMap';

class TorrentFilterStore {
locationFilter: Array<string> = [];
searchFilter = '';
statusFilter: Array<TorrentStatus> = [];
tagFilter: Array<string> = [];
Expand All @@ -14,6 +15,9 @@ class TorrentFilterStore {
filterTrigger = false;

taxonomy: Taxonomy = {
locationCounts: {},
locationSizes: {},
locationTree: [],
statusCounts: {},
tagCounts: {},
tagSizes: {},
Expand All @@ -22,14 +26,21 @@ class TorrentFilterStore {
};

@computed get isFilterActive() {
return this.searchFilter !== '' || this.statusFilter.length || this.tagFilter.length || this.trackerFilter.length;
return (
this.locationFilter.length ||
this.searchFilter !== '' ||
this.statusFilter.length ||
this.tagFilter.length ||
this.trackerFilter.length
);
}

constructor() {
makeAutoObservable(this);
}

clearAllFilters() {
this.locationFilter = [];
this.searchFilter = '';
this.statusFilter = [];
this.tagFilter = [];
Expand All @@ -50,6 +61,13 @@ class TorrentFilterStore {
this.filterTrigger = !this.filterTrigger;
}

setLocationFilters(filter: string | '', event: KeyboardEvent | MouseEvent | TouchEvent) {
const locations = Object.keys(this.taxonomy.locationCounts).sort((a, b) => a.localeCompare(b));

this.computeFilters(locations, this.locationFilter, filter, event);
this.filterTrigger = !this.filterTrigger;
}

setStatusFilters(filter: TorrentStatus | '', event: KeyboardEvent | MouseEvent | TouchEvent) {
this.computeFilters(torrentStatusMap, this.statusFilter, filter, event);
this.filterTrigger = !this.filterTrigger;
Expand Down
9 changes: 8 additions & 1 deletion client/src/javascript/stores/TorrentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ class TorrentStore {
}

@computed get filteredTorrents(): Array<TorrentProperties> {
const {searchFilter, statusFilter, tagFilter, trackerFilter} = TorrentFilterStore;
const {locationFilter, searchFilter, statusFilter, tagFilter, trackerFilter} = TorrentFilterStore;

let filteredTorrents = Object.assign([], this.sortedTorrents) as Array<TorrentProperties>;

if (locationFilter.length) {
filteredTorrents = filterTorrents(filteredTorrents, {
type: 'location',
filter: locationFilter,
});
}

if (searchFilter !== '') {
filteredTorrents = termMatch(filteredTorrents, (properties) => properties.name, searchFilter);
}
Expand Down
11 changes: 10 additions & 1 deletion client/src/javascript/util/filterTorrents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type {TorrentProperties} from '@shared/types/Torrent';
import type {TorrentStatus} from '@shared/constants/torrentStatusMap';

interface LocationFilter {
type: 'location';
filter: string[];
}

interface StatusFilter {
type: 'status';
filter: TorrentStatus[];
Expand All @@ -18,9 +23,13 @@ interface TagFilter {

function filterTorrents(
torrentList: TorrentProperties[],
opts: StatusFilter | TrackerFilter | TagFilter,
opts: LocationFilter | StatusFilter | TrackerFilter | TagFilter,
): TorrentProperties[] {
if (opts.filter.length) {
if (opts.type === 'location') {
return torrentList.filter((torrent) => opts.filter.some((directory) => torrent.directory.startsWith(directory)));
}

if (opts.type === 'status') {
return torrentList.filter((torrent) => torrent.status.some((status) => opts.filter.includes(status)));
}
Expand Down
16 changes: 16 additions & 0 deletions client/src/sass/components/_sidebar-filter.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
padding-top: 0;
}

&__expander,
&__item {
@include themes.theme('color', 'sidebar-filter--foreground');
cursor: pointer;
Expand Down Expand Up @@ -75,9 +76,24 @@

.size {
margin-left: auto;
white-space: nowrap;
}
}

&__expander {
display: block;
width: 14px;
padding: 0 0 0 20px;

&.expanded svg {
transform: rotate(90deg);
}
}

&__nested {
margin-left: 8px;
}

.badge {
@include themes.theme('background', 'sidebar-filter--count--background');
@include themes.theme('color', 'sidebar-filter--count--foreground');
Expand Down
Loading

0 comments on commit bc6b386

Please sign in to comment.