diff --git a/src-tauri/src/demo/mod.rs b/src-tauri/src/demo/mod.rs index 0fee8b3..55b8510 100644 --- a/src-tauri/src/demo/mod.rs +++ b/src-tauri/src/demo/mod.rs @@ -275,6 +275,7 @@ pub enum Filter { FreeText(String), Has(String), MapName(String), + Not(Box), PlayerName(String), TagName(String), } @@ -287,28 +288,28 @@ struct Filters { free_text: Vec, has: Vec, map_name: Vec, + not: Vec, player_name: Vec, tag_name: Vec, } impl Filters { - fn from_filter_list(filters: &[Filter]) -> Self { - let mut result = Self::default(); - - for filter in filters { - match filter { - Filter::DemoType(value) => result.demo_type.push(value.to_lowercase()), - Filter::Event(value) => result.event.push(value.to_lowercase()), - Filter::FileName(value) => result.file_name.push(value.to_lowercase()), - Filter::FreeText(value) => result.free_text.push(value.to_lowercase()), - Filter::Has(value) => result.has.push(value.to_lowercase()), - Filter::MapName(value) => result.map_name.push(value.to_lowercase()), - Filter::PlayerName(value) => result.player_name.push(value.to_lowercase()), - Filter::TagName(value) => result.tag_name.push(value.to_lowercase()), + fn add_filter(&mut self, filter: &Filter) { + match filter { + Filter::DemoType(value) => self.demo_type.push(value.to_lowercase()), + Filter::Event(value) => self.event.push(value.to_lowercase()), + Filter::FileName(value) => self.file_name.push(value.to_lowercase()), + Filter::FreeText(value) => self.free_text.push(value.to_lowercase()), + Filter::Has(value) => self.has.push(value.to_lowercase()), + Filter::MapName(value) => self.map_name.push(value.to_lowercase()), + Filter::Not(filter) => { + let mut excluded = Self::default(); + excluded.add_filter(filter); + self.not.push(excluded); } + Filter::PlayerName(value) => self.player_name.push(value.to_lowercase()), + Filter::TagName(value) => self.tag_name.push(value.to_lowercase()), } - - result } fn matches(&self, demo: &Demo) -> bool { @@ -320,6 +321,7 @@ impl Filters { && self.matches_map(demo) && self.matches_client(demo) && self.matches_tag(demo) + && self.not.iter().all(|filter| !filter.matches(demo)) } fn matches_type(&self, demo: &Demo) -> bool { @@ -428,12 +430,24 @@ impl Filters { } } +impl<'a> FromIterator<&'a Filter> for Filters { + fn from_iter>(filters: T) -> Self { + let mut result = Self::default(); + + for filter in filters { + result.add_filter(filter); + } + + result + } +} + pub fn sort_demos(demos: &mut [Arc], sort_key: SortKey, reverse: bool) { demos.sort_unstable_by(|d1, d2| compare_demos_by(sort_key, reverse, d1, d2)); } pub fn filter_demos(demos: &[Arc], filters: &[Filter]) -> Vec> { - let filters = Filters::from_filter_list(filters); + let filters: Filters = filters.iter().collect(); demos .iter() diff --git a/src/demo.ts b/src/demo.ts index 5046e21..5b9ce7f 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -270,5 +270,6 @@ export type DemoFilter = | { free_text: string } | { has: string } | { map_name: string } + | { not: DemoFilter } | { player_name: string } | { tag_name: string }; diff --git a/src/routes/demos/demoDir/KeyValueQueryLanguage.tsx b/src/routes/demos/demoDir/KeyValueQueryLanguage.tsx index 1d68fd5..35348eb 100644 --- a/src/routes/demos/demoDir/KeyValueQueryLanguage.tsx +++ b/src/routes/demos/demoDir/KeyValueQueryLanguage.tsx @@ -61,11 +61,11 @@ export const keyValueQueryLanguage: QueryLanguage = { return tokens; }, parser: (token: string, { filterPatterns }): Token => { - const matches = /^([a-zA-Z0-9-_]+):(.*)$/.exec(token); + const matches = /^(!?[a-zA-Z0-9-_]+):(.*)$/.exec(token); if (matches !== null) { const [_, key, value] = matches; - if (Object.keys(filterPatterns).includes(key)) { + if (Object.keys(filterPatterns).includes(key.replace(/^!/, ""))) { return { type: "filter", value: { key, value } }; } else { return { type: "invalid-filter", value: { key, value } }; diff --git a/src/routes/demos/demoDir/index.tsx b/src/routes/demos/demoDir/index.tsx index f7033b4..f3af0f2 100644 --- a/src/routes/demos/demoDir/index.tsx +++ b/src/routes/demos/demoDir/index.tsx @@ -61,6 +61,14 @@ function tokenToDemoFilter(token: Token): DemoFilter | null { return null; } + if (token.value.key.startsWith("!")) { + const filter = tokenToDemoFilter({ + ...token, + value: { ...token.value, key: token.value.key.slice(1) }, + }); + return filter === null ? null : { not: filter }; + } + switch (token.value.key as FilterPatternKey | string) { case "type": return { demo_type: replaceBackslashPlaceholder(token.value.value) }; diff --git a/src/routes/demos/demoDir/useAutocomplete.tsx b/src/routes/demos/demoDir/useAutocomplete.tsx index bf3414e..fef9fe4 100644 --- a/src/routes/demos/demoDir/useAutocomplete.tsx +++ b/src/routes/demos/demoDir/useAutocomplete.tsx @@ -59,13 +59,14 @@ export const useAutocomplete = ( useEffect(() => { if (currentTokenKeyValue === null) { const keys = Object.keys(filterPatterns) + .map((key) => (currentToken.startsWith("!") ? `!${key}` : key)) .filter((key) => key.includes(currentToken)) .map((key) => `${key}:`); setDropdownItems(keys); } else { const [currentTokenKey, currentTokenValue] = currentTokenKeyValue; - const values = filterPatterns[currentTokenKey]; + const values = filterPatterns[currentTokenKey.replace(/^!/, "")]; if (values !== undefined) { setDropdownItems(