From b1a632f5cfbf421e44058ff6158d6722f4ec67eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sackers?= Date: Sun, 23 Jul 2023 16:31:35 +0200 Subject: [PATCH] Filter & refresh --- .../Client/Components/ClipViewer.razor.cs | 2 +- .../Client/Components/EventFilter.razor | 134 ++++++++++++++++++ .../Client/Models/CamEvents.cs | 10 ++ .../Client/Models/EventFilterValues.cs | 44 ++++++ .../Client/Pages/Index.razor | 25 +++- .../Client/Pages/Index.razor.cs | 83 ++++++++--- .../TeslaCamPlayer.BlazorHosted.Client.csproj | 1 - .../Client/wwwroot/scss/app.scss | 21 +++ .../Server/Controllers/ApiController.cs | 4 +- .../Server/Services/ClipsService.cs | 7 +- .../Services/Interfaces/IClipsService.cs | 2 +- 11 files changed, 300 insertions(+), 33 deletions(-) create mode 100644 src/TeslaCamPlayer.BlazorHosted/Client/Components/EventFilter.razor create mode 100644 src/TeslaCamPlayer.BlazorHosted/Client/Models/CamEvents.cs create mode 100644 src/TeslaCamPlayer.BlazorHosted/Client/Models/EventFilterValues.cs diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Components/ClipViewer.razor.cs b/src/TeslaCamPlayer.BlazorHosted/Client/Components/ClipViewer.razor.cs index c0bcd8d..6c0edc8 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Client/Components/ClipViewer.razor.cs +++ b/src/TeslaCamPlayer.BlazorHosted/Client/Components/ClipViewer.razor.cs @@ -100,7 +100,7 @@ private async Task ExecuteOnPlayers(Func player) await player(_videoPlayerRightRepeater); await player(_videoPlayerBack); } - catch (Exception e) + catch { // ignore } diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Components/EventFilter.razor b/src/TeslaCamPlayer.BlazorHosted/Client/Components/EventFilter.razor new file mode 100644 index 0000000..b3b3206 --- /dev/null +++ b/src/TeslaCamPlayer.BlazorHosted/Client/Components/EventFilter.razor @@ -0,0 +1,134 @@ +@using TeslaCamPlayer.BlazorHosted.Client.Models + + + + + +
+ + @context.Text +
+
+
+
+
+
+
+ +@code { + private class TreeItemData + { + private readonly Action _isCheckedChangedHandler; + + public TreeItemData Parent { get; set; } = null; + + public string Text { get; } + public string Icon { get; } + + public bool IsChecked { get; set; } + + public bool HasChild => TreeItems?.Any() == true; + + public HashSet TreeItems { get; set; } = new(); + + public TreeItemData(string text, string icon, bool isChecked, Action isCheckedChangedHandler = null) + { + _isCheckedChangedHandler = isCheckedChangedHandler; + Text = text; + Icon = icon; + IsChecked = isChecked; + } + + public TreeItemData AddChild(string itemName, string icon, bool isChecked, Action isCheckedChangedHandler = null) + { + var item = new TreeItemData(itemName, icon, isChecked, isCheckedChangedHandler) + { + Parent = this + }; + TreeItems.Add(item); + + return item; + } + + public bool? IsCheckedState() + { + if (!HasChild) + return IsChecked; + + if (TreeItems.All(i => i.IsChecked)) + return true; + + if (TreeItems.All(i => !i.IsChecked)) + return false; + + return null; + } + + public void CheckedChanged() + { + IsChecked = !IsChecked; + if (HasChild) + { + foreach (var child in TreeItems) + { + child.IsChecked = IsChecked; + } + } + + if (Parent != null) + { + Parent.IsChecked = Parent.TreeItems.All(i => i.IsChecked); + } + + _isCheckedChangedHandler?.Invoke(IsChecked); + } + } + + [Parameter] + public EventFilterValues Values { get; set; } = new(); + + [Parameter] + public EventCallback ValuesChanged { get; set; } + + private HashSet _treeItems = new(); + + private void ValueSetterAction(bool isChecked, Action valueSetter) + { + valueSetter.Invoke(); + ValuesChanged.InvokeAsync(Values); + } + + protected override void OnInitialized() + { + // TODO: Set IsChecked for parent items correctly + var dashcamEvents = new TreeItemData("Dashcam", Icons.Material.Filled.CameraAlt, true, c => ValueSetterAction(c, () => + { + Values.DashcamHonk = c; + Values.DashcamSaved = c; + Values.DashcamOther = c; + })); + dashcamEvents.AddChild("Honk", Icons.Material.Filled.Campaign, Values.DashcamHonk, c => ValueSetterAction(c, () => Values.DashcamHonk = c)); + dashcamEvents.AddChild("Saved", Icons.Material.Filled.Archive, Values.DashcamSaved, c => ValueSetterAction(c, () => Values.DashcamSaved = c)); + dashcamEvents.AddChild("Other", Icons.Material.Filled.QuestionMark, Values.DashcamOther, c => ValueSetterAction(c, () => Values.DashcamOther = c)); + + var sentryEvents = new TreeItemData("Sentry", Icons.Material.Filled.RadioButtonChecked, true, c => ValueSetterAction(c, () => + { + Values.SentryObjectDetection = c; + Values.SentryAccelerationDetection = c; + Values.SentryOther = c; + })); + sentryEvents.AddChild("Object detection", Icons.Material.Filled.Animation, Values.SentryObjectDetection, c => ValueSetterAction(c, () => Values.SentryObjectDetection = c)); + sentryEvents.AddChild("Acceleration detection", Icons.Material.Filled.OpenWith, Values.SentryAccelerationDetection, c => ValueSetterAction(c, () => Values.SentryAccelerationDetection = c)); + sentryEvents.AddChild("Other", Icons.Material.Filled.QuestionMark, Values.SentryOther, c => ValueSetterAction(c, () => Values.SentryOther = c)); + + var recentEvents = new TreeItemData("Recent", Icons.Material.Filled.History, Values.Recent, c => ValueSetterAction(c, () => Values.Recent = c)); + + _treeItems.Add(dashcamEvents); + _treeItems.Add(sentryEvents); + _treeItems.Add(recentEvents); + } + +} \ No newline at end of file diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Models/CamEvents.cs b/src/TeslaCamPlayer.BlazorHosted/Client/Models/CamEvents.cs new file mode 100644 index 0000000..3329680 --- /dev/null +++ b/src/TeslaCamPlayer.BlazorHosted/Client/Models/CamEvents.cs @@ -0,0 +1,10 @@ +namespace TeslaCamPlayer.BlazorHosted.Client.Models; + +public static class CamEvents +{ + public const string UserInteractionHonk = "user_interaction_honk"; + public const string UserInteractionDashcamPanelSave = "user_interaction_dashcam_panel_save"; + public const string UserInteractionDashcamIconTapped = "user_interaction_dashcam_icon_tapped"; + public const string SentryAwareObjectDetection = "sentry_aware_object_detection"; + public const string SentryAwareAccelerationPrefix = "sentry_aware_accel_"; +} \ No newline at end of file diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Models/EventFilterValues.cs b/src/TeslaCamPlayer.BlazorHosted/Client/Models/EventFilterValues.cs new file mode 100644 index 0000000..1311e69 --- /dev/null +++ b/src/TeslaCamPlayer.BlazorHosted/Client/Models/EventFilterValues.cs @@ -0,0 +1,44 @@ +using TeslaCamPlayer.BlazorHosted.Shared.Models; + +namespace TeslaCamPlayer.BlazorHosted.Client.Models +{ + public class EventFilterValues + { + public bool DashcamHonk { get; set; } = true; + + public bool DashcamSaved { get; set; } = true; + + public bool DashcamOther { get; set; } = true; + + public bool SentryObjectDetection { get; set; } = true; + + public bool SentryAccelerationDetection { get; set; } = true; + + public bool SentryOther { get; set; } = true; + + public bool Recent { get; set; } = true; + + public bool IsInFilter(Clip clip) + { + if (DashcamHonk && clip.Event.Reason == CamEvents.UserInteractionHonk) + return true; + + if (DashcamSaved && (clip.Event.Reason == CamEvents.UserInteractionDashcamPanelSave || clip.Event.Reason == CamEvents.UserInteractionDashcamIconTapped)) + return true; + + if (DashcamOther && clip.Type == ClipType.Saved) + return true; + + if (SentryObjectDetection && clip.Event.Reason == CamEvents.SentryAwareObjectDetection) + return true; + + if (SentryAccelerationDetection && clip.Event.Reason.StartsWith(CamEvents.SentryAwareAccelerationPrefix)) + return true; + + if (SentryOther && clip.Type == ClipType.Sentry) + return true; + + return Recent && clip.Type == ClipType.Recent; + } + } +} diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor b/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor index 5598d15..c486107 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor +++ b/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor @@ -1,9 +1,11 @@ @page "/" @using TeslaCamPlayer.BlazorHosted.Shared.Models -@using TeslaCamPlayer.BlazorHosted.Client.Helpers -@if (_clips == null) + +@if (_filteredclips == null) { - Loading... +
+ Loading... +
} else { @@ -21,8 +23,23 @@ else DateChanged="DatePicked" @onmousewheel="@DatePickerOnMouseWheel"/> + + + + + +
+ + + + + + +
+
+
- +
@if (!string.IsNullOrWhiteSpace(context.ThumbnailUrl)) diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs b/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs index 3053382..9af40d6 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs +++ b/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs @@ -6,6 +6,7 @@ using TeslaCamPlayer.BlazorHosted.Client.Components; using TeslaCamPlayer.BlazorHosted.Client.Helpers; using TeslaCamPlayer.BlazorHosted.Shared.Models; +using TeslaCamPlayer.BlazorHosted.Client.Models; namespace TeslaCamPlayer.BlazorHosted.Client.Pages; @@ -20,6 +21,7 @@ public partial class Index : ComponentBase private IJSRuntime JsRuntime { get; set; } private Clip[] _clips; + private Clip[] _filteredclips; private HashSet _eventDates; private MudDatePicker _datePicker; private bool _setDatePickerInitialDate; @@ -28,31 +30,71 @@ public partial class Index : ComponentBase private DateTime _ignoreDatePicked; private Clip _activeClip; private ClipViewer _clipViewer; + private bool _showFilter; + private bool _filterChanged; + private EventFilterValues _eventFilter = new(); protected override async Task OnInitializedAsync() { _scrollDebounceTimer = new(100); _scrollDebounceTimer.Elapsed += ScrollDebounceTimerTick; - _clips = await HttpClient.GetFromNewtonsoftJsonAsync("Api/GetClips"); - _eventDates = _clips - .Select(c => c.StartDate.Date) - .Concat(_clips.Select(c => c.EndDate.Date)) - .Distinct() - .ToHashSet(); + await RefreshEventsAsync(false); } protected override async Task OnAfterRenderAsync(bool firstRender) { - if (!_setDatePickerInitialDate && _clips?.Any() == true && _datePicker != null) + if (!_setDatePickerInitialDate && _filteredclips?.Any() == true && _datePicker != null) { _setDatePickerInitialDate = true; - var latestClip = _clips.MaxBy(c => c.EndDate)!; + var latestClip = _filteredclips.MaxBy(c => c.EndDate)!; await _datePicker.GoToDate(latestClip.EndDate); await SetActiveClip(latestClip); } } - + + private async Task RefreshEventsAsync(bool refreshCache) + { + _filteredclips = null; + _clips = null; + await Task.Delay(10); + await InvokeAsync(StateHasChanged); + + _setDatePickerInitialDate = false; + _clips = await HttpClient.GetFromNewtonsoftJsonAsync("Api/GetClips?refreshCache=" + refreshCache); + + FilterClips(); + } + + private void FilterClips() + { + _filteredclips = (_clips ??= Array.Empty()) + .Where(_eventFilter.IsInFilter) + .ToArray(); + + _eventDates = _filteredclips + .Select(c => c.StartDate.Date) + .Concat(_filteredclips.Select(c => c.EndDate.Date)) + .Distinct() + .ToHashSet(); + } + + private async Task ToggleFilter() + { + _showFilter = !_showFilter; + if (_showFilter || !_filterChanged) + return; + + FilterClips(); + await InvokeAsync(StateHasChanged); + } + + private void EventFilterValuesChanged(EventFilterValues values) + { + _eventFilter = values; + _filterChanged = true; + } + private bool IsDateDisabledFunc(DateTime date) => !_eventDates.Contains(date); @@ -76,13 +118,14 @@ private static string[] GetClipIcons(Clip clip) var secondIcon = clip.Event.Reason switch { - "sentry_aware_object_detection" => Icons.Material.Filled.Animation, - "user_interaction_honk" => Icons.Material.Filled.Campaign, - "user_interaction_dashcam_panel_save" => Icons.Material.Filled.Archive, + CamEvents.SentryAwareObjectDetection => Icons.Material.Filled.Animation, + CamEvents.UserInteractionHonk => Icons.Material.Filled.Campaign, + CamEvents.UserInteractionDashcamPanelSave => Icons.Material.Filled.Archive, + CamEvents.UserInteractionDashcamIconTapped => Icons.Material.Filled.Archive, _ => null }; - if (clip.Event.Reason.StartsWith("sentry_aware_accel_")) + if (clip.Event.Reason.StartsWith(CamEvents.SentryAwareAccelerationPrefix)) secondIcon = Icons.Material.Filled.OpenWith; return secondIcon == null ? new [] { baseIcon } : new[] { baseIcon, secondIcon }; @@ -102,7 +145,7 @@ private async Task DatePicked(DateTime? pickedDate) if (!pickedDate.HasValue || _ignoreDatePicked == pickedDate) return; - var firstClipAtDate = _clips.FirstOrDefault(c => c.StartDate.Date == pickedDate); + var firstClipAtDate = _filteredclips.FirstOrDefault(c => c.StartDate.Date == pickedDate); if (firstClipAtDate == null) return; @@ -114,7 +157,7 @@ private async Task DatePicked(DateTime? pickedDate) private async Task ScrollListToActiveClip() { var listBoundingRect = await _eventsList.MudGetBoundingClientRectAsync(); - var index = Array.IndexOf(_clips, _activeClip); + var index = Array.IndexOf(_filteredclips, _activeClip); var top = (int)(index * EventItemHeight - listBoundingRect.Height / 2 + EventItemHeight / 2); await JsRuntime.InvokeVoidAsync("HTMLElement.prototype.scrollTo.call", _eventsList, new ScrollToOptions @@ -144,7 +187,7 @@ private async void ScrollDebounceTimerTick(object _, ElapsedEventArgs __) var listBoundingRect = await _eventsList.MudGetBoundingClientRectAsync(); var centerScrollPosition = scrollTop + listBoundingRect.Height / 2 + EventItemHeight / 2; var itemIndex = (int)centerScrollPosition / EventItemHeight; - var atClip = _clips.ElementAt(Math.Min(_clips.Length - 1, itemIndex)); + var atClip = _filteredclips.ElementAt(Math.Min(_filteredclips.Length - 1, itemIndex)); _ignoreDatePicked = atClip.StartDate.Date; await _datePicker.GoToDate(atClip.StartDate.Date); @@ -155,7 +198,7 @@ private async void ScrollDebounceTimerTick(object _, ElapsedEventArgs __) private async Task PreviousButtonClicked() { // Go to an OLDER clip, so start date should be GREATER than current - var previous = _clips + var previous = _filteredclips .OrderByDescending(c => c.StartDate) .FirstOrDefault(c => c.StartDate < _activeClip.StartDate); @@ -169,7 +212,7 @@ private async Task PreviousButtonClicked() private async Task NextButtonClicked() { // Go to a NEWER clip, so start date should be LESS than current - var next = _clips + var next = _filteredclips .OrderBy(c => c.StartDate) .FirstOrDefault(c => c.StartDate > _activeClip.StartDate); @@ -189,8 +232,8 @@ private async Task DatePickerOnMouseWheel(WheelEventArgs e) var targetDate = _datePicker.PickerMonth.Value.AddMonths(goToNextMonth ? 1 : -1); var endOfMonth = targetDate.AddMonths(1); - var clipsInOrAfterTargetMonth = _clips.Any(c => c.StartDate >= targetDate); - var clipsInOrBeforeTargetMonth = _clips.Any(c => c.StartDate <= endOfMonth); + var clipsInOrAfterTargetMonth = _filteredclips.Any(c => c.StartDate >= targetDate); + var clipsInOrBeforeTargetMonth = _filteredclips.Any(c => c.StartDate <= endOfMonth); if (goToNextMonth && !clipsInOrAfterTargetMonth) return; diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj b/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj index c0fe4be..cf8dff9 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj +++ b/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj @@ -24,7 +24,6 @@ - diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/wwwroot/scss/app.scss b/src/TeslaCamPlayer.BlazorHosted/Client/wwwroot/scss/app.scss index bf7542e..209936e 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Client/wwwroot/scss/app.scss +++ b/src/TeslaCamPlayer.BlazorHosted/Client/wwwroot/scss/app.scss @@ -11,6 +11,15 @@ html, body { height: 100%; } +.loading-screen { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 30px; + font-weight: bold; +} + .main-content { display: flex; height: 100%; @@ -70,4 +79,16 @@ $event-item-inner-height: $event-item-height - $event-item-padding * 2; .mud-picker-datepicker-toolbar { display: none; } +} + +.event-filter-arrow { + position: absolute; + width: 10px; + height: 20px; + left: 100%; + top: 50%; + transform: translateY(-50%); + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-left: 5px solid var(--mud-palette-surface); } \ No newline at end of file diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs index d88ebc9..880024b 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs +++ b/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs @@ -21,8 +21,8 @@ public ApiController(ISettingsProvider settingsProvider, IClipsService clipsServ } [HttpGet] - public async Task GetClips() - => await _clipsService.GetClipsAsync(); + public async Task GetClips(bool refreshCache = false) + => await _clipsService.GetClipsAsync(refreshCache); private bool IsUnderRootPath(string path) => path.StartsWith(_rootFullPath); diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs index 7ff3050..e5a2900 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs +++ b/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs @@ -1,5 +1,4 @@ -using System.IO; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Newtonsoft.Json; using Serilog; using TeslaCamPlayer.BlazorHosted.Server.Providers.Interfaces; @@ -28,9 +27,9 @@ private async Task GetCachedAsync() ? JsonConvert.DeserializeObject(await File.ReadAllTextAsync(CacheFilePath)) : null; - public async Task GetClipsAsync() + public async Task GetClipsAsync(bool refreshCache = false) { - if ((_cache ??= await GetCachedAsync()) != null) + if (!refreshCache && (_cache ??= await GetCachedAsync()) != null) return _cache; var videoFileInfos = (await Task.WhenAll(Directory diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs index ddb7806..47f210a 100644 --- a/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs +++ b/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs @@ -4,5 +4,5 @@ namespace TeslaCamPlayer.BlazorHosted.Server.Services.Interfaces; public interface IClipsService { - Task GetClipsAsync(); + Task GetClipsAsync(bool refreshCache = false); } \ No newline at end of file