mirror of
https://github.com/zoriya/flood.git
synced 2025-12-06 07:16:18 +00:00
client: allow multi-select of filters with Ctrl and Shift keys
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import classnames from 'classnames';
|
||||
import {FC, ReactNode} from 'react';
|
||||
import {FC, ReactNode, KeyboardEvent, MouseEvent, TouchEvent} from 'react';
|
||||
import {useLingui} from '@lingui/react';
|
||||
|
||||
import Badge from '../general/Badge';
|
||||
@@ -12,7 +12,7 @@ interface SidebarFilterProps {
|
||||
slug: string;
|
||||
count: number;
|
||||
size?: number;
|
||||
handleClick: (slug: string) => void;
|
||||
handleClick: (slug: string, event: KeyboardEvent | MouseEvent | TouchEvent) => void;
|
||||
}
|
||||
|
||||
const SidebarFilter: FC<SidebarFilterProps> = ({
|
||||
@@ -57,7 +57,7 @@ const SidebarFilter: FC<SidebarFilterProps> = ({
|
||||
},
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => handleClick(slug)}
|
||||
onClick={(event) => handleClick(slug, event)}
|
||||
role="menuitem"
|
||||
>
|
||||
{icon}
|
||||
|
||||
@@ -66,11 +66,14 @@ const StatusFilters: FC = observer(() => {
|
||||
|
||||
const filterElements = filters.map((filter) => (
|
||||
<SidebarFilter
|
||||
handleClick={(selection) => TorrentFilterStore.setStatusFilter(selection as TorrentStatus)}
|
||||
handleClick={(selection, event) => TorrentFilterStore.setStatusFilters(selection as TorrentStatus, event)}
|
||||
count={TorrentFilterStore.taxonomy.statusCounts[filter.slug] || 0}
|
||||
key={filter.slug}
|
||||
icon={filter.icon}
|
||||
isActive={filter.slug === TorrentFilterStore.filters.statusFilter}
|
||||
isActive={
|
||||
(filter.slug === '' && !TorrentFilterStore.filters.statusFilter.length) ||
|
||||
TorrentFilterStore.filters.statusFilter.includes(filter.slug as TorrentStatus)
|
||||
}
|
||||
name={filter.label}
|
||||
slug={filter.slug}
|
||||
/>
|
||||
|
||||
@@ -28,10 +28,13 @@ const TagFilters: FC = observer(() => {
|
||||
|
||||
const filterElements = filterItems.map((filter) => (
|
||||
<SidebarFilter
|
||||
handleClick={(tag) => TorrentFilterStore.setTagFilter(tag)}
|
||||
handleClick={(tag, event) => TorrentFilterStore.setTagFilters(tag, event)}
|
||||
count={TorrentFilterStore.taxonomy.tagCounts[filter] || 0}
|
||||
key={filter}
|
||||
isActive={filter === TorrentFilterStore.filters.tagFilter}
|
||||
isActive={
|
||||
(filter === '' && !TorrentFilterStore.filters.tagFilter.length) ||
|
||||
TorrentFilterStore.filters.tagFilter.includes(filter)
|
||||
}
|
||||
name={filter}
|
||||
slug={filter}
|
||||
size={TorrentFilterStore.taxonomy.tagSizes[filter]}
|
||||
|
||||
@@ -27,10 +27,13 @@ const TrackerFilters: FC = observer(() => {
|
||||
|
||||
const filterElements = filterItems.map((filter) => (
|
||||
<SidebarFilter
|
||||
handleClick={(tracker) => TorrentFilterStore.setTrackerFilter(tracker)}
|
||||
handleClick={(tracker, event) => TorrentFilterStore.setTrackerFilters(tracker, event)}
|
||||
count={TorrentFilterStore.taxonomy.trackerCounts[filter] || 0}
|
||||
key={filter}
|
||||
isActive={filter === TorrentFilterStore.filters.trackerFilter}
|
||||
isActive={
|
||||
(filter === '' && !TorrentFilterStore.filters.trackerFilter.length) ||
|
||||
TorrentFilterStore.filters.trackerFilter.includes(filter)
|
||||
}
|
||||
name={filter}
|
||||
slug={filter}
|
||||
size={TorrentFilterStore.taxonomy.trackerSizes[filter]}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {computed, makeAutoObservable} from 'mobx';
|
||||
import jsonpatch, {Operation} from 'fast-json-patch';
|
||||
import {KeyboardEvent, MouseEvent, TouchEvent} from 'react';
|
||||
|
||||
import type {Taxonomy} from '@shared/types/Taxonomy';
|
||||
import type {TorrentStatus} from '@shared/constants/torrentStatusMap';
|
||||
import torrentStatusMap, {TorrentStatus} from '@shared/constants/torrentStatusMap';
|
||||
|
||||
class TorrentFilterStore {
|
||||
filters: {
|
||||
searchFilter: string;
|
||||
statusFilter: TorrentStatus | '';
|
||||
tagFilter: string;
|
||||
trackerFilter: string;
|
||||
statusFilter: Array<TorrentStatus>;
|
||||
tagFilter: Array<string>;
|
||||
trackerFilter: Array<string>;
|
||||
} = {
|
||||
searchFilter: '',
|
||||
statusFilter: '',
|
||||
tagFilter: '',
|
||||
trackerFilter: '',
|
||||
statusFilter: [],
|
||||
tagFilter: [],
|
||||
trackerFilter: [],
|
||||
};
|
||||
|
||||
taxonomy: Taxonomy = {
|
||||
@@ -28,9 +29,9 @@ class TorrentFilterStore {
|
||||
@computed get isFilterActive() {
|
||||
return (
|
||||
this.filters.searchFilter !== '' ||
|
||||
this.filters.statusFilter !== '' ||
|
||||
this.filters.tagFilter !== '' ||
|
||||
this.filters.trackerFilter !== ''
|
||||
this.filters.statusFilter.length ||
|
||||
this.filters.tagFilter.length ||
|
||||
this.filters.trackerFilter.length
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,9 +42,9 @@ class TorrentFilterStore {
|
||||
clearAllFilters() {
|
||||
this.filters = {
|
||||
searchFilter: '',
|
||||
statusFilter: '',
|
||||
tagFilter: '',
|
||||
trackerFilter: '',
|
||||
statusFilter: [],
|
||||
tagFilter: [],
|
||||
trackerFilter: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,25 +63,73 @@ class TorrentFilterStore {
|
||||
};
|
||||
}
|
||||
|
||||
setStatusFilter(filter: TorrentStatus) {
|
||||
this.filters = {
|
||||
...this.filters,
|
||||
statusFilter: filter,
|
||||
};
|
||||
setStatusFilters(filter: TorrentStatus | '', event: KeyboardEvent | MouseEvent | TouchEvent) {
|
||||
this.computeFilters(torrentStatusMap, this.filters.statusFilter, filter, event);
|
||||
}
|
||||
|
||||
setTagFilter(filter: string) {
|
||||
this.filters = {
|
||||
...this.filters,
|
||||
tagFilter: filter,
|
||||
};
|
||||
setTagFilters(filter: string, event: KeyboardEvent | MouseEvent | TouchEvent) {
|
||||
const tags = Object.keys(this.taxonomy.tagCounts).sort((a, b) => {
|
||||
if (a === 'untagged') return -1;
|
||||
else if (b === 'untagged') return 1;
|
||||
else return a.localeCompare(b);
|
||||
});
|
||||
|
||||
// Put 'untagged' in the correct second position for shift click ordering
|
||||
tags.splice(tags.indexOf('untagged'), 1);
|
||||
tags.splice(1, 0, 'untagged');
|
||||
|
||||
this.computeFilters(tags, this.filters.tagFilter, filter, event);
|
||||
}
|
||||
|
||||
setTrackerFilter(filter: string) {
|
||||
this.filters = {
|
||||
...this.filters,
|
||||
trackerFilter: filter,
|
||||
};
|
||||
setTrackerFilters(filter: string, event: KeyboardEvent | MouseEvent | TouchEvent) {
|
||||
const trackers = Object.keys(this.taxonomy.trackerCounts).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.computeFilters(trackers, this.filters.trackerFilter, filter, event);
|
||||
}
|
||||
|
||||
private computeFilters<T extends TorrentStatus | string>(
|
||||
keys: readonly T[],
|
||||
currentFilters: Array<T>,
|
||||
newFilter: T,
|
||||
event: KeyboardEvent | MouseEvent | TouchEvent,
|
||||
) {
|
||||
if (newFilter === ('' as T)) {
|
||||
currentFilters.splice(0);
|
||||
} else if (event.shiftKey) {
|
||||
if (currentFilters.length) {
|
||||
const lastKey = currentFilters[currentFilters.length - 1];
|
||||
const lastKeyIndex = keys.indexOf(lastKey);
|
||||
let currentKeyIndex = keys.indexOf(newFilter);
|
||||
|
||||
if (!~currentKeyIndex || !~lastKeyIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// from the previously selected index to the currently selected index,
|
||||
// add all filters to the selected array.
|
||||
// if the newly selcted index is larger than the previous, start from
|
||||
// the newly selected index and work backwards. otherwise go forwards.
|
||||
const increment = currentKeyIndex > lastKeyIndex ? -1 : 1;
|
||||
|
||||
for (; currentKeyIndex !== lastKeyIndex; currentKeyIndex += increment) {
|
||||
const foundKey = keys[currentKeyIndex] as T;
|
||||
// if the filter isn't already selected, add the filter to the array.
|
||||
if (!currentFilters.includes(foundKey)) {
|
||||
currentFilters.push(foundKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentFilters.splice(0, currentFilters.length, newFilter);
|
||||
}
|
||||
} else if (event.metaKey || event.ctrlKey) {
|
||||
if (currentFilters.includes(newFilter)) {
|
||||
currentFilters.splice(currentFilters.indexOf(newFilter), 1);
|
||||
} else {
|
||||
currentFilters.push(newFilter);
|
||||
}
|
||||
} else {
|
||||
currentFilters.splice(0, currentFilters.length, newFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,21 +32,21 @@ class TorrentStore {
|
||||
filteredTorrents = termMatch(filteredTorrents, (properties) => properties.name, searchFilter);
|
||||
}
|
||||
|
||||
if (statusFilter !== '') {
|
||||
if (statusFilter.length) {
|
||||
filteredTorrents = filterTorrents(filteredTorrents, {
|
||||
type: 'status',
|
||||
filter: statusFilter,
|
||||
});
|
||||
}
|
||||
|
||||
if (tagFilter !== '') {
|
||||
if (tagFilter.length) {
|
||||
filteredTorrents = filterTorrents(filteredTorrents, {
|
||||
type: 'tag',
|
||||
filter: tagFilter,
|
||||
});
|
||||
}
|
||||
|
||||
if (trackerFilter !== '') {
|
||||
if (trackerFilter.length) {
|
||||
filteredTorrents = filterTorrents(filteredTorrents, {
|
||||
type: 'tracker',
|
||||
filter: trackerFilter,
|
||||
|
||||
@@ -3,40 +3,38 @@ import type {TorrentStatus} from '@shared/constants/torrentStatusMap';
|
||||
|
||||
interface StatusFilter {
|
||||
type: 'status';
|
||||
filter: TorrentStatus;
|
||||
filter: TorrentStatus[];
|
||||
}
|
||||
|
||||
interface TrackerFilter {
|
||||
type: 'tracker';
|
||||
filter: string;
|
||||
filter: string[];
|
||||
}
|
||||
|
||||
interface TagFilter {
|
||||
type: 'tag';
|
||||
filter: string;
|
||||
filter: string[];
|
||||
}
|
||||
|
||||
function filterTorrents(
|
||||
torrentList: TorrentProperties[],
|
||||
opts: StatusFilter | TrackerFilter | TagFilter,
|
||||
): TorrentProperties[] {
|
||||
const {type, filter} = opts;
|
||||
|
||||
if (filter !== '') {
|
||||
if (type === 'status') {
|
||||
return torrentList.filter((torrent) => torrent.status.includes(filter as TorrentStatus));
|
||||
}
|
||||
if (type === 'tracker') {
|
||||
return torrentList.filter((torrent) => torrent.trackerURIs.includes(filter));
|
||||
}
|
||||
if (type === 'tag') {
|
||||
return torrentList.filter((torrent) => {
|
||||
if (filter === 'untagged') {
|
||||
return torrent.tags.length === 0;
|
||||
if (opts.filter.length) {
|
||||
if (opts.type === 'status') {
|
||||
return torrentList.filter((torrent) => torrent.status.some((status) => opts.filter.includes(status)));
|
||||
}
|
||||
|
||||
return torrent.tags.includes(filter);
|
||||
});
|
||||
if (opts.type === 'tracker') {
|
||||
return torrentList.filter((torrent) => torrent.trackerURIs.some((uri) => opts.filter.includes(uri)));
|
||||
}
|
||||
|
||||
if (opts.type === 'tag') {
|
||||
const includeUntagged = opts.filter.includes('untagged');
|
||||
return torrentList.filter(
|
||||
(torrent) =>
|
||||
(includeUntagged && torrent.tags.length === 0) || torrent.tags.some((tag) => opts.filter.includes(tag)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const torrentStatusMap = [
|
||||
'checking',
|
||||
'seeding',
|
||||
'complete',
|
||||
'downloading',
|
||||
'seeding',
|
||||
'checking',
|
||||
'complete',
|
||||
'stopped',
|
||||
'error',
|
||||
'inactive',
|
||||
'active',
|
||||
'inactive',
|
||||
'error',
|
||||
] as const;
|
||||
|
||||
export type TorrentStatus = typeof torrentStatusMap[number];
|
||||
|
||||
Reference in New Issue
Block a user