diff --git a/TarkovMonitor/App.config b/TarkovMonitor/App.config index 9eb8272..283cbf1 100644 --- a/TarkovMonitor/App.config +++ b/TarkovMonitor/App.config @@ -73,6 +73,18 @@ {} + + {} + + + {} + + + {"version":6,"keys":[],"accounts":[],"profiles":[]} + + + + 00:07:10 diff --git a/TarkovMonitor/Blazor/AppLayout.razor b/TarkovMonitor/Blazor/AppLayout.razor index d74099f..3e0cbc5 100644 --- a/TarkovMonitor/Blazor/AppLayout.razor +++ b/TarkovMonitor/Blazor/AppLayout.razor @@ -21,11 +21,11 @@
- +
- + + to check past EFT logs, or launch EFT with the profile you want to use. + + } + else + { +
+ @foreach (var profile in MatchingProfiles) + { + var disabled = !IsReassign && profile.HasBoundKey; + var stateLabel = disabled + ? "Already assigned" + : profile.HasBoundKey + ? "Swap" + : IsReassign + ? "Move" + : "Select"; + + } +
+ } + + @if (selectedProfile == null && !string.IsNullOrWhiteSpace(scanMessage)) + { + @scanMessage + } + + + @if (selectedProfile == null) + { + Cancel + + + Scan profiles + + + } + else + { + Back + @ActionLabel + } + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string KeyPrefix { get; set; } = ""; + [Parameter] public string KeyDisplayName { get; set; } = ""; + [Parameter] public bool KeyIsBound { get; set; } + [Parameter] public bool RequiresVerification { get; set; } + [Parameter] public string BoundAccountId { get; set; } = ""; + [Parameter] public string BoundProfileId { get; set; } = ""; + [Parameter] public EftSessionMode BoundSessionMode { get; set; } + [Parameter] public string ProfileNickname { get; set; } = ""; + [Parameter] public bool IsReassign { get; set; } + + private bool scanning; + private string scanMessage = ""; + private TarkovTracker.OrgProfileSummary? selectedProfile; + private IReadOnlyList profiles = Array.Empty(); + + private List MatchingProfiles => profiles + .Where(profile => string.Equals( + KeyPrefix, + TarkovTracker.GetPrefixForSessionMode(profile.SessionMode), + StringComparison.OrdinalIgnoreCase)) + .Where(profile => !IsReassign + || !KeyIsBound + || !string.Equals(profile.AccountId, BoundAccountId, StringComparison.Ordinal) + || !string.Equals(profile.ProfileId, BoundProfileId, StringComparison.Ordinal) + || profile.SessionMode != BoundSessionMode) + .OrderByDescending(profile => profile.IsCurrent) + .ThenByDescending(profile => profile.LastSeenUtc) + .ToList(); + + protected override void OnInitialized() + { + ReloadProfiles(); + } + + private void ReloadProfiles() + { + profiles = TarkovTracker.GetKnownOrgProfiles(); + } + + private void Cancel() => MudDialog.Cancel(); + + private void Select(TarkovTracker.OrgProfileSummary profile) + { + selectedProfile = profile; + } + + private void Back() => selectedProfile = null; + + private void Confirm() + { + if (selectedProfile != null) + { + MudDialog.Close(DialogResult.Ok(selectedProfile)); + } + } + + private bool WillSwap => IsReassign && selectedProfile?.HasBoundKey == true; + + private string NoMatchingProfilesLead => + $"{(IsReassign ? "No other" : "No")} saved {KeyDisplayName} profiles are available."; + + private string ActionLabel => WillSwap ? "Swap" : IsReassign ? "Reassign" : "Assign"; + + private async Task ScanProfiles() + { + scanning = true; + scanMessage = ""; + try + { + var discovered = await Task.Run(eft.DiscoverProfiles); + var changed = TarkovTracker.SaveDiscoveredOrgProfiles(discovered); + ReloadProfiles(); + scanMessage = discovered.Count == 0 + ? "No EFT profiles were found in past logs." + : $"Found {discovered.Count} profile{(discovered.Count == 1 ? "" : "s")} and updated {changed} saved entr{(changed == 1 ? "y" : "ies")}."; + } + catch (Exception ex) + { + scanMessage = $"Could not scan profiles: {ex.Message}"; + } + finally + { + scanning = false; + } + } + + private static string AccountLabel(TarkovTracker.OrgProfileSummary profile) + { + return string.IsNullOrWhiteSpace(profile.AccountNickname) + ? $"Account {profile.AccountId}" + : profile.AccountNickname; + } + + private static string LastPlayed(TarkovTracker.OrgProfileSummary profile) + { + return $"Last played: {LastPlayedValue(profile)}"; + } + + private static string LastPlayedValue(TarkovTracker.OrgProfileSummary profile) + { + return profile.LastSeenUtc is { } lastSeen + ? lastSeen.ToLocalTime().ToString("MMM d, yyyy h:mm tt") + : "Unknown"; + } + + private static string ShortProfileId(string profileId) + { + var suffix = profileId.Length <= 6 ? profileId : profileId[^6..]; + return $"Profile …{suffix}"; + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/AssignTarkovTrackerKeyDialog.razor.css b/TarkovMonitor/Blazor/Pages/Settings/AssignTarkovTrackerKeyDialog.razor.css new file mode 100644 index 0000000..ea256c3 --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/AssignTarkovTrackerKeyDialog.razor.css @@ -0,0 +1,218 @@ +.tracker-profile-scan-state { + display: flex; + align-items: center; + gap: 0.6rem; + min-height: 5rem; + justify-content: center; +} + +.tracker-inline-scan-action { + appearance: none; + margin: 0; + padding: 0; + border: 0; + color: var(--mud-palette-info); + background: transparent; + font: inherit; + text-decoration: underline; + text-underline-offset: 0.12em; + cursor: pointer; +} + +.tracker-inline-scan-action:hover { + text-decoration-thickness: 2px; +} + +.tracker-inline-scan-action:focus-visible { + border-radius: 2px; + outline: 1px solid currentColor; + outline-offset: 2px; +} + +.tracker-profile-picker { + display: flex; + max-height: clamp(12rem, 46vh, 21rem); + flex-direction: column; + gap: 0.45rem; + overflow-y: auto; + padding: 0.2rem 0.35rem 0.2rem 0.2rem; + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 11px; + background: rgba(0, 0, 0, 0.12); + scrollbar-color: rgba(120, 174, 251, 0.72) rgba(255, 255, 255, 0.04); + scrollbar-width: thin; +} + +.tracker-profile-guidance { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.42rem; + margin-bottom: 0.65rem; +} + +.tracker-profile-guidance-item { + display: flex; + min-width: 0; + flex-direction: column; + gap: 0.02rem; + padding: 0.38rem 0.52rem; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 8px; + color: rgba(255, 255, 255, 0.55); + background: rgba(255, 255, 255, 0.02); + font-size: 0.67rem; + line-height: 1.25; +} + +.tracker-profile-guidance-item b { + color: rgba(255, 255, 255, 0.83); + font-size: 0.7rem; +} + +.tracker-profile-confirm { + padding: 0.25rem 0; +} + +.tracker-profile-confirm-title { + margin-bottom: 0.65rem; + color: rgba(255, 255, 255, 0.88); +} + +.tracker-profile-confirm-grid { + display: grid; + grid-template-columns: 6.5rem minmax(0, 1fr); + gap: 0.34rem 0.75rem; + margin: 0; + padding: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.11); + border-radius: 10px; + background: rgba(255, 255, 255, 0.025); +} + +.tracker-profile-confirm-grid dt { + color: rgba(255, 255, 255, 0.57); + font-size: 0.75rem; + font-weight: 600; +} + +.tracker-profile-confirm-grid dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + color: rgba(255, 255, 255, 0.9); + font-size: 0.8rem; + font-weight: 650; +} + +.tracker-profile-choice { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.65rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.11); + border-radius: 10px; + color: inherit; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.015)); + text-align: left; + cursor: pointer; + transition: border-color 150ms ease, background-color 150ms ease; +} + +.tracker-profile-choice:not(:disabled):hover, +.tracker-profile-choice:not(:disabled):focus-visible { + border-color: rgba(104, 164, 255, 0.6); + background-color: rgba(104, 164, 255, 0.08); + outline: none; +} + +.tracker-profile-choice--disabled { + cursor: not-allowed; + opacity: 0.64; +} + +.tracker-profile-choice-main { + display: flex; + min-width: 0; + flex-direction: column; +} + +.tracker-profile-choice-title { + overflow: hidden; + font-size: 0.9rem; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tracker-profile-choice-id, +.tracker-profile-choice-meta { + color: rgba(255, 255, 255, 0.62); + font-size: 0.72rem; +} + +.tracker-profile-choice-meta { + display: flex; + flex-wrap: wrap; + gap: 0.25rem 0.65rem; + margin-top: 0.16rem; +} + +.tracker-profile-mode { + color: #acd0ff; + font-weight: 700; +} + +.tracker-profile-choice-state { + display: flex; + flex: 0 0 auto; + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; +} + +.tracker-profile-state { + padding: 0.08rem 0.38rem; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 999px; + font-size: 0.65rem; + font-weight: 700; +} + +.tracker-profile-state--current { + border-color: rgba(87, 194, 142, 0.46); + color: #8ce0b8; + background: rgba(48, 146, 99, 0.14); +} + +.tracker-profile-state--bound { + border-color: rgba(212, 148, 255, 0.4); + color: #deb0ff; + background: rgba(139, 74, 180, 0.14); +} + +.tracker-profile-state--available { + border-color: rgba(111, 166, 255, 0.42); + color: #a9cbff; + background: rgba(61, 112, 194, 0.13); +} + +@media (max-width: 500px) { + .tracker-profile-guidance { + grid-template-columns: 1fr; + } + + .tracker-profile-choice { + align-items: flex-start; + flex-direction: column; + } + + .tracker-profile-choice-state { + flex-direction: row; + } + + .tracker-profile-confirm-grid { + grid-template-columns: 5.5rem minmax(0, 1fr); + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/ConfirmTarkovTrackerKeyActionDialog.razor b/TarkovMonitor/Blazor/Pages/Settings/ConfirmTarkovTrackerKeyActionDialog.razor new file mode 100644 index 0000000..96537ad --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/ConfirmTarkovTrackerKeyActionDialog.razor @@ -0,0 +1,60 @@ + + +
+ + + @Heading + @Message + +
+ +
+ + API key +
+
+
+
Mode
+
@Mode
+
+
+
API key
+
@MaskedToken
+
+
+
Account ID
+
@AccountId
+
+
+
+ + Cancel + @ConfirmLabel + +
+ +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string Heading { get; set; } = "Confirm this change"; + [Parameter] public string Message { get; set; } = "Check the API key details before continuing."; + [Parameter] public string Mode { get; set; } = ""; + [Parameter] public string MaskedToken { get; set; } = ""; + [Parameter] public string AccountId { get; set; } = ""; + [Parameter] public string ConfirmLabel { get; set; } = "Confirm"; + [Parameter] public string Icon { get; set; } = Icons.Material.Filled.Warning; + [Parameter] public bool IsDestructive { get; set; } + + private string ModeBadgeClass => Mode.Contains("PVE", StringComparison.OrdinalIgnoreCase) + ? "tracker-dialog-mode-badge--pve" + : Mode.Contains("Seasonal", StringComparison.OrdinalIgnoreCase) + ? "tracker-dialog-mode-badge--seasonal" + : ""; + + private void Cancel() => MudDialog.Cancel(); + private void Confirm() => MudDialog.Close(DialogResult.Ok(true)); +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/EditTarkovTrackerKeyDialog.razor b/TarkovMonitor/Blazor/Pages/Settings/EditTarkovTrackerKeyDialog.razor new file mode 100644 index 0000000..99b61bf --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/EditTarkovTrackerKeyDialog.razor @@ -0,0 +1,131 @@ + + +
+ + + Linked to this EFT profile. + Tarkov Monitor uses this key when the account, profile, and game mode below are active. + +
+ +
+ + Binding details +
+
+
+ Mode + @Mode +
+
+ Account ID + @AccountId +
+
+ API key + @MaskedToken +
+
+ Last played + @LastPlayed +
+
+ Profile ID + @ProfileId +
+
+ Profile nickname + + + @(HasProfileNickname ? ProfileNickname : "Character name") + + + @(HasProfileNickname ? "Rename" : "Set name") + + +
+
+ @if (IsQuarantined) + { + + This key has conflicting or invalid profile information. Remove it and import it again before assigning it. + + } + @if (KeyChangesDisabled) + { + @KeyChangesDisabledReason + } + +
+ + Manage key +
+
+ + + +
+
+ + Close + +
+ +@code { + public enum KeyAction + { + Reassign, + SetProfileNickname, + Unbind, + Remove, + } + + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string Mode { get; set; } = ""; + [Parameter] public string MaskedToken { get; set; } = ""; + [Parameter] public string AccountId { get; set; } = ""; + [Parameter] public string ProfileId { get; set; } = ""; + [Parameter] public string ProfileNickname { get; set; } = ""; + [Parameter] public string LastPlayed { get; set; } = "Unknown"; + [Parameter] public bool IsQuarantined { get; set; } + [Parameter] public bool KeyChangesDisabled { get; set; } + [Parameter] public string KeyChangesDisabledReason { get; set; } = "Saved key storage must be repaired before this action is available."; + + private string ModeBadgeClass => Mode.Contains("PVE", StringComparison.OrdinalIgnoreCase) + ? "tracker-dialog-mode-badge--pve" + : Mode.Contains("Seasonal", StringComparison.OrdinalIgnoreCase) + ? "tracker-dialog-mode-badge--seasonal" + : ""; + + private bool HasProfileNickname => !string.IsNullOrWhiteSpace(ProfileNickname); + private string ReassignTitle => KeyChangesDisabled + ? KeyChangesDisabledReason + : IsQuarantined + ? "Repair or remove this invalid binding before reassigning it" + : "Move this key to another saved profile, or swap it with a key already assigned there"; + + private void Close() => MudDialog.Close(); + private void Reassign() => MudDialog.Close(DialogResult.Ok(KeyAction.Reassign)); + private void SetProfileNickname() => MudDialog.Close(DialogResult.Ok(KeyAction.SetProfileNickname)); + private void Unbind() => MudDialog.Close(DialogResult.Ok(KeyAction.Unbind)); + private void Remove() => MudDialog.Close(DialogResult.Ok(KeyAction.Remove)); +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/EditTarkovTrackerKeyDialog.razor.css b/TarkovMonitor/Blazor/Pages/Settings/EditTarkovTrackerKeyDialog.razor.css new file mode 100644 index 0000000..8ca936f --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/EditTarkovTrackerKeyDialog.razor.css @@ -0,0 +1,91 @@ +.tracker-key-binding-grid { + display: grid; + overflow: hidden; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 0; + border: 1px solid rgba(255, 255, 255, 0.11); + border-radius: 11px; + background: linear-gradient(145deg, rgba(255, 255, 255, 0.038), rgba(0, 0, 0, 0.11)); +} + +.tracker-key-binding-item { + display: flex; + min-width: 0; + min-height: 1.68rem; + flex-direction: column; + justify-content: center; + gap: 0; + padding: 0.08rem 0.5rem; + border-right: 1px solid rgba(255, 255, 255, 0.065); + border-bottom: 1px solid rgba(255, 255, 255, 0.065); +} + +.tracker-key-binding-item:nth-child(2n) { + border-right: 0; +} + +.tracker-key-binding-item--wide { + grid-column: 1 / -1; + border-right: 0; + border-bottom: 0; +} + +.tracker-key-binding-label { + color: rgba(255, 255, 255, 0.55); + font-size: 0.64rem; + font-weight: 650; +} + +.tracker-key-binding-value { + min-width: 0; + overflow-wrap: anywhere; + color: rgba(255, 255, 255, 0.92); + font-size: 0.73rem; + font-weight: 650; +} + +.tracker-key-binding-value--muted { + color: rgba(255, 255, 255, 0.46); + font-weight: 500; +} + +.tracker-key-nickname-item { + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.tracker-key-nickname-value { + display: flex; + min-width: 0; + align-items: center; + justify-content: flex-end; + gap: 0.35rem; +} + +.tracker-key-edit-token { + font-family: Consolas, monospace; + letter-spacing: 0.03em; +} + +@media (max-width: 440px) { + .tracker-key-binding-grid { + grid-template-columns: 1fr; + } + + .tracker-key-binding-item, + .tracker-key-binding-item:nth-child(2n) { + grid-column: 1; + border-right: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.065); + } + + .tracker-key-binding-item--wide { + border-bottom: 0; + } + + .tracker-key-nickname-item { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/ImportTarkovTrackerKeyDialog.razor b/TarkovMonitor/Blazor/Pages/Settings/ImportTarkovTrackerKeyDialog.razor new file mode 100644 index 0000000..9945721 --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/ImportTarkovTrackerKeyDialog.razor @@ -0,0 +1,165 @@ +@implements IDisposable +@inject MessageLog MessageLog + + + +
+ + + Add a TarkovTracker.org API key. + Tarkov Monitor verifies the key and identifies its game mode before saving it as Unassigned. + +
+ +
+ + API key +
+
+ + Paste the full key exactly as copied from TarkovTracker.org. +
+ @if (!string.IsNullOrWhiteSpace(errorMessage)) + { + @errorMessage + } +
+ + Cancel + + @if (saving) + { + + Verifying + } + else if (cooldownSeconds > 0) + { + @($"Try again in {cooldownSeconds}s") + } + else + { + Verify and save + } + + +
+ +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + [Parameter] public string InitialApiKey { get; set; } = ""; + + private string apiKey = ""; + private string errorMessage = ""; + private Severity messageSeverity = Severity.Error; + private bool saving; + private int cooldownSeconds; + private CancellationTokenSource? cooldownCancellation; + private bool showKey; + private InputType passwordInput = InputType.Password; + private string passwordIcon = Icons.Material.Filled.VisibilityOff; + + protected override void OnInitialized() + { + apiKey = InitialApiKey; + StartCooldownCountdown(); + } + + private void Cancel() => MudDialog.Cancel(); + + private void ToggleKeyVisibility() + { + showKey = !showKey; + passwordInput = showKey ? InputType.Text : InputType.Password; + passwordIcon = showKey ? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff; + } + + private async Task Save() + { + saving = true; + errorMessage = ""; + messageSeverity = Severity.Error; + try + { + var importedToken = await TarkovTracker.ImportOrgToken(apiKey); + MudDialog.Close(DialogResult.Ok(importedToken)); + } + catch (Exception ex) + { + errorMessage = ex.Message; + messageSeverity = ex is TarkovTracker.DuplicateImportedTokenException + ? Severity.Info + : Severity.Error; + StartCooldownCountdown(); + if (cooldownSeconds > 0) + { + var report = string.Join(Environment.NewLine, + "TarkovTracker.org API key validation failed.", + $"UTC time: {DateTimeOffset.UtcNow:O}", + $"Tarkov Monitor version: {System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}", + "Operation: Verify and save TarkovTracker.org API key", + "Endpoint: GET https://api.tarkovtracker.org/token", + $"Key type: {TarkovTracker.GetPrefixDisplayName(TarkovTracker.GetTokenPrefix(apiKey))}", + $"Error type: {ex.GetType().Name}", + $"Error: {ex.Message}", + "The API key was intentionally excluded from this report."); + MessageLog.AddMessage(report, "exception"); + errorMessage += " A sanitized error report was added to Messages so you can copy it when reporting the problem."; + } + } + finally + { + saving = false; + } + } + + private void StartCooldownCountdown() + { + cooldownSeconds = TarkovTracker.GetImportValidationCooldownSeconds(); + if (cooldownSeconds <= 0 || cooldownCancellation is { IsCancellationRequested: false }) + { + return; + } + + cooldownCancellation = new CancellationTokenSource(); + _ = RunCooldownCountdown(cooldownCancellation.Token); + } + + private async Task RunCooldownCountdown(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + cooldownSeconds = TarkovTracker.GetImportValidationCooldownSeconds(); + await InvokeAsync(StateHasChanged); + if (cooldownSeconds <= 0) + { + break; + } + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + } + catch (OperationCanceledException) + { + } + finally + { + cooldownCancellation?.Dispose(); + cooldownCancellation = null; + } + } + + public void Dispose() + { + cooldownCancellation?.Cancel(); + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/SetTarkovTrackerAccountNicknameDialog.razor b/TarkovMonitor/Blazor/Pages/Settings/SetTarkovTrackerAccountNicknameDialog.razor new file mode 100644 index 0000000..0491236 --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/SetTarkovTrackerAccountNicknameDialog.razor @@ -0,0 +1,67 @@ + + +
+ + + @(HasNickname ? "Change this account's nickname." : "Set a nickname for this EFT account.") + This nickname identifies Account ID @AccountId and appears on every profile assigned to the account. It does not change your EFT display name. + +
+ +
+ + Account nickname +
+
+ + Use 1-32 characters. You can change it later. +
+ @if (!string.IsNullOrWhiteSpace(errorMessage)) + { + @errorMessage + } +
+ + Cancel + + @(HasNickname ? "Save name" : "Set name") + + +
+ +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string ExistingNickname { get; set; } = ""; + [Parameter] public string AccountId { get; set; } = ""; + + private string nickname = ""; + private string errorMessage = ""; + private bool HasNickname => !string.IsNullOrWhiteSpace(ExistingNickname); + + protected override void OnInitialized() + { + nickname = ExistingNickname; + } + + private void Cancel() => MudDialog.Cancel(); + + private void Save() + { + var normalizedNickname = nickname.Trim(); + if (normalizedNickname.Length is < 1 or > 32 || normalizedNickname.Any(char.IsControl)) + { + errorMessage = "Enter a name between 1 and 32 characters."; + return; + } + MudDialog.Close(DialogResult.Ok(normalizedNickname)); + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/SetTarkovTrackerProfileNicknameDialog.razor b/TarkovMonitor/Blazor/Pages/Settings/SetTarkovTrackerProfileNicknameDialog.razor new file mode 100644 index 0000000..256e6a9 --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/SetTarkovTrackerProfileNicknameDialog.razor @@ -0,0 +1,67 @@ + + +
+ + + @(HasNickname ? $"Change this {Mode} profile's nickname." : $"Set a nickname for this {Mode} profile.") + If you reassign this API key, the nickname moves with it. Unbinding the key clears the nickname. It does not change your EFT character name. + +
+ +
+ + Profile nickname +
+
+ + Use 1-32 characters. You can change it while the key remains assigned. +
+ @if (!string.IsNullOrWhiteSpace(errorMessage)) + { + @errorMessage + } +
+ + Cancel + + @(HasNickname ? "Save name" : "Set name") + + +
+ +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string ExistingNickname { get; set; } = ""; + [Parameter] public string Mode { get; set; } = ""; + + private string nickname = ""; + private string errorMessage = ""; + private bool HasNickname => !string.IsNullOrWhiteSpace(ExistingNickname); + + protected override void OnInitialized() + { + nickname = ExistingNickname; + } + + private void Cancel() => MudDialog.Cancel(); + + private void Save() + { + var normalizedNickname = nickname.Trim(); + if (normalizedNickname.Length is < 1 or > 32 || normalizedNickname.Any(char.IsControl)) + { + errorMessage = "Enter a name between 1 and 32 characters."; + return; + } + MudDialog.Close(DialogResult.Ok(normalizedNickname)); + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/Settings.razor b/TarkovMonitor/Blazor/Pages/Settings/Settings.razor index ced1186..cf4c10a 100644 --- a/TarkovMonitor/Blazor/Pages/Settings/Settings.razor +++ b/TarkovMonitor/Blazor/Pages/Settings/Settings.razor @@ -7,15 +7,48 @@ @inject IDialogService DialogService @inject LocalizationService LocalizationService @inject TimersManager timersManager +@inject MainBlazorUI WindowHost @layout AppLayout @implements IDisposable - + - @LocalizationService.GetString("Notifications") + @LocalizationService.GetString("Language")
- + + @foreach (var language in AvailableLanguages) + { + @language.Name + } + +
+
+
+ + + + @LocalizationService.GetString("WindowBehavior") +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + + + @LocalizationService.GetString("Notifications") +
+ @foreach (var device in PlaybackDevices) { @device.Value @@ -23,33 +56,33 @@
+ -
+ -
+ -
+ -
+ -
+ -
+ - - + @if (TraderFence != null) { @foreach (TarkovDev.TraderReputationLevel lvl in TraderFence.reputationLevels) @@ -60,52 +93,20 @@
- +
- + - Timers - - Shows a movable timer inside Tarkov Monitor after deployment finishes. It is hidden while viewing the Timers tab. + Timers + + Shows a movable timer in Tarkov Monitor after deployment finishes. It stays hidden on the Timers page. Displayed timers - At least one timer must remain selected. Scav cooldown is not currently available in the floating panel. - - - - - - @LocalizationService.GetString("Language") -
- - @foreach (var language in AvailableLanguages) - { - @language.Name - } - -
-
-
- - - - @LocalizationService.GetString("WindowBehavior") -
- -
-
- -
-
- -
-
- -
+ Keep at least one timer selected. Scav cooldown is not available in the floating panel.
@@ -116,10 +117,10 @@ - - - @LocalizationService.GetString("TarkovTracker") - + + + @LocalizationService.GetString("TarkovTracker") + @foreach (var pair in TarkovTracker.Domains) { @pair.Value @@ -134,24 +135,217 @@ @if (!IsRetiredTrackerService) { @LocalizationService.GetString("GetAToken") - @LocalizationService.GetString("TestToken") - @LocalizationService.GetString("CurrentProfile"): + @LocalizationService.GetString("CurrentProfile"):  @if (HasCurrentProfileRoute) { - @CurrentProfileName (@GameWatcher.CurrentProfile.Type) + @CurrentProfileName (@GetProfileTypeDisplayName(GameWatcher.CurrentProfile.Type)) } else { No EFT profile detected. } - + Add a TarkovTracker.org API key. Tarkov Monitor verifies the key and its game mode before it can be assigned to an EFT profile. + @foreach (var warning in TrackerStorageWarnings) + { + @warning + } +
+ @if (CanImportOrgKey) + { + Import API Key + } + else + { + + Import API Key + + } + + Scan profiles + +
+ +
+
+ + Tarkov Monitor manages API keys automatically. Only change an assignment if you understand how EFT profiles and game modes are linked. +
+ +
+ + +
+
+ + Verified, but unassigned. Tarkov Monitor automatically assigns one eligible key to the first complete EFT account, profile, and game mode it observes. If more than one key is eligible, or you intentionally unbound a key, click Assign for the manual fallback. +
+ @if (PendingOrgKeys.Count == 0) + { + No unassigned API key is stored. + } + @foreach (var key in PendingOrgKeys) + { +
+
+
+
+ @key.DisplayName + Not assigned + + @(key.IsVerified ? "Verified" : "Verify on assign") + + @if (key.IsAutoBindBlocked) + { + + MANUAL ASSIGNMENT ONLY + + } +
+ @key.MaskedToken + @if (key.IsLegacyRecovery) + { + Previously saved TarkovTracker.org key + } +
+
+ @if (!OrgKeyChangesEnabled || key.IsQuarantined) + { + + Assign + + } + else + { + Assign + } + @if (OrgKeyChangesEnabled) + { + Remove + } + else + { + + Remove + + } +
+
+ @if (key.IsAutoBindBlocked || key.HasPendingConflict || key.IsQuarantined) + { + @PendingKeyIssue(key) + } +
+ } +
+
+
+ +
+ + +
+
+ + Verified and assigned. Nothing else is required. Tarkov Monitor uses the key linked to your active EFT account, profile, and game mode. If it was assigned incorrectly—such as when using multiple EFT accounts—open Edit and choose Reassign. Set or rename account and profile nicknames as needed. +
+ @if (BoundOrgKeys.Count == 0) + { + No TarkovTracker.org API keys are assigned. + } + else + { + @if (BoundKeyFilterEnabled) + { + + } + + } +
+
+
+
}
- + - @LocalizationService.GetString("TarkovDevWebsiteRemote") + @TarkovDevWebsiteRemoteTitle
@@ -162,7 +356,7 @@
- + @LocalizationService.GetString("None") @foreach (var map in TarkovDev.Maps) { @@ -172,10 +366,10 @@ - + - @LocalizationService.GetString("LogsFolder")@LocalizationService.GetString("DontChangeThisUnlessYouReallyKnowWhatYoureDoing") - + @LocalizationService.GetString("LogsFolder")@LocalizationService.GetString("DontChangeThisUnlessYouReallyKnowWhatYoureDoing") + @if (Properties.Settings.Default.customLogsPath != null && Properties.Settings.Default.customLogsPath != "") {
@@ -185,11 +379,11 @@ - + - @LocalizationService.GetString("InitialSetup") + @LocalizationService.GetString("InitialSetup")
- @LocalizationService.GetString("ReadPastLogs") + @LocalizationService.GetString("ReadPastLogs")
@@ -204,6 +398,18 @@ private Dictionary PlaybackDevices; private List<(string Code, string Name)> AvailableLanguages = new(); private string resolvedCustomLogsFolder = ""; + private bool BoundKeyFilterEnabled => false; + private string boundKeyFilter = ""; + // Drawer state survives navigation within this process, but is never persisted. + private static bool pendingDrawerExpanded; + private static bool boundDrawerExpanded; + private bool scanningOrgProfiles; + private static readonly HashSet expandedAccountIds = new(StringComparer.Ordinal); + + private sealed record OrgAccountGroup( + string AccountId, + string Nickname, + List Keys); private TarkovDev.Trader? TraderFence { get @@ -212,7 +418,35 @@ } } - private string currentProfileName = ""; + private string currentProfileName = GameWatcher.CurrentProfile.HasTarkovDevPlayerRoute + ? GameWatcher.CurrentProfile.AccountId + : ""; + + private RenderFragment TarkovDevWebsiteRemoteTitle => builder => + { + var sequence = 0; + var title = LocalizationService.GetString("TarkovDevWebsiteRemote"); + const string linkText = "Tarkov.dev"; + var linkStart = title.IndexOf(linkText, StringComparison.OrdinalIgnoreCase); + if (linkStart < 0) + { + builder.AddContent(sequence++, title); + return; + } + + builder.AddContent(sequence++, title[..linkStart]); + builder.OpenElement(sequence++, "a"); + builder.AddAttribute(sequence++, "href", "https://tarkov.dev"); + builder.AddAttribute(sequence++, "target", "_blank"); + builder.AddAttribute(sequence++, "rel", "noopener noreferrer"); + builder.AddAttribute(sequence++, "class", "tm-section-link"); + builder.AddContent(sequence++, title.Substring(linkStart, linkText.Length)); + builder.CloseElement(); + builder.OpenElement(sequence++, "span"); + builder.AddAttribute(sequence++, "class", "tm-section-suffix"); + builder.AddContent(sequence++, title[(linkStart + linkText.Length)..].TrimStart()); + builder.CloseElement(); + }; private string CurrentProfileName { get { @@ -220,42 +454,32 @@ } } - private bool HasCurrentProfileRoute => - !string.IsNullOrWhiteSpace(GameWatcher.CurrentProfile.Id) - && !string.IsNullOrWhiteSpace(GameWatcher.CurrentProfile.AccountId) - && GameWatcher.CurrentProfile.Type != ProfileType.Unknown; + private static string GetProfileTypeDisplayName(ProfileType profileType) => profileType switch + { + ProfileType.PVE => "PVE", + ProfileType.Regular => "PVP", + ProfileType.PvpSeason => "Seasonal PVP", + _ => profileType.ToString(), + }; private async Task UpdateProfileName() { - if (!HasCurrentProfileRoute) + var profile = GameWatcher.CurrentProfile.Snapshot(); + if (!eft.IsGameRunning || !profile.HasTarkovDevPlayerRoute) { currentProfileName = ""; - await InvokeAsync(StateHasChanged); return; } - var profile = new Profile - { - Id = GameWatcher.CurrentProfile.Id, - AccountId = GameWatcher.CurrentProfile.AccountId, - Type = GameWatcher.CurrentProfile.Type, - }; currentProfileName = profile.AccountId; var profileName = await TarkovDev.GetPlayerName(profile); - if (HasCurrentProfileRoute - && string.Equals(GameWatcher.CurrentProfile.Id, profile.Id, StringComparison.Ordinal) + if (GameWatcher.CurrentProfile.HasTarkovDevPlayerRoute && string.Equals(GameWatcher.CurrentProfile.AccountId, profile.AccountId, StringComparison.Ordinal) - && GameWatcher.CurrentProfile.Type == profile.Type) + && string.Equals(GameWatcher.CurrentProfile.Id, profile.Id, StringComparison.Ordinal) + && GameWatcher.CurrentProfile.SessionMode == profile.SessionMode) { currentProfileName = profileName; } - - await InvokeAsync(StateHasChanged); - } - - private async void OnProfileChanged(object? sender, ProfileEventArgs e) - { - await UpdateProfileName(); } protected override void OnInitialized() @@ -264,8 +488,10 @@ PlaybackDevices = Sound.GetPlaybackDevices(); AvailableLanguages = LocalizationService.GetAvailableLanguages(); LocalizationService.LanguageChanged += OnLanguageChanged; - eft.ProfileChanged += OnProfileChanged; ResolveCustomLogsFolder(); + eft.GameStarted += OnGameClientStateChanged; + eft.GameStopped += OnGameClientStateChanged; + eft.ProfileChanged += OnGameClientStateChanged; _ = UpdateProfileName(); } @@ -286,7 +512,18 @@ public void Dispose() { LocalizationService.LanguageChanged -= OnLanguageChanged; - eft.ProfileChanged -= OnProfileChanged; + eft.GameStarted -= OnGameClientStateChanged; + eft.GameStopped -= OnGameClientStateChanged; + eft.ProfileChanged -= OnGameClientStateChanged; + } + + private void OnGameClientStateChanged(object? sender, EventArgs e) + { + _ = InvokeAsync(async () => + { + await UpdateProfileName(); + StateHasChanged(); + }); } private void OnLanguageChanged(object? sender, EventArgs e) @@ -306,7 +543,7 @@ if (Properties.Settings.Default.language != value) { LocalizationService.SetCulture(value); - messageLog.AddMessage("Please restart this application to complete the language change.", "warning"); + messageLog.AddMessage("Restart Tarkov Monitor to finish applying the language change.", "warning"); //Properties.Settings.Default.language = value; // handled by LocalizationService.SetCulture //Properties.Settings.Default.Save(); //InvokeAsync(StateHasChanged); // should be unnecessary since we're handling the OnLanguageChanged event @@ -549,19 +786,6 @@ } } - //public bool TestTokenButtonDisabled { get; set; } - public bool TestTokenButtonDisabled - { - get - { - return TarkovTracker.IsLegacyService || TarkovTracker.GetToken(GameWatcher.CurrentProfile.Id).Length != 22; - } - set - { - - } - } - public TimeSpan RunthroughTime { get @@ -590,28 +814,11 @@ void ReadPastLogs() { - var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.ExtraLarge, FullWidth = true }; - DialogService.ShowAsync("Read Past Log Files", options); + var options = new DialogOptions { CloseOnEscapeKey = false, BackdropClick = false, MaxWidth = MaxWidth.ExtraLarge, FullWidth = true }; + DialogService.ShowAsync("Read past log files", options); } private string trackerIcon = ""; - bool tokenShow; - InputType PasswordInput = InputType.Password; - string PasswordInputIcon = Icons.Material.Filled.VisibilityOff; - public string TarkovTrackerToken - { - get - { - return TarkovTracker.IsLegacyService ? "" : TarkovTracker.GetToken(GameWatcher.CurrentProfile.Id); - } - set - { - if (!TarkovTracker.IsLegacyService) - { - TarkovTracker.SetToken(GameWatcher.CurrentProfile.Id, value); - } - } - } public string TarkovTrackerDomain { get @@ -620,32 +827,75 @@ } set { - if (Properties.Settings.Default.tarkovTrackerDomain == value) + var previousDomain = Properties.Settings.Default.tarkovTrackerDomain; + var previousServiceName = TarkovTracker.Domains.TryGetValue(previousDomain, out var serviceName) + ? serviceName + : previousDomain; + if (previousDomain == value) { return; } - Properties.Settings.Default.tarkovTrackerDomain = value; - TarkovTracker.ResetActiveState(); - TarkovTracker.InitAPI(); - Properties.Settings.Default.Save(); - if (!TarkovTracker.IsLegacyService) + + WindowHost.BeginTrackerStatusTransition(); + var switchSucceeded = false; + try { - _ = RevalidateTrackerProfile(); + TarkovTracker.ResetActiveProfile(); + Properties.Settings.Default.tarkovTrackerDomain = value; + TarkovTracker.InitAPI(); + Properties.Settings.Default.Save(); + switchSucceeded = true; + } + catch (Exception switchError) + { + Properties.Settings.Default.tarkovTrackerDomain = previousDomain; + try + { + TarkovTracker.InitAPI(); + Properties.Settings.Default.Save(); + messageLog.AddMessage( + $"Tarkov Tracker service change was canceled and {previousServiceName} was restored: {switchError.Message}", + "warning"); + } + catch (Exception rollbackError) + { + TarkovTracker.ResetActiveProfile(); + messageLog.AddMessage( + $"Tarkov Tracker service change failed and could not be fully restored. Restart Tarkov Monitor before using Tarkov Tracker. Change error: {switchError.Message} Restore error: {rollbackError.Message}", + "exception"); + return; + } + } + finally + { + WindowHost.CompleteTrackerStatusTransition(); + } + + if (switchSucceeded || Properties.Settings.Default.tarkovTrackerDomain == previousDomain) + { + _ = ReactivateCurrentTrackerProfile(); } } } - private async Task RevalidateTrackerProfile() + private async Task ReactivateCurrentTrackerProfile() { - if (string.IsNullOrWhiteSpace(GameWatcher.CurrentProfile.Id)) + if (!eft.IsGameRunning + || !GameWatcher.CurrentProfile.SupportsTarkovTrackerWrites + || string.IsNullOrWhiteSpace(GameWatcher.CurrentProfile.Id)) { + TarkovTracker.DeactivateProfile(); return; } var startedUtc = DateTime.UtcNow; try { - await TarkovTracker.SetProfile(GameWatcher.CurrentProfile.Id); + await TarkovTracker.SetProfile(GameWatcher.CurrentProfile); + } + catch (ProfileActivationSupersededException) + { + // A newer profile/key/mode activation owns the result. } catch (Exception ex) { @@ -658,50 +908,535 @@ "tarkovtracker.io", StringComparison.OrdinalIgnoreCase); + private bool HasCurrentProfileRoute => eft.IsGameRunning && GameWatcher.CurrentProfile.HasTarkovDevPlayerRoute; + private string CurrentProfileUrl => HasCurrentProfileRoute - ? $"https://tarkov.dev/players/{GameWatcher.CurrentProfile.Type.ToString().ToLower()}/{GameWatcher.CurrentProfile.AccountId}" - : "#"; + ? $"https://tarkov.dev/players/{GameWatcher.CurrentProfile.TarkovDevPlayerMode}/{GameWatcher.CurrentProfile.AccountId}" + : ""; + + private IReadOnlyList OrgKeys => TarkovTracker.GetOrgKeys(); + private IReadOnlyList TrackerStorageWarnings => TarkovTracker.GetStorageWarnings(); + private bool OrgKeyChangesEnabled => TarkovTracker.CanChangeOrgKeyStorage; + private List PendingOrgKeys => OrgKeys.Where(key => !key.IsBound).ToList(); + private List BoundOrgKeys => OrgKeys.Where(key => key.IsBound).ToList(); + private const string OrgKeyChangesBlockedReason = "TarkovTracker.org key storage must be repaired before saved keys or profiles can be changed."; + private bool CanImportOrgKey => OrgKeyChangesEnabled + && (!PendingOrgKeys.Any(key => string.Equals(key.Prefix, "PVE", StringComparison.OrdinalIgnoreCase)) + || !PendingOrgKeys.Any(key => string.Equals(key.Prefix, "PVP", StringComparison.OrdinalIgnoreCase)) + || !PendingOrgKeys.Any(key => string.Equals(key.Prefix, "SZN", StringComparison.OrdinalIgnoreCase))); + private string OrgImportBlockedReason => OrgKeyChangesEnabled + ? "PVE, Regular (PVP), and Seasonal already have unassigned keys." + : OrgKeyChangesBlockedReason; + private string OrgProfileScanHelpText => OrgKeyChangesEnabled + ? "Reads account, profile, mode, and last-played information from past EFT logs. It does not replay tasks." + : OrgKeyChangesBlockedReason; + private List BoundOrgKeyGroups + { + get + { + var filter = boundKeyFilter.Trim(); + return BoundOrgKeys + .GroupBy(key => key.AccountId, StringComparer.Ordinal) + .Select(group => new OrgAccountGroup( + group.Key, + group.Select(key => key.AccountNickname).FirstOrDefault(nickname => !string.IsNullOrWhiteSpace(nickname)) ?? "", + group.ToList())) + .Where(group => string.IsNullOrWhiteSpace(filter) + || group.AccountId.Contains(filter, StringComparison.OrdinalIgnoreCase) + || group.Nickname.Contains(filter, StringComparison.OrdinalIgnoreCase) + || group.Keys.Any(key => key.ProfileNickname.Contains(filter, StringComparison.OrdinalIgnoreCase))) + .OrderByDescending(group => string.Equals(group.AccountId, GameWatcher.CurrentProfile.AccountId, StringComparison.Ordinal)) + .ThenBy(group => group.AccountId, StringComparer.Ordinal) + .ToList(); + } + } + + private void TogglePendingDrawer() => pendingDrawerExpanded = !pendingDrawerExpanded; + + private string PendingAssignBlockedReason(TarkovTracker.OrgKeySummary key) + { + return OrgKeyChangesEnabled + ? "This saved record must be removed or repaired before assignment." + : OrgKeyChangesBlockedReason; + } - void TokenShowClick() + private static string PendingKeyIssue(TarkovTracker.OrgKeySummary key) { - if (tokenShow) + if (key.IsAutoBindBlocked) { - tokenShow = false; - PasswordInputIcon = Icons.Material.Filled.VisibilityOff; - PasswordInput = InputType.Password; + return "MANUAL ASSIGNMENT ONLY: automatic assignment is blocked for the EFT account that previously owned this key. Use Assign to choose its account, profile, and game mode."; } - else + + return key.HasPendingConflict + ? $"Manual assignment required: more than one {key.DisplayName} key is waiting. Automatic assignment is paused for this mode." + : "This key is inactive because its saved record is ambiguous or invalid."; + } + + private void ToggleBoundDrawer() => boundDrawerExpanded = !boundDrawerExpanded; + + private void ToggleAccountDrawer(string accountId) + { + if (!expandedAccountIds.Add(accountId)) { - tokenShow = true; - PasswordInputIcon = Icons.Material.Filled.Visibility; - PasswordInput = InputType.Text; + expandedAccountIds.Remove(accountId); } } - private async void TestToken() + private bool IsAccountDrawerExpanded(string accountId) + { + return !string.IsNullOrWhiteSpace(boundKeyFilter) || expandedAccountIds.Contains(accountId); + } + + private static string AccountDrawerTitle(OrgAccountGroup accountGroup) { - if (!TarkovTracker.IsSupportedOrgToken(TarkovTrackerToken)) + return string.IsNullOrWhiteSpace(accountGroup.Nickname) + ? "No nickname" + : accountGroup.Nickname; + } + + private static string AccountNicknameAction(OrgAccountGroup accountGroup) + { + return string.IsNullOrWhiteSpace(accountGroup.Nickname) ? "Set name" : "Rename"; + } + + private static bool CanSetAccountNickname(OrgAccountGroup accountGroup) + { + return accountGroup.Keys.Any(key => !key.IsQuarantined); + } + + private string AccountNicknameBlockedReason(OrgAccountGroup accountGroup) + { + return OrgKeyChangesEnabled + ? "Repair or remove invalid profile bindings before naming this account." + : OrgKeyChangesBlockedReason; + } + + private static string ModeBadgeClass(TarkovTracker.OrgKeySummary key) + { + return key.Prefix.ToUpperInvariant() switch + { + "PVE" => "tracker-mode-badge--pve", + "PVP" => "tracker-mode-badge--pvp", + "SZN" => "tracker-mode-badge--seasonal", + _ => "tracker-mode-badge--unknown", + }; + } + + private static string ModeBadgeShortLabel(TarkovTracker.OrgKeySummary key) + { + return key.Prefix.ToUpperInvariant() switch + { + "PVE" => "PVE", + "PVP" => "PVP", + "SZN" => "Seasonal", + _ => "?", + }; + } + + private async Task OpenImportTrackerKey() + { + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + var dialog = await DialogService.ShowAsync("Import TarkovTracker.org API key", options); + var result = await dialog.Result; + if (result is { Canceled: false, Data: TarkovTracker.ImportedToken importedToken }) + { + messageLog.AddMessage( + importedToken.IsBound + ? $"Imported, verified, and automatically assigned the {importedToken.DisplayName} TarkovTracker.org API key to the active EFT profile." + : $"Imported and verified the {importedToken.DisplayName} TarkovTracker.org API key. It will auto-assign on the first matching EFT profile, or you can assign it manually.", + "update"); + await ReactivateCurrentTrackerProfile(); + StateHasChanged(); + } + } + + private async Task ScanOrgProfiles() + { + scanningOrgProfiles = true; + try + { + var discovered = await Task.Run(eft.DiscoverProfiles); + var updated = TarkovTracker.SaveDiscoveredOrgProfiles(discovered); + messageLog.AddMessage(discovered.Count == 0 + ? "No EFT profiles were found in past logs. Launch EFT once, then scan again." + : $"Found {discovered.Count} EFT profile{(discovered.Count == 1 ? "" : "s")} in past logs and updated {updated} saved profile record{(updated == 1 ? "" : "s")}.", + discovered.Count == 0 ? "warning" : "update"); + } + catch (Exception ex) + { + messageLog.AddMessage($"Could not scan EFT profiles: {ex.Message}", "exception"); + } + finally + { + scanningOrgProfiles = false; + } + StateHasChanged(); + } + + private async Task OpenAssignOrgKey(TarkovTracker.OrgKeySummary key, bool reassign) + { + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + var parameters = new DialogParameters + { + [nameof(AssignTarkovTrackerKeyDialog.KeyPrefix)] = key.Prefix, + [nameof(AssignTarkovTrackerKeyDialog.KeyDisplayName)] = key.DisplayName, + [nameof(AssignTarkovTrackerKeyDialog.KeyIsBound)] = key.IsBound, + [nameof(AssignTarkovTrackerKeyDialog.RequiresVerification)] = !key.IsVerified, + [nameof(AssignTarkovTrackerKeyDialog.BoundAccountId)] = key.AccountId, + [nameof(AssignTarkovTrackerKeyDialog.BoundProfileId)] = key.ProfileId, + [nameof(AssignTarkovTrackerKeyDialog.BoundSessionMode)] = key.SessionMode, + [nameof(AssignTarkovTrackerKeyDialog.ProfileNickname)] = key.ProfileNickname, + [nameof(AssignTarkovTrackerKeyDialog.IsReassign)] = reassign, + }; + var dialog = await DialogService.ShowAsync( + $"{(reassign ? "Reassign" : "Assign")} {key.DisplayName} API key", + parameters, + options); + var result = await dialog.Result; + if (result is not { Canceled: false, Data: TarkovTracker.OrgProfileSummary profile }) { - messageLog.AddMessage("Enter a PVE_, PVP_, or SZN_ TarkovTracker.org key followed by its 18-character hexadecimal identifier.", "warning"); return; - } else { - var startedUtc = DateTime.UtcNow; - try + } + + var willSwap = reassign && profile.HasBoundKey; + var action = willSwap ? "Swap" : reassign ? "Reassign" : "Assign"; + try + { + TarkovTracker.OrgKeySummary assignedKey; + if (reassign) { - var tokenResponse = await TarkovTracker.TestToken(TarkovTrackerToken); - if (!tokenResponse.permissions.Contains("WP")) + var reassignment = TarkovTracker.ReassignOrgKey(key.Id, profile.AccountId, profile.ProfileId, profile.SessionMode); + assignedKey = reassignment.Key; + if (reassignment.SwappedKey == null) { - messageLog.AddMessage("You do not have write permissions with this token.", "warning"); - return; + var nicknameNotice = string.IsNullOrWhiteSpace(assignedKey.ProfileNickname) ? "" : " Its profile nickname moved with the key."; + AddProtectedKeyMessage( + $"Reassigned the verified {assignedKey.DisplayName} TarkovTracker.org API key.{nicknameNotice}", + ("Previous Account ID", key.AccountId), + ("Previous Profile ID", key.ProfileId), + ("New Account ID", assignedKey.AccountId), + ("New Profile ID", assignedKey.ProfileId)); + } + else + { + var nicknameNotice = string.IsNullOrWhiteSpace(assignedKey.ProfileNickname) + && string.IsNullOrWhiteSpace(reassignment.SwappedKey.ProfileNickname) + ? "" + : " Profile nicknames moved with their keys."; + AddProtectedKeyMessage( + $"Swapped the verified {assignedKey.DisplayName} TarkovTracker.org API keys between the two saved profiles.{nicknameNotice}", + ("First Account ID", key.AccountId), + ("First Profile ID", key.ProfileId), + ("Second Account ID", assignedKey.AccountId), + ("Second Profile ID", assignedKey.ProfileId)); } } - catch (Exception ex) + else { - messageLog.AddException("The Tarkov Tracker key test failed.", "TM-API-TRACKER-006", "TestToken", ex, "TarkovTracker", "TokenTest", $"https://{TarkovTrackerDomain}", DiagnosticsService.ElapsedMilliseconds(startedUtc)); + assignedKey = await TarkovTracker.AssignOrgKey(key.Id, profile.AccountId, profile.ProfileId, profile.SessionMode); + AddProtectedKeyMessage( + $"Assigned the verified {assignedKey.DisplayName} TarkovTracker.org API key manually.", + ("Account ID", assignedKey.AccountId), + ("Profile ID", assignedKey.ProfileId)); + } + + pendingDrawerExpanded = PendingOrgKeys.Count > 0; + boundDrawerExpanded = true; + expandedAccountIds.Add(assignedKey.AccountId); + await ReactivateCurrentTrackerProfile(); + } + catch (Exception ex) + { + messageLog.AddMessage($"Could not {action.ToLowerInvariant()} the TarkovTracker.org API key: {ex.Message}", "exception"); + } + StateHasChanged(); + } + private async Task SetOrgAccountNickname(OrgAccountGroup accountGroup) + { + var parameters = new DialogParameters + { + [nameof(SetTarkovTrackerAccountNicknameDialog.ExistingNickname)] = accountGroup.Nickname, + [nameof(SetTarkovTrackerAccountNicknameDialog.AccountId)] = accountGroup.AccountId, + }; + var title = string.IsNullOrWhiteSpace(accountGroup.Nickname) ? "Set account nickname" : "Change account nickname"; + var dialog = await DialogService.ShowAsync( + title, + parameters, + new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var result = await dialog.Result; + if (result is not { Canceled: false, Data: string nickname }) + { + return; + } + + try + { + var savedNickname = TarkovTracker.SetOrgAccountNickname(accountGroup.AccountId, nickname); + AddProtectedKeyMessage($"Saved the TarkovTracker.org account nickname “{savedNickname}”.", ("Account ID", accountGroup.AccountId)); + } + catch (Exception ex) + { + messageLog.AddMessage($"Could not save the account nickname: {ex.Message}", "exception"); + } + StateHasChanged(); + } + + private async Task SetOrgProfileNickname(TarkovTracker.OrgKeySummary key) + { + var parameters = new DialogParameters + { + [nameof(SetTarkovTrackerProfileNicknameDialog.ExistingNickname)] = key.ProfileNickname, + [nameof(SetTarkovTrackerProfileNicknameDialog.Mode)] = key.DisplayName, + }; + var title = string.IsNullOrWhiteSpace(key.ProfileNickname) + ? $"Set {key.DisplayName} profile nickname" + : $"Change {key.DisplayName} profile nickname"; + var dialog = await DialogService.ShowAsync( + title, + parameters, + new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var result = await dialog.Result; + if (result is not { Canceled: false, Data: string nickname }) + { + return; + } + + try + { + var savedNickname = TarkovTracker.SetOrgProfileNickname(key.Id, nickname); + AddProtectedKeyMessage( + $"Saved the {key.DisplayName} profile nickname “{savedNickname}”.", + ("Account ID", key.AccountId), + ("Profile ID", key.ProfileId)); + } + catch (Exception ex) + { + messageLog.AddMessage($"Could not save the profile nickname: {ex.Message}", "exception"); + } + StateHasChanged(); + } + + private async Task UnbindOrgKey(TarkovTracker.OrgKeySummary key) + { + if (!await ConfirmOrgKeyAction( + key, + $"Unbind {key.DisplayName} API key", + "Keep the key, but stop using it.", + string.IsNullOrWhiteSpace(key.ProfileNickname) + ? "The key will remain saved and move to Unassigned and will be marked MANUAL ASSIGNMENT ONLY for its previous EFT account. Assign it explicitly to use it again." + : "The key will remain saved and move to Unassigned. Its profile nickname will be cleared, and it will be marked MANUAL ASSIGNMENT ONLY for its previous EFT account.", + "Unbind", + Icons.Material.Filled.LinkOff, + false)) + { + return; + } + + try + { + TarkovTracker.UnbindOrgKey(key.Id); + var nicknameNotice = string.IsNullOrWhiteSpace(key.ProfileNickname) ? "" : " Its profile nickname was cleared."; + AddProtectedKeyMessage( + $"Moved the verified {key.DisplayName} TarkovTracker.org API key to Unassigned.{nicknameNotice}", + ("Previous Account ID", key.AccountId), + ("Previous Profile ID", key.ProfileId)); + pendingDrawerExpanded = true; + await ReactivateCurrentTrackerProfile(); + } + catch (Exception ex) + { + messageLog.AddMessage($"Could not unbind the TarkovTracker.org API key: {ex.Message}", "exception"); + } + StateHasChanged(); + } + + private async Task RemoveOrgKey(TarkovTracker.OrgKeySummary key) + { + if (!await ConfirmOrgKeyAction( + key, + $"Remove {key.DisplayName} API key", + "Delete this API key?", + "This removes the key from Tarkov Monitor on this PC. To restore it, you will need to import it again.", + "Remove", + Icons.Material.Filled.DeleteOutline, + true)) + { + return; + } + + try + { + if (!TarkovTracker.RemoveOrgKey(key.Id)) + { + messageLog.AddMessage("This TarkovTracker.org API key has already been removed.", "warning"); + return; + } + if (key.IsBound) + { + AddProtectedKeyMessage( + $"Removed the verified {key.DisplayName} TarkovTracker.org API key from Tarkov Monitor.", + ("Previous Account ID", key.AccountId), + ("Previous Profile ID", key.ProfileId)); + } + else + { + var verificationState = key.IsVerified ? "verified" : "unverified recovered"; + messageLog.AddMessage($"Removed the {verificationState} unassigned {key.DisplayName} TarkovTracker.org API key.", "update"); } + await ReactivateCurrentTrackerProfile(); } + catch (Exception ex) + { + messageLog.AddMessage($"Could not remove the TarkovTracker.org API key: {ex.Message}", "exception"); + } + StateHasChanged(); + } + + private async Task ConfirmOrgKeyAction( + TarkovTracker.OrgKeySummary key, + string title, + string heading, + string message, + string confirmLabel, + string icon, + bool isDestructive) + { + var parameters = new DialogParameters + { + [nameof(ConfirmTarkovTrackerKeyActionDialog.Heading)] = heading, + [nameof(ConfirmTarkovTrackerKeyActionDialog.Message)] = message, + [nameof(ConfirmTarkovTrackerKeyActionDialog.Mode)] = key.DisplayName, + [nameof(ConfirmTarkovTrackerKeyActionDialog.MaskedToken)] = key.MaskedToken, + [nameof(ConfirmTarkovTrackerKeyActionDialog.AccountId)] = key.AccountId, + [nameof(ConfirmTarkovTrackerKeyActionDialog.ConfirmLabel)] = confirmLabel, + [nameof(ConfirmTarkovTrackerKeyActionDialog.Icon)] = icon, + [nameof(ConfirmTarkovTrackerKeyActionDialog.IsDestructive)] = isDestructive, + }; + var dialog = await DialogService.ShowAsync( + title, + parameters, + new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var result = await dialog.Result; + return result is { Canceled: false, Data: true }; + } + + private async Task EditOrgKey(TarkovTracker.OrgKeySummary key) + { + var savedProfile = TarkovTracker.GetKnownOrgProfiles().FirstOrDefault(profile => + string.Equals(profile.AccountId, key.AccountId, StringComparison.Ordinal) + && string.Equals(profile.ProfileId, key.ProfileId, StringComparison.Ordinal) + && profile.SessionMode == key.SessionMode); + var lastPlayed = savedProfile?.LastSeenUtc is { } lastSeen + ? lastSeen.ToLocalTime().ToString("MMM d, yyyy h:mm tt") + : "Unknown"; + var parameters = new DialogParameters + { + [nameof(EditTarkovTrackerKeyDialog.Mode)] = key.DisplayName, + [nameof(EditTarkovTrackerKeyDialog.MaskedToken)] = key.MaskedToken, + [nameof(EditTarkovTrackerKeyDialog.AccountId)] = key.AccountId, + [nameof(EditTarkovTrackerKeyDialog.ProfileId)] = key.ProfileId, + [nameof(EditTarkovTrackerKeyDialog.ProfileNickname)] = key.ProfileNickname, + [nameof(EditTarkovTrackerKeyDialog.LastPlayed)] = lastPlayed, + [nameof(EditTarkovTrackerKeyDialog.IsQuarantined)] = key.IsQuarantined, + [nameof(EditTarkovTrackerKeyDialog.KeyChangesDisabled)] = !OrgKeyChangesEnabled, + [nameof(EditTarkovTrackerKeyDialog.KeyChangesDisabledReason)] = OrgKeyChangesBlockedReason, + }; + var dialog = await DialogService.ShowAsync( + $"Edit {key.DisplayName} API key", + parameters, + new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var result = await dialog.Result; + if (result is not { Canceled: false, Data: EditTarkovTrackerKeyDialog.KeyAction action }) + { + return; + } + + switch (action) + { + case EditTarkovTrackerKeyDialog.KeyAction.Reassign: + await OpenAssignOrgKey(key, true); + break; + case EditTarkovTrackerKeyDialog.KeyAction.SetProfileNickname: + await SetOrgProfileNickname(key); + break; + case EditTarkovTrackerKeyDialog.KeyAction.Unbind: + await UnbindOrgKey(key); + break; + case EditTarkovTrackerKeyDialog.KeyAction.Remove: + await RemoveOrgKey(key); + break; + } + } + private async Task ShowOrgAccountDetails(OrgAccountGroup accountGroup) + { + var nickname = string.IsNullOrWhiteSpace(accountGroup.Nickname) ? "Not set" : accountGroup.Nickname; + var modes = string.Join(", ", accountGroup.Keys + .Select(key => key.DisplayName) + .Distinct(StringComparer.Ordinal) + .OrderBy(mode => mode, StringComparer.Ordinal)); + var parameters = new DialogParameters + { + [nameof(TarkovTrackerDetailsDialog.Details)] = new List> + { + new("Account nickname", nickname), + new("Account ID", accountGroup.AccountId), + new("Assigned profiles", accountGroup.Keys.Count.ToString()), + new("Modes", modes), + }, + }; + var dialog = await DialogService.ShowAsync( + "EFT account details", + parameters, + new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + await dialog.Result; + } + + private void AddProtectedKeyMessage(string message, params (string Label, string Value)[] protectedValues) + { + messageLog.AddProtectedMessage( + message, + "update", + protectedValues.Select(value => new MonitorMessageProtectedValue(value.Label, value.Value))); + } + + private string CurrentAccountMarker(string accountId) + { + return eft.IsGameRunning + && string.Equals(accountId, GameWatcher.CurrentProfile.AccountId, StringComparison.Ordinal) + ? "• Current account" + : ""; + } + + private string ActiveKeyMarker(TarkovTracker.OrgKeySummary key) + { + if (eft.IsGameRunning + && !TarkovTracker.IsLegacyService + && TarkovTracker.ValidToken + && string.Equals(key.AccountId, TarkovTracker.CurrentAccountId, StringComparison.Ordinal) + && string.Equals(key.ProfileId, TarkovTracker.CurrentProfileId, StringComparison.Ordinal) + && key.SessionMode == TarkovTracker.CurrentSessionMode) + { + return "• Active profile"; + } + + return string.Equals(key.AccountId, GameWatcher.CurrentProfile.AccountId, StringComparison.Ordinal) + && string.Equals(key.ProfileId, GameWatcher.CurrentProfile.Id, StringComparison.Ordinal) + && key.SessionMode == GameWatcher.CurrentProfile.SessionMode + && string.IsNullOrWhiteSpace(TarkovTracker.GetToken(key.ProfileId)) + ? "• Last active profile" + : ""; + } + + private string ProfileRowLabel(TarkovTracker.OrgKeySummary key) + { + var marker = ActiveKeyMarker(key); + if (string.IsNullOrWhiteSpace(key.ProfileNickname)) + { + return marker; + } + return string.IsNullOrWhiteSpace(marker) ? key.ProfileNickname : $"{key.ProfileNickname} {marker}"; } private void OpenTrackerSettings() { @@ -846,7 +1581,7 @@ var currentFolder = CustomLogsFolder; using var dialog = new System.Windows.Forms.FolderBrowserDialog { - Description = "Select the Escape from Tarkov logs folder.", + Description = "Select your Escape from Tarkov logs folder.", UseDescriptionForTitle = true, ShowNewFolderButton = false, SelectedPath = Directory.Exists(currentFolder) ? currentFolder : "", diff --git a/TarkovMonitor/Blazor/Pages/Settings/Settings.razor.css b/TarkovMonitor/Blazor/Pages/Settings/Settings.razor.css new file mode 100644 index 0000000..992b6d0 --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/Settings.razor.css @@ -0,0 +1,432 @@ +.tracker-key-manager { + --tracker-border: rgba(255, 255, 255, 0.11); + --tracker-border-hover: rgba(104, 164, 255, 0.42); + --tracker-surface: rgba(255, 255, 255, 0.025); + --tracker-surface-raised: rgba(255, 255, 255, 0.045); + margin-top: 0.42rem; +} + +.tracker-key-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.3rem; + margin: 0; + padding: 0 0 0.45rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.085); +} + +.tracker-drawer, +.tracker-account-drawer { + overflow: hidden; + border: 1px solid var(--tracker-border); + background: linear-gradient(135deg, rgba(32, 33, 37, 0.96), rgba(21, 22, 25, 0.96)); + transition: border-color 160ms ease, box-shadow 160ms ease; +} + +.tracker-drawer { + border-radius: 13px; +} + +.tracker-account-drawer { + border-radius: 9px; +} + +.tracker-drawer:hover, +.tracker-account-drawer:hover { + border-color: var(--tracker-border-hover); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); +} + +.tracker-drawer-toggle, +.tracker-account-toggle { + display: flex; + align-items: center; + gap: 0.55rem; + min-width: 0; + flex: 1 1 auto; + padding: 0.68rem 0.8rem; + border: 0; + color: inherit; + background: transparent; + text-align: left; + cursor: pointer; +} + +.tracker-drawer-toggle--primary { + width: 100%; + padding: 0.5rem 0.72rem; + background: transparent; +} + +.tracker-drawer-toggle:hover, +.tracker-account-toggle:hover, +.tracker-drawer-toggle:focus-visible, +.tracker-account-toggle:focus-visible { + background-color: rgba(94, 151, 235, 0.09); + outline: none; +} + +.tracker-drawer-toggle:focus-visible, +.tracker-account-toggle:focus-visible { + box-shadow: inset 0 0 0 2px rgba(104, 164, 255, 0.72); +} + +.tracker-chevron { + flex: 0 0 auto; + color: #78aefb; +} + +.tracker-drawer-title, +.tracker-account-title { + overflow: hidden; + font-size: 0.92rem; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tracker-count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.55rem; + height: 1.35rem; + padding: 0 0.38rem; + border: 1px solid rgba(120, 174, 251, 0.35); + border-radius: 999px; + color: #b9d4ff; + background: rgba(72, 126, 206, 0.16); + font-size: 0.68rem; + font-weight: 700; +} + +.tracker-drawer-content { + padding: 0 0.65rem 0.65rem; +} + +.tracker-state-note { + display: flex; + align-items: flex-start; + gap: 0.5rem; + margin: 0 0 0.55rem; + padding: 0.48rem 0.58rem; + border: 1px solid rgba(143, 130, 95, 0.35); + border-radius: 8px; + color: rgba(255, 255, 255, 0.76); + background: rgba(143, 130, 95, 0.08); + font-size: 0.72rem; + line-height: 1.35; +} + +.tracker-state-note-icon { + flex: 0 0 auto; + margin-top: 0.03rem; + color: #c2b27e; +} + +.tracker-state-note-icon--bound { + color: #8ce0b8; +} + +.tracker-manager-warning { + margin-bottom: 0.45rem; + padding: 0.36rem 0.55rem; + border-color: rgba(229, 176, 70, 0.42); + background: rgba(159, 112, 28, 0.1); +} + +.tracker-empty-state { + padding: 0.7rem 0.25rem; + color: rgba(255, 255, 255, 0.6); +} + +.tracker-pending-card, +.tracker-profile-row { + padding: 0.48rem 0.62rem; + border: 1px solid var(--tracker-border); + border-radius: 7px; + background: var(--tracker-surface); +} + +.tracker-profile-row { + padding: 0.34rem 0.55rem; +} + +.tracker-pending-card + .tracker-pending-card, +.tracker-profile-row + .tracker-profile-row { + margin-top: 0.35rem; +} + +.tracker-profile-row + .tracker-profile-row { + margin-top: 0.26rem; +} + +.tracker-key-main { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 0.65rem; +} + +.tracker-key-copy { + min-width: 0; + flex: 1 1 auto; +} + +.tracker-key-actions, +.tracker-account-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: nowrap; + gap: 0.05rem; +} + +.tracker-filter { + margin: 0.05rem 0 0.5rem; +} + +.tracker-account-list { + max-height: 360px; + overflow-y: auto; + padding: 0.1rem 0.28rem 0.1rem 0; + scrollbar-color: rgba(120, 174, 251, 0.7) rgba(255, 255, 255, 0.04); + scrollbar-width: thin; +} + +.tracker-account-heading { + display: grid; + grid-template-columns: minmax(11rem, 1fr) 10rem 10.4rem; + align-items: center; + column-gap: 0.35rem; + min-height: 2.65rem; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.035), transparent); + transition: background-color 160ms ease, box-shadow 160ms ease; +} + +.tracker-account-heading:hover, +.tracker-account-heading:focus-within { + background-color: rgba(94, 151, 235, 0.09); +} + +.tracker-account-heading:focus-within { + box-shadow: inset 0 0 0 2px rgba(104, 164, 255, 0.72); +} + +.tracker-account-drawer { + position: relative; +} + +.tracker-account-drawer::before { + position: absolute; + z-index: 1; + top: 0; + bottom: 0; + left: 0; + width: 2px; + background: linear-gradient(180deg, #78aefb, rgba(139, 92, 246, 0.55)); + content: ""; + opacity: 0.58; + pointer-events: none; +} + +.tracker-account-toggle { + width: 100%; + padding: 0.34rem 0.62rem; +} + +.tracker-account-toggle:hover, +.tracker-account-toggle:focus-visible { + background-color: transparent; + box-shadow: none; +} + +.tracker-account-actions { + display: grid; + width: 10.4rem; + grid-template-columns: 4.35rem 5.8rem; + gap: 0.25rem; + padding-right: 0.35rem; +} + +.tracker-account-action-slot { + display: flex; + min-width: 0; + align-items: center; + justify-content: center; +} + +.tracker-account-copy { + display: flex; + min-width: 0; + flex: 1 1 auto; + align-items: baseline; + gap: 0.48rem; +} + +.tracker-account-subtitle, +.tracker-token { + overflow: hidden; + color: rgba(255, 255, 255, 0.58); + font-size: 0.7rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tracker-account-title { + flex: 0 1 auto; +} + +.tracker-account-subtitle { + flex: 1 1 auto; +} + +.tracker-profile-identity { + display: flex; + min-width: 0; + align-items: center; + gap: 0.45rem; +} + +.tracker-profile-identity .mud-typography { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tracker-profile-identity .tracker-token { + flex: 0 1 auto; +} + +.tracker-mode-summary { + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: nowrap; + gap: 0.2rem; +} + +.tracker-mode-badge { + display: inline-flex; + align-items: center; + min-height: 1.22rem; + padding: 0.03rem 0.36rem; + border: 1px solid transparent; + border-radius: 999px; + font-size: 0.64rem; + font-weight: 700; + letter-spacing: 0.025em; +} + +.tracker-verification-badge { + display: inline-flex; + align-items: center; + min-height: 1.15rem; + padding: 0.02rem 0.38rem; + border: 1px solid transparent; + border-radius: 999px; + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.tracker-verification-badge--verified { + border-color: rgba(108, 203, 154, 0.32); + color: rgba(150, 226, 187, 0.92); + background: rgba(55, 137, 94, 0.12); +} + +.tracker-verification-badge--pending { + border-color: rgba(242, 188, 87, 0.38); + color: #f2ca7f; + background: rgba(181, 123, 37, 0.14); +} + +.tracker-assignment-badge { + display: inline-flex; + align-items: center; + min-height: 1.15rem; + padding: 0.02rem 0.42rem; + border: 1px solid transparent; + border-radius: 999px; + font-size: 0.66rem; + font-weight: 800; + letter-spacing: 0.035em; + line-height: 1.2; + white-space: nowrap; +} + +.tracker-assignment-badge--manual { + border-color: rgba(255, 112, 112, 0.62); + color: #ffd0d0; + background: rgba(177, 47, 47, 0.24); +} + +.tracker-mode-badge--pve { + border-color: rgba(87, 194, 142, 0.45); + color: #8ce0b8; + background: rgba(48, 146, 99, 0.15); +} + +.tracker-mode-badge--pvp { + border-color: rgba(111, 166, 255, 0.45); + color: #9fc5ff; + background: rgba(61, 112, 194, 0.16); +} + +.tracker-mode-badge--seasonal { + border-color: rgba(212, 148, 255, 0.46); + color: #deb0ff; + background: rgba(139, 74, 180, 0.16); +} + +.tracker-mode-badge--unknown { + border-color: rgba(255, 255, 255, 0.24); + color: rgba(255, 255, 255, 0.72); + background: rgba(255, 255, 255, 0.06); +} + +.tracker-profile-list { + padding: 0.28rem 0.5rem 0.42rem; + border-top: 1px solid rgba(255, 255, 255, 0.055); +} + +@media (max-width: 640px) { + .tracker-account-heading { + grid-template-columns: minmax(0, 1fr) 10.4rem; + } + + .tracker-mode-summary { + display: none; + } +} + +@media (max-width: 520px) { + .tracker-account-heading, + .tracker-key-main { + grid-template-columns: minmax(0, 1fr); + } + + .tracker-account-actions, + .tracker-key-actions { + justify-content: flex-start; + padding: 0 0.45rem 0.35rem; + } + + .tracker-account-actions { + width: auto; + grid-column: 1; + } + + .tracker-account-heading { + min-height: auto; + } + + .tracker-account-copy, + .tracker-profile-identity { + flex-wrap: wrap; + row-gap: 0.12rem; + } +} diff --git a/TarkovMonitor/Blazor/Pages/Settings/TarkovTrackerDetailsDialog.razor b/TarkovMonitor/Blazor/Pages/Settings/TarkovTrackerDetailsDialog.razor new file mode 100644 index 0000000..3682840 --- /dev/null +++ b/TarkovMonitor/Blazor/Pages/Settings/TarkovTrackerDetailsDialog.razor @@ -0,0 +1,37 @@ + + +
+ + + Account overview. + These details summarize the API keys assigned to this EFT account. + +
+ +
+ + Account details +
+
+ @foreach (var detail in Details) + { +
+
@detail.Key
+
@detail.Value
+
+ } +
+
+ + Close + +
+ +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public IReadOnlyList> Details { get; set; } + = Array.Empty>(); + + private void Close() => MudDialog.Close(); +} diff --git a/TarkovMonitor/Blazor/Pages/Sounds/Sounds.razor b/TarkovMonitor/Blazor/Pages/Sounds/Sounds.razor index 11fb122..636357d 100644 --- a/TarkovMonitor/Blazor/Pages/Sounds/Sounds.razor +++ b/TarkovMonitor/Blazor/Pages/Sounds/Sounds.razor @@ -11,42 +11,42 @@ - @LocalizationService.GetString("Sounds") + @LocalizationService.GetString("Sounds")
+ -
+ -
+ -
+ -
+ -
+ -
+ -
- Media Control + Media controls
- Automatically fade and pause playing music apps when entering a raid, then resume them when leaving + Automatically fade and pause music when you enter a raid, then resume it when you leave.
@@ -144,7 +144,7 @@ { if (value) { - fileDialog.Title = "Select raid starting sound"; + fileDialog.Title = "Select a sound for raid start"; } ToggleCustomSound("raid_starting", value); } @@ -159,7 +159,7 @@ { if (value) { - fileDialog.Title = "Select match found sound"; + fileDialog.Title = "Select a sound for match found"; } ToggleCustomSound("match_found", value); } @@ -175,7 +175,7 @@ { if (value) { - fileDialog.Title = "Select restart failed tasks sound"; + fileDialog.Title = "Select a sound for task restart"; } ToggleCustomSound("restart_failed_tasks", value); } @@ -191,7 +191,7 @@ { if (value) { - fileDialog.Title = "Select runthrough period over sound"; + fileDialog.Title = "Select a sound for run-through"; } ToggleCustomSound("runthrough_over", value); } @@ -207,7 +207,7 @@ { if (value) { - fileDialog.Title = "Select air filter on sound"; + fileDialog.Title = "Select a sound for air filter on"; } ToggleCustomSound("air_filter_on", value); } @@ -223,7 +223,7 @@ { if (value) { - fileDialog.Title = "Select air filter off sound"; + fileDialog.Title = "Select a sound for air filter off"; } ToggleCustomSound("air_filter_off", value); } @@ -239,7 +239,7 @@ { if (value) { - fileDialog.Title = "Select scav available sound"; + fileDialog.Title = "Select a sound for Scav availability"; } ToggleCustomSound("scav_available", value); } diff --git a/TarkovMonitor/Blazor/Pages/Stats/Stats.razor b/TarkovMonitor/Blazor/Pages/Stats/Stats.razor index fb5e579..21614bf 100644 --- a/TarkovMonitor/Blazor/Pages/Stats/Stats.razor +++ b/TarkovMonitor/Blazor/Pages/Stats/Stats.razor @@ -11,7 +11,7 @@ - @LocalizationService.GetString("FleaMarketSales") + @LocalizationService.GetString("FleaMarketSales") @foreach (KeyValuePair currency in Currencies) { @GetSales(currency.Key, currency.Value) @@ -20,7 +20,7 @@ - @LocalizationService.GetString("Raids") + @LocalizationService.GetString("Raids") @foreach (var map in TarkovDev.Maps) { @map.name: @GetRaidCount(map.nameId) @@ -95,6 +95,6 @@ void ClearData() { var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.ExtraLarge, FullWidth = true }; - DialogService.ShowAsync("Clear Data?", options); + DialogService.ShowAsync("Clear data?", options); } } diff --git a/TarkovMonitor/Blazor/Pages/Timers/Timers.razor b/TarkovMonitor/Blazor/Pages/Timers/Timers.razor index b9463fc..11d9974 100644 --- a/TarkovMonitor/Blazor/Pages/Timers/Timers.razor +++ b/TarkovMonitor/Blazor/Pages/Timers/Timers.razor @@ -8,14 +8,14 @@ - @LocalizationService.GetString("Timers") + @LocalizationService.GetString("Timers")
- Floating timer panel: + Floating timer panel @(timersManager.FloatingPanelEnabled ? "Enabled" : "Disabled")
- Displayed: @FloatingPanelTimerSummary + Timers shown: @FloatingPanelTimerSummary @LocalizationService.GetString("TimeInRaid"): @FormatTimer(TimeInRaidTime) @LocalizationService.GetString("RunthroughPeriod"): @FormatTimer(RunThroughRemainingTime) diff --git a/TarkovMonitor/Diagnostics/DiagnosticModels.cs b/TarkovMonitor/Diagnostics/DiagnosticModels.cs index 6c8c0f5..bcd9931 100644 --- a/TarkovMonitor/Diagnostics/DiagnosticModels.cs +++ b/TarkovMonitor/Diagnostics/DiagnosticModels.cs @@ -50,6 +50,10 @@ public static class DiagnosticRedactor @"(?ix)(?\b(?:authorization|bearer|token|api[_-]?key|sessionid|accountid|profileid|remoteid|cookie)\b\s*(?:[:=]|=>)\s*)(?Bearer\s+[^\s,;}&]+|[^\s,;}&]+)", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex DictionaryKey = new( + @"(?ix)(?\bkey\b\s*:\s*)(?\d+)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex JsonSensitiveKeyValue = new( @"(?ix)(?(?[^""\s,}]+)""?", RegexOptions.Compiled | RegexOptions.CultureInvariant); @@ -92,6 +96,7 @@ public static string Sanitize(string? value, int maxLength = 16_384) sanitized = sanitized.Replace("%TMP%", "", StringComparison.OrdinalIgnoreCase); sanitized = ApiKey.Replace(sanitized, "[REDACTED_API_KEY]"); sanitized = SensitiveKeyValue.Replace(sanitized, match => $"{match.Groups["key"].Value}[REDACTED]"); + sanitized = DictionaryKey.Replace(sanitized, match => $"{match.Groups["key"].Value}[REDACTED_ID]"); sanitized = JsonSensitiveKeyValue.Replace(sanitized, match => $"{match.Groups["key"].Value}\"[REDACTED]\""); sanitized = BearerToken.Replace(sanitized, "Bearer [REDACTED]"); sanitized = QueryString.Replace(sanitized, "$url?[REDACTED_QUERY]"); diff --git a/TarkovMonitor/GameWatcher.cs b/TarkovMonitor/GameWatcher.cs index 0e2db54..a6c0292 100644 --- a/TarkovMonitor/GameWatcher.cs +++ b/TarkovMonitor/GameWatcher.cs @@ -16,6 +16,9 @@ internal class GameWatcher private readonly System.Timers.Timer processTimer; private readonly FileSystemWatcher logFileCreateWatcher; private readonly FileSystemWatcher screenshotWatcher; + private readonly bool historicalReplay; + private Profile parsingProfile = new(); + private Profile ActiveProfile => historicalReplay ? parsingProfile : CurrentProfile; private string _logsPath = ""; private bool _logsPathResolutionFailed; private readonly object initialReadGate = new(); @@ -36,6 +39,38 @@ internal class GameWatcher public static Profile CurrentProfile { get; set; } = new(); public static bool ReadingPastLogs = false; public bool InitialLogsRead { get; private set; } = false; + public bool IsGameRunning + { + get + { + try + { + if (process != null && !process.HasExited) + { + return true; + } + + var processes = Process.GetProcessesByName("EscapeFromTarkov"); + try + { + return processes.Length > 0; + } + finally + { + foreach (var candidate in processes) + { + candidate.Dispose(); + } + } + } + catch + { + // If process state cannot be read, block manual historical + // assignment from replacing a potentially live identity. + return true; + } + } + } public string LogsPath { get { @@ -153,6 +188,7 @@ public string ScreenshotsPath public event EventHandler? ExceptionThrown; public event EventHandler? DebugMessage; public event EventHandler? GameStarted; + public event EventHandler? GameStopped; public event EventHandler>? GroupInviteAccept; public event EventHandler>? GroupRaidSettings; public event EventHandler>? GroupMemberReady; @@ -178,6 +214,7 @@ public string ScreenshotsPath public event EventHandler? PlayerPosition; public event EventHandler ProfileChanged; public event EventHandler InitialReadComplete; + public event EventHandler? ProfileReady; public event EventHandler ControlSettings; private static string logPatternPrefix = @"(?^\d{4}-\d{2}-\d{2}) (?