diff --git a/src-tauri/lang/en/ui.json b/src-tauri/lang/en/ui.json index d6188a1..99051fe 100644 --- a/src-tauri/lang/en/ui.json +++ b/src-tauri/lang/en/ui.json @@ -42,6 +42,8 @@ "player-overmasteries": "Overmasteries", "select-enemy": "Select Enemy", "select-quest": "Select Quest", + "select-player": "Select Player", + "select-character": "Select Character", "sba": { "OnAttemptSBA": "Attempted SBA", "OnPerformSBA": "Executed SBA", @@ -76,6 +78,8 @@ "saved-count_other": "{{count}} logs saved", "delete-selected-btn": "Delete Selected ({{count}})", "delete-all-btn": "Delete All", + "show-advanced-filters": "Show Advanced Filters", + "hide-advanced-filters": "Hide Advanced Filters", "date": "Date", "name": "Name", "primary-target": "Enemy", diff --git a/src-tauri/src/db/logs.rs b/src-tauri/src/db/logs.rs index 5199771..d0f1cb8 100644 --- a/src-tauri/src/db/logs.rs +++ b/src-tauri/src/db/logs.rs @@ -1,6 +1,6 @@ use anyhow::Result; use rusqlite::Connection; -use sea_query::{Expr, Iden, Order, Query, SqliteQueryBuilder}; +use sea_query::{Expr, Condition, Iden, Order, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; use serde::Serialize; @@ -87,6 +87,8 @@ pub fn get_logs( sort_by: &SortType, sort_direction: &SortDirection, cleared: Option, + filter_by_player_id: &Option, + filter_by_player_character: &Option ) -> anyhow::Result> { let sort_column = match sort_by { SortType::Time => Logs::Time, @@ -141,6 +143,56 @@ pub fn get_logs( }, |_| {}, ) + .conditions( + filter_by_player_id.is_some() && filter_by_player_character.is_some(), + |q| { + let player_id = filter_by_player_id.as_ref().unwrap(); + let player_character = filter_by_player_character.as_ref().unwrap(); + + q.cond_where( + Condition::any() + .add(Expr::col(Logs::P1Name).eq(player_id.clone()) + .and(Expr::col(Logs::P1Type).eq(player_character.clone()))) + .add(Expr::col(Logs::P2Name).eq(player_id.clone()) + .and(Expr::col(Logs::P2Type).eq(player_character.clone()))) + .add(Expr::col(Logs::P3Name).eq(player_id.clone()) + .and(Expr::col(Logs::P3Type).eq(player_character.clone()))) + .add(Expr::col(Logs::P4Name).eq(player_id) + .and(Expr::col(Logs::P4Type).eq(player_character))), + ); + }, + |_| {}, + ) + .conditions( + filter_by_player_id.is_some() && filter_by_player_character.is_none(), + |q| { + let player_id = filter_by_player_id.as_ref().unwrap(); + + q.cond_where( + Condition::any() + .add(Expr::col(Logs::P1Name).eq(player_id.clone())) + .add(Expr::col(Logs::P2Name).eq(player_id.clone())) + .add(Expr::col(Logs::P3Name).eq(player_id.clone())) + .add(Expr::col(Logs::P4Name).eq(player_id)), + ); + }, + |_| {}, + ) + .conditions( + filter_by_player_id.is_none() && filter_by_player_character.is_some(), + |q| { + let player_character = filter_by_player_character.as_ref().unwrap(); + + q.cond_where( + Condition::any() + .add(Expr::col(Logs::P1Type).eq(player_character.clone())) + .add(Expr::col(Logs::P2Type).eq(player_character.clone())) + .add(Expr::col(Logs::P3Type).eq(player_character.clone())) + .add(Expr::col(Logs::P4Type).eq(player_character)), + ); + }, + |_| {}, + ) .order_by_with_nulls(sort_column, order, sea_query::NullOrdering::Last) .limit(per_page.into()) .offset(offset.into()) @@ -182,6 +234,8 @@ pub fn get_logs_count( filter_by_enemy_id: Option, filter_by_quest_id: Option, cleared: Option, + filter_by_player_id: &Option, + filter_by_player_character: &Option ) -> Result { let (sql, values) = Query::select() .expr(Expr::col(Logs::Id).count()) @@ -207,6 +261,56 @@ pub fn get_logs_count( }, |_| {}, ) + .conditions( + filter_by_player_id.is_some() && filter_by_player_character.is_some(), + |q| { + let player_id = filter_by_player_id.as_ref().unwrap(); + let player_character = filter_by_player_character.as_ref().unwrap(); + + q.cond_where( + Condition::any() + .add(Expr::col(Logs::P1Name).eq(player_id.clone()) + .and(Expr::col(Logs::P1Type).eq(player_character.clone()))) + .add(Expr::col(Logs::P2Name).eq(player_id.clone()) + .and(Expr::col(Logs::P2Type).eq(player_character.clone()))) + .add(Expr::col(Logs::P3Name).eq(player_id.clone()) + .and(Expr::col(Logs::P3Type).eq(player_character.clone()))) + .add(Expr::col(Logs::P4Name).eq(player_id) + .and(Expr::col(Logs::P4Type).eq(player_character))), + ); + }, + |_| {}, + ) + .conditions( + filter_by_player_id.is_some() && filter_by_player_character.is_none(), + |q| { + let player_id = filter_by_player_id.as_ref().unwrap(); + + q.cond_where( + Condition::any() + .add(Expr::col(Logs::P1Name).eq(player_id.clone())) + .add(Expr::col(Logs::P2Name).eq(player_id.clone())) + .add(Expr::col(Logs::P3Name).eq(player_id.clone())) + .add(Expr::col(Logs::P4Name).eq(player_id)), + ); + }, + |_| {}, + ) + .conditions( + filter_by_player_id.is_none() && filter_by_player_character.is_some(), + |q| { + let player_character = filter_by_player_character.as_ref().unwrap(); + + q.cond_where( + Condition::any() + .add(Expr::col(Logs::P1Type).eq(player_character.clone())) + .add(Expr::col(Logs::P2Type).eq(player_character.clone())) + .add(Expr::col(Logs::P3Type).eq(player_character.clone())) + .add(Expr::col(Logs::P4Type).eq(player_character)), + ); + }, + |_| {}, + ) .build_rusqlite(SqliteQueryBuilder); let mut stmt = conn.prepare(&sql).unwrap(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 198f9f9..2190404 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -134,6 +134,10 @@ struct SearchResult { enemy_ids: Vec, /// IDs of the quests that can be filtered by. quest_ids: Vec, + /// Names of the Players that can be filtered by. + player_ids: Vec, + /// Names of the Characters that can be filtered by. + player_types: Vec, } #[tauri::command] @@ -144,6 +148,8 @@ fn fetch_logs( sort_direction: Option, sort_type: Option, quest_completed: Option, + filter_by_player_id: Option, + filter_by_player_character: Option, ) -> Result { let conn = db::connect_to_db().map_err(|e| e.to_string())?; let page = page.unwrap_or(1); @@ -175,6 +181,8 @@ fn fetch_logs( &sort_type_param, &sort_direction_param, quest_completed, + &filter_by_player_id, + &filter_by_player_character ) .map_err(|e| e.to_string())?; @@ -183,6 +191,8 @@ fn fetch_logs( filter_by_enemy_id, filter_by_quest_id, quest_completed, + &filter_by_player_id, + &filter_by_player_character ) .map_err(|e| e.to_string())?; @@ -190,22 +200,32 @@ fn fetch_logs( let mut enemy_ids = Vec::new(); let mut quest_ids = Vec::new(); + let mut player_ids = Vec::new(); + let mut player_types = Vec::new(); let mut query = conn - .prepare("SELECT primary_target, quest_id from logs") + .prepare("SELECT primary_target, quest_id, p1_name, p1_type, p2_name, p2_type, p3_name, p3_type, p4_name, p4_type from logs") .map_err(|e| e.to_string())?; let rows = query .query_map([], |row| { Ok(( - row.get::>(0)?, - row.get::>(1)?, + row.get::>(0)?, // primary_target + row.get::>(1)?, // quest_id + row.get::>(2)?, // p1_name + row.get::>(3)?, // p1_type + row.get::>(4)?, // p2_name + row.get::>(5)?, // p2_type + row.get::>(6)?, // p3_name + row.get::>(7)?, // p3_type + row.get::>(8)?, // p4_name + row.get::>(9)?, // p4_type )) }) .map_err(|e| e.to_string())?; for row in rows { - let (primary_target, quest_id) = row.map_err(|e| e.to_string())?; + let (primary_target, quest_id, p1_name, p1_type, p2_name, p2_type, p3_name, p3_type, p4_name, p4_type) = row.map_err(|e| e.to_string())?; if let Some(primary_target) = primary_target { if !enemy_ids.contains(&primary_target) { @@ -218,6 +238,22 @@ fn fetch_logs( quest_ids.push(quest_id); } } + + for p_name in [p1_name, p2_name, p3_name, p4_name] { + if let Some(p_name) = p_name { + if !player_ids.contains(&p_name) { + player_ids.push(p_name) + } + } + } + + for p_type in [p1_type, p2_type, p3_type, p4_type] { + if let Some(p_type) = p_type { + if !player_types.contains(&p_type) { + player_types.push(p_type) + } + } + } } Ok(SearchResult { @@ -227,6 +263,8 @@ fn fetch_logs( log_count, enemy_ids, quest_ids, + player_ids, + player_types }) } diff --git a/src/pages/logs/Index.tsx b/src/pages/logs/Index.tsx index 90159b2..c447a07 100644 --- a/src/pages/logs/Index.tsx +++ b/src/pages/logs/Index.tsx @@ -43,6 +43,7 @@ export const IndexPage = () => { toggleSort, setFilters, filters, + toggleAdvancedFilters, } = useIndex(); const { streamer_mode, show_display_names } = useMeterSettingsStore( @@ -115,11 +116,26 @@ export const IndexPage = () => { )} - - - - - + + + + + + + + + + + {filters.showAdvancedFilters && ( + + )} + {filters.showAdvancedFilters && ( + + )} + + {searchResult.logs.length === 0 && } {searchResult.logs.length > 0 && ( @@ -367,3 +383,57 @@ function SelectableQuestCompletion({ /> ); } + +function SelectablePlayer({ + playerIds, + filters, + setFilters, +}: { + playerIds: string[]; + filters: FilterState; + setFilters: (filters: Partial) => void; +}) { + const { t } = useTranslation(); + const targetOptions = useMemo( + () => playerIds.map((id) => ({ value: id.toString(), label: id.toString() })), + [playerIds] + ); + + return ( + setFilters({ filterByPlayerCharacter: value ? String(value) : null })} + value={filters.filterByPlayerCharacter ?? null} + placeholder={t("ui.select-character")} + searchable + clearable + /> + ); +} diff --git a/src/pages/logs/useIndex.tsx b/src/pages/logs/useIndex.tsx index 7fcef70..3370f89 100644 --- a/src/pages/logs/useIndex.tsx +++ b/src/pages/logs/useIndex.tsx @@ -79,6 +79,10 @@ export default function useIndex() { fetchLogs(); }; + const toggleAdvancedFilters = () => { + setFilters({ showAdvancedFilters: !filters.showAdvancedFilters }); + }; + const toggleSort = (newSortType: LogSortType) => { setCurrentPage(1); @@ -100,6 +104,7 @@ export default function useIndex() { currentPage, filters, setFilters, + toggleAdvancedFilters, toggleSort, }; } diff --git a/src/stores/useEncounterStore.ts b/src/stores/useEncounterStore.ts index ee9c669..42da324 100644 --- a/src/stores/useEncounterStore.ts +++ b/src/stores/useEncounterStore.ts @@ -1,4 +1,4 @@ -import { EncounterState, EnemyType, PlayerData, SBAEvent } from "@/types"; +import { CharacterType, EncounterState, EnemyType, PlayerData, SBAEvent } from "@/types"; import { create } from "zustand"; interface EncounterStore { @@ -10,11 +10,15 @@ interface EncounterStore { sbaChartLen: number; targets: EnemyType[]; selectedTargets: EnemyType[]; + selectedPlayers: string[]; + selectedPlayerTypes: EnemyType[]; players: PlayerData[]; questId: number | null; questTimer: number | null; questCompleted: boolean; setSelectedTargets: (targets: EnemyType[]) => void; + setSelectedPlayers: (playerNames: string[]) => void; + setSelectedPlayerTypes: (playerTypes: CharacterType[]) => void; loadFromResponse: (response: EncounterStateResponse) => void; } @@ -41,11 +45,15 @@ export const useEncounterStore = create((set) => ({ sbaChartLen: 0, targets: [], selectedTargets: [], + selectedPlayers: [], + selectedPlayerTypes: [], players: [], questId: null, questTimer: null, questCompleted: false, setSelectedTargets: (targets: EnemyType[]) => set({ selectedTargets: targets }), + setSelectedPlayers: (playerNames: string[]) => set({ selectedPlayers: playerNames }), + setSelectedPlayerTypes: (playerTypes: CharacterType[]) => set({ selectedPlayerTypes: playerTypes }), loadFromResponse: (response: EncounterStateResponse) => { const filteredPlayers = response.players.filter((player) => player !== null); diff --git a/src/stores/useLogIndexStore.ts b/src/stores/useLogIndexStore.ts index f5fe6df..181d845 100644 --- a/src/stores/useLogIndexStore.ts +++ b/src/stores/useLogIndexStore.ts @@ -11,6 +11,8 @@ export type SearchResult = { logCount: number; enemyIds: number[]; questIds: number[]; + playerIds: string[]; + playerTypes: string[]; }; const DEFAULT_SEARCH_RESULT = { @@ -20,6 +22,8 @@ const DEFAULT_SEARCH_RESULT = { logCount: 0, enemyIds: [], questIds: [], + playerIds: [], + playerTypes: [], }; type LogIndexState = { @@ -42,6 +46,9 @@ export type FilterState = { sortDirection: SortDirection; sortType: LogSortType; questCompletedFilter: boolean | null; + filterByPlayerId: string | null; + filterByPlayerCharacter: string | null; + showAdvancedFilters: boolean; }; const DEFAULT_FILTERS: FilterState = { @@ -50,6 +57,9 @@ const DEFAULT_FILTERS: FilterState = { sortDirection: "desc", sortType: "time", questCompletedFilter: null, + filterByPlayerId: null, + filterByPlayerCharacter: null, + showAdvancedFilters: false, }; export const useLogIndexStore = create((set, get) => ({ @@ -96,6 +106,8 @@ export const useLogIndexStore = create((set, get) => ({ page: currentPage, filterByEnemyId: filters.filterByEnemyId, filterByQuestId: filters.filterByQuestId, + filterByPlayerId: filters.filterByPlayerId, + filterByPlayerCharacter: filters.filterByPlayerCharacter, sortDirection: filters.sortDirection, sortType: filters.sortType, questCompleted: filters.questCompletedFilter,