Skip to content

Feature: Search bar for subtitles #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions FlyleafLib/Engine/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ private void UpdateDefault()
Demuxer.FormatOpt = DemuxerConfig.DefaultVideoFormatOpt();
Demuxer.AudioFormatOpt = DemuxerConfig.DefaultVideoFormatOpt();
Demuxer.SubtitlesFormatOpt = DemuxerConfig.DefaultVideoFormatOpt();


// for subtitles search #91
int ctrlFBindingIdx = Player.KeyBindings.Keys
.FindIndex(k => k.Key == System.Windows.Input.Key.F &&
k.Ctrl && !k.Alt && !k.Shift);
if (ctrlFBindingIdx != -1)
{
// remove existing binding
Player.KeyBindings.Keys.RemoveAt(ctrlFBindingIdx);
}
// set CTRL+F to subtitles search
Player.KeyBindings.Keys.Add(new KeyBinding { Ctrl = true, Key = System.Windows.Input.Key.F, IsKeyUp = true, Action = KeyBindingAction.Custom, ActionName = "ActivateSubsSearch" });
}
}

Expand Down
5 changes: 5 additions & 0 deletions FlyleafLib/MediaPlayer/SubtitlesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ internal void Refresh()
});
}

public void RaisePropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}

/// <summary>
/// This must be called when doing heavy operation
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions LLPlayer/Extensions/FocusBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace LLPlayer.Extensions;

public static class FocusBehavior
{
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached(
"IsFocused",
typeof(bool),
typeof(FocusBehavior),
new UIPropertyMetadata(false, OnIsFocusedChanged));

public static bool GetIsFocused(DependencyObject obj) =>
(bool)obj.GetValue(IsFocusedProperty);

public static void SetIsFocused(DependencyObject obj, bool value) =>
obj.SetValue(IsFocusedProperty, value);

private static void OnIsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is UIElement element) || !(e.NewValue is bool isFocused) || !isFocused)
return;

// Set focus to element
element.Dispatcher.BeginInvoke(() =>
{
element.Focus();
if (element is TextBox tb)
{
// if TextBox, then select text
tb.SelectAll();
//tb.CaretIndex = tb.Text.Length;
}
}, DispatcherPriority.Input);
}
}
21 changes: 21 additions & 0 deletions LLPlayer/Services/AppActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Windows.Input;
using System.Windows.Media;
using FlyleafLib;
using FlyleafLib.Controls.WPF;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Views;
Expand All @@ -18,6 +19,7 @@ public class AppActions
{
private readonly Player _player;
private readonly AppConfig _config;
private FlyleafHost? _flyleafHost => _player.Host as FlyleafHost;
private readonly IDialogService _dialogService;

public AppActions(Player player, AppConfig config, IDialogService dialogService)
Expand Down Expand Up @@ -73,6 +75,7 @@ private Dictionary<CustomKeyBindingAction, Action> GetCustomActions()
[CustomKeyBindingAction.ToggleSubsAutoTextCopy] = CmdToggleSubsAutoTextCopy.Execute,
[CustomKeyBindingAction.ToggleSidebarShowSecondary] = CmdToggleSidebarShowSecondary.Execute,
[CustomKeyBindingAction.ToggleSidebarShowOriginalText] = CmdToggleSidebarShowOriginalText.Execute,
[CustomKeyBindingAction.ActivateSubsSearch] = CmdActivateSubsSearch.Execute,

[CustomKeyBindingAction.ToggleSidebar] = CmdToggleSidebar.Execute,
[CustomKeyBindingAction.ToggleDebugOverlay] = CmdToggleDebugOverlay.Execute,
Expand Down Expand Up @@ -103,6 +106,7 @@ public static List<KeyBinding> DefaultCustomActionsMap()
new() { ActionName = nameof(CustomKeyBindingAction.SubsDistanceDecrease), Key = Key.Down, Ctrl = true, Shift = true },
new() { ActionName = nameof(CustomKeyBindingAction.SubsPrimaryTextCopy), Key = Key.C, Ctrl = true, IsKeyUp = true },
new() { ActionName = nameof(CustomKeyBindingAction.ToggleSubsAutoTextCopy), Key = Key.A, Alt = true, IsKeyUp = true },
new() { ActionName = nameof(CustomKeyBindingAction.ActivateSubsSearch), Key = Key.F, Ctrl = true, IsKeyUp = true },
new() { ActionName = nameof(CustomKeyBindingAction.ToggleSidebar), Key = Key.B, Ctrl = true, IsKeyUp = true },
new() { ActionName = nameof(CustomKeyBindingAction.ToggleDebugOverlay), Key = Key.D, Ctrl = true, Shift = true, IsKeyUp = true },
new() { ActionName = nameof(CustomKeyBindingAction.OpenWindowSettings), Key = Key.OemComma, Ctrl = true, IsKeyUp = true },
Expand Down Expand Up @@ -342,6 +346,20 @@ private void SubsTextCopyInternal(int subIndex, bool? suppressOsd)
_config.Subs.SubsAutoTextCopy = !_config.Subs.SubsAutoTextCopy;
});

