using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using BililiveRecorder.Core; using BililiveRecorder.WPF.Controls; using ModernWpf.Controls; using Serilog; #nullable enable namespace BililiveRecorder.WPF.Pages { /// /// Interaction logic for RoomList.xaml /// public partial class RoomListPage { private static readonly ILogger logger = Log.ForContext(); private static readonly Regex RoomIdRegex = new Regex(@"^(?:https?:\/\/)?live\.bilibili\.com\/(?:blanc\/|h5\/)?(\d*)(?:\?.*)?$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); private readonly IRoom?[] NullRoom = new IRoom?[] { null }; private readonly KeyIndexMappingReadOnlyList NullRoomWithMapping; public RoomListPage() { this.NullRoomWithMapping = new KeyIndexMappingReadOnlyList(this.NullRoom); this.DataContextChanged += this.RoomListPage_DataContextChanged; this.InitializeComponent(); } private void RoomListPage_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (e.OldValue is IRecorder data_old) ((INotifyCollectionChanged)data_old.Rooms).CollectionChanged -= this.DataSource_CollectionChanged; if (e.NewValue is IRecorder data_new) ((INotifyCollectionChanged)data_new.Rooms).CollectionChanged += this.DataSource_CollectionChanged; this.ApplySort(); } public static readonly DependencyProperty RoomListProperty = DependencyProperty.Register( nameof(RoomList), typeof(object), typeof(RoomListPage), new PropertyMetadata(OnPropertyChanged)); public object RoomList { get => this.GetValue(RoomListProperty); set => this.SetValue(RoomListProperty, value); } public static readonly DependencyProperty SortByProperty = DependencyProperty.Register( nameof(SortBy), typeof(SortedBy), typeof(RoomListPage), new PropertyMetadata(OnPropertyChanged)); public SortedBy SortBy { get => (SortedBy)this.GetValue(SortByProperty); set { this.SetValue(SortByProperty, value); this.ApplySort(); } } private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((RoomListPage)d).PrivateOnPropertyChanged(e); private void PrivateOnPropertyChanged(DependencyPropertyChangedEventArgs e) { } private void DataSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => this.ApplySort(); private void ApplySort() { try { if (this.DataContext is not IRecorder recorder || recorder.Rooms.Count == 0) { this.RoomList = this.NullRoomWithMapping; } else { var data = recorder.Rooms; IEnumerable orderedData = this.SortBy switch { SortedBy.RoomId => data.OrderBy(x => x.ShortId == 0 ? x.RoomConfig.RoomId : x.ShortId), SortedBy.Status => from x in data orderby x.Recording descending, x.RoomConfig.AutoRecord descending, x.Streaming descending select x, _ => data, }; var result = new KeyIndexMappingReadOnlyList(orderedData.Concat(this.NullRoom).ToArray()); this.RoomList = result; } } catch (Exception ex) { logger.Error(ex, "Error Sorting"); } } #pragma warning disable VSTHRD100 // Avoid async void methods private async void RoomCard_DeleteRequested(object sender, EventArgs e) #pragma warning restore VSTHRD100 // Avoid async void methods { if (this.DataContext is IRecorder rec && sender is IRoom room) { try { var dialog = new DeleteRoomConfirmDialog { DataContext = room, Owner = Application.Current.MainWindow }; var result = await dialog.ShowAndDisableMinimizeToTrayAsync(); if (result == ContentDialogResult.Primary) { rec.RemoveRoom(room); } } catch (Exception) { } } } #pragma warning disable VSTHRD100 // Avoid async void methods private async void RoomCard_ShowSettingsRequested(object sender, EventArgs e) #pragma warning restore VSTHRD100 // Avoid async void methods { try { await new PerRoomSettingsDialog { DataContext = sender, Owner = Application.Current.MainWindow }.ShowAndDisableMinimizeToTrayAsync(); } catch (Exception) { } } #pragma warning disable VSTHRD100 // Avoid async void methods private async void AddRoomCard_AddRoomRequested(object sender, string e) #pragma warning restore VSTHRD100 // Avoid async void methods { var input = e.Trim(); if (string.IsNullOrWhiteSpace(input) || this.DataContext is not IRecorder rec) return; if (!int.TryParse(input, out var roomid)) { var m = RoomIdRegex.Match(input); if (m.Success && m.Groups.Count > 1 && int.TryParse(m.Groups[1].Value, out var result2)) { roomid = result2; } else { try { await new AddRoomFailedDialog { DataContext = AddRoomFailedDialog.AddRoomFailedErrorText.InvalidInput, Owner = Application.Current.MainWindow }.ShowAndDisableMinimizeToTrayAsync(); } catch (Exception) { } return; } } if (roomid < 0) { try { await new AddRoomFailedDialog { DataContext = AddRoomFailedDialog.AddRoomFailedErrorText.RoomIdNegative, Owner = Application.Current.MainWindow }.ShowAndDisableMinimizeToTrayAsync(); } catch (Exception) { } return; } else if (roomid == 0) { try { await new AddRoomFailedDialog { DataContext = AddRoomFailedDialog.AddRoomFailedErrorText.RoomIdZero, Owner = Application.Current.MainWindow }.ShowAndDisableMinimizeToTrayAsync(); } catch (Exception) { } return; } if (rec.Rooms.Any(x => x.RoomConfig.RoomId == roomid || x.ShortId == roomid)) { try { await new AddRoomFailedDialog { DataContext = AddRoomFailedDialog.AddRoomFailedErrorText.Duplicate, Owner = Application.Current.MainWindow }.ShowAndDisableMinimizeToTrayAsync(); } catch (Exception) { } return; } rec.AddRoom(roomid); } private void MenuItem_EnableAutoRecAll_Click(object sender, RoutedEventArgs e) { if (this.DataContext is not IRecorder rec) return; foreach (var room in rec.Rooms) room.RoomConfig.AutoRecord = true; rec.SaveConfig(); } private void MenuItem_DisableAutoRecAll_Click(object sender, RoutedEventArgs e) { if (this.DataContext is not IRecorder rec) return; foreach (var room in rec.Rooms) room.RoomConfig.AutoRecord = false; rec.SaveConfig(); } private void MenuItem_SortBy_Click(object sender, RoutedEventArgs e) => this.SortBy = (SortedBy)((MenuItem)sender).Tag; private void MenuItem_ShowLog_Click(object sender, RoutedEventArgs e) { this.Splitter.Visibility = Visibility.Visible; this.LogElement.Visibility = Visibility.Visible; this.RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star); this.LogRowDefinition.Height = new GridLength(1, GridUnitType.Star); } private void MenuItem_HideLog_Click(object sender, RoutedEventArgs e) { this.Splitter.Visibility = Visibility.Collapsed; this.LogElement.Visibility = Visibility.Collapsed; this.RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star); this.LogRowDefinition.Height = new GridLength(0); } private void Log_ScrollViewer_Loaded(object sender, RoutedEventArgs e) => (sender as ScrollViewer)?.ScrollToEnd(); private void TextBlock_Copy_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { try { if (sender is TextBlock textBlock) { Clipboard.SetText(textBlock.Text); } } catch (Exception) { } } private void MenuItem_OpenWorkDirectory_Click(object sender, RoutedEventArgs e) { try { if (this.DataContext is IRecorder rec) Process.Start("explorer.exe", rec.Config.Global.WorkDirectory); } catch (Exception) { } } private void MenuItem_SaveConfig_Click(object sender, RoutedEventArgs e) { try { if (this.DataContext is IRecorder rec) rec.SaveConfig(); } catch (Exception) { } } private void MenuItem_ChangeWorkPath_Click(object sender, RoutedEventArgs e) { try { logger.Debug("ChangeWorkPath menu button invoked"); Process.Start(typeof(RoomListPage).Assembly.Location, "run --ask-path"); (Application.Current.MainWindow as NewMainWindow)?.CloseWithoutConfirmAction(); } catch (Exception) { } } private void MenuItem_ShowHideTitleArea_Click(object sender, RoutedEventArgs e) { if (((MenuItem)sender).Tag is bool b && this.DataContext is IRecorder rec) rec.Config.Global.WpfShowTitleAndArea = b; } private void MenuItem_ShowLogFilesInExplorer_Click(object sender, RoutedEventArgs e) { try { var logPath = Path.Combine(Path.GetDirectoryName(typeof(RoomListPage).Assembly.Location), "logs"); Process.Start("explorer.exe", logPath); } catch (Exception) { } } private void MenuItem_RefreshAllRoomInfo_Click(object sender, RoutedEventArgs e) { _ = Task.Run(async () => { await Task.Delay(200); if (MessageBox.Show("录播姬会自动检测直播间状态,不需要手动刷新。\n录播姬主要通过接收B站服务器的推送来更新状态,直播服务器会给录播姬实时发送开播通知,延迟极低。\n\n" + "频繁刷新直播间状态、短时间内大量请求B站直播 API 可能会导致你的 IP 被屏蔽,完全无法录播。\n\n本功能是特殊情况下确实需要刷新所有直播间信息时使用的。\n\n是否要刷新所有直播间的信息?\n(每个直播间会发送一个请求)", "B站录播姬", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) return; if (this.DataContext is IRecorder rec) { foreach (var room in rec.Rooms.ToArray()) { await room.RefreshRoomInfoAsync(); await Task.Delay(500); } } }); } } public enum SortedBy { None = 0, RoomId, Status, } internal class KeyIndexMappingReadOnlyList : IReadOnlyList, IKeyIndexMapping { private readonly IReadOnlyList data; public KeyIndexMappingReadOnlyList(IReadOnlyList data) { this.data = data; } public IRoom? this[int index] => this.data[index]; public int Count => this.data.Count; public IEnumerator GetEnumerator() => this.data.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.data).GetEnumerator(); #region IKeyIndexMapping private int lastRequestedIndex = IndexNotFound; private const int IndexNotFound = -1; // When UniqueIDs are supported, the ItemsRepeater caches the unique ID for each item // with the matching UIElement that represents the item. When a reset occurs the // ItemsRepeater pairs up the already generated UIElements with items in the data // source. // ItemsRepeater uses IndexForUniqueId after a reset to probe the data and identify // the new index of an item to use as the anchor. If that item no // longer exists in the data source it may try using another cached unique ID until // either a match is found or it determines that all the previously visible items // no longer exist. public int IndexFromKey(string uniqueId) { // We'll try to increase our odds of finding a match sooner by starting from the // position that we know was last requested and search forward. var start = this.lastRequestedIndex; for (var i = start; i < this.Count; i++) { if ((this[i]?.ObjectId ?? Guid.Empty).Equals(uniqueId)) return i; } // Then try searching backward. start = Math.Min(this.Count - 1, this.lastRequestedIndex); for (var i = start; i >= 0; i--) { if ((this[i]?.ObjectId ?? Guid.Empty).Equals(uniqueId)) return i; } return IndexNotFound; } public string KeyFromIndex(int index) { var key = this[index]?.ObjectId ?? Guid.Empty; this.lastRequestedIndex = index; return key.ToString(); } #endregion } }