public DelegateCommand CmdActivateSubsSearch => field ?? new(() =>
{
if (_flyleafHost is { IsFullScreen: true })
{
return;
}

_config.ShowSidebar = true;

// for getting focus to TextBox
_config.SidebarSearchActive = false;
_config.SidebarSearchActive = true;
});

public DelegateCommand CmdToggleSidebarShowSecondary => field ?? new(() =>
{
_config.SidebarShowSecondary = !_config.SidebarShowSecondary;
Expand Down Expand Up @@ -651,6 +669,8 @@ public enum CustomKeyBindingAction
ToggleSidebarShowSecondary,
[Description("Toggle to show original text in Subtitles Sidebar")]
ToggleSidebarShowOriginalText,
[Description("Activate Subtitles Search in Sidebar")]
ActivateSubsSearch,

[Description("Toggle Subitltes Sidebar")]
ToggleSidebar,
Expand Down Expand Up @@ -728,6 +748,7 @@ public static KeyBindingActionGroup ToGroup(this CustomKeyBindingAction action)
case CustomKeyBindingAction.ToggleSubsAutoTextCopy:
case CustomKeyBindingAction.ToggleSidebarShowSecondary:
case CustomKeyBindingAction.ToggleSidebarShowOriginalText:
case CustomKeyBindingAction.ActivateSubsSearch:
return KeyBindingActionGroup.Subtitles;

case CustomKeyBindingAction.ToggleSidebar:
Expand Down
3 changes: 3 additions & 0 deletions LLPlayer/Services/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ public bool SidebarLeft

public bool SidebarTextMask { get; set => Set(ref field, value); }

[JsonIgnore]
public bool SidebarSearchActive { get; set => Set(ref field, value); }

public string SidebarFontFamily { get; set => Set(ref field, value); } = "Segoe UI";

public double SidebarFontSize { get; set => Set(ref field, value); } = 16;
Expand Down
140 changes: 112 additions & 28 deletions LLPlayer/ViewModels/SubtitlesSidebarVM.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.ComponentModel;
using FlyleafLib;
using System.Windows.Data;
using FlyleafLib.MediaPlayer;
using LLPlayer.Extensions;
using LLPlayer.Services;
Expand All @@ -15,6 +15,18 @@ public SubtitlesSidebarVM(FlyleafManager fl)
FL = fl;

FL.Config.PropertyChanged += OnConfigOnPropertyChanged;

// Initialize filtered view for the sidebar
for (int i = 0; i < _filteredSubs.Length; i++)
{
// TODO: L: Address issue of incorrect SelectedIndex during filtering
_filteredSubs[i] = CollectionViewSource.GetDefaultView(FL.Player.SubtitlesManager[i].Subs);
}
}

public void Dispose()
{
FL.Config.PropertyChanged -= OnConfigOnPropertyChanged;
}

private void OnConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs args)
Expand All @@ -24,23 +36,47 @@ private void OnConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs
case nameof(FL.Config.SidebarShowSecondary):
OnPropertyChanged(nameof(SubIndex));
OnPropertyChanged(nameof(SubManager));

// prevent unnecessary filter update
if (_lastSearchText[SubIndex] != _trimSearchText)
{
ApplyFilter(); // ensure filter applied
}
break;
case nameof(FL.Config.SidebarShowOriginalText):
// Update ListBox
OnPropertyChanged(nameof(SubManager));

if (_trimSearchText.Length != 0)
{
// because of switch between Text and DisplayText
ApplyFilter();
}
break;
}
}

public void Dispose()
{
FL.Config.PropertyChanged -= OnConfigOnPropertyChanged;
}

public int SubIndex => !FL.Config.SidebarShowSecondary ? 0 : 1;

public SubManager SubManager => FL.Player.SubtitlesManager[SubIndex];

private readonly ICollectionView[] _filteredSubs = new ICollectionView[2];

private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set
{
if (Set(ref _searchText, value))
{
DebounceFilter();
}
}
}

private string _trimSearchText = string.Empty; // for performance

// ReSharper disable NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract

// TODO: L: Fix implicit changes to reflect
Expand Down Expand Up @@ -82,28 +118,6 @@ public void Dispose()
return;
}

// If paused, start playing after seek
if (!FL.Player.IsPlaying)
{
FL.Player.SeekCompleted += PlayerOnSeekCompleted;

void PlayerOnSeekCompleted(object? sender, int args)
{
FL.Player.SeekCompleted -= PlayerOnSeekCompleted;

if (args != -1)
{
Utils.UI(() =>
{
if (!FL.Player.IsPlaying)
{
FL.Player.Play();
}
});
}
}
}

var sub = SubManager.Subs[index.Value];
FL.Player.SeekAccurate(sub.StartTime, SubIndex);
});
Expand All @@ -124,5 +138,75 @@ void PlayerOnSeekCompleted(object? sender, int args)
FL.PlayerConfig.Subtitles[SubIndex].Delay = newDelay;
});

public DelegateCommand CmdClearSearch => field ??= new(() =>
{
Set(ref _searchText, string.Empty, nameof(SearchText));
_trimSearchText = string.Empty;

_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = null;

for (int i = 0; i < _filteredSubs.Length; i++)
{
_lastSearchText[i] = string.Empty;
_filteredSubs[i].Filter = null; // remove filter completely
_filteredSubs[i].Refresh();
}

FL.Config.SidebarSearchActive = false;

// move focus to video and enable keybindings
FL.FlyleafHost!.Surface.Focus();
// for scrolling to current sub
// TODO: L: not working sometimes?
SubManager.RaisePropertyChanged(nameof(SubManager.CurrentIndex));
});

// ReSharper restore NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract

// Debounce logic
private CancellationTokenSource? _debounceCts;
private async void DebounceFilter()
{
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;

try
{
await Task.Delay(300, token); // 300ms debounce

if (!token.IsCancellationRequested)
{
ApplyFilter();
}
}
catch (OperationCanceledException)
{
// ignore
}
}

private readonly string[] _lastSearchText = [string.Empty, string.Empty];

private void ApplyFilter()
{
_trimSearchText = SearchText.Trim();
_lastSearchText[SubIndex] = _trimSearchText;

// initialize filter lazily
_filteredSubs[SubIndex].Filter ??= SubFilter;
_filteredSubs[SubIndex].Refresh();
}

private bool SubFilter(object obj)
{
if (_trimSearchText.Length == 0) return true;
if (obj is not SubtitleData sub) return false;

string? source = FL.Config.SidebarShowOriginalText ? sub.Text : sub.DisplayText;
return source?.IndexOf(_trimSearchText, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
Loading