From 2d8d2c71926917e9edae2731f7a6177f8b552a2f Mon Sep 17 00:00:00 2001 From: ycastonguay Date: Thu, 22 Aug 2013 20:30:24 -0400 Subject: [PATCH] Windows: Several visual changes to SongGridView to make it cleaner. No more grid lines except for each album. No more artist/album captions over album art. No more mouse over color change. A bit sad that I took a lot of amount of time to do these features a few years ago... to realize they are more noise than useful. But after these changes... I think my designer was right! Added a bit of logic to PlaylistPresenter. Related to issue #422. --- MPfm/MPfm.MVP/Presenters/PlaylistPresenter.cs | 453 +- MPfm/MPfm.MVP/Views/IPlaylistView.cs | 86 +- MPfm/MPfm.Sound/Playlists/Playlist.cs | 117 +- .../Classes/Forms/frmMain.Designer.cs | 4 +- MPfm/MPfm.Windows/Classes/Forms/frmMain.resx | 940 +-- .../Classes/Forms/frmPlaylist.Designer.cs | 3 - .../MPfm.Windows/Classes/Forms/frmPlaylist.cs | 515 +- .../Library/SongGridView.cs | 5669 ++++++++--------- MPfm/MPfm.WindowsControls/Misc/Gradient.cs | 324 +- 9 files changed, 3806 insertions(+), 4305 deletions(-) diff --git a/MPfm/MPfm.MVP/Presenters/PlaylistPresenter.cs b/MPfm/MPfm.MVP/Presenters/PlaylistPresenter.cs index d8548814..1a771f81 100644 --- a/MPfm/MPfm.MVP/Presenters/PlaylistPresenter.cs +++ b/MPfm/MPfm.MVP/Presenters/PlaylistPresenter.cs @@ -1,133 +1,320 @@ -// Copyright © 2011-2013 Yanick Castonguay -// -// This file is part of MPfm. -// -// MPfm is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// MPfm is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with MPfm. If not, see . - -using MPfm.MVP.Presenters.Interfaces; -using MPfm.MVP.Views; -using TinyMessenger; -using MPfm.MVP.Messages; -using System; -using MPfm.MVP.Services.Interfaces; - -namespace MPfm.MVP.Presenters -{ - public class PlaylistPresenter : BasePresenter, IPlaylistPresenter - { - readonly ITinyMessengerHub _messageHub; - readonly IPlayerService _playerService; - - public PlaylistPresenter(ITinyMessengerHub messageHub, IPlayerService playerService) - { - _messageHub = messageHub; - _playerService = playerService; - - _messageHub.Subscribe((PlayerPlaylistUpdatedMessage m) => { - Console.WriteLine("PlaylistPresenter - PlayerPlaylistUpdated - Refreshing items..."); - RefreshItems(); - }); - _messageHub.Subscribe(PlayerPlaylistIndexChanged); - } - - public override void BindView(IPlaylistView view) - { - view.OnNewPlaylist = NewPlaylist; - view.OnLoadPlaylist = LoadPlaylist; - view.OnSavePlaylist = SavePlaylist; - view.OnShufflePlaylist = ShufflePlaylist; - view.OnChangePlaylistItemOrder = ChangePlaylistItemOrder; - view.OnRemovePlaylistItem = RemovePlaylistItem; - view.OnSelectPlaylistItem = SelectPlaylistItem; - - base.BindView(view); - Initialize(); - } - - private void Initialize() - { - RefreshItems(); - - if(_playerService.IsPlaying) - View.RefreshCurrentlyPlayingSong(_playerService.CurrentPlaylist.CurrentItemIndex, _playerService.CurrentPlaylist.CurrentItem.AudioFile); - } - - private void PlayerPlaylistIndexChanged(PlayerPlaylistIndexChangedMessage message) - { - View.RefreshCurrentlyPlayingSong(_playerService.CurrentPlaylist.CurrentItemIndex, _playerService.CurrentPlaylist.CurrentItem.AudioFile); - } - - private void RefreshItems() - { - try - { - View.RefreshPlaylist(_playerService.CurrentPlaylist); - } - catch(Exception ex) - { - Console.WriteLine("PlaylistPresenter - RefreshItems - Exception: {0}", ex); - View.PlaylistError(ex); - } - } - - private void NewPlaylist() - { - try - { - if(_playerService.IsPlaying) - _playerService.Stop(); - - _playerService.CurrentPlaylist.Clear(); - } - catch(Exception ex) - { - Console.WriteLine("PlaylistPresenter - NewPlaylist - Exception: {0}", ex); - View.PlaylistError(ex); - } - } - - private void LoadPlaylist(string filePath) - { - } - - private void SavePlaylist() - { - } - - private void ShufflePlaylist() - { - } - - private void ChangePlaylistItemOrder(Guid playlistItemId, int newIndex) - { - } - - private void SelectPlaylistItem(Guid playlistItemId) - { - try - { - _playerService.GoTo(playlistItemId); - } - catch(Exception ex) - { - Console.WriteLine("PlaylistPresenter - SelectPlaylistItem - Exception: {0}", ex); - View.PlaylistError(ex); - } - } - - private void RemovePlaylistItem(Guid playlistItemId) - { - } - } -} +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System.Collections.Generic; +using MPfm.MVP.Presenters.Interfaces; +using MPfm.MVP.Views; +using TinyMessenger; +using MPfm.MVP.Messages; +using System; +using MPfm.MVP.Services.Interfaces; + +namespace MPfm.MVP.Presenters +{ + public class PlaylistPresenter : BasePresenter, IPlaylistPresenter + { + readonly ITinyMessengerHub _messageHub; + readonly IPlayerService _playerService; + + public PlaylistPresenter(ITinyMessengerHub messageHub, IPlayerService playerService) + { + _messageHub = messageHub; + _playerService = playerService; + + _messageHub.Subscribe((PlayerPlaylistUpdatedMessage m) => { + Console.WriteLine("PlaylistPresenter - PlayerPlaylistUpdated - Refreshing items..."); + RefreshItems(); + }); + _messageHub.Subscribe(PlayerPlaylistIndexChanged); + } + + public override void BindView(IPlaylistView view) + { + view.OnNewPlaylist = NewPlaylist; + view.OnLoadPlaylist = LoadPlaylist; + view.OnSavePlaylist = SavePlaylist; + view.OnShufflePlaylist = ShufflePlaylist; + view.OnChangePlaylistItemOrder = ChangePlaylistItemOrder; + view.OnRemovePlaylistItems = RemovePlaylistItems; + view.OnSelectPlaylistItem = SelectPlaylistItem; + + base.BindView(view); + Initialize(); + } + + private void Initialize() + { + RefreshItems(); + + if(_playerService.IsPlaying) + View.RefreshCurrentlyPlayingSong(_playerService.CurrentPlaylist.CurrentItemIndex, _playerService.CurrentPlaylist.CurrentItem.AudioFile); + } + + private void PlayerPlaylistIndexChanged(PlayerPlaylistIndexChangedMessage message) + { + View.RefreshCurrentlyPlayingSong(_playerService.CurrentPlaylist.CurrentItemIndex, _playerService.CurrentPlaylist.CurrentItem.AudioFile); + } + + private void RefreshItems() + { + try + { + View.RefreshPlaylist(_playerService.CurrentPlaylist); + } + catch(Exception ex) + { + Console.WriteLine("PlaylistPresenter - RefreshItems - Exception: {0}", ex); + View.PlaylistError(ex); + } + } + + private void NewPlaylist() + { + try + { + if(_playerService.IsPlaying) + _playerService.Stop(); + + _playerService.CurrentPlaylist.Clear(); + } + catch(Exception ex) + { + Console.WriteLine("PlaylistPresenter - NewPlaylist - Exception: {0}", ex); + View.PlaylistError(ex); + } + } + + private void LoadPlaylist(string filePath) + { + } + + private void SavePlaylist() + { + } + + private void ShufflePlaylist() + { + } + + private void ChangePlaylistItemOrder(Guid playlistItemId, int newIndex) + { + } + + private void SelectPlaylistItem(Guid playlistItemId) + { + try + { + _playerService.GoTo(playlistItemId); + } + catch(Exception ex) + { + Console.WriteLine("PlaylistPresenter - SelectPlaylistItem - Exception: {0}", ex); + View.PlaylistError(ex); + } + } + + private void RemovePlaylistItems(List playlistItemIds) + { + try + { + _playerService.CurrentPlaylist.RemoveItems(playlistItemIds); + View.RefreshPlaylist(_playerService.CurrentPlaylist); + } + catch (Exception ex) + { + Console.WriteLine("PlaylistPresenter - SelectPlaylistItem - Exception: {0}", ex); + View.PlaylistError(ex); + } + } + + ///// + ///// Loads a playlist. + ///// + ///// Playlist file path + //public void LoadPlaylist(string playlistFilePath) + //{ + // //try + // //{ + // // // Create window + // // formLoadPlaylist = new frmLoadPlaylist(Main, playlistFilePath); + + // // // Show Load playlist dialog (progress bar) + // // DialogResult dialogResult = formLoadPlaylist.ShowDialog(this); + // // if (dialogResult == System.Windows.Forms.DialogResult.OK) + // // { + // // // Get audio files + // // List audioFiles = formLoadPlaylist.AudioFiles; + + // // // Check if any audio files have failed loading + // // List failedAudioFilePaths = formLoadPlaylist.FailedAudioFilePaths; + + // // // Clear player playlist + // // Main.Player.Playlist.Clear(); + // // Main.Player.Playlist.FilePath = playlistFilePath; + + // // // Make sure there are audio files to add to the player playlist + // // if (audioFiles.Count > 0) + // // { + // // // Add audio files + // // Main.Player.Playlist.AddItems(audioFiles); + // // Main.Player.Playlist.First(); + // // } + + // // // Refresh song browser + // // RefreshTitle(); + // // RefreshPlaylist(); + // // RefreshPlaylistPlayIcon(Guid.Empty); + + // // // Check if any files could not be loaded + // // if (failedAudioFilePaths != null && failedAudioFilePaths.Count > 0) + // // { + // // // Check if the user wants to see the list of files + // // if (MessageBox.Show("Some files in the playlist could not be loaded. Do you wish to see the list of files in a text editor?", "Some files could not be loaded", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == System.Windows.Forms.DialogResult.Yes) + // // { + // // // Get temp file path + // // string tempFilePath = Path.GetTempPath() + "MPfm_PlaylistLog_" + Conversion.DateTimeToUnixTimestamp(DateTime.Now).ToString("0.0000000").Replace(".", "") + ".txt"; + + // // // Create temporary file + // // TextWriter tw = null; + // // try + // // { + // // // Open text writer + // // tw = new StreamWriter(tempFilePath); + // // foreach (string item in failedAudioFilePaths) + // // { + // // tw.WriteLine(item); + // // } + // // } + // // catch (Exception ex) + // // { + // // // Display error + // // MessageBox.Show("Failed to save the file to " + tempFilePath + "!\n\nException:\n" + ex.Message + "\n" + ex.StackTrace, "Failed to save the file", MessageBoxButtons.OK, MessageBoxIcon.Error); + // // } + // // finally + // // { + // // tw.Close(); + // // } + + // // // Start notepad + // // Process.Start(tempFilePath); + // // } + // // } + + // // // Check if the playlist is empty + // // if (audioFiles.Count == 0) + // // { + // // // Warn user that the playlist is empty + // // MessageBox.Show("The playlist file is empty or does not contain any valid file!", "Playlist is empty", MessageBoxButtons.OK, MessageBoxIcon.Information); + // // } + // // } + + // // // Dispose form + // // formLoadPlaylist.Dispose(); + // // formLoadPlaylist = null; + // //} + // //catch (Exception ex) + // //{ + // // MessageBox.Show(ex.Message, "Error loading playlist", MessageBoxButtons.OK, MessageBoxIcon.Error); + // //} + //} + + ///// + ///// Saves the playlist in the specified file path. + ///// + ///// Playlist file path + //public void SavePlaylist(string playlistFilePath) + //{ + // //bool relativePath = false; + + // //// Change cursor + // //Cursor.Current = Cursors.WaitCursor; + + // //// Set playlist file path + // //Main.Player.Playlist.FilePath = playlistFilePath; + + // //// Check if the path is in the same + // //DialogResult dialogResult = MessageBox.Show("Do you wish to use relative paths instead of absolute paths when possible?\n\nA full path or absolute path is a path that points to the same location on one file system regardless of the working directory or combined paths.\n\nA relative path is a path relative to the working directory of the user or application, so the full absolute path will not have to be given.", "Use relative paths", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); + + // //// Check result + // //if (dialogResult == System.Windows.Forms.DialogResult.Cancel) + // //{ + // // // Cancel + // // return; + // //} + // //else if (dialogResult == System.Windows.Forms.DialogResult.Yes) + // //{ + // // // Save playlist + // // //SavePlaylist(dialogSavePlaylist.FileName, true); + // // relativePath = true; + // //} + // //else if (dialogResult == System.Windows.Forms.DialogResult.No) + // //{ + // // // Save playlist + // // //SavePlaylist(dialogSavePlaylist.FileName, false); + // // relativePath = false; + // //} + + // //// Determine what format the user has chosen + // //if (dialogSavePlaylist.FileName.ToUpper().Contains(".M3U8")) + // //{ + // // // Save playlist + // // PlaylistTools.SaveM3UPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath, true); + // //} + // //else if (dialogSavePlaylist.FileName.ToUpper().Contains(".M3U")) + // //{ + // // // Save playlist + // // PlaylistTools.SaveM3UPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath, false); + // //} + // //else if (dialogSavePlaylist.FileName.ToUpper().Contains(".PLS")) + // //{ + // // // Save playlist + // // PlaylistTools.SavePLSPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath); + // //} + // //else if (dialogSavePlaylist.FileName.ToUpper().Contains(".XSPF")) + // //{ + // // // Save playlist + // // PlaylistTools.SaveXSPFPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath); + // //} + + // //// Change cursor + // //Cursor.Current = Cursors.Default; + + // //// Refresh title + // //RefreshTitle(); + //} + + //// Check if playlist file is valid + //if (!String.IsNullOrEmpty(Main.Player.Playlist.FilePath)) + //{ + // // Check if file exists + // if (File.Exists(Main.Player.Playlist.FilePath)) + // { + // // Warn user is he wants to save + // if(MessageBox.Show("Warning: The playlist file already exists. Do you wish to overwrite this file?", "The playlist file already exists", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == System.Windows.Forms.DialogResult.Yes) + // { + // // The user wants to save + // SavePlaylist(Main.Player.Playlist.FilePath); + // } + // } + //} + //else + //{ + // // Ask user for file path + // SavePlaylistAs(); + //} + } +} diff --git a/MPfm/MPfm.MVP/Views/IPlaylistView.cs b/MPfm/MPfm.MVP/Views/IPlaylistView.cs index e7d91cd4..9546faac 100644 --- a/MPfm/MPfm.MVP/Views/IPlaylistView.cs +++ b/MPfm/MPfm.MVP/Views/IPlaylistView.cs @@ -1,43 +1,43 @@ -// Copyright © 2011-2013 Yanick Castonguay -// -// This file is part of MPfm. -// -// MPfm is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// MPfm is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with MPfm. If not, see . - -using System; -using System.Collections.Generic; -using MPfm.Sound.AudioFiles; -using MPfm.Sound.Playlists; - -namespace MPfm.MVP.Views -{ - /// - /// Playlist view interface. - /// - public interface IPlaylistView : IBaseView - { - // Note: These actions use the PlaylistItemId, not AudioFileId! - Action OnChangePlaylistItemOrder { get; set; } - Action OnSelectPlaylistItem { get; set; } - Action OnRemovePlaylistItem { get; set; } - Action OnNewPlaylist { get; set; } - Action OnLoadPlaylist { get; set; } - Action OnSavePlaylist { get; set; } - Action OnShufflePlaylist { get; set; } - - void PlaylistError(Exception ex); - void RefreshPlaylist(Playlist playlist); - void RefreshCurrentlyPlayingSong(int index, AudioFile audioFile); - } -} +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; +using System.Collections.Generic; +using MPfm.Sound.AudioFiles; +using MPfm.Sound.Playlists; + +namespace MPfm.MVP.Views +{ + /// + /// Playlist view interface. + /// + public interface IPlaylistView : IBaseView + { + // Note: These actions use the PlaylistItemId, not AudioFileId! + Action OnChangePlaylistItemOrder { get; set; } + Action OnSelectPlaylistItem { get; set; } + Action> OnRemovePlaylistItems { get; set; } + Action OnNewPlaylist { get; set; } + Action OnLoadPlaylist { get; set; } + Action OnSavePlaylist { get; set; } + Action OnShufflePlaylist { get; set; } + + void PlaylistError(Exception ex); + void RefreshPlaylist(Playlist playlist); + void RefreshCurrentlyPlayingSong(int index, AudioFile audioFile); + } +} diff --git a/MPfm/MPfm.Sound/Playlists/Playlist.cs b/MPfm/MPfm.Sound/Playlists/Playlist.cs index 316acee5..6a0d4527 100644 --- a/MPfm/MPfm.Sound/Playlists/Playlist.cs +++ b/MPfm/MPfm.Sound/Playlists/Playlist.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.Linq; using MPfm.Core; using MPfm.Sound.AudioFiles; @@ -27,11 +28,6 @@ namespace MPfm.Sound.Playlists /// public class Playlist { - #region Properties - - /// - /// Private value for the Items property. - /// private List items = null; /// /// List of playlist items. @@ -44,9 +40,6 @@ public List Items } } - /// - /// Private value for the CurrentItemIndex property. - /// private int currentItemIndex = 0; /// /// Returns the current playlist item index. @@ -59,9 +52,6 @@ public int CurrentItemIndex } } - /// - /// Private value for the CurrentItem property. - /// private PlaylistItem currentItem = null; /// /// Returns the current item. @@ -84,10 +74,6 @@ public PlaylistItem CurrentItem /// public PlaylistFileFormat Format { get; set; } - #endregion - - #region Constructors - /// /// Default constructor for the Playlist class. /// @@ -99,8 +85,6 @@ public Playlist() Format = PlaylistFileFormat.Unknown; } - #endregion - /// /// Loads a playlist (from any of the following formats: M3U, M3U8, PLS and XSPF). /// Note: long playlists may take a while to load using this method! @@ -109,65 +93,39 @@ public Playlist() /// Playlist public void LoadPlaylist(string filePath) { - // Declare variables List files = new List(); - // Determine the playlist format if (filePath.ToUpper().Contains(".M3U")) { - // Set format Format = PlaylistFileFormat.M3U; - - // Load playlist file files = PlaylistTools.LoadM3UPlaylist(filePath); } else if (filePath.ToUpper().Contains(".M3U8")) { - // Set format Format = PlaylistFileFormat.M3U8; - - // Load playlist file files = PlaylistTools.LoadM3UPlaylist(filePath); } else if (filePath.ToUpper().Contains(".PLS")) { - // Set format Format = PlaylistFileFormat.PLS; - - // Load playlist file files = PlaylistTools.LoadPLSPlaylist(filePath); } else if (filePath.ToUpper().Contains(".XSPF")) { - // Set format Format = PlaylistFileFormat.XSPF; - - // Load playlist file files = PlaylistTools.LoadXSPFPlaylist(filePath); } else if (filePath.ToUpper().Contains(".ASX")) { - // Set format Format = PlaylistFileFormat.ASX; } - // Check if the playlist is empty if (files == null || files.Count == 0) - { - // Display error throw new Exception("Error: The playlist is empty or does not contain any valid audio file paths!"); - } - // Clear current playlist Clear(); - - // Set properties FilePath = filePath; - - // Add files to playlist AddItems(files); - - // Set first item First(); } @@ -191,7 +149,6 @@ public void DisposeChannels() // Free current channel if (currentItem.Channel != null) { - // Stop and free channel currentItem.Dispose(); currentItem = null; } @@ -210,12 +167,8 @@ public void DisposeChannels() /// private void UpdateCurrentItem() { - // Check if there is at least one item but no current item set if (currentItem == null && items.Count > 0) - { - // Set current item to the first in the list currentItem = items[0]; - } } /// @@ -224,13 +177,8 @@ private void UpdateCurrentItem() /// Audio file path public void AddItem(string filePath) { - // Create audio file and read metadata AudioFile audioFile = new AudioFile(filePath); - - // Add new playlist item at the end Items.Add(new PlaylistItem(this, audioFile)); - - // Update current item UpdateCurrentItem(); } @@ -240,12 +188,7 @@ public void AddItem(string filePath) /// Audio file metadata public void AddItem(AudioFile audioFile) { - // Add new playlist item at the end - //Tracing.Log("Playlist.cs -- Adding " + audioFile.FilePath); Items.Add(new PlaylistItem(this, audioFile)); - - // Update current item - //Tracing.Log("Playlist.cs -- Updating " + audioFile.FilePath); UpdateCurrentItem(); } @@ -289,8 +232,6 @@ public void AddItems(List filePaths) { // Create audio file and read metadata audioFile = new AudioFile(filePaths[a]); - - // Set flag addItem = true; } catch @@ -299,15 +240,10 @@ public void AddItems(List filePaths) addItem = false; } - // Make sure the item needs to be added if (addItem) - { - // Add new playlist item at the end Items.Add(new PlaylistItem(this, audioFile)); - } } - // Update current item UpdateCurrentItem(); } @@ -317,12 +253,8 @@ public void AddItems(List filePaths) /// List of AudioFile instances public void AddItems(List audioFiles) { - // Loop through file paths foreach (AudioFile audioFile in audioFiles) - { - // Add item AddItem(audioFile); - } } /// @@ -340,12 +272,8 @@ public void InsertItem(string filePath, int index) // Increment current item index if an item was inserted before the current item if (index <= CurrentItemIndex) - { - // Increment index currentItemIndex++; - } - // Update current item UpdateCurrentItem(); } @@ -361,12 +289,8 @@ public void InsertItem(AudioFile audioFile, int index) // Increment current item index if an item was inserted before the current item if (index <= CurrentItemIndex) - { - // Increment index currentItemIndex++; - } - // Update current item UpdateCurrentItem(); } @@ -378,21 +302,26 @@ public void RemoveItem(int index) { // Make sure the item is not playing if (CurrentItemIndex == index) - { throw new Exception("You cannot remove a playlist item which is currently playing."); - } - // Dispose item Items[index].Dispose(); - - // Remove playlist item Items.RemoveAt(index); // Decrement current item index if an item was removed before the current item if (index <= CurrentItemIndex) - { - // Decrement current item index currentItemIndex--; + } + + /// + /// Remove all playlist items matching the ids in the list. + /// + /// List of playlist ids to remove + public void RemoveItems(List playlistIds) + { + foreach (var playlistId in playlistIds) + { + var stuff = Items.Select(x => x.Id == playlistId).ToList(); + Items.RemoveAll(x => x.Id == playlistId); } } @@ -401,7 +330,6 @@ public void RemoveItem(int index) /// public void First() { - // Set first index currentItemIndex = 0; currentItem = items[currentItemIndex]; } @@ -412,7 +340,6 @@ public void First() /// Playlist item index public void GoTo(int index) { - // Set index currentItemIndex = index; currentItem = items[currentItemIndex]; } @@ -427,10 +354,8 @@ public void GoTo(Guid id) int index = -1; for (int a = 0; a < Items.Count; a++) { - // Does the id match? if (Items[a].Id == id || Items[a].AudioFile.Id == id) { - // The item has been found, exit loop index = a; break; } @@ -438,10 +363,7 @@ public void GoTo(Guid id) // Check if we have a valid item if (index >= 0) - { - // Skip to this item GoTo(index); - } } /// @@ -454,10 +376,8 @@ public void GoTo(string filePath) int index = -1; for (int a = 0; a < Items.Count; a++) { - // Does the id match? if (Items[a].AudioFile.FilePath == filePath) { - // The item has been found, exit loop index = a; break; } @@ -465,10 +385,7 @@ public void GoTo(string filePath) // Check if we have a valid item if (index >= 0) - { - // Skip to this item GoTo(index); - } } /// @@ -476,12 +393,8 @@ public void GoTo(string filePath) /// public void Previous() { - // Check if the previous channel needs to be loaded if (currentItemIndex > 0) - { - // Increment item currentItemIndex--; - } currentItem = items[currentItemIndex]; } @@ -490,12 +403,8 @@ public void Previous() /// public void Next() { - // Check if the next channel needs to be loaded if (currentItemIndex < items.Count - 1) - { - // Increment item currentItemIndex++; - } currentItem = items[currentItemIndex]; } } diff --git a/MPfm/MPfm.Windows/Classes/Forms/frmMain.Designer.cs b/MPfm/MPfm.Windows/Classes/Forms/frmMain.Designer.cs index a1ed1ed3..d86ae14f 100644 --- a/MPfm/MPfm.Windows/Classes/Forms/frmMain.Designer.cs +++ b/MPfm/MPfm.Windows/Classes/Forms/frmMain.Designer.cs @@ -3859,8 +3859,8 @@ private void InitializeComponent() songGridViewTheme1.RowNowPlayingTextGradient = textGradient65; textGradient66.BorderColor = System.Drawing.Color.Gray; textGradient66.BorderWidth = 0; - textGradient66.Color1 = System.Drawing.Color.FromArgb(((int)(((byte)(225)))), ((int)(((byte)(234)))), ((int)(((byte)(240))))); - textGradient66.Color2 = System.Drawing.Color.FromArgb(((int)(((byte)(225)))), ((int)(((byte)(234)))), ((int)(((byte)(240))))); + textGradient66.Color1 = System.Drawing.Color.White; + textGradient66.Color2 = System.Drawing.Color.White; customFont64.Color = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); customFont64.EmbeddedFontName = "Junction"; customFont64.IsBold = false; diff --git a/MPfm/MPfm.Windows/Classes/Forms/frmMain.resx b/MPfm/MPfm.Windows/Classes/Forms/frmMain.resx index 1b699304..afe19672 100644 --- a/MPfm/MPfm.Windows/Classes/Forms/frmMain.resx +++ b/MPfm/MPfm.Windows/Classes/Forms/frmMain.resx @@ -194,8 +194,8 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 - ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAACW - bAAAAk1TRnQBSQFMAgEBGwEAARwBCgEcAQoBEAEAARABAAT/ASEBAAj/AUIBTQE2BwABNgMAASgDAAFA + ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAACY + bAAAAk1TRnQBSQFMAgEBGwEAASQBCgEkAQoBEAEAARABAAT/ASEBAAj/AUIBTQE2BwABNgMAASgDAAFA AwABcAMAAQEBAAEgBgABcBIAA14B5QNRAfcDUQH3A1EB9wNRAfcDUQH3A1EB9wNRAfcDUQH3A1EB9wNR AfcDUQH3A1EB9wNRAfcDUQH3A1kB7BAAAyoBQQNQAaQDWAHjAwAB/gMAAf4DXQHiA1EBoQMnATsfAAEB AwAB/gMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DKQH6QAADSgGLAwAB/wMA @@ -207,458 +207,458 @@ AwAB/wMAAf8DAAH/AwAB/wMAAf8DWwHIAxIBGEgAAzUBVgNdAeIDAAH/AwAB/wMAAf8DAAH/AwAB/wMA Af8DAAH/AwAB/wNYAeYDOQFgDAADUwGqAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/ AwAB/wMAAf8DAAH/A1EBogQAAycBOwMoATwDKAE8AygBPAMoATwDEQEXAzsBZQNcAdwDAAH/AwAB/wMA - Af8DAAH/A1wB3AM8AWcDAgEDUAADBgEIAzoBYgNXAb0DKQH6AwAB/wMAAf8DJAH8A1YBwQM8AWYDBwEJ + Af8DAAH/A1wB3AM8AWcDAgEDUAADBgEIAzoBYgNXAb0DKQH6AwAB/wMAAf8DJQH8A1YBwQM8AWYDBwEJ DAADKgFBAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMA Af8DJQE3A1kBwwMAAf8DAAH/AwAB/wMAAf8DWAHvA0YBgQMTARoDOAFeAwAB/wMAAf8DOQFfAwABAWQA - A08BnAMAAf8DAAH/A1QBqxgAA1IBqQMAAf8DAAH/AwAB/wMAAf8DAAH/ATcBOAE7Af8B3QHeAd8B/wLa - AdwB/wEyATMBNgH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DTwGcAxABFgNZAcMDAAH/AwAB/wMAAf8DAAH/ + A08BnAMAAf8DAAH/A1QBqxgAA1IBqQMAAf8DAAH/AwAB/wMAAf8DAAH/ATYBNwE6Af8B3QHeAd8B/wLa + AdwB/wExATIBNQH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DTwGcAxABFgNZAcMDAAH/AwAB/wMAAf8DAAH/ AwAB/wMuAUcDTAGRAwAB/wMAAf8DSwGQZAADDAEQA1oB3gMAAf8DAAH/A1gB5gMRARcUAANZAesDAAH/ - AwAB/wMAAf8DAAH/ATcBOAE7Ef8BMwE0ATYB/wMAAf8DAAH/AwAB/wMAAf8DWgHbBAADAQECAzoBYgNe - AdoDAAH/AwAB/wNXAb8DFAQcAfwDAAH/AwAB/wMhAfsDEgEZYAADSgGNAwAB/wMAAf8DAAH/AwAB/wNP - AZwXAAH/AwAB/wMAAf8DAAH/AwAB/wHdAd4B3wX/AqEBogH+Au0B7gX/AdsB3AHeAf8DAAH/AwAB/wMA - Af8DAAH/AykB+g8AAQEDOAFdAwAB/wMtAUYDUgGpAwAB/wMAAf8DAAH/AwAB/wNTAapcAAMQARUDVQHq - AwAB/wMAAf8DAAH/AwAB/wNSAfQDEgEZEwAB/wMAAf8DAAH/AwAB/wMAAf8B2wHcAd4F/wKfAaAB/gLr - AewF/wLaAdwB/wMAAf8DAAH/AwAB/wMAAf8DKQH6EAADSwGPAwAB/wMuAUcDVAGvAwAB/wMAAf8DAAH/ - AwAB/wNTAbBcAANQAaQDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DUwGtEAADWAHpAwAB/wMAAf8DAAH/ - AwAB/wEyATMBNhH/Ai4BMQH/AwAB/wMAAf8DAAH/AwAB/wNeAdoMAAMUARsDHAH8AwAB/wM8AWcDTwGb - AwAB/wMAAf8DAAH/AwAB/wNPAZtcAANRAaADAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DUwGqEAADUgGm - AwAB/wMAAf8DAAH/AwAB/wMAAf8BMgEzATYB/wHbAdwB3gH/AtkB2wH/Ai4BMQH/AwAB/wMAAf8DAAH/ - AwAB/wMAAf8DTgGZDAADUwGqAwAB/wMAAf8DRAF7A0cBggMAAf8DAAH/AwAB/wMAAf8DRgGBXAADQAFw - AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/A0QBfBAAAykBPwMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMA - Af8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AyQBNQwAA1QBrgMAAf8DAAH/A1wBzgMWAR4DVwHf - AwAB/wMAAf8DWgHeAxMBGlwAA0IBdQMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wNGAYAUAANRAacDAAH/ - AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DUAGfEAADUAGaAwAB/wMA - Af8DAAH/A08BnAMQARUDIwE0AyQBNQMEAQZgAAMwAUwDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DNgFZ - FAADCQEMA1sByAMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DWQHDAwcBChAA - A0YBgAMAAf8DAAH/AwAB/wMAAf8DRgGBbAADBAEFA1gBywMAAf8DAAH/AwAB/wMAAf8DWwHWAwcBCRgA - AwkBDANQAaQDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/gNPAZwDBwEKFAADFAEbA1gB3QMA - Af8DAAH/A1gB3QMTARpwAAMNARIDUQGnA1IB9ANIAfYDVAGuAxIBGCQAAycBOwNRAaEDXQHiAwAB/gMA - Af4DXwHgA1ABngMkATUgAAMQARUDWQHJA1gBywMQARbgAED/wAAE/wOBAf8D8QH/A4EB/wOBAf8DgQH/ - A6kB/wOkAf8DoAH/A6QB/wOBAf8DgQH/A4EB/wO4Af8DhgH/A4EB/wwAAxUBHQNAAXABVQFXAVUBtwFV - AVwBVQHqA0AB/QMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/ygAAwcBCVwABP8DgQH/A+4B/wOBAf8DgQH/ - A4kB/wOYAf8DgQH/A4EB/wOBAf8DpAH/A4EB/wOBAf8DgQH/A8IB/wOBAf8IAAMmATkBUwFVAVMBsAEA - AaMBAAH/AQABsQEAAf8BAAG8AQAB/wEAAb8BAAH/AQABvgEAAf8BAAG9AQAB/wEAAbwBAAH/AQABuwEA - Af8BAAG5AQAB/wMAAf8kAANZAfUDgQH/AxgBIVgABP8DgQH/A8UB/wOBAf8DgQH/A68B/wOBAf8DgQH/ - A4EB/wOBAf8DhgH/A4EB/wOBAf8DgQH/A+sB/wOBAf8EAAMVAR0BUwFVAVMBsAEAAbUBAAH/AYUBxwGL - Af8BAAHBAQEB/wEBAcQBhgH/AQABwgGCAf8BAAHBAYAB/wEAAb8BAAH/AQABvQEAAf8BAAG9AQAB/wEA - AbsBAAH/AwAB/yQAA1kB9QGBAaEBgQH/A2EB5AMcASdAAANXAbgDBAEGDAAE/wOeAf8DlQH/A4EB/wOB - Af8DmwH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DowH/A4EB/wOBAf8D6AH/A4EB/wQAA0ABcAEAAbIBAAH/ - AYsBywGTAf8BiQHKAZAB/wEAAcEBhQH/AQABogEAAf8BAAGFAQAB/wEAAYABAAH/AwAB/wMAAf8DAAH/ - AwAB/wMAAf8UAAN/Af4DfwH+A38B/gN/Af4BgQGfAYEB/wGBAcEBlgH/AYEBowGBAf8DZAHnAx0BKjgA - AyYBOANtAfcBWwFeAVsB2QwABP8DwgH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/ - A6QB/wOBAf8DgQH/A+YB/wOBAf8EAAFVAlcBtwEBAcIBhwH/AYcBygGQAf8BhQHGAYwB/wFdAWEBXQHi - AUsBTAFLAY8wAAGBAZsBgQH/AZABygGpAf8BjQHIAaUB/wGKAcYBoQH/AYgBxQGeAf8BgQG2AYUB/wGC - AcIBlwH/AYEBpQGBAf8DXAHqAyEBMBAAAYEBvgGBAf8BgQG4AYEB/wGBAbEBgQH/AYEBqQGBAf8BgQGh - AYEB/wGBAZkBgQH/AYEBkQGBAf8BgQGIAYEB/wOBAf8BgQGIAYEB/wGBAaEBgQH/AYEBiwGBAf8DXAHP - CAAE/wPdAf8DgQH/A4EB/wOdAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DoQH/A4EB/wOBAf8D5gH/ - A4EB/wQAAVUCXAHqAZABzgGYAf8BkQHPAZkB/wEAAbkBAAH/AUsCTAGPNAABgQGfAYEB/wGUAc0BrQH/ - AYEBugGOAf8BgQG4AYkB/wGBAbYBhQH/AYEBswGBAf8BgQG1AYIB/wGDAcIBmAH/AYEBoAGBAf8DKwH8 - EAABgQHDAYEB/wGgAdcBqQH/AZwB1QGlAf8BmAHTAaEB/wGUAdABnQH/AZABzgGYAf8BiwHLAZMB/wGH - AckBjgH/AYIBxgGJAf8BgQHDAYQB/wGBAcEBgQH/AYEBvgGBAf8BgQGMAYEB/wNbAcQEAAT/A/AB/wOB - Af8DgQH/A7EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOgAf8DgQH/A4EB/wPlAf8DgQH/BAADXAHq - AZQB0QGcAf8BlQHRAZ4B/wEAAbwBAQH/AUsCTAGPFAADRAF7AwIBAxgAAYEBowGBAf8BlgHOAbAB/wGU - Ac0BrQH/AZEBywGqAf8BkAHLAagB/wGBAbwBkAH/AYoBxwGhAf8BgQGlAYEB/wFAAYcBQAH9AwsBDxAA - AYEBxwGBAf8BpQHaAa4B/wGiAdgBqwH/AZ4B1gGnAf8BmgHUAaMB/wGWAdIBnwH/AZMBzwGaAf8BjgHM - AZUB/wGJAcoBkAH/AYUBxwGLAf8BgQHFAYcB/wGBAcIBggH/AYEBkQGBAf8DXAHJBAAE/wPwAf8DgQH/ - A4EB/wOxAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DoQH/A4EB/wOBAf8D5gH/A4EB/wQAA1cBtwGK - AcwBkwH/AZMB0QGdAf8BkQHOAZoB/wFdAWEBXQHiA0wBjxAAAVkBXQFZAdIBUAFRAVABnxgAAYEBpQGB - Af8BQAGjAUAB/QFAAaEBQAH9AUABnQFAAf0BgQGvAYEB/wGRAcsBqgH/AYEBqwGBAf8BQAGPAUAB/QMJ - AQwUAAGBAccBgQH/AYEBxwGBAf8BgQHDAYEB/wGBAb4BgQH/AYEBuAGBAf8BgQGxAYEB/wGBAakBgQH/ - AYEBoQGBAf8BgQGZAYEB/wGBAZEBgQH/AYEBowGBAf8BgQGdAYEB/wNcAdEIAAT/A90B/wOBAf8DgQH/ - A54B/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOkAf8DgQH/A4EB/wPmAf8DgQH/BAADQAFwAQABxQGE - Af8BngHWAacB/wGcAdUBpQH/AZIBzwGaAf8BAAG4AQAB/wEAAaABAAH/AQABnAEAAf8BAAGXAQAB/wEA - AZIBAAH/AQABpgEAAf8BAAGjAQAB/wNMAZMkAANZAfUBgQGzAYEB/wGBAZgBgQH/AwcBCUAAA20B9wNf - AdsMAAT/A8IB/wOBAf8DgQH/A4MB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOpAf8DgQH/A4EB/wPo - Af8DgQH/BAADFQEdA1UBsAGIAcwBkgH/AaAB1wGpAf8BlgHTAaAB/wGbAdQBowH/AZkB0wGhAf8BlgHR - AZ4B/wGTAc8BmwH/AZABzgGZAf8BjQHMAZUB/wGKAcoBkQH/AQABowEAAf8DSgGKIAADWQH1AYEBnwGB - Af8DBwEJRAADWAG7AwQBBgwABP8DngH/A5cB/wOBAf8DgQH/A6EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/ - A6kB/wOBAf8DgQH/A+sB/wOBAf8IAAMmATkDVQGwAQABxQGFAf8BjQHOAZYB/wGYAdMBoQH/AZwB1QGl - Af8BmQHUAaIB/wGXAdIBnwH/AZQB0AGdAf8BkQHPAZkB/wGNAcwBlAH/AQABqAEAAf8DSAGEhAAE/wOB - Af8DxgH/A4EB/wOBAf8DtQH/A4EB/wOBAf8DgQH/A4EB/wOMAf8DgQH/A4EB/wOBAf8D7gH/A4EB/wwA - AxUBHQNAAXADVwG3A1wB6gEAAbABAAH/AQABrAEAAf8BAAGnAQAB/wEAAaMBAAH/AQABswEAAf8BAAGv - AQAB/wFLAkwBkIgABP8DgQH/A/EB/wOBAf8DgQH/A40B/wOhAf8DgQH/A4EB/wOBAf8DrwH/A4EB/wOB - Af8DgQH/A8MB/wOBAf8sAAFZAV0BWQHSAU8CUQGcjAAE/wOBAf8D9gH/A4EB/wOBAf8DgQH/A7IB/wOv - Af8DrAH/A68B/wOBAf8DgQH/A4EB/wO9Af8DiQH/A4EB/ywAA0MBeAMCAQOMAED/QAADJgE5A0QBegNV - AbABywKBAf8B0gKBAf8B0wKBAf8B0QKBAf8BzQKBAf8DXAHfA0cBgAM3AVskAAMnATsDTgGUA14BzgNg - AesDYAHrA14BzgNtAfcDgQH/AwMBBFgAAxgBIgNTAaoDWAG7AxgBIggAAzUBVQNYAbsDQwF3Aw0BEQwA - A2EB5gGBAYUBrAH/AcgCgQL/AboBgQL/AbcBgQL/AbABgQL/AbIBgQL/AbQBgQH/Ad4BiAGBAf8BhwKB - Af8BgQGbAeYB/xwAAwoBDQNOAZYDTQH6AeEBzQG5Af8B8QHgAc4B/wH5AecB1gH/AfgB5gHUAf8B8AHd - AcwB/wGBAYkBgQH/AYEBoQGBAf8DXAHqAw4BEwwAA1sBxANlAfQB0QGWAYEB/wHOAZIBgQH/AcsBjgGB - Af8ByQGKAYEB/wHHAYcBgQH/AcMBhAGBAf8BwwGEAYEB/wHDAYQBgQH/AcMBhAGBAf8BwwGEAYEB/wHD - AYQBgQH/A1UBsAwAAzUBVQNhAe4BgQGhAfMB/wGBAZoB7wH/A0MBdwgAA1wBzAGBAaEB8gH/AYEBiAHj - Af8DYQHuAzUBVQgAAysB/AGTAawBzAH/AecBrwGBAv8BuAGBAf8B/gGwAYEB/wH+AacBgQH/Af0BoAGB - Av8BlwGBAf8B/QGTAYEB/wGzAZ8BogH/AYEBlgHeAf8YAAMKAQ0DVwG4AdkBvwGlAf8B/gHyAeQC/wHn - Ac8B/wGBAZgBgQH/AYEBkgGBAf8BgQGQAYEB/wGBAYwBgQH/AYEBnwGBAf8BgQHBAZYB/wGBAaMBgQH/ - AVkBZwFZAfIDDwEUCAAB1wGhAYEB/wH4AfIB7QH/AfcB8AHqAf8B9gHtAeYB/wH0AeoB4gH/AfMB5wHe - Af8B8QHkAdsB/wHwAeIB2AH/AeoB1gHIAf8B8gHlAdwB/wH6AfQB8QH/AfkB8wHwAf8B+gH1AfIB/wGo - AYoBQAH9DAADNQFVAYEBmAHwAf8BgQG7Af4B/wGBAbQB/AH/A2EB7gMNAREDLAFEAYEBhQHiAf8BgQGz - Av8BgQGqAfkB/wKBAdUB/wMjATMIAAM1AVUDWQH1Af0B1wGyAv8B1gGEAv8BygGBAv8BwAGBAv8BtAGB - Av8BpwGBAv8BxQGSAf8B1gKBAf8DTgGWGAADTgGWAdkBvwGlAv8B+AHrAv8B6wHRAv8B4QHDAf8BgQGb - AYEB/wGQAcoBqQH/AY0ByAGlAf8BigHGAaEB/wGIAcUBngH/AYEBtgGFAf8BggHCAZcB/wGBAaUBgQH/ - AVgBYgFYAe8DBwEJBAAB2QGkAYEB/wH5AfMB7gH/AesB0gG+Bf8B6wHTAb8F/wHrAdMBwAX/AeoBxwGt - Af8B7AHZAc0B/wHxAeQB2wH/AfkB8wHwAf8B+QHyAe8B/wHGAYwBgQH/EAADSQGIAYEBnQHxAf8BgQG9 - Av8BgQGYAe8B/wM8AWYDTgGZAYEBngHyAf8BgQG0Av8BgQGMAeYB/wNDAXcQAAM7AWQDXwHzAf8B6AHA - Af8BrQGqAYYB/wGnAZ4BgQH/AaABkgGBAf8B9wHKAZwB/wHkAZQBgQH/A1EBoAMHAQoUAAMmATgBqAGH - AUAB/QL+AfoC/wH5AekC/wHvAdgC/wHnAc0B/wGBAZ8BgQH/AZQBzQGtAf8BgQG6AY4B/wGBAbgBiQH/ - AYEBtgGFAf8BgQGzAYEB/wGBAbUBggH/AYMBwgGYAf8BgQGgAYEB/wNNAfoEAAHdAagBgQH/AfkB8wHv - Af8B6wHQAboB/wHrAdABuwH/AYEBtQGBAf8BgQG1AYEB/wGBAbUBgQH/AesB0QG9Af8B6gHNAbUB/wH6 - AfQB8AH/AesB2QHMAf8B8QHkAdsB/wH6AfQB8QH/AcYBigGBAf8UAANYAbsBgQGhAfQB/wGBAbIB+gH/ - A14B3QGBAaIB8wH/AYEBtgL/AYEBlwHuAf8DTgGZGAADRAF5A00B+gGBAa0B5wH/AYEBsQHjAf8BgQGy - AecB/wGBAZgBxAH/A4EB/wGBAaQBgQH/AYEBqAGBAf8BgQGnAYEB/wGBAaMBgQH/A1wB3wNHAYADNwFb - A04BlgHiAc4BugH/AfcB3gHGAv8B6QHRAv8B9gHiAv8B8gHdAf8BgQGjAYEB/wGWAc4BsAH/AZQBzQGt - Af8BkQHLAaoB/wGQAcsBqAH/AYEBvAGQAf8BigHHAaEB/wGBAaUBgQH/AYEBiAGBAf8DTgGWBAAB3wGq - AYIB/wH5AfMB7wH/AeoBzgG3Bf8BgQG1AYEB/wGUAdQBmwH/AYEBtQGBBf8B6gHPAboB/wH7AfYB8gH/ - AfoB8wHwAf8B6wHYAcsB/wHyAeYB3QH/AcgBjQGBAf8UAAMNAREDYQHuAYEBuQH+Af8BgQGyAfwB/wGB - AbcC/wGBAasB+QH/A1wBzAMNAREYAAM8AWYDfwH+AawB0QHyAf8BrgHXAf0B/wGwAdQB9QH/AZIBwAHn - Af8BgQGDAYkB/wGBAc8BlAH/AYEBzgGOAf8BgQHOAY8B/wGBAc4BkQH/AYEBtAGBAf8BgQGPAYEB/wGB - AZsB5gH/A14B0AHvAeAB0AH/AecBvgGWAf8B5wG8AZQB/wHwAcwBqQH/Af4B7AHaAf8BgQGlAYEB/wGB - AaMBgQH/AYEBoQGBAf8BgQGeAYEB/wGBAa8BgQH/AZEBywGqAf8BgQGrAYEB/wGBAZABgQH/AfAB3wHO - Af8DXgHQBAAB4QGuAYcB/wH6AfQB8AH/AeoBywGyAf8B6gHMAbMB/wGBAbUBgQH/AYEBtQGBAf8BgQG0 - AYEB/wHqAc4BtwH/AYEBswGBAf8BgQGyAYEB/wGBAbEBgQH/AegByAGuAf8B6gHXAckB/wHEAYYBgQH/ - BAADWAG7AYEBkAH0Af8BgQGOAfMB/wNcAcwDWAG7A1gBuwGBAa4B+gH/AYEBtwL/AYEBtQL/AYEBrQH6 - Af8DXAHMA1gBuwNeAd0CgQHcAf8CgQHXAf8DWAG7CAABVAJWAasBpwG/AdsB/wGWAcUB8QH/AaEB0AH7 - Af8BlgHGAfMB/wG2Ac8B5AH/AoEBpwH/AYEBzgGPAf8BgQHNAYkB/wGBAcsBgwH/AYEBywGBAf8BgQHJ - AYEB/wGBAZYBgQH/AYEBlgHeAf8DZAHsAfUB5AHVAf8B6QG+AZcB/wHoAbwBlgH/AecBvAGUAf8B8wHe - AcoB/wNhAeYDFAEbAxQBGwNhAeYBgQGkAYEB/wGBAbMBgQH/AYEBmAGBAf8B7QHJAaQB/wH2AeQB0gH/ - A2QB7AQAAeMBsQGMAf8B+gH2AfEB/wHqAckBrgX/AeoByQGwBf8B6QHLAbMF/wGBAbEBgQH/AY4B0gGV - Af8BgQGvAYEF/wHxAeUB2wH/AcYBhgGBAf8EAANYAbsBgQG6Af0B/wGBAccC/wGBAbsB/QH/AYEBtQH7 - Af8BgQGzAfsB/wGBAb0C/wGBAbgC/wGBAbUC/wGBAbgC/wGBAa8B+gH/AYEBqgH4Af8BgQGxAfsB/wGB - AbcC/wGBAaUB9gH/A1gBuwgAA1sBxAKBAZoB/wKBAa4B/wGBAaYB1gH/AoEBpQH/AoEBkgH/AoEBmAH/ - AYEB0AGfAf8BgQHOAZcB/wGBAc0BjgH/AYEBzQGIAf8BnQHYAasB/wGBAbABgQH/A04BlgNkAewB9QHk - AdUB/wHpAb8BmQH/AekBwAGZAf8B6QHBAZoB/wHzAd0ByQH/A2EB5gMUARsDFAEbA2EB5gGBAagBgQH/ - AYEBnwGBAf8B8AHMAasB/wHwAcwBqwH/AfYB5AHTAf8DZAHsBAAB5QG0AY8B/wH6AfYB8gH/AekBxgGq - Af8B6QHGAawB/wHqAccBrAH/AekBxwGtAf8B6QHJAa4B/wHpAckBsAH/AYEBsAGBAf8BgQGvAYEB/wGB - Aa0BgQH/AegBzAG1Af8B8gHnAd4B/wHIAYoBgQH/BAADWAG7AYEBnwH5Af8BgQGTAfYB/wNhAe4DWAG7 - A1wBzAGBAbQB/AH/AYEBuQL/AYEBtwL/AYEBrwH7Af8DXAHMA1gBuwNeAd0CgQHiAf8BgQGGAeYB/wNY - AbsIAAMsAUMCQAGVAf0CgQGQAf8CgQGQAf8CgQGQAf8CgQGQAf8BgQGPAZ4B/wGEAZkBkwH/AYEBlAGJ - Af8BgQGMAYEB/wGkAdUBsQH/AYIBvwGFAf8DUQGgAwcBCgNeAdAB8AHhAdEB/wHqAcQBoQH/AekBwQGb - Af8B6QHAAZcB/wHxAc4BsgH/AdwBwwGqAf8DZAHnA2QB5wHdAcQBrAL/AfwB8wL/AfQB5QL/Ae4B2gH/ - AfwB5QHRAf8B8QHhAdEB/wNeAdAEAAHnAbcBlAH/AfsB9wH0Af8B6QHDAaYF/wHoAcQBqQX/AekBxgGq - Bf8B6AHHAawF/wHoAcgBsAX/AfcB8QHrAf8BywGPAYEB/wQAAyMBMwMjATMIAAMNAREDYQHuAYEBvQH+ - Af8BgQG5Af4B/wGBAboC/wGBAbEB+wH/A1wBzAMNAREIAAMjATMDIwEzDAADOgFhA2QB8QKBAZIB/wNf - AfsDXAHMA00B+gGBAa0B5wH/AYEBsQHjAf8BgQGyAecB/wGBAZgBxAH/A1YBtgMIAQsEAANOAZYB4gHO - AboB/wHxAdIBtgH/AesBwQGbAf8B6wG/AZkC/wHkAdMC/wHoAdsB/wHzAdoBxwH/AfgB8QHkAv8B+AHt - Av8B9wHpAv8B/gHxAv8B/AHvAv8B/AHyAf8B4gHOAboB/wNOAZYEAAHpAboBmAH/AfsB9wH0Af8BgQGk - Av8BgQGjAv8BgQGiAv8BgQGhAv8BgQGfAv8BgQGdAv8BgQGaAv8BgQGYAv8BgQGVAv8BgQGUAv8B+wH3 - AfQB/wHOAZMBgQH/FAADWAG7AYEBqwH6Af8BgQHAAf4B/wNhAe4BgQGsAfkB/wGBAbwC/wGBAaIB9gH/ - A04BmSwAAzwBZgN/Af4BrAHRAfIB/wGuAdcB/QH/AbAB1AH1Af8BkgHAAecB/wNVAa0IAAMmATgBqAGH - AUAB/QH6Ae0B4QH/AewBxwGjAv8B7AHaBf8B9wHNAbYB/wHsAb0BlwL/AfQB3AP/AfcC/wH3AegD/wHz - A/8B+QH/Av4B+QH/AagBhwFAAf0DJgE4BAAB6wG9AZsB/wH7AfcB9AH/AYEBpAL/AYEBvQL/AYEBuwL/ - AYEBuQL/AYEBuAL/AYEBswL/AYEBsAL/AYEBqwL/AYEBpwL/AoEC/wH7AfcB9AH/AdEBlwGBAf8QAAND - AXcBgQGqAfsB/wGBAcQC/wGBAa4B+wH/A04BmQNOAZkBgQGrAfkB/wGBAbsC/wGBAZ0B9AH/AzwBZigA - A0oBjQG4AdAB5wH/AZYBxQHxAf8BoQHQAfsB/wGWAcYB8wH/AbYBzwHkAf8DXAHfDAADTgGWAdkBvwGl - Av8B+gHwBv8B/QHzAf8B8AHBAaMB/wHuAcYBoAH/Af0B8AHRA/8B/gL/Af0B8AL/Af0B8QX/AdkBvwGl - Af8DTgGWCAAB7AG/AZ4B/wH7AfcB9AH/AYEBpAL/AYEBowL/AYEBoAL/AYEBngL/AYEBmQL/AYEBlgL/ - AYEBkAL/AYEBiwL/AYEBhAL/AoEC/wH7AfcB9AH/AdQBmwGBAf8MAAMsAUQBgQGlAf0B/wGBAcAB/gH/ - AYEBxAL/AYEBmwH4Af8DGAEiAywBRAGBAZwB9gH/AYEBvAL/AYEBtAH8Af8DYQHuAyMBMyQAA0wBkgGB - Aa4B5QH/AYEBuAHmAf8BjQHIAfYB/wGBAa0B5gH/AYEBpwHlAf8BWAJiAe8MAAMKAQ0DVwG4AdkBvwGl - Af8B/gH9AfwC/wHxAeQB/wHyAcoBrgH/AfIB0gGtAf8B+QHtAcgD/wH3A/8B/QH/Af4B/QH8Af8B2QG/ - AaUB/wNXAbgDCgENCAADYAHrAfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7 - AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/A1wB+AwA - AzwBZgGBAaQB/QH/AYEBtgH+Af8BgQG7Af4B/wNYAbsIAANcAcwBgQG4Af0B/wGBAagB+QH/A2EB7gM1 - AVUkAAMsAUMBQAGjAagB/QGBAaYB5QH/AYEBpgHlAf8BgQGmAeUB/wGBAaYB5QH/A0wBkBAAAwoBDQNO - AZYDTQH6AeEBzQG5Af8B8QHiAdQB/wH5AewB3wH/AfsB8wHlAf8B9AHtAeQB/wHiAc4BuwH/A00B+gNO - AZYDCgENDAADRgF+A18B4wHtAcABnwH/AesBvgGdAf8B6wG8AZoB/wHpAboBlgH/AecBtwGTAf8B5gG1 - AZAB/wHkAbIBjAH/AeIBrwGIAf8B4AGsAYQB/wHdAakBgQH/AdwBpQGBAf8DXQHKEAADIwEzA1gBuwGB - AZkB/AH/AzUBVQgAAzwBZgGBAZEB9QH/A1gBuwMYASIsAAM6AWEDZAHxAYEBpQHkAf8DXwH7A1ABnQME - AQYYAAMnATsDTgGUA14BzgNgAesCYgFYAe8CXgFbAdkDTwGXAycBO6gAAwoBDQMiATEDIAEvAwUBB+QA - AxgBIQNQAZ0DXQHwA6EB/wOrAf8DpwH/A5UB/wNkAecDSgGKAxABFhQAAwoBDQMaASUDXgHVA38B/gGB - AYkBgQH/AYEBiAGBAf8BgQGHAYEB/wGBAYUBgQH/A38B/gNkAewDHQEqAwoBDRAAAwoBDQMfAS0DYQHh - A38B/gGBAYQB1QH/AYEBggHWAf8BgQGCAdQB/wKBAdIB/wN/Af4DXwHzAyMBMwMKAQ0QAAMKAQ0DHwEt - A2EB4QN/Af4CgQHeAf8CgQHeAf8CgQHdAf8CgQHaAf8DfwH+A18B8wMjATMDCgENEAADLwFKA2UB9APO - Af8D7QH/A/QB/wP1Af8D9AH/A+8B/wPiAf8DugH/A2QB5wMkATUMAANEAXkDXwH7AYEBnQGBAf8BgQGP - AYEB/wGBAcEBigH/AYEBwQGKAf8BgQHBAYoB/wGBAcEBigH/AYEBwQGKAf8BgQHBAYoB/wGBAYoBgQH/ - AYEBiAGBAf8DXAH4A0gBgwgAA0QBeQNfAfsBgQGQAeoB/wGBAYgB3gH/AYEBqwL/AYEBqwL/AYEBqwL/ - AYEBqwL/AYEBqwL/AYEBqwL/AYEBhAHZAf8CgQHTAf8DXAH4A0gBgwgAA0QBeQNfAfsCgQH8Af8CgQHr - Af8BgQGLAv8BgQGLAv8BgQGLAv8BgQGLAv8BgQGLAv8BgQGLAv8CgQHkAf8CgQHaAf8DXAH4A0gBgwwA - A1wB6gPeAf8D8wH/A9sB/wPSAf8D2wH/A9YB/wPAAf8DyQH/A+YB/wPEAf8DXAHqDAADfwH+AYIBugHu - Af8BgQGeAYEB/wGBAcwBlQH/AYEBuAGBAf8BgQG1AYEB/wGBAbEBgQH/AYEBrAGBAf8BgQGxAYEB/wGB - Aa0BgQH/AYEBwQGKAf8BgQGSAYEB/wGDAbwB7wH/A38B/ggAA38B/gGCAboB7gH/AYEBkQHsAf8BgQG2 - Av8BgQGkAfsB/wGBAaIB+QH/AYEBnwH4Af8BgQGbAfUB/wGBAZ8B9wH/AYEBnAH1Af8BgQGsAf0B/wGB - AYoB3gH/AYMBvAHvAf8DfwH+CAADfwH+AYIBugHuAf8CgQH+Af8BkQGZAv8CgQH+Af8CgQH+Af8CgQL/ - AoEB/gH/AoEB/gH/AoEB/QH/AYMBjAH9Af8CgQHsAf8BgwG8Ae8B/wN/Af4MAANcAeoD8AH/A94B/wPU - Af8D0gH/A9sB/wPWAf8DvwH/A7AB/wOzAf8D3gH/A1wB6gwAAysB/AGBAbMB6gH/AYEBuAGBAf8BgQHA - AYkB/wGBAcABiQH/AYEBvQGGAf8BgQG4AYEB/wGBAbMBgQH/AYEBrQGBAf8BgQGnAYEB/wGBAZ4BgQH/ - AYEBqwGBAf8BgQG4Ae0B/wNNAfoIAAMrAfwBgQGzAeoB/wGBAaQB/AH/AYEBqwL/AYEBqwL/AYEBqAH+ - Af8BgQGkAfwB/wGBAaAB+QH/AYEBnAH2Af8BgQGXAfMB/wGBAZEB7AH/AYEBmgH1Af8BgQG4Ae0B/wNN - AfoIAAMrAfwBgQGzAeoB/wKBAv8BgQGKAv8BgQGKAv8BgQGGAv8CgQL/AoEC/wKBAv8CgQL/AoEB/QH/ - AoEC/wGBAbgB7QH/A1wB+AwAA1wB6gPyAf8D4gH/A9gB/wPVAf8D3AH/A9gB/wPAAf8DswH/A7cB/wPg - Af8DXAHqDAADSAGGAYEBogGBAf8BggHgAakB/wGCAeABqQH/AYEBzQGWAf8BgQHHAZAB/wGBAcIBiwH/ - AYEBvAGFAf8BgQG1AYEB/wGBAa4BgQH/AYEByAGRAf8BgQHAAYoB/wGBAZoBgQH/A04BlggAA04BmQGB - AZMB8AH/AYEByQL/AYEByQL/AYEBtwL/AYEBsgL/AYEBrAL/AYEBpwH+Af8BgQGiAfoB/wGBAZwB9gH/ - AYEBsgH+Af8BgQGsAf0B/wGBAY4B4gH/A1IBpwgAA0cBggKBAf4B/wGtAbQC/wGtAbQC/wGSAZsC/wGK - AZMC/wGCAYwC/wGBAYQC/wKBAv8CgQL/AYwBlAH+Af8BgwGMAf0B/wN/Af4DSQGHDAADXAHqA/MB/wPn - Af8D3QH/A9kB/wPgAf8D2wH/A8QB/wO4Af8DuwH/A+EB/wNcAeoMAAMBAQIDSgGKAYEBlAGBAf8BnwH9 - AcYB/wGRAe8BuAH/AYEB1gGfAf8BgQHNAZYB/wGBAcYBjwH/AYEBvwGIAf8BgQHPAZgB/wGUAfABuQH/ - AYEBiAGBAf8DUgGjAwYBCAgAAwEBAgNRAaABgQGHAdwB/wG6AeQC/wGXAdcC/wGBAcAC/wGBAbcC/wGB - AbAB/gH/AYEBqgH+Af8BgQG5Af4B/wGbAdgB/gH/AYEBhAHWAf8DVgG0AwcBCggAAwEBAgNRAZ8CgQHn - Af8B1gHaAv8BwgHHAv8BnwGmAv8BkgGbAv8BiQGSAf4B/wGBAYgB/gH/AZYBngH9Af8BxAHJAf4B/wN/ - Af4DSgGNAwQBBgwAA1wB6gP0Af8D6gH/A+EB/wPdAf8D4wH/A94B/wPJAf8DvQH/A78B/wPiAf8DXAHq - EAADCgEOA1MBpQGBAYUBgQH/AbMB9QHIAf8BgQGJAY8B/wKBAaAB/wKBAaAB/wGBAYkBjwH/AakB6AG6 - Af8BgQGEAYEB/wNUAa4DDwEUEAADDQERA1cBtwKBAdUB/wHEAeIB+AH/AYEBgwHKAf8CgQGyAf8CgQGy - Af8BgQGDAcoB/wGsAdUB9QH/AoEB1AH/A1kBvwMSARkQAAMNAREDUQGcAoEB6gH/AdkB2gH3Af8CgQHS - Af8CgQG1Af8CgQG1Af8CgQHSAf8ByQHKAfQB/wKBAeYB/wNMAZADDAEQEAADXAHqA/UB/wPuAf8D5gH/ - A+IB/wPmAf8D4QH/A80B/wPCAf8DwgH/A+MB/wNcAeoUAAMHAQoBWgFdAVoB0wKBAZkB/wGcAcwB+AH/ - Aa8B1AH3Af8BrwHUAfcB/wGlAc8B9gH/AoEBoQH/AVoBXQFaAdMDDAEQGAADCQEMA2AB3gKBAbUB/wGc - AcwB+AH/Aa8B1AH3Af8BrwHUAfcB/wGlAc8B9gH/AoEBugH/A14B3QMOARMYAAMHAQoDWAHBAoEBuQH/ - AZwBzAH4Af8BrwHUAfcB/wGvAdQB9wH/AaUBzwH2Af8CgQG+Af8DWQG/AwoBDRQAA1wB6gP2Af8D6wH/ - A94B/wPWAf8D1QH/A9EB/wPDAf8DvAH/A8AB/wPlAf8DXAHqGAADWwHIAaYBygHuAf8BqwHMAeoB/wGn + AwAB/wMAAf8DAAH/ATYBNwE6Ef8BMgEzATUB/wMAAf8DAAH/AwAB/wMAAf8DWgHbBAADAQECAzoBYgNe + AdoDAAH/AwAB/wNXAb8DFAEcAx0B/AMAAf8DAAH/AyEB+wMSARlgAANKAY0DAAH/AwAB/wMAAf8DAAH/ + A08BnBcAAf8DAAH/AwAB/wMAAf8DAAH/Ad0B3gHfBf8CnwGgAf4C7QHuBf8B2wHcAd4B/wMAAf8DAAH/ + AwAB/wMAAf8DKQH6DwABAQM4AV0DAAH/Ay0BRgNSAakDAAH/AwAB/wMAAf8DAAH/A1MBqlwAAxABFQNV + AeoDAAH/AwAB/wMAAf8DAAH/A1IB9AMSARkTAAH/AwAB/wMAAf8DAAH/AwAB/wHbAdwB3gX/Ap0BngH+ + AusB7AX/AtoB3AH/AwAB/wMAAf8DAAH/AwAB/wMpAfoQAANLAY8DAAH/Ay4BRwNUAa8DAAH/AwAB/wMA + Af8DAAH/A1MBsFwAA1ABpAMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wNTAa0QAANYAekDAAH/AwAB/wMA + Af8DAAH/ATEBMgE1Ef8CLQEwAf8DAAH/AwAB/wMAAf8DAAH/A14B2gwAAxQBGwMdAfwDAAH/AzwBZwNP + AZsDAAH/AwAB/wMAAf8DAAH/A08Bm1wAA1EBoAMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wNTAaoQAANS + AaYDAAH/AwAB/wMAAf8DAAH/AwAB/wExATIBNQH/AdsB3AHeAf8C2QHbAf8CLQEwAf8DAAH/AwAB/wMA + Af8DAAH/AwAB/wNOAZkMAANTAaoDAAH/AwAB/wNEAXsDRwGCAwAB/wMAAf8DAAH/AwAB/wNGAYFcAANA + AXADAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DRAF8EAADKQE/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/ + AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DJAE1DAADVAGuAwAB/wMAAf8DXAHOAxYBHgNX + Ad8DAAH/AwAB/wNaAd4DEwEaXAADQgF1AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/A0YBgBQAA1EBpwMA + Af8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wNQAZ8QAANQAZoDAAH/ + AwAB/wMAAf8DTwGcAxABFQMjATQDJAE1AwQBBmAAAzABTAMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wM2 + AVkUAAMJAQwDWwHIAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wNZAcMDBwEK + EAADRgGAAwAB/wMAAf8DAAH/AwAB/wNGAYFsAAMEAQUDWAHLAwAB/wMAAf8DAAH/AwAB/wNbAdYDBwEJ + GAADCQEMA1ABpAMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH+A08BnAMHAQoUAAMUARsDWAHd + AwAB/wMAAf8DWAHdAxMBGnAAAw0BEgNRAacDUgH0A0gB9gNUAa4DEgEYJAADJwE7A1EBoQNdAeIDAAH+ + AwAB/gNfAeADUAGeAyQBNSAAAxABFQNZAckDWAHLAxABFuAAQP/AAAT/A4EB/wPxAf8DgQH/A4EB/wOB + Af8DqQH/A6QB/wOgAf8DpAH/A4EB/wOBAf8DgQH/A7gB/wOGAf8DgQH/DAADFQEdA0ABcAFVAVcBVQG3 + AVUBXAFVAeoDQAH9AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/KAADBwEJXAAE/wOBAf8D7gH/A4EB/wOB + Af8DiQH/A5gB/wOBAf8DgQH/A4EB/wOkAf8DgQH/A4EB/wOBAf8DwgH/A4EB/wgAAyYBOQFTAVUBUwGw + AQABowEAAf8BAAGxAQAB/wEAAbwBAAH/AQABvwEAAf8BAAG+AQAB/wEAAb0BAAH/AQABvAEAAf8BAAG7 + AQAB/wEAAbkBAAH/AwAB/yQAA1kB9QOBAf8DGAEhWAAE/wOBAf8DxQH/A4EB/wOBAf8DrwH/A4EB/wOB + Af8DgQH/A4EB/wOGAf8DgQH/A4EB/wOBAf8D6wH/A4EB/wQAAxUBHQFTAVUBUwGwAQABtQEAAf8BhQHH + AYsB/wEAAcEBAAH/AQABxAGGAf8BAAHCAYIB/wEAAcEBgAH/AQABvwEAAf8BAAG9AQAB/wEAAb0BAAH/ + AQABuwEAAf8DAAH/JAADWQH1AYEBoQGBAf8DYQHkAxwBJ0AAA1cBuAMEAQYMAAT/A54B/wOVAf8DgQH/ + A4EB/wObAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOjAf8DgQH/A4EB/wPoAf8DgQH/BAADQAFwAQABsgEA + Af8BiwHLAZMB/wGJAcoBkAH/AQABwQGFAf8BAAGiAQAB/wEAAYUBAAH/AQABgAEAAf8DAAH/AwAB/wMA + Af8DAAH/AwAB/xQAA38B/gN/Af4DfwH+A38B/gGBAZ8BgQH/AYEBwQGWAf8BgQGjAYEB/wNkAecDHQEq + OAADJgE4A20B9wFbAV4BWwHZDAAE/wPCAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOB + Af8DpAH/A4EB/wOBAf8D5gH/A4EB/wQAAVUCVwG3AQABwgGHAf8BhwHKAZAB/wGFAcYBjAH/AV0BYQFd + AeIBSwFMAUsBjzAAAYEBmwGBAf8BkAHKAakB/wGNAcgBpQH/AYoBxgGhAf8BiAHFAZ4B/wGBAbYBhQH/ + AYIBwgGXAf8BgQGlAYEB/wNcAeoDIQEwEAABgQG+AYEB/wGBAbgBgQH/AYEBsQGBAf8BgQGpAYEB/wGB + AaEBgQH/AYEBmQGBAf8BgQGRAYEB/wGBAYgBgQH/A4EB/wGBAYgBgQH/AYEBoQGBAf8BgQGLAYEB/wNc + Ac8IAAT/A90B/wOBAf8DgQH/A50B/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOhAf8DgQH/A4EB/wPm + Af8DgQH/BAABVQJcAeoBkAHOAZgB/wGRAc8BmQH/AQABuQEAAf8BSwJMAY80AAGBAZ8BgQH/AZQBzQGt + Af8BgQG6AY4B/wGBAbgBiQH/AYEBtgGFAf8BgQGzAYEB/wGBAbUBggH/AYMBwgGYAf8BgQGgAYEB/wMr + AfwQAAGBAcMBgQH/AaAB1wGpAf8BnAHVAaUB/wGYAdMBoQH/AZQB0AGdAf8BkAHOAZgB/wGLAcsBkwH/ + AYcByQGOAf8BggHGAYkB/wGBAcMBhAH/AYEBwQGBAf8BgQG+AYEB/wGBAYwBgQH/A1sBxAQABP8D8AH/ + A4EB/wOBAf8DsQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A6AB/wOBAf8DgQH/A+UB/wOBAf8EAANc + AeoBlAHRAZwB/wGVAdEBngH/AQABvAEAAf8BSwJMAY8UAANEAXsDAgEDGAABgQGjAYEB/wGWAc4BsAH/ + AZQBzQGtAf8BkQHLAaoB/wGQAcsBqAH/AYEBvAGQAf8BigHHAaEB/wGBAaUBgQH/AUABhwFAAf0DCwEP + EAABgQHHAYEB/wGlAdoBrgH/AaIB2AGrAf8BngHWAacB/wGaAdQBowH/AZYB0gGfAf8BkwHPAZoB/wGO + AcwBlQH/AYkBygGQAf8BhQHHAYsB/wGBAcUBhwH/AYEBwgGCAf8BgQGRAYEB/wNcAckEAAT/A/AB/wOB + Af8DgQH/A7EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOhAf8DgQH/A4EB/wPmAf8DgQH/BAADVwG3 + AYoBzAGTAf8BkwHRAZ0B/wGRAc4BmgH/AV0BYQFdAeIDTAGPEAABWQFdAVkB0gFQAVEBUAGfGAABgQGl + AYEB/wFAAaMBQAH9AUABoQFAAf0BQAGdAUAB/QGBAa8BgQH/AZEBywGqAf8BgQGrAYEB/wFAAY8BQAH9 + AwkBDBQAAYEBxwGBAf8BgQHHAYEB/wGBAcMBgQH/AYEBvgGBAf8BgQG4AYEB/wGBAbEBgQH/AYEBqQGB + Af8BgQGhAYEB/wGBAZkBgQH/AYEBkQGBAf8BgQGjAYEB/wGBAZ0BgQH/A1wB0QgABP8D3QH/A4EB/wOB + Af8DngH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A6QB/wOBAf8DgQH/A+YB/wOBAf8EAANAAXABAAHF + AYQB/wGeAdYBpwH/AZwB1QGlAf8BkgHPAZoB/wEAAbgBAAH/AQABoAEAAf8BAAGcAQAB/wEAAZcBAAH/ + AQABkgEAAf8BAAGmAQAB/wEAAaMBAAH/A0wBkyQAA1kB9QGBAbMBgQH/AYEBmAGBAf8DBwEJQAADbQH3 + A18B2wwABP8DwgH/A4EB/wOBAf8DgwH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A6kB/wOBAf8DgQH/ + A+gB/wOBAf8EAAMVAR0DVQGwAYgBzAGSAf8BoAHXAakB/wGWAdMBoAH/AZsB1AGjAf8BmQHTAaEB/wGW + AdEBngH/AZMBzwGbAf8BkAHOAZkB/wGNAcwBlQH/AYoBygGRAf8BAAGjAQAB/wNKAYogAANZAfUBgQGf + AYEB/wMHAQlEAANYAbsDBAEGDAAE/wOeAf8DlwH/A4EB/wOBAf8DoQH/A4EB/wOBAf8DgQH/A4EB/wOB + Af8DqQH/A4EB/wOBAf8D6wH/A4EB/wgAAyYBOQNVAbABAAHFAYUB/wGNAc4BlgH/AZgB0wGhAf8BnAHV + AaUB/wGZAdQBogH/AZcB0gGfAf8BlAHQAZ0B/wGRAc8BmQH/AY0BzAGUAf8BAAGoAQAB/wNIAYSEAAT/ + A4EB/wPGAf8DgQH/A4EB/wO1Af8DgQH/A4EB/wOBAf8DgQH/A4wB/wOBAf8DgQH/A4EB/wPuAf8DgQH/ + DAADFQEdA0ABcANXAbcDXAHqAQABsAEAAf8BAAGsAQAB/wEAAacBAAH/AQABowEAAf8BAAGzAQAB/wEA + Aa8BAAH/AUsCTAGQiAAE/wOBAf8D8QH/A4EB/wOBAf8DjQH/A6EB/wOBAf8DgQH/A4EB/wOvAf8DgQH/ + A4EB/wOBAf8DwwH/A4EB/ywAAVkBXQFZAdIBTwJRAZyMAAT/A4EB/wP2Af8DgQH/A4EB/wOBAf8DsgH/ + A68B/wOsAf8DrwH/A4EB/wOBAf8DgQH/A70B/wOJAf8DgQH/LAADQwF4AwIBA4wAQP9AAAMmATkDRAF6 + A1UBsAHLAoEB/wHSAoEB/wHTAoEB/wHRAoEB/wHNAoEB/wNcAd8DRwGAAzcBWyQAAycBOwNOAZQDXgHO + A2AB6wNgAesDXgHOA20B9wOBAf8DAwEEWAADGAEiA1MBqgNYAbsDGAEiCAADNQFVA1gBuwNDAXcDDQER + DAADYQHmAYEBhQGsAf8ByAKBAv8BugGBAv8BtwGBAv8BsAGBAv8BsgGBAv8BtAGBAf8B3gGIAYEB/wGH + AoEB/wGBAZsB5gH/HAADCgENA04BlgNNAfoB4QHNAbkB/wHxAeABzgH/AfkB5wHWAf8B+AHmAdQB/wHw + Ad0BzAH/AYEBiQGBAf8BgQGhAYEB/wNcAeoDDgETDAADWwHEA2UB9AHRAZYBgQH/Ac4BkgGBAf8BywGO + AYEB/wHJAYoBgQH/AccBhwGBAf8BwwGEAYEB/wHDAYQBgQH/AcMBhAGBAf8BwwGEAYEB/wHDAYQBgQH/ + AcMBhAGBAf8DVQGwDAADNQFVA2EB7gGBAaEB8wH/AYEBmgHvAf8DQwF3CAADXAHMAYEBoQHyAf8BgQGI + AeMB/wNhAe4DNQFVCAADKwH8AZMBrAHMAf8B5wGvAYEC/wG4AYEB/wH+AbABgQH/Af4BpwGBAf8B/QGg + AYEC/wGXAYEB/wH9AZMBgQH/AbMBnwGiAf8BgQGWAd4B/xgAAwoBDQNXAbgB2QG/AaUB/wH+AfIB5AL/ + AecBzwH/AYEBmAGBAf8BgQGSAYEB/wGBAZABgQH/AYEBjAGBAf8BgQGfAYEB/wGBAcEBlgH/AYEBowGB + Af8BWQFnAVkB8gMPARQIAAHXAaEBgQH/AfgB8gHtAf8B9wHwAeoB/wH2Ae0B5gH/AfQB6gHiAf8B8wHn + Ad4B/wHxAeQB2wH/AfAB4gHYAf8B6gHWAcgB/wHyAeUB3AH/AfoB9AHxAf8B+QHzAfAB/wH6AfUB8gH/ + AagBigFAAf0MAAM1AVUBgQGYAfAB/wGBAbsB/gH/AYEBtAH8Af8DYQHuAw0BEQMsAUQBgQGFAeIB/wGB + AbMC/wGBAaoB+QH/AoEB1QH/AyMBMwgAAzUBVQNZAfUB/QHXAbIC/wHWAYQC/wHKAYEC/wHAAYEC/wG0 + AYEC/wGnAYEC/wHFAZIB/wHWAoEB/wNOAZYYAANOAZYB2QG/AaUC/wH4AesC/wHrAdEC/wHhAcMB/wGB + AZsBgQH/AZABygGpAf8BjQHIAaUB/wGKAcYBoQH/AYgBxQGeAf8BgQG2AYUB/wGCAcIBlwH/AYEBpQGB + Af8BWAFiAVgB7wMHAQkEAAHZAaQBgQH/AfkB8wHuAf8B6wHSAb4F/wHrAdMBvwX/AesB0wHABf8B6gHH + Aa0B/wHsAdkBzQH/AfEB5AHbAf8B+QHzAfAB/wH5AfIB7wH/AcYBjAGBAf8QAANJAYgBgQGdAfEB/wGB + Ab0C/wGBAZgB7wH/AzwBZgNOAZkBgQGeAfIB/wGBAbQC/wGBAYwB5gH/A0MBdxAAAzsBZANfAfMB/wHo + AcAB/wGtAaoBhgH/AacBngGBAf8BoAGSAYEB/wH3AcoBnAH/AeQBlAGBAf8DUQGgAwcBChQAAyYBOAGo + AYcBQAH9Av4B+gL/AfkB6QL/Ae8B2AL/AecBzQH/AYEBnwGBAf8BlAHNAa0B/wGBAboBjgH/AYEBuAGJ + Af8BgQG2AYUB/wGBAbMBgQH/AYEBtQGCAf8BgwHCAZgB/wGBAaABgQH/A00B+gQAAd0BqAGBAf8B+QHz + Ae8B/wHrAdABugH/AesB0AG7Af8BgQG1AYEB/wGBAbUBgQH/AYEBtQGBAf8B6wHRAb0B/wHqAc0BtQH/ + AfoB9AHwAf8B6wHZAcwB/wHxAeQB2wH/AfoB9AHxAf8BxgGKAYEB/xQAA1gBuwGBAaEB9AH/AYEBsgH6 + Af8DXgHdAYEBogHzAf8BgQG2Av8BgQGXAe4B/wNOAZkYAANEAXkDTQH6AYEBrQHnAf8BgQGxAeMB/wGB + AbIB5wH/AYEBmAHEAf8DgQH/AYEBpAGBAf8BgQGoAYEB/wGBAacBgQH/AYEBowGBAf8DXAHfA0cBgAM3 + AVsDTgGWAeIBzgG6Af8B9wHeAcYC/wHpAdEC/wH2AeIC/wHyAd0B/wGBAaMBgQH/AZYBzgGwAf8BlAHN + Aa0B/wGRAcsBqgH/AZABywGoAf8BgQG8AZAB/wGKAccBoQH/AYEBpQGBAf8BgQGIAYEB/wNOAZYEAAHf + AaoBggH/AfkB8wHvAf8B6gHOAbcF/wGBAbUBgQH/AZQB1AGbAf8BgQG1AYEF/wHqAc8BugH/AfsB9gHy + Af8B+gHzAfAB/wHrAdgBywH/AfIB5gHdAf8ByAGNAYEB/xQAAw0BEQNhAe4BgQG5Af4B/wGBAbIB/AH/ + AYEBtwL/AYEBqwH5Af8DXAHMAw0BERgAAzwBZgN/Af4BrAHRAfIB/wGuAdcB/QH/AbAB1AH1Af8BkgHA + AecB/wGBAYMBiQH/AYEBzwGUAf8BgQHOAY4B/wGBAc4BjwH/AYEBzgGRAf8BgQG0AYEB/wGBAY8BgQH/ + AYEBmwHmAf8DXgHQAe8B4AHQAf8B5wG+AZYB/wHnAbwBlAH/AfABzAGpAf8B/gHsAdoB/wGBAaUBgQH/ + AYEBowGBAf8BgQGhAYEB/wGBAZ4BgQH/AYEBrwGBAf8BkQHLAaoB/wGBAasBgQH/AYEBkAGBAf8B8AHf + Ac4B/wNeAdAEAAHhAa4BhwH/AfoB9AHwAf8B6gHLAbIB/wHqAcwBswH/AYEBtQGBAf8BgQG1AYEB/wGB + AbQBgQH/AeoBzgG3Af8BgQGzAYEB/wGBAbIBgQH/AYEBsQGBAf8B6AHIAa4B/wHqAdcByQH/AcQBhgGB + Af8EAANYAbsBgQGQAfQB/wGBAY4B8wH/A1wBzANYAbsDWAG7AYEBrgH6Af8BgQG3Av8BgQG1Av8BgQGt + AfoB/wNcAcwDWAG7A14B3QKBAdwB/wKBAdcB/wNYAbsIAAFUAlYBqwGnAb8B2wH/AZYBxQHxAf8BoQHQ + AfsB/wGWAcYB8wH/AbYBzwHkAf8CgQGnAf8BgQHOAY8B/wGBAc0BiQH/AYEBywGDAf8BgQHLAYEB/wGB + AckBgQH/AYEBlgGBAf8BgQGWAd4B/wNkAewB9QHkAdUB/wHpAb4BlwH/AegBvAGWAf8B5wG8AZQB/wHz + Ad4BygH/A2EB5gMUARsDFAEbA2EB5gGBAaQBgQH/AYEBswGBAf8BgQGYAYEB/wHtAckBpAH/AfYB5AHS + Af8DZAHsBAAB4wGxAYwB/wH6AfYB8QH/AeoByQGuBf8B6gHJAbAF/wHpAcsBswX/AYEBsQGBAf8BjgHS + AZUB/wGBAa8BgQX/AfEB5QHbAf8BxgGGAYEB/wQAA1gBuwGBAboB/QH/AYEBxwL/AYEBuwH9Af8BgQG1 + AfsB/wGBAbMB+wH/AYEBvQL/AYEBuAL/AYEBtQL/AYEBuAL/AYEBrwH6Af8BgQGqAfgB/wGBAbEB+wH/ + AYEBtwL/AYEBpQH2Af8DWAG7CAADWwHEAoEBmgH/AoEBrgH/AYEBpgHWAf8CgQGlAf8CgQGSAf8CgQGY + Af8BgQHQAZ8B/wGBAc4BlwH/AYEBzQGOAf8BgQHNAYgB/wGdAdgBqwH/AYEBsAGBAf8DTgGWA2QB7AH1 + AeQB1QH/AekBvwGZAf8B6QHAAZkB/wHpAcEBmgH/AfMB3QHJAf8DYQHmAxQBGwMUARsDYQHmAYEBqAGB + Af8BgQGfAYEB/wHwAcwBqwH/AfABzAGrAf8B9gHkAdMB/wNkAewEAAHlAbQBjwH/AfoB9gHyAf8B6QHG + AaoB/wHpAcYBrAH/AeoBxwGsAf8B6QHHAa0B/wHpAckBrgH/AekByQGwAf8BgQGwAYEB/wGBAa8BgQH/ + AYEBrQGBAf8B6AHMAbUB/wHyAecB3gH/AcgBigGBAf8EAANYAbsBgQGfAfkB/wGBAZMB9gH/A2EB7gNY + AbsDXAHMAYEBtAH8Af8BgQG5Av8BgQG3Av8BgQGvAfsB/wNcAcwDWAG7A14B3QKBAeIB/wGBAYYB5gH/ + A1gBuwgAAywBQwJAAZUB/QKBAZAB/wKBAZAB/wKBAZAB/wKBAZAB/wGBAY8BngH/AYQBmQGTAf8BgQGU + AYkB/wGBAYwBgQH/AaQB1QGxAf8BggG/AYUB/wNRAaADBwEKA14B0AHwAeEB0QH/AeoBxAGhAf8B6QHB + AZsB/wHpAcABlwH/AfEBzgGyAf8B3AHDAaoB/wNkAecDZAHnAd0BxAGsAv8B/AHzAv8B9AHlAv8B7gHa + Af8B/AHlAdEB/wHxAeEB0QH/A14B0AQAAecBtwGUAf8B+wH3AfQB/wHpAcMBpgX/AegBxAGpBf8B6QHG + AaoF/wHoAccBrAX/AegByAGwBf8B9wHxAesB/wHLAY8BgQH/BAADIwEzAyMBMwgAAw0BEQNhAe4BgQG9 + Af4B/wGBAbkB/gH/AYEBugL/AYEBsQH7Af8DXAHMAw0BEQgAAyMBMwMjATMMAAM6AWEDZAHxAoEBkgH/ + A18B+wNcAcwDTQH6AYEBrQHnAf8BgQGxAeMB/wGBAbIB5wH/AYEBmAHEAf8DVgG2AwgBCwQAA04BlgHi + Ac4BugH/AfEB0gG2Af8B6wHBAZsB/wHrAb8BmQL/AeQB0wL/AegB2wH/AfMB2gHHAf8B+AHxAeQC/wH4 + Ae0C/wH3AekC/wH+AfEC/wH8Ae8C/wH8AfIB/wHiAc4BugH/A04BlgQAAekBugGYAf8B+wH3AfQB/wGB + AaQC/wGBAaMC/wGBAaIC/wGBAaEC/wGBAZ8C/wGBAZ0C/wGBAZoC/wGBAZgC/wGBAZUC/wGBAZQC/wH7 + AfcB9AH/Ac4BkwGBAf8UAANYAbsBgQGrAfoB/wGBAcAB/gH/A2EB7gGBAawB+QH/AYEBvAL/AYEBogH2 + Af8DTgGZLAADPAFmA38B/gGsAdEB8gH/Aa4B1wH9Af8BsAHUAfUB/wGSAcAB5wH/A1UBrQgAAyYBOAGo + AYcBQAH9AfoB7QHhAf8B7AHHAaMC/wHsAdoF/wH3Ac0BtgH/AewBvQGXAv8B9AHcA/8B9wL/AfcB6AP/ + AfMD/wH5Af8C/gH5Af8BqAGHAUAB/QMmATgEAAHrAb0BmwH/AfsB9wH0Af8BgQGkAv8BgQG9Av8BgQG7 + Av8BgQG5Av8BgQG4Av8BgQGzAv8BgQGwAv8BgQGrAv8BgQGnAv8CgQL/AfsB9wH0Af8B0QGXAYEB/xAA + A0MBdwGBAaoB+wH/AYEBxAL/AYEBrgH7Af8DTgGZA04BmQGBAasB+QH/AYEBuwL/AYEBnQH0Af8DPAFm + KAADSgGNAbgB0AHnAf8BlgHFAfEB/wGhAdAB+wH/AZYBxgHzAf8BtgHPAeQB/wNcAd8MAANOAZYB2QG/ + AaUC/wH6AfAG/wH9AfMB/wHwAcEBowH/Ae4BxgGgAf8B/QHwAdED/wH+Av8B/QHwAv8B/QHxBf8B2QG/ + AaUB/wNOAZYIAAHsAb8BngH/AfsB9wH0Af8BgQGkAv8BgQGjAv8BgQGgAv8BgQGeAv8BgQGZAv8BgQGW + Av8BgQGQAv8BgQGLAv8BgQGEAv8CgQL/AfsB9wH0Af8B1AGbAYEB/wwAAywBRAGBAaUB/QH/AYEBwAH+ + Af8BgQHEAv8BgQGbAfgB/wMYASIDLAFEAYEBnAH2Af8BgQG8Av8BgQG0AfwB/wNhAe4DIwEzJAADTAGS + AYEBrgHlAf8BgQG4AeYB/wGNAcgB9gH/AYEBrQHmAf8BgQGnAeUB/wFYAmIB7wwAAwoBDQNXAbgB2QG/ + AaUB/wH+Af0B/AL/AfEB5AH/AfIBygGuAf8B8gHSAa0B/wH5Ae0ByAP/AfcD/wH9Af8B/gH9AfwB/wHZ + Ab8BpQH/A1cBuAMKAQ0IAANgAesB+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/ + AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8DXAH4 + DAADPAFmAYEBpAH9Af8BgQG2Af4B/wGBAbsB/gH/A1gBuwgAA1wBzAGBAbgB/QH/AYEBqAH5Af8DYQHu + AzUBVSQAAywBQwFAAaMBqAH9AYEBpgHlAf8BgQGmAeUB/wGBAaYB5QH/AYEBpgHlAf8DTAGQEAADCgEN + A04BlgNNAfoB4QHNAbkB/wHxAeIB1AH/AfkB7AHfAf8B+wHzAeUB/wH0Ae0B5AH/AeIBzgG7Af8DTQH6 + A04BlgMKAQ0MAANGAX4DXwHjAe0BwAGfAf8B6wG+AZ0B/wHrAbwBmgH/AekBugGWAf8B5wG3AZMB/wHm + AbUBkAH/AeQBsgGMAf8B4gGvAYgB/wHgAawBhAH/Ad0BqQGBAf8B3AGlAYEB/wNdAcoQAAMjATMDWAG7 + AYEBmQH8Af8DNQFVCAADPAFmAYEBkQH1Af8DWAG7AxgBIiwAAzoBYQNkAfEBgQGlAeQB/wNfAfsDUAGd + AwQBBhgAAycBOwNOAZQDXgHOA2AB6wJiAVgB7wJeAVsB2QNPAZcDJwE7qAADCgENAyIBMQMgAS8DBQEH + 5AADGAEhA1ABnQNdAfADoQH/A6sB/wOnAf8DlQH/A2QB5wNKAYoDEAEWFAADCgENAxoBJQNeAdUDfwH+ + AYEBiQGBAf8BgQGIAYEB/wGBAYcBgQH/AYEBhQGBAf8DfwH+A2QB7AMdASoDCgENEAADCgENAx8BLQNh + AeEDfwH+AYEBhAHVAf8BgQGCAdYB/wGBAYIB1AH/AoEB0gH/A38B/gNfAfMDIwEzAwoBDRAAAwoBDQMf + AS0DYQHhA38B/gKBAd4B/wKBAd4B/wKBAd0B/wKBAdoB/wN/Af4DXwHzAyMBMwMKAQ0QAAMvAUoDZQH0 + A84B/wPtAf8D9AH/A/UB/wP0Af8D7wH/A+IB/wO6Af8DZAHnAyQBNQwAA0QBeQNfAfsBgQGdAYEB/wGB + AY8BgQH/AYEBwQGKAf8BgQHBAYoB/wGBAcEBigH/AYEBwQGKAf8BgQHBAYoB/wGBAcEBigH/AYEBigGB + Af8BgQGIAYEB/wNcAfgDSAGDCAADRAF5A18B+wGBAZAB6gH/AYEBiAHeAf8BgQGrAv8BgQGrAv8BgQGr + Av8BgQGrAv8BgQGrAv8BgQGrAv8BgQGEAdkB/wKBAdMB/wNcAfgDSAGDCAADRAF5A18B+wKBAfwB/wKB + AesB/wGBAYsC/wGBAYsC/wGBAYsC/wGBAYsC/wGBAYsC/wGBAYsC/wKBAeQB/wKBAdoB/wNcAfgDSAGD + DAADXAHqA94B/wPzAf8D2wH/A9IB/wPbAf8D1gH/A8AB/wPJAf8D5gH/A8QB/wNcAeoMAAN/Af4BggG6 + Ae4B/wGBAZ4BgQH/AYEBzAGVAf8BgQG4AYEB/wGBAbUBgQH/AYEBsQGBAf8BgQGsAYEB/wGBAbEBgQH/ + AYEBrQGBAf8BgQHBAYoB/wGBAZIBgQH/AYMBvAHvAf8DfwH+CAADfwH+AYIBugHuAf8BgQGRAewB/wGB + AbYC/wGBAaQB+wH/AYEBogH5Af8BgQGfAfgB/wGBAZsB9QH/AYEBnwH3Af8BgQGcAfUB/wGBAawB/QH/ + AYEBigHeAf8BgwG8Ae8B/wN/Af4IAAN/Af4BggG6Ae4B/wKBAf4B/wGRAZkC/wKBAf4B/wKBAf4B/wKB + Av8CgQH+Af8CgQH+Af8CgQH9Af8BgwGMAf0B/wKBAewB/wGDAbwB7wH/A38B/gwAA1wB6gPwAf8D3gH/ + A9QB/wPSAf8D2wH/A9YB/wO/Af8DsAH/A7MB/wPeAf8DXAHqDAADKwH8AYEBswHqAf8BgQG4AYEB/wGB + AcABiQH/AYEBwAGJAf8BgQG9AYYB/wGBAbgBgQH/AYEBswGBAf8BgQGtAYEB/wGBAacBgQH/AYEBngGB + Af8BgQGrAYEB/wGBAbgB7QH/A00B+ggAAysB/AGBAbMB6gH/AYEBpAH8Af8BgQGrAv8BgQGrAv8BgQGo + Af4B/wGBAaQB/AH/AYEBoAH5Af8BgQGcAfYB/wGBAZcB8wH/AYEBkQHsAf8BgQGaAfUB/wGBAbgB7QH/ + A00B+ggAAysB/AGBAbMB6gH/AoEC/wGBAYoC/wGBAYoC/wGBAYYC/wKBAv8CgQL/AoEC/wKBAv8CgQH9 + Af8CgQL/AYEBuAHtAf8DXAH4DAADXAHqA/IB/wPiAf8D2AH/A9UB/wPcAf8D2AH/A8AB/wOzAf8DtwH/ + A+AB/wNcAeoMAANIAYYBgQGiAYEB/wGCAeABqQH/AYIB4AGpAf8BgQHNAZYB/wGBAccBkAH/AYEBwgGL + Af8BgQG8AYUB/wGBAbUBgQH/AYEBrgGBAf8BgQHIAZEB/wGBAcABigH/AYEBmgGBAf8DTgGWCAADTgGZ + AYEBkwHwAf8BgQHJAv8BgQHJAv8BgQG3Av8BgQGyAv8BgQGsAv8BgQGnAf4B/wGBAaIB+gH/AYEBnAH2 + Af8BgQGyAf4B/wGBAawB/QH/AYEBjgHiAf8DUgGnCAADRwGCAoEB/gH/Aa0BtAL/Aa0BtAL/AZIBmwL/ + AYoBkwL/AYIBjAL/AYEBhAL/AoEC/wKBAv8BjAGUAf4B/wGDAYwB/QH/A38B/gNJAYcMAANcAeoD8wH/ + A+cB/wPdAf8D2QH/A+AB/wPbAf8DxAH/A7gB/wO7Af8D4QH/A1wB6gwAAwEBAgNKAYoBgQGUAYEB/wGf + Af0BxgH/AZEB7wG4Af8BgQHWAZ8B/wGBAc0BlgH/AYEBxgGPAf8BgQG/AYgB/wGBAc8BmAH/AZQB8AG5 + Af8BgQGIAYEB/wNSAaMDBgEICAADAQECA1EBoAGBAYcB3AH/AboB5AL/AZcB1wL/AYEBwAL/AYEBtwL/ + AYEBsAH+Af8BgQGqAf4B/wGBAbkB/gH/AZsB2AH+Af8BgQGEAdYB/wNWAbQDBwEKCAADAQECA1EBnwKB + AecB/wHWAdoC/wHCAccC/wGfAaYC/wGSAZsC/wGJAZIB/gH/AYEBiAH+Af8BlgGeAf0B/wHEAckB/gH/ + A38B/gNKAY0DBAEGDAADXAHqA/QB/wPqAf8D4QH/A90B/wPjAf8D3gH/A8kB/wO9Af8DvwH/A+IB/wNc + AeoQAAMKAQ4DUwGlAYEBhQGBAf8BswH1AcgB/wGBAYkBjwH/AoEBoAH/AoEBoAH/AYEBiQGPAf8BqQHo + AboB/wGBAYQBgQH/A1QBrgMPARQQAAMNAREDVwG3AoEB1QH/AcQB4gH4Af8BgQGDAcoB/wKBAbIB/wKB + AbIB/wGBAYMBygH/AawB1QH1Af8CgQHUAf8DWQG/AxIBGRAAAw0BEQNRAZwCgQHqAf8B2QHaAfcB/wKB + AdIB/wKBAbUB/wKBAbUB/wKBAdIB/wHJAcoB9AH/AoEB5gH/A0wBkAMMARAQAANcAeoD9QH/A+4B/wPm + Af8D4gH/A+YB/wPhAf8DzQH/A8IB/wPCAf8D4wH/A1wB6hQAAwcBCgFaAV0BWgHTAoEBmQH/AZwBzAH4 + Af8BrwHUAfcB/wGvAdQB9wH/AaUBzwH2Af8CgQGhAf8BWgFdAVoB0wMMARAYAAMJAQwDYAHeAoEBtQH/ + AZwBzAH4Af8BrwHUAfcB/wGvAdQB9wH/AaUBzwH2Af8CgQG6Af8DXgHdAw4BExgAAwcBCgNYAcECgQG5 + Af8BnAHMAfgB/wGvAdQB9wH/Aa8B1AH3Af8BpQHPAfYB/wKBAb4B/wNZAb8DCgENFAADXAHqA/YB/wPr + Af8D3gH/A9YB/wPVAf8D0QH/A8MB/wO8Af8DwAH/A+UB/wNcAeoYAANbAcgBpgHKAe4B/wGrAcwB6gH/ + AacB0AH2Af8BqAHQAfYB/wGrAcwB6gH/AacBzQHuAf8DXAHMIAADXQHKAaYBygHuAf8BqwHMAeoB/wGn AdAB9gH/AagB0AH2Af8BqwHMAeoB/wGnAc0B7gH/A1wBzCAAA10BygGmAcoB7gH/AasBzAHqAf8BpwHQ - AfYB/wGoAdAB9gH/AasBzAHqAf8BpwHNAe4B/wNcAcwgAANdAcoBpgHKAe4B/wGrAcwB6gH/AacB0AH2 - Af8BqAHQAfYB/wGrAcwB6gH/AacBzQHuAf8DXAHMGAADXAHqA/cB/wPnAf8D7wH/A/YB/wP7Af8D+gH/ - A/AB/wPeAf8DwwH/A+YB/wNcAeoYAAFZAmcB8gG9AdcB7wH/AaMByQHtAf8BnQHFAegB/wGBAc4B7wH/ - AYEBzQHvAf8BqAHcAfEB/wNiAfYDBAEGHAADZAHxAb0B1wHvAf8BowHJAe0B/wGdAcUB6AH/AYEBwwHq - Af8BgQHFAewB/wGmAdkB8AH/A1kB9QMEAQYcAAJYAWIB7wG9AdcB7wH/AaMByQHtAf8BnwHFAegB/wGB - AaEBzQH/AYEBsAHdAf8BtwHTAesB/wNfAfMDBAEGFAADXAHqA/gB/wP+Gf8D+wH/A+oB/wNcAeoYAAGB - AZ4B8wH/AYEBqwHOAf8BigG3AeQB/wGBAcYB6wH/AYEBnwHtAf8BgQGkAe4B/wGBAaUB6wH/AYEBmgHp - Af8DDQERHAACgQHqAf8BgQGkAckB/wGKAbcB5AH/AYEBuQHlAf8CgQHfAf8CgQHiAf8CgQHdAf8CgQHZ - Af8DCwEPHAADfwH+AYEBkwG1Af8BigG3AeQB/wGBAZMBwwH/A4EB/wOBAf8DgQH/A4EB/wMIAQsUAANZ - Ab8D4QH/A/4Z/wP7Af8DzwH/A1IBoRgAAYEBugH1Af8BgQG2AfUB/wGBAaoB9AH/AYEBrAH0Af8BgQGs - AfQB/wGBAawB8wH/AYEBoQHwAf8BgQGTAeQB/yAAAYEBpwHtAf8BgQGWAfAB/wKBAewB/wKBAewB/wKB - AewB/wKBAesB/wKBAeQB/wKBAdEB/yAAA4EB/wKBAY8B/wOBAf8DgQH/A4EB/wOBAf8DgQH/A38B/hgA - Ax0BKgNcAdED0AH/A+gB/wPzAf8D/QH/A/wB/wPtAf8D4AH/A8IB/wNbAcMDFAEbGAADYAHeAYEBtgH1 - Af8BgQG2AfUB/wGBAbYB9QH/AYEBsgH1Af8BgQGrAfQB/wGBAaoB8AH/A2EB4SAAA1gBuwGBAZwB7QH/ - AYEBlgHvAf8BgQGWAe8B/wGBAY0B7gH/AoEB7AH/AoEB4wH/A1kBxyAAA0cBgAOBAf8CgQGKAf8CgQGI - Af8DgQH/A4EB/wOBAf8DSwGOHAADBQEHAzUBVQNUAa4DYAHWA18B+wNqAfkDXAHNA1IBqQMtAUYDAgED - IAADXwHjAYEBpwHzAf8BgQGsAfQB/wGBAagB9AH/AYEBoQHxAf8DYQHmKAADWgHFAoEB6gH/AoEB7QH/ - AoEB7AH/AoEB5wH/A14B0CgAA0oBjQOBAf8DgQH/A4EB/wOBAf8DUAGd/wBZAAMVAR0DIwE0AyQBNgMk - ATYDJAE2AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2AyQBNgMjATMDFQEdiAADUQGfA2EB4QNhAeEDVwG6 - MAADIwE0A1kB9QGoAkAB/QP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/AagCQAH9A18B8wMj - ATMIAAG1AoEB/wOBAf8DgQH/A4EB/wHGAoEB/2gAAYcCgQH/AYICgQH/AZMCgQH/AZACgQH/A4EB/wOB - Af8DgQH/A4EB/wOBAf8DgQH/A4EB/wMWAR4TAAEBAyQBNgN/Af4D/AH/A/wB/wP8Af8D/AH/A/wB/wP8 - Af8D/AH/A/wB/wP8Af8D/AH/AagCQAH9AyQBNgQAAaUCgQH/A4EB/wOEAf8DjAH/A4EB/wOBAf8BjAKB - Af8IAAGtAoEB/wOBAf8DgQH/A4EB/wHGAoEB/wgAA1IBqQHDAY4BgQH/AcABiwGBAf8BvgGIAYEB/wG7 - AYUBgQH/AbkBgwGBAf8BtAKBAf8BsgKBAf8BsQKBAf8BrgKBAf8BrQKBAf8BqwKBAf8BqQKBAf8BqQKB - Af8DUgGpBAABswGLAYEB/wGzAYsBgQH/A4EB/wOBAf8BswGLAYEB/wGzAYsBgQH/AbMBiwGBAf8BswGL - AYEB/wGzAYsBgQH/AbMBiwGBAf8DgQH/AxYBHhMAAQEDJAE2A/wB/wP8Af8D3QH/A9sB/wPZAf8D1wH/ - A9UB/wPUAf8D1AH/A9QB/wP7Af8D/AH/AyQBNgMAAQEBrQKBAf8DlAH/A60B/wOtAf8DrQH/A4wB/wOB - Af8BrQKBAf8BlAKBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wGEAoEB/wQAAcgBkgGBNf8BqQKBAf8EAAGH - AoEB/wGCAoEB/wGrAZABgQH/AaoBjwGBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/AxYBHhMA - AQEDJAE2A/wB/wP8Af8D/AH/A/wB/wP7Af8D+wH/A/oB/wP6Af8D+gH/A/oB/wP6Af8D/AH/AyQBNgMA - AQEEAAGtAoEB/wOMAf8DnAH/A7UB/wOtAf8DgQH/A4EB/wGcAoEB/wOBAf8DlAH/A5wB/wOUAf8DgQH/ - A4EB/wGlAoEB/wHKAZQBgQX/AesBsAGBBf8DsgH/A64B/wOlAf8B/gH6AfcB/wOiAf8B/gH6AfcB/wOY - Af8DlQH/A5EF/wGqAoEB/wQAA/gB/wPxAf8D4wH/A/MB/wP4Af8D+QH/A/gB/wPpAf8D+AH/A4EB/wOB - Af8DFgEeA1cBuAMEAQYLAAEBAyQBNgP8Af8D/AH/A9YB/wPTAf8DzwH/A80B/wPLAf8DyAH/A8gB/wPG - Af8D+AH/A/wB/wMkATYDAAEBCAABvQKBAf8BhAKBAf8DgQH/A4wB/wOcAf8DgQH/BAABnAKBAf8DgQH/ - A4QB/wOcAf8DlAH/A4EB/wOBAf8BzAGXAYEF/wH+Af0B+wH/Af4B/AH7Af8B/gH7AfkB/wH+AfsB+QH/ - Af4B+gH3Af8B/gH6AfcB/wH+AfoB9wH/Af4B+gH3Af8B/QH4AfQB/wH8AfYB8wH/AfwB9gHzBf8BrAKB - Af8EAAPnAf8D0wH/A6UB/wPXAf8D9AH/A+4B/wPqAf8D3AH/A/gB/wOBAf8DgQH/AzEBTwNtAfcDWwHZ - CwABAQMkATYD/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/A/sB/wP5Af8D+QH/A/gB/wP8Af8DJAE2 - AwABARQAA4EB/wO1Af8DgQH/CAABtQKBAf8DgQH/A4EB/wOBAf8DhAH/A4EB/wHRAZwBgQX/Ae8BtAGB - Bf8DtQH/A7MB/wOrAf8B/gH6AfcB/wOoAf8B/QH4AfUB/wOiAf8DngH/A5sF/wGwAoEB/wQAA8UB/wPT - Af8DpgH/A9gB/wP0Af8BgQG+AYEB/wGBAbgBgQH/AYEBsQGBAf8BgQGpAYEB/wGBAaEBgQH/AYEBmQGB - Af8BgQGRAYEB/wGBAaEBgQH/AYEBiwGBAf8DXAHPBwABAQMkATYD/AH/A/wB/wPOAf8DygH/A8YB/wPD - Af8DwAH/A70B/wO8Af8DugH/A/YB/wP8Af8DJAE2AwABARQAA4EB/wO9Af8DgQH/FAADgQH/A5wB/wOB - Af8B1AGeAYEF/wH+AfwB+gH/Af4B+wH5Af8B/gH6AfgB/wH+AfoB+AH/Af4B+gH2Af8B/gH6AfcB/wH8 - AfcB9AH/AfwB9wHyAf8B/AH2AfAB/wH8AfUB7wH/AfwB9AHuBf8BsgKBAf8EAAPpAf8D1AH/A6YB/wPY - Af8D9QH/AYEBwwGBAf8BoAHXAakB/wGcAdUBpQH/AZgB0wGhAf8BlAHQAZ0B/wGQAc4BmAH/AYsBywGT - Af8BgQHBAYEB/wGBAb4BgQH/AYEBjAGBAf8DWwHEAwABAQMkATYD/AH/A/wB/wP8Af8D/AH/A/wB/wP8 - Af8D+wH/A/gB/wP2Af8D8wH/A/IB/wP8Af8DJAE2AwABARQAA4EB/wO9Af8DgQH/FAADgQH/A6UB/wOB - Af8B1QGgAYEF/wHzAbYBgQX/A7oB/wO4Af8DsgH/Af4B+gH3Af8DrwH/AfwB9gHwAf8DqQH/A6cB/wOl - Bf8BtQKBAf8EAAPFAf8D1AH/A6cB/wPYAf8D9QH/AYEBxwGBAf8BpQHaAa4B/wGiAdgBqwH/AZ4B1gGn - Af8BmgHUAaMB/wGWAdIBnwH/AZMBzwGaAf8BgQHFAYcB/wGBAcIBggH/AYEBkQGBAf8DXAHJAwABAQMk - ATYD/AH/A/wB/wPHAf8DwgH/A74B/wO4Af8DtAH/A7EB/wOuAf8DrAH/A+0B/wP8Af8DJAE2AwABARQA - A4EB/wPGAf8DgQH/FAADgQH/A6UB/wOBAf8B2AGiAYEF/wH+AfoB+AH/Af4B+gH3Af8B/gH6AfYB/wH9 - AfgB9gH/AfwB9wHyAf8B/gH6AfcB/wH8AfYB8AH/AfwB9QHvAf8B+wHzAewB/wH6AfEB6gH/AfoB8AHo - Bf8BtwKBAf8EAAPqAf8D1QH/A7UB/wPfAf8D9QH/AYEBxwGBAf8BgQHHAYEB/wGBAcMBgQH/AYEBvgGB - Af8BgQG4AYEB/wGBAbEBgQH/AYEBqQGBAf8BgQGjAYEB/wGBAZ0BgQH/A1wB0QcAAQEDJAE2A/wB/wP7 - Af8D/AH/A/wB/wP7Af8D+AH/A/UB/wPxAf8D7AH/A+oB/wPmAf8D/AH/AyQBNgMAAQEUAAOEAf8DxgH/ - A4EB/xQAA4EB/wOtAf8DgQH/AdkBowGBBf8B9gG5AYEF/wO9Af8DuwH/A7cB/wH+AfoB9wH/A7UB/wH8 - AfMB7QH/A7EB/wOwAf8DrQX/AboBhQGBAf8EAAPGAf8D8gH/Ad0B2AHSAf8B6wHlAd8B/wP8Af8DzAH/ - A8kB/wO9Af8D+QH/AYQCgQH/AZECgQH/AxYBHgNtAfcDXwHbCwABAQMkATYD/AH/A/kB/wPAAf8DugH/ - A7QB/wOvAf8DqgH/A6UB/wP8Af8D/AH/A/wB/wP8Af8DJAE2AwABARQAA4wB/wPOAf8DgQH/FAADgQH/ - A60B/wOBAf8B2wGkAYE1/wG9AYcBgQH/BAAD+AH/AdkBxgGxAf8DYQHmA2EB5gHXAcQBrwH/A/kB/wP5 - Af8D+QH/A/kB/wGLAoEB/wGXAoEB/wMWAR4DWAG7AwQBBgsAAQEDJAE2A/wB/wP3Af8D+QH/A/cB/wP3 - Af8D8wH/A/AB/wPqAf8D/AH/A/YB/wP0Af8DTQGRAxcBIBgAA5QB/wPOAf8DhAH/A4EB/wOBAf8DgQH/ - A4EB/wOBAf8DgQH/A7UB/wOBAf8B3AGnAYEB/wHcAacBgQH/AdwBpwGBAf8B3AGnAYEB/wHcAacBgQH/ - AdwBpwGBAf8B3AGnAYEB/wHcAacBgQH/AdwBpwGBAf8B3AGnAYEB/wHcAacBgQH/AdwBpwGBAf8B3AGn - AYEB/wHcAacBgQH/AcABiwGBAf8EAAHLAaYBgQH/AcYBoAGBAf8DPQFnAz0BZwG7AZUBgQH/AbgBlAGB - Af8BtAGQAYEB/wGvAYsBgQH/AasBhwGBAf8BkgKBAf8DIwEzAwQBBhQAAyQBNgGoAkAB/QP0Af8D9QH/ - A/UB/wP1Af8D8QH/A+8B/wPpAf8D/AH/A+cB/wNNAZEDFwEgAwEBAhgAA5QB/wPWAf8DzgH/A84B/wPO - Af8DxgH/A8YB/wO9Af8DvQH/A70B/wOBAf8BqAJAAf0B6AG5AZIB/wHoAbkBkgH/AegBuQGSAf8B6AG5 - AZIB/wHoAbkBkgH/AegBuQGSAf8B6AG5AZIB/wHoAbkBkgH/AegBuQGSAf8B6AG5AZIB/wHoAbkBkgH/ - AegBuQGSAf8B6AG5AZIB/wGoAkAB/QQAAyMBMwMjATMIAAMjATMDIwEzAyMBMwMjATMDIwEzAyMBMxwA - AyMBMwNdAfABqAJAAf0D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D+AH/A00BkQMXASADAQECHAADnAH/ - A5wB/wOUAf8DlAH/A4wB/wOMAf8DhAH/A4EB/wOBAf8DgQH/A4EB/wM+AWsDZQH0AdwBpwGBAf8B3AGm - AYEB/wHaAaQBgQH/AdgBogGBAf8B1QGgAYEB/wHUAZ4BgQH/AdIBnQGBAf8BzwGaAYEB/wHOAZkBgQH/ - AcsBlgGBAf8ByQGUAYEB/wNlAfQDPgFrSAADFAEcAyMBMwMkATYDJAE2AyQBNgMkATYDJAE2AyQBNgMk - ATYDJAE2AxcBIAMBAQLUAAMVAR0DIwE0AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2 - AyQBNgMjATMDFQEdIAADJgE4AxYBHjAAA4EB/ygAA1sBxANlAfQB0QGWAYEB/wHOAZIBgQH/AcsBjgGB - Af8ByQGKAYEB/wHHAYcBgQH/AcMBhAGBAf8BwwGEAYEB/wHDAYQBgQH/AcMBhAGBAf8BwwGEAYEB/wHD - AYQBgQH/A1UBsAwAAyMBNANZAfUBqAJAAf0D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wGo - AkAB/QNfAfMDIwEzHAADJgE4A2IB9gO2Af8DGAEhKAABgQGDAYEB/wOBAf8oAAHXAaEBgQH/AfgB8gHt - Af8B9wHwAeoB/wH2Ae0B5gH/AfQB6gHiAf8B8wHnAd4B/wHxAeQB2wH/AfAB4gHYAf8B8AHiAdgB/wHw - AeIB2AH/AfAB4gHYAf8B8AHiAdgB/wHwAeIB2AH/AagCQAH9CwABAQMkATYDfwH+A/wB/wP8Af8D/AH/ - A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wGoAkAB/QMkATYYAAMmATgDYgH2A+AB/wPYAf8DtgH/ - AxgBISAAAYEBjQGBAf8BgQGhAYEB/wGBAZ4BgQH/A4EB/wOBAf8DWgHTAz4BaxgAAdkBpAGBAf8B+QHz - Ae4B/wHrAdIBvgX/AesB0wG/Df8B6gHHAa0N/wHwAeIB2AH/AcYBjAGBAf8LAAEBAyQBNgP8Af8D/AH/ - A/wB/wP8Af8D/AH/A/sB/wP7Af8D+wH/A/sB/wP7Af8D+wH/A/wB/wMkATYUAAMnATsDYgH2A+IB/wP9 - Af8D/QH/A9kB/wO2Af8DGAEhGAABgQGYAYEB/wGBAakBgQH/AYMBxgGLAf8BgQHFAYcB/wGBAcMBhQH/ - AYEBwgGCAf8BgQG1AYEB/wGBAZUBgQH/A1sBywMTARoQAAHdAagBgQH/AfkB8wHvAf8B6wHQAboB/wHr - AdABuwH/AesB0AG7Af8B6wHQAbsB/wHrAdABuwH/AesB0QG9Af8B6gHNAbUB/wHqAc0BtQH/AeoBzQG1 - Af8B6gHNAbUB/wHwAeIB2AH/AcYBigGBAf8IAAM4AV4DUQGgAewB+AH8Af8B7AH4AfsB/wGFAdcB9wH/ - AaoB4gH4Af8D+wH/A/sB/wP6Af8D+gH/A/oB/wP6Af8D+gH/A/wB/wMkATYQAAMnATsDYgH2A+QB/wP9 - Af8B+wHsAdUB/wH9AdIBlwH/Af0B+wH3Af8D2QH/A7cB/wMYASEUAAGBAZ0BgQH/AYEBrQGBAf8BhwHJ - AY8B/wGFAccBiwH/AYIBxgGJAf8BgQHEAYcB/wGBAb8BgQH/AYEBwgGCAf8BgQGdAYEB/wNbAcsQAAHf - AaoBggH/AfkB8wHvAf8B6gHOAbcF/wHrAdABuw3/AeoBzwG6Af8B+wH2AfIJ/wHwAeIB2AH/AcgBjQGB - Af8IAAMvAUkBgQHPAfcB/wGBAccB9QH/AYEBwwHzAf8BgQHDAfQB/wG8AegB+QH/A/sB/wP7Af8D+wH/ - A/oB/wP6Af8D+AH/A/gB/wP8Af8DJAE2DAADJwE7A2IB9gPlAf8D/QH/AfsB7AHWAf8B/QHNAYYC/wHP - AYwB/wH6AdEBlwH/AfkB9wHzAf8D2QH/A7cB/wMYASEUAAGBAZ4BgQH/AYEBrgGBAf8BgQGqAYEB/wGB - AZABgQH/AYEBowGBAf8BgQHFAYcB/wGBAcQBhQH/AYEBwgGCAf8BgQGWAYEB/wM+AWsMAAHhAa4BhwH/ - AfoB9AHwAf8B6gHLAbIB/wHqAcwBswH/AeoBzAGzAf8B6gHMAbMB/wHqAcwBswH/AeoBzgG3Af8B6AHH - AawB/wHoAccBrAH/AegByAGwAf8B6AHIAa4B/wHwAeIB2AH/AcQBhgGBAf8IAAMgAS4BgQHQAfcB/wGD - Ad4B+QH/AYEB2QH5Af8BgQHBAfQB/wHUAfAB+gH/A/wB/wP8Af8D/AH/A/sB/wP5Af8D+QH/A/gB/wP8 - Af8DJAE2CAADJwE7A2IB9gPnAf8D/QH/AfsB7AHWAf8B/QHNAYgC/wHVAZgC/wHWAZwC/wHQAY4B/wH3 - AcwBjQH/Af0B/AH7Af8D2QH/A7cB/wMWAR8UAAGBAZ8BgQH/AYEBmgGBAf8EAANSAaYDQAH9AYEBwQGE - Af8BgQHBAYEB/wGBAbcBgQH/A14B2AwAAeMBsQGMAf8B+gH2AfEB/wHqAckBrgX/AeoByQGwDf8B6AHH - AawN/wHxAeUB2wH/AcYBhgGBAf8EAAMCAQMDVgGxAYEB2gH4Af8BjAHgAfoB/wGDAd0B+QH/AYEB0AH2 - Af8BgQHPAfYB/wH5AfsB/AH/A/wB/wP8Af8D+gH/A/kB/wP2Af8D9gH/A/wB/wMkATYEAAMnATsDYgH2 - A+gB/wP9Af8B+wHsAdYB/wH9Ac0BiAL/AdUBmAL/AdcBngL/AdYBmwL/Ac4BiQH/Af0B4QG6Af8D/QH/ - A9cB/wO1Af8DEAEVGAABgQGgAYEB/wgAA0cBggGBAasBgQH/AYMBxwGKAf8BggHFAYgB/wOBAf8MAAHl - AbQBjwH/AfoB9gHyAf8B6QHGAaoB/wHpAcYBrAH/AeoBxwGsAf8B6QHHAa0B/wHpAckBrgH/AekByQGw - Af8B6AHHAawB/wHpAckBsAH/AegByAGwAf8B6AHMAbUB/wHyAecB3gH/AcgBigGBAf8EAANOAZkBgQHY - AfkB/wGBAdoB+AH/AYsB4AH6Af8BggHdAfkB/wGBAc4B9wH/AYEBwwH0Af8BgQHTAfcB/wP8Af8D+wH/ - A/gB/wP2Af8D8wH/A/IB/wP8Af8DJAE2AygBPQNiAfYD6QH/A/0B/wH7AesB0wL/AcwBgwL/AdQBmAL/ - AdcBngL/AdYBmwL/Ac8BiwH/Af0B4gG8Af8D/QH/A9kB/wO5Af8DEAEVDAABgQG2AYEB/wGBAbIBgQH/ - AYEBrgGBAf8BgQGpAYEB/wNDAXcIAANHAYIBgQGvAYEB/wGIAckBjwH/AYUByAGMAf8BgQGEAYEB/wwA - AecBtwGUAf8B+wH3AfQB/wHpAcMBpgX/AegBxAGpDf8B6AHHAawN/wH3AfEB6wH/AcsBjwGBAf8EAAMH - AQkDKgFAA1ABngGBAdcB+AH/AYEB0gH4Af8BiwHeAfgB/wHFAe0B+gH/AfQB+gH8Af8D+wH/A/gB/wP1 - Af8D8gH/A+8B/wPtAf8D/AH/AyQBNgNiAfYD6wH/A/0B/wP6Af8B+wHzAecB/wH+Ac4BiQL/AdQBlgL/ - AdUBmgL/Ac8BiwH/Af0B4gG8Af8D/QH/A9oB/wO9Af8DEAEVEAADXgHYAZQB0QGdAf8BnQHVAaYB/wGT - Ac8BmwH/A0AB/QNHAYIDRwGCA0AB/QGJAckBkAH/AYUByAGMAf8BgQHAAYQB/wNeAdgMAAHpAboBmAH/ - AfsB9wH0Af8B6QHDAaYB/wHpAcMBpgH/AekBwwGmAf8B6QHDAaYB/wHpAcMBpgH/AekBwwGmAf8B6QHD - AaYB/wHpAcMBpgH/AekBwwGmAf8B6QHDAaYB/wH7AfcB9AH/Ac4BkwGBAf8LAAEBAyQBNgGBAdkB9wH/ - AYEB1gH3Af8D/AH/A/wB/wP7Af8D+AH/A/UB/wPxAf8D7AH/A+oB/wPmAf8D/AH/AyQBNgPNAf8D/QH/ - A/0B/wP8Af8D9wH/Af0B9QHqAf8B/gHPAYoC/wHMAYMB/wH9AeIBvAH/A/0B/wPcAf8DwAH/AxABFRQA - Az4BawGBAccBhQH/AaAB1wGpAf8BngHWAacB/wGXAdEBoAH/AYEBwAGBAf8BgQG8AYEB/wGOAcwBlwH/ - AZMBzwGbAf8BkAHOAZgB/wGBAasBgQH/Az4BawwAAesBvQGbAf8B+wH3AfQp/wH7AfcB9AH/AdEBlwGB - Af8LAAEBAyQBNgHQAfEB+wH/AcsB7gH4Af8D+QH/A/kB/wP3Af8D9gH/A/IB/wPrAf8D/AH/A/wB/wP8 - Af8D/AH/AyQBNgPOAf8D/QH/A+AB/wNqAfkBYQJdAeID9wH/AfsB8wHoAf8B/QHjAb0B/wP9Af8D3gH/ - A8MB/wMQARUcAANbAcsBggHKAYwB/wGhAdgBqwH/AZkB1AGiAf8BnAHVAaYB/wGbAdQBpAH/AZIB0AGb - Af8BlgHRAZ4B/wGBAbgBgQH/A1sByxAAAewBvwGeAf8B+wH3AfQB/wGcAdUBpQH/AZgB0wGhAf8BlAHQ - AZ0B/wGQAc4BmAH/AYsBywGTAf8BhwHJAY4B/wGCAcYBiQH/AYEBwwGEAf8BgQHBAYEB/wGBAb4BgQH/ - AfsB9wH0Af8B1AGbAYEB/wsAAQEDJAE2A/wB/wP3Af8D+QH/A/cB/wP3Af8D8wH/A/AB/wPqAf8D/AH/ - A/YB/wP0Af8DTQGRAxcBIAPQAf8D/QH/A80B/wQAAVwCWgHNA/MB/wP7Af8D/QH/A+AB/wPHAf8DEAEV - IAADEwEaA1sBywGBAcgBhwH/AZYB0wGhAf8BoAHYAaoB/wGeAdYBpwH/AZABzgGZAf8BgQG6AYEB/wNb - AcsDEwEaEAADYAHrAfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/ - AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/A1wB+AwAAyQBNgGo - AkAB/QP0Af8D9QH/A/UB/wP1Af8D8QH/A+8B/wPpAf8D/AH/A+cB/wNNAZEDFwEgAwEBAgPSAf8D/QH/ - A+IB/wPOAf8D4AH/A/0B/wP9Af8D4gH/A8sB/wMQARUsAAM+AWsDXgHYAYEBuQGBAf8BgQG1AYEB/wNe - AdgDPgFrGAADRgF+A18B4wHtAcABnwH/AesBvgGdAf8B6wG8AZoB/wHpAboBlgH/AecBtwGTAf8B5gG1 - AZAB/wHkAbIBjAH/AeIBrwGIAf8B4AGsAYQB/wHdAakBgQH/AdwBpQGBAf8DXQHKDAADIwEzA10B8AGo - AkAB/QP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP4Af8DTQGRAxcBIAMBAQIEAAPTAf8D/QH/A/0B/wP9 - Af8D/QH/A/0B/wPkAf8DzQH/AxABFaQAAxQBHAMjATMDJAE2AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2 - AyQBNgMXASADAQECCAAD1QH/A9QB/wPSAf8D0QH/A9AB/wPOAf8DzQH/AxABFWAAA4EB/wOBBf8DgQH/ - A4EB/wOBAf8BtQGlAZwB/wGMAoEB/wOBAf8C5wHeAf8DgQX/A4EB/wOBAf8DgQH/A4EB/xAAAxMBGgMk - ATUDLwFKAzYBWQM2AVkDLwFKAyQBNQMTARogAAMnATsDTgGUA14BzgNgAesDYAHrA14BzgNOAZQDJwE7 - HAADOAFcIAADNQFXAwQBBggAA4EB/wOBBf8DgQH/A4EB/wOBAf8BzgHGAb0F/wHOAcYBvQH/AucB3gH/ - A4EF/wOBAf8DgQH/A4EB/wOBAf8IAAMJAQwDIwE0Az4BawNOAZUDWgHFA10B7QNdAe0DWgHFA04BlQM+ - AWsDIwE0AwkBDBAAAwoBDQNOAZYDTQH6AeEBzQG5Af8B8QHgAc4B/wH5AecB1gH/AfgB5gHUAf8B8AHd - AcwB/wHhAc0BuQH/A00B+gNOAZYDCgENFAADZQH0A2UB9ANKAYwQAANAAW4DZAHsA38B/gwAA4EB/wOB - Bf8BjAKBBf8DgQH/AucB3g3/A4EF/wGMAoEF/wOBAf8DgQH/BAADCQEMAygBPQNHAYEBXgJbAdkB3AHR - AcgB/wHxAegB4QH/AfsB9gHyAf8B+gHzAe4B/wHtAeAB1gH/AdIBwwG4Af8BXgJbAdkDRwGBAygBPQMJ - AQwIAAMKAQ0DVwG4AdkBvwGlAf8B/gHyAeQC/wHnAc8B/wH8AdsBuwH/AfkB0wGyAf8B9gHQAawB/wH0 - AdEBrgH/AfYB2AG7Af8B+gHpAdcB/wHZAb8BpQH/A1cBuAMKAQ0QAANhAeEBgQHLAe8B/wGBAbcB5QH/ - A2oB+QNSAaMDTAGQA1kB9QGBAaMB3QH/AYEBrgHlAf8DYgH2DAADgQH/A4EF/wOBBf8DgQH/AvcB7wH/ - AZwBjAGBAf8BpQGcAYwF/wOBBf8DgQX/A4EB/wOBAf8EAAMjATQDRwGBA18B8wH0Ae4B6AL/Av4B/wH7 - AfQB7wH/AfcB6wHhAf8B9gHqAeAB/wH6AfMB7QH/Af4B/AH6Af8B7gHkAdsB/wNfAfMDRwGBAyMBNAgA - A04BlgHZAb8BpQL/AfgB6wL/AesB0QL/AeEBwwH/Af4B2wG6Af8B+wHWAbIB/wH3AdEBrAH/AfYBzgGp - Af8B8wHLAaUB/wHyAc0BqAH/AfoB4QHKAf8B2QG/AaUB/wNOAZYQAANZAccBgQHKAe4B/wGBAc4B8QH/ - AYEByAHvAf8BgQG6AegB/wGBAbUB5QH/AYEBvQHrAf8BgQG6AesB/wGBAbMB6AH/A2QB5wwAA4EB/wOB - Af8C5wHeBf8BpQGcAYwB/wOBBf8DgQH/A4EF/wOBAf8C5wHeBf8BpQGcAYwB/wOBAf8DgQH/AxMBGgM+ - AWsBXgJbAdkB9wHxAe0F/wH2AegB3QH/AfYB6AHdAf8B+QHxAesB/wH1AeYB2wH/AfUB5QHaAf8B9AHl - AdgC/wL+Af8B7wHkAdwB/wFeAlsB2QM+AWsDEwEaAyYBOAGoAkAB/QL+AfoC/wH5AekC/wHvAdgC/wHn - Ac0C/wHhAcIB/wH8AdwBuwH/AfkB1QGzAf8B9gHQAawB/wH2Ac0BqAH/AfMBywGlAf8B8wHNAakB/wH6 - AeoB2QH/AagCQAH9AyYBOAwAA1IBowGBAcgB7AH/AYEBzAHxAf8BgQHDAe0B/wGBAcQB7gH/AYEBwwHt - Af8BgQG2AeoB/wGBAbEB6AH/AYEBtAHnAf8DXAHPDAADgQH/AoQBtQH/AYQBjAG1Af8ChAG1Af8ChAG1 - Af8CgQGtAf8CgQG1Af8CgQG1Af8CgQG1Af8CgQG1Af8CgQG1Af8CgQG1Af8BhAGBAbUB/wKBAbUB/wKE - AbUB/wGlAZwBzgH/AyQBNQNOAZUB7AHlAeEC/wL+Af8B9wHrAeEB/wH2AeoB4AH/AfYB6QHfAf8BvQKB - Af8B+QHxAesB/wH1AecB3AH/AfUB5gHbAf8B9QHlAdoB/wH+AfwB+gH/AdYByQG/Af8DTgGVAyQBNQNO - AZYB4gHOAboB/wH3Ad4BxgL/AekB0QL/AfYB4gL/AfIB3QL/AfAB3QH/AfgB6gHZAf8B+AHmAdQB/wH7 - Ad8BxwH/AfUBzwGrAf8B8gHKAaQB/wHzAcwBqAH/AfgB3QHDAf8B4gHOAboB/wNOAZYMAANFAXwBgQHG - AeoB/wGBAdIB8gH/AYEBwgHtAf8BgQG9AewB/wGBAbcB6gH/AYEBswHpAf8BgQG5AeoB/wGBAbMB5gH/ - A1QBrgwAA4EB/wH3Ab0BgQL/AcYBgQH/AfcBvQGBAv8BxgGBAf8B9wG9AYEB/wH3Ab0BgQL/AcYBgQL/ - Ab0BgQL/Ab0BgQH/AfcBvQGBAv8BvQGBAv8BvQGBAf8B7wG9AYEB/wH3AcYBgQH/AdYBlAGBAf8DLwFK - A1oBxQH4AfMB7wH/AfsB9gHyAf8B9wHsAeMB/wH3AewB4wH/AfcB6wHhAf8BxQGGAYEB/wG+AoEB/wH5 - AfEB6wH/AfYB6AHdAf8B9QHnAdwB/wH6AfMB7gH/Ae8B5AHbAf8DWgHFAy8BSgNeAdAB7wHgAdAB/wHn - Ab4BlgH/AecBvAGUAf8B8AHMAakB/wH+AewB2gH/Ad0BxAGsAf8DZAHnA2QB5wHdAcMBqgH/AfgB3QHE - Af8B9AHSAbIB/wH2AdQBswH/AfUB1gG4Af8B8QHfAc8B/wNeAdAIAAMJAQwDYgHpAYEB1QHyAf8BgQHQ - AfIB/wGBAcQB7gH/AYEBwAHtAf8BgQG7AesB/wGBAbYB6QH/AYEBtAHpAf8BgQG9AesB/wNtAfcDFQEd - CAADgQH/Ab0BlAGEAf8BxgGUAYEB/wG9AZQBhAH/AcYBlAGEAf8BvQGUAYQB/wGMAoEB/wGUAoEB/wHO - AZQBjAH/Ab0BlAGBAf8BxgGUAYEB/wHGAZQBgQH/Ab0BlAGBAf8BzgGUAYwB/wG1AZQBgQH/Aa0CgQH/ - AzYBWQNdAe0B/gH9AfsB/wH5AfEB6wH/AfgB7gHlAf8B9wHtAeQB/wH3AewB4wH/AcABgwGBAf8BxgGG - AYEB/wG+AoEB/wH5AfEB6wH/AfYB6AHdAf8B9wHrAeEB/wH6AfUB8QH/A10B7QM2AVkDZAHsAfUB5AHV - Af8B6QG+AZcB/wHoAbwBlgH/AecBvAGUAf8B8wHeAcoB/wNhAeYDFAEbAxQBGwNhAeYB9gHjAdMB/wHx - Ac0BqwH/Ae8ByQGkAf8B7QHJAaQB/wH2AeQB0gH/A2QB7AQAAwQBBgNfAeMBgQHOAe0B/wGBAdsB9QH/ - AYEBzQHxAf8BgQHIAe8B/wGBAcMB7gH/AYEBvgHsAf8BgQG4AeoB/wGBAbQB6QH/AYEBwAHsAf8BgQGy - AeQB/wNlAfQDEQEXBAADgQH/AecBtQGBAf8B7wG9AYEC/wHOAYQC/wHOAYQB/wH3AcYBgQH/Ab0CgQH/ - Aa0CgQH/Aa0CgQH/Ae8BtQGBAf8B7wG1AYEB/wHvAbUBgQH/Ae8BvQGBAf8BrQKBAf8B9wG1AYEB/wHO - AZwBgQH/AzYBWQNdAe0B/gH9AfsB/wH6AfIB7AH/AfkB7wHoAf8B+AHuAeUB/wH3Ae0B5AH/AcABgwGB - Af8BwAGDAYEB/wHIAYcBgQH/Ae0B0wG+Af8B9gHpAd8B/wH3AewB4wH/Af0B+wH5Af8DXQHtAzYBWQNk - AewB9QHkAdUB/wHpAb8BmQH/AekBwAGZAf8B6QHBAZoB/wHzAd0ByQH/A2EB5gMUARsDFAEbA2EB5gH4 - AegB2QH/AfIB0gGyAf8B8AHMAasB/wHwAcwBqwH/AfYB5AHTAf8DZAHsBAADXgHdAYEB0gHuAf8BgQHh - AfYB/wGBAdYB9AH/AYEBzwHyAf8BgQHLAfAB/wGBAcYB7wH/AYEBwQHtAf8BgQG8AesB/wGBAbcB6gH/ - AYEBtAHpAf8BgQG+AewB/wGBAbIB5AH/A1gB7wMNARIDgQH/Aa0BlAGMAf8BhAKBAf8DgQH/AYQCgQH/ - A4EB/wOBAf8BhAKBAf8DgQH/A4EB/wGEAoEB/wOBAf8DgQH/AYQCgQH/A4EB/wGlAoEB/wMvAUoDWgHF - AfoB9gHzAf8B/AH4AfQB/wH5Ae8B6AH/AfkB7wHoAf8B+AHvAeYB/wHBAYMBgQH/AcEBgwGBAf8B6AHI - Aa0B/wH3AewB4wH/AfcB6wHhAf8B+wH1AfEB/wH2Ae8B6gH/A1oBxQMvAUoDXgHQAfAB4QHRAf8B6gHE - AaEB/wHpAcEBmwH/AekBwAGXAf8B8QHOAbIB/wHcAcMBqgH/A2QB5wNkAecB3QHEAawC/wH8AfMC/wH0 - AeUC/wHuAdoB/wH8AeUB0QH/AfEB4QHRAf8DXgHQA2AB1AGBAdUB7wH/AYEB3AHyAf8BgQHeAfUB/wGB - Ad4B9QH/AYEB3AH2Af8BgQHUAfMB/wGBAckB7wH/AYEBxAHuAf8BgQHEAe0B/wGBAckB7wH/AYEBxAHu - Af8BgQG+AesB/wGBAbcB5wH/AYEBsAHiAf8DYAHrA4EB/wHnAcYBgQH/Aa0CgQH/Aa0CgQH/Aa0CgQH/ - Aa0CgQH/Aa0CgQH/Aa0CgQH/Aa0CgQH/AfcBzgGUAf8BtQKBAf8BrQKBAf8BrQKBAf8BvQKBAf8BpQKB - Af8BzgGUAYEB/wMkATUDTgGVAfEB7AHoBf8B+QHxAesB/wH5AfAB6QH/AfkB7wHoAf8BwwGEAYEB/wHp - AckBsAH/AfgB7gHlAf8B9wHsAeMB/wH3AewB4wX/AecB3wHYAf8DTgGVAyQBNQNOAZYB4gHOAboB/wHx - AdIBtgH/AesBwQGbAf8B6wG/AZkC/wHkAdMC/wHoAdsB/wHzAdoBxwH/AfgB8QHkAv8B+AHtAv8B9wHp - Av8B/gHxAv8B/AHvAv8B/AHyAf8B4gHOAboB/wNOAZYDCQEMAzoBYQNSAacDYAHWA1kB9QGBAc4B7QH/ - AYEB2QH0Af8BgQHTAfIB/wGBAcsB8AH/AYEBzwHxAf8BgQG/AegB/wNiAfYDXwHbA1QBrgNAAW4DFQEd - A4EB/wG9AZQBnAH/AbUBlAGlAf8BlAKBAf8BtQGcAa0B/wG1AZQBpQH/AYwCgQH/AZQCgQH/Ac4BpQG9 - Af8BrQGcAaUB/wGtAZQBnAH/Aa0BlAGcAf8BxgGcAa0B/wGUAoEB/wGlAYwBlAH/AZQCgQH/AxMBGgM+ - AWsBXgJbAdkB+wH4AfcF/wH5AfEB6wH/AfkB8AHpAf8B6QHKAbEB/wH5Ae8B6AH/AfgB7wHmAf8B+AHu - AeUF/wH5AfYB8wH/AV4CWwHZAz4BawMTARoDJgE4AagCQAH9AfoB7QHhAf8B7AHHAaMC/wHsAdoF/wH3 - Ac0BtgH/AewBvQGXAv8B9AHcA/8B9wL/AfcB6AP/AfMD/wH5Af8C/gH5Af8BqAJAAf0DJgE4FAADRwGA - AYEBzgHtAf8BgQHZAfQB/wGBAdYB8wH/AYEBxQHqAf8DUgGnFAADgQH/AecBxgGUAf8B5wG9AZQB/wHn - AcYBnAH/Ad4BvQGcAf8B5wHGAZwB/wHeAcYBnAH/AbUBjAGBAf8B5wG9AZQB/wHnAb0BlAH/AecBxgGc - Af8B5wHGAZwB/wHeAb0BlAH/AfcBxgGlAf8B5wHGAZwB/wG9AYQBgQH/BAADIwE0A0cBgQNfAfMB+wH4 - AfcF/wH8AfkB9gH/AfoB8wHuAf8B+gHzAe0B/wH8AfgB9AX/AfkB9gHzAf8DXwHzA0cBgQMjATQIAANO - AZYB2QG/AaUC/wH6AfAG/wH9AfMB/wHwAcEBowH/Ae4BxgGgAf8B/QHwAdED/wH+Av8B/QHwAv8B/QHx - Bf8B2QG/AaUB/wNOAZYcAANhAe4BgQHWAfIB/wGBAdYB8wH/A00B+gMEAQYUAAOBAf8BvQGlAbUB/wG1 - AaUBvQH/AbUBnAG9Af8BtQGcAbUB/wG1AZwBtQH/AbUBnAG1Af8BtQGlAb0B/wG1AZwBtQH/AbUBnAG1 - Af8BtQGcAbUB/wG1AZwBtQH/AbUBnAG1Af8BtQGcAb0B/wG9Aa0BtQH/AZQCgQH/BAADCQEMAygBPQNH - AYEBXgJbAdkB8QHsAegB/wH6AfYB8wH/Af4B/QH7Af8B/gH9AfsB/wH5AfUB8gH/Ae4B6QHlAf8BXgJb - AdkDRwGBAygBPQMJAQwIAAMKAQ0DVwG4AdkBvwGlAf8B/gH9AfwC/wHxAeQB/wHyAcoBrgH/AfIB0gGt - Af8B+QHtAcgD/wH3A/8B/QH/Af4B/QH8Af8B2QG/AaUB/wNXAbgDCgENHAADNQFXAYEBzgHtAf8BgQHN - Ae0B/wNHAYAYAAOBAf8BtQKBAf8BvQGEAYEB/wG9AoEB/wG1AYQBgQH/Ab0BhAGBAf8BvQGEAYEB/wG1 - AoEB/wG9AYQBgQH/Ab0BhAGBAf8BvQGEAYEB/wG9AYQBgQH/Ab0BhAGBAf8BvQGEAYEB/wG1AYQBgQH/ - AdYBpQGEAf8IAAMJAQwDIwE0Az4BawNOAZUDWgHFA10B7QNdAe0DWgHFA04BlQM+AWsDIwE0AwkBDBAA - AwoBDQNOAZYDTQH6AeEBzQG5Af8B8QHiAdQB/wH5AewB3wH/AfsB8wHlAf8B9AHtAeQB/wHiAc4BuwH/ - A00B+gNOAZYDCgENJAADXwHbA1gB7xwAA4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/ - A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/xAAAxMBGgMkATUDLwFKAzYBWQM2AVkDLwFKAyQBNQMT - ARogAAMnATsDTgGUA14BzgNgAesBYgJYAe8BXgJbAdkDTwGXAycBOywAAyMEMwFSHAABQgFNAT4HAAE+ - AwABKAMAAUADAAFwAwABAQEAAQEFAAGAAQMWAAP/AwAB8AEPAeAFAAHAAQMB8AUAAYABAQHwAwABwAED - AYABAQEAAQECAAHgAQcDAAEHAgAB/AE/AwABDwIAAfgBHwIAAYABBwIAAfgBHwIAAeABBwIAAfABDwIA - AfABBwIAAfABDwIAAeABBwIAAfABDwIAAeABBwIAAfABDwIAAeABBwIAAfABDwGAAQEB4AEPAgAB8AEP - AYABAQHgAX8CAAHwAQ8BwAEDAeABfwIAAfgBHwHwAQ8B8AH/AgAE/wIABv8CAAHgAQMB/wF/Av8CAAHA - AQMB/gE/Av8CAAGAAQMB/gEfAf8B5wIAAYABAwHgAQ8B/wHHAgABgQH/AeABBwGAAQMCAAGDAf8B4AEH - AYABAQIAAYMB5wHgAQcBgAEBAgABgQHnAeABDwGAAQMCAAGAAQMB/gEfAf8B5wIAAYABAQH+AT8B/wHn - AgABwAEBBP8CAAHgAQME/wIAAf8B5wT/AgAB/wHnBP8CAAL/AQABHwHwAQcC/wHhAYcBAAEfAcABAwGA - AQEBwQGDAQABHwGAAQEBgAEBAcABAwEAAR8BgAEAAYABAQHgAQcBgAEfAgABgAEBAfABDwHAAwABgAEB - AfABDwHAAwABgAEBAgABwAMAAYABAQIAAcADAAGAAQECAAHAAwABgAEBATABDAHgAQECAAGAAQEB8AEP - Af4BAwIAAYABAQHgAQcB/gEDAYABAQGAAQEBwAEDAf4BAwGAAQEBgAEBAcEBgwH+AQMBwAEDAYABAQHh - AYcB/wEDAfABDwT/AfwBPwb/AeABBwHAAQMBwAEDAcABAwHAAQMBgAEBAYABAQGAAQEBwAEDAYABAQGA - AQEBgAEBAcABAwGAAQEBgAEBAYABAQHAAQMBgAEBAYABAQGAAQEBwAEDAYABAQGAAQEBgAEBAcABAwHA - AQMBwAEDAcABAwHAAQMB4AEHAeABBwHgAQcBwAEDAfABDwHwAQ8B8AEPAcABAwHwAQcB8AEHAfABBwHA - AQMB8AEHAfABBwHwAQcBwAEDAfABDwHwAQ8B8AEPAcABAwHwAQ8B8AEPAfABDwHgAQcB+AEfAfgBHwH4 - AR8K/wGAAQEE/wGHAf8BgAEBAYMD/wEAAQ8BAAIBAYMBAAEBAQABDwMAAQEBAAEBAQABDwIAAYACAAEB - AQABAwIAAcABgAEAAQEBAAEDAgAB+AHAAQABAQEAAQECAAL4AQABAQQAAvgBAAEBBAAC+AEAAQEBAAEB - AgAC+AEAAQEBAAEDAgAC+AEAAQEBAAEDAQABAQH4AgABAQEAAQ8BgAEBAfgCAAEBATABPwGAAQMB+AIA - AQEC/wGAAQcG/wHAAQAB/wE/Af0B/wGAAQEBwAEAAf4BHwH5Af8BgAEBAYABAAH8AQ8B8AEfAYABAQGA - AQAB+AEHAeABBwGAAQEBgAEAAfABAwHgAQcBgAEBAYABAAHgAQEB8AEDAYABAQGAAQABwAEAAfkBAwGA - AQECAAGAAQAB/QGDAYABAQMAAQEBwQGDAYABAQMAAQMBwAEDAYABAQGAAgABBwHAAQMBgAEBAYACAAEP - AeABBwGAAQEBgAEAARABHwHgAQcBgAEBAcACAAE/AfgBHwGAAQEBwAEBAQABfwT/AcABAwEAA/8CAAHw - AQ8B8AEPAe8B8wIAAcABAwHAAQMB4wHHAgABgAEBAYABAQHgAQcCAAGAAQEBgAEBAeABBwYAAeABBwYA - AeABBwYAAcABAwYAAYABAQYAAYAXAAH4AR8CAAGAAQEBgAEBAfwBHwIAAYABAQGAAQEB/AE/AgABwAED - AcABAwH+AX8CAAHwAQ8B8AEPAf4Bfws= + AfYB/wGoAdAB9gH/AasBzAHqAf8BpwHNAe4B/wNcAcwYAANcAeoD9wH/A+cB/wPvAf8D9gH/A/sB/wP6 + Af8D8AH/A94B/wPDAf8D5gH/A1wB6hgAAVkCZwHyAb0B1wHvAf8BowHJAe0B/wGdAcUB6AH/AYEBzgHv + Af8BgQHNAe8B/wGoAdwB8QH/A2IB9gMEAQYcAANkAfEBvQHXAe8B/wGjAckB7QH/AZ0BxQHoAf8BgQHD + AeoB/wGBAcUB7AH/AaYB2QHwAf8DWQH1AwQBBhwAAlgBYgHvAb0B1wHvAf8BowHJAe0B/wGfAcUB6AH/ + AYEBoQHNAf8BgQGwAd0B/wG3AdMB6wH/A18B8wMEAQYUAANcAeoD+AH/A/4Z/wP7Af8D6gH/A1wB6hgA + AYEBngHzAf8BgQGrAc4B/wGKAbcB5AH/AYEBxgHrAf8BgQGfAe0B/wGBAaQB7gH/AYEBpQHrAf8BgQGa + AekB/wMNAREcAAKBAeoB/wGBAaQByQH/AYoBtwHkAf8BgQG5AeUB/wKBAd8B/wKBAeIB/wKBAd0B/wKB + AdkB/wMLAQ8cAAN/Af4BgQGTAbUB/wGKAbcB5AH/AYEBkwHDAf8DgQH/A4EB/wOBAf8DgQH/AwgBCxQA + A1kBvwPhAf8D/hn/A/sB/wPPAf8DUgGhGAABgQG6AfUB/wGBAbYB9QH/AYEBqgH0Af8BgQGsAfQB/wGB + AawB9AH/AYEBrAHzAf8BgQGhAfAB/wGBAZMB5AH/IAABgQGnAe0B/wGBAZYB8AH/AoEB7AH/AoEB7AH/ + AoEB7AH/AoEB6wH/AoEB5AH/AoEB0QH/IAADgQH/AoEBjwH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DfwH+ + GAADHQEqA1wB0QPQAf8D6AH/A/MB/wP9Af8D/AH/A+0B/wPgAf8DwgH/A1sBwwMUARsYAANgAd4BgQG2 + AfUB/wGBAbYB9QH/AYEBtgH1Af8BgQGyAfUB/wGBAasB9AH/AYEBqgHwAf8DYQHhIAADWAG7AYEBnAHt + Af8BgQGWAe8B/wGBAZYB7wH/AYEBjQHuAf8CgQHsAf8CgQHjAf8DWQHHIAADRwGAA4EB/wKBAYoB/wKB + AYgB/wOBAf8DgQH/A4EB/wNLAY4cAAMFAQcDNQFVA1QBrgNgAdYDXwH7A2oB+QNcAc0DUgGpAy0BRgMC + AQMgAANfAeMBgQGnAfMB/wGBAawB9AH/AYEBqAH0Af8BgQGhAfEB/wNhAeYoAANaAcUCgQHqAf8CgQHt + Af8CgQHsAf8CgQHnAf8DXgHQKAADSgGNA4EB/wOBAf8DgQH/A4EB/wNQAZ3/AFkAAxUBHQMjATQDJAE2 + AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2AyMBMwMVAR2IAANRAZ8DYQHhA2EB4QNX + AbowAAMjATQDWQH1AagCQAH9A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8BqAJAAf0DXwHz + AyMBMwgAAbUCgQH/A4EB/wOBAf8DgQH/AcYCgQH/aAABhwKBAf8BggKBAf8BkwKBAf8BkAKBAf8DgQH/ + A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/AxYBHhMAAQEDJAE2A38B/gP8Af8D/AH/A/wB/wP8Af8D/AH/ + A/wB/wP8Af8D/AH/A/wB/wP8Af8BqAJAAf0DJAE2BAABpQKBAf8DgQH/A4QB/wOMAf8DgQH/A4EB/wGM + AoEB/wgAAa0CgQH/A4EB/wOBAf8DgQH/AcYCgQH/CAADUgGpAcMBjgGBAf8BwAGLAYEB/wG+AYgBgQH/ + AbsBhQGBAf8BuQGDAYEB/wG0AoEB/wGyAoEB/wGxAoEB/wGuAoEB/wGtAoEB/wGrAoEB/wGpAoEB/wGp + AoEB/wNSAakEAAGzAYsBgQH/AbMBiwGBAf8DgQH/A4EB/wGzAYsBgQH/AbMBiwGBAf8BswGLAYEB/wGz + AYsBgQH/AbMBiwGBAf8BswGLAYEB/wOBAf8DFgEeEwABAQMkATYD/AH/A/wB/wPdAf8D2wH/A9kB/wPX + Af8D1QH/A9QB/wPUAf8D1AH/A/sB/wP8Af8DJAE2AwABAQGtAoEB/wOUAf8DrQH/A60B/wOtAf8DjAH/ + A4EB/wGtAoEB/wGUAoEB/wOBAf8DgQH/A4EB/wOBAf8DgQH/AYQCgQH/BAAByAGSAYE1/wGpAoEB/wQA + AYcCgQH/AYICgQH/AasBkAGBAf8BqgGPAYEB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DFgEe + EwABAQMkATYD/AH/A/wB/wP8Af8D/AH/A/sB/wP7Af8D+gH/A/oB/wP6Af8D+gH/A/oB/wP8Af8DJAE2 + AwABAQQAAa0CgQH/A4wB/wOcAf8DtQH/A60B/wOBAf8DgQH/AZwCgQH/A4EB/wOUAf8DnAH/A5QB/wOB + Af8DgQH/AaUCgQH/AcoBlAGBBf8B6wGwAYEF/wOyAf8DrgH/A6UB/wH+AfoB9wH/A6IB/wH+AfoB9wH/ + A5gB/wOVAf8DkQX/AaoCgQH/BAAD+AH/A/EB/wPjAf8D8wH/A/gB/wP5Af8D+AH/A+kB/wP4Af8DgQH/ + A4EB/wMWAR4DVwG4AwQBBgsAAQEDJAE2A/wB/wP8Af8D1gH/A9MB/wPPAf8DzQH/A8sB/wPIAf8DyAH/ + A8YB/wP4Af8D/AH/AyQBNgMAAQEIAAG9AoEB/wGEAoEB/wOBAf8DjAH/A5wB/wOBAf8EAAGcAoEB/wOB + Af8DhAH/A5wB/wOUAf8DgQH/A4EB/wHMAZcBgQX/Af4B/QH7Af8B/gH8AfsB/wH+AfsB+QH/Af4B+wH5 + Af8B/gH6AfcB/wH+AfoB9wH/Af4B+gH3Af8B/gH6AfcB/wH9AfgB9AH/AfwB9gHzAf8B/AH2AfMF/wGs + AoEB/wQAA+cB/wPTAf8DpQH/A9cB/wP0Af8D7gH/A+oB/wPcAf8D+AH/A4EB/wOBAf8DMQFPA20B9wNb + AdkLAAEBAyQBNgP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D+wH/A/kB/wP5Af8D+AH/A/wB/wMk + ATYDAAEBFAADgQH/A7UB/wOBAf8IAAG1AoEB/wOBAf8DgQH/A4EB/wOEAf8DgQH/AdEBnAGBBf8B7wG0 + AYEF/wO1Af8DswH/A6sB/wH+AfoB9wH/A6gB/wH9AfgB9QH/A6IB/wOeAf8DmwX/AbACgQH/BAADxQH/ + A9MB/wOmAf8D2AH/A/QB/wGBAb4BgQH/AYEBuAGBAf8BgQGxAYEB/wGBAakBgQH/AYEBoQGBAf8BgQGZ + AYEB/wGBAZEBgQH/AYEBoQGBAf8BgQGLAYEB/wNcAc8HAAEBAyQBNgP8Af8D/AH/A84B/wPKAf8DxgH/ + A8MB/wPAAf8DvQH/A7wB/wO6Af8D9gH/A/wB/wMkATYDAAEBFAADgQH/A70B/wOBAf8UAAOBAf8DnAH/ + A4EB/wHUAZ4BgQX/Af4B/AH6Af8B/gH7AfkB/wH+AfoB+AH/Af4B+gH4Af8B/gH6AfYB/wH+AfoB9wH/ + AfwB9wH0Af8B/AH3AfIB/wH8AfYB8AH/AfwB9QHvAf8B/AH0Ae4F/wGyAoEB/wQAA+kB/wPUAf8DpgH/ + A9gB/wP1Af8BgQHDAYEB/wGgAdcBqQH/AZwB1QGlAf8BmAHTAaEB/wGUAdABnQH/AZABzgGYAf8BiwHL + AZMB/wGBAcEBgQH/AYEBvgGBAf8BgQGMAYEB/wNbAcQDAAEBAyQBNgP8Af8D/AH/A/wB/wP8Af8D/AH/ + A/wB/wP7Af8D+AH/A/YB/wPzAf8D8gH/A/wB/wMkATYDAAEBFAADgQH/A70B/wOBAf8UAAOBAf8DpQH/ + A4EB/wHVAaABgQX/AfMBtgGBBf8DugH/A7gB/wOyAf8B/gH6AfcB/wOvAf8B/AH2AfAB/wOpAf8DpwH/ + A6UF/wG1AoEB/wQAA8UB/wPUAf8DpwH/A9gB/wP1Af8BgQHHAYEB/wGlAdoBrgH/AaIB2AGrAf8BngHW + AacB/wGaAdQBowH/AZYB0gGfAf8BkwHPAZoB/wGBAcUBhwH/AYEBwgGCAf8BgQGRAYEB/wNcAckDAAEB + AyQBNgP8Af8D/AH/A8cB/wPCAf8DvgH/A7gB/wO0Af8DsQH/A64B/wOsAf8D7QH/A/wB/wMkATYDAAEB + FAADgQH/A8YB/wOBAf8UAAOBAf8DpQH/A4EB/wHYAaIBgQX/Af4B+gH4Af8B/gH6AfcB/wH+AfoB9gH/ + Af0B+AH2Af8B/AH3AfIB/wH+AfoB9wH/AfwB9gHwAf8B/AH1Ae8B/wH7AfMB7AH/AfoB8QHqAf8B+gHw + AegF/wG3AoEB/wQAA+oB/wPVAf8DtQH/A98B/wP1Af8BgQHHAYEB/wGBAccBgQH/AYEBwwGBAf8BgQG+ + AYEB/wGBAbgBgQH/AYEBsQGBAf8BgQGpAYEB/wGBAaMBgQH/AYEBnQGBAf8DXAHRBwABAQMkATYD/AH/ + A/sB/wP8Af8D/AH/A/sB/wP4Af8D9QH/A/EB/wPsAf8D6gH/A+YB/wP8Af8DJAE2AwABARQAA4QB/wPG + Af8DgQH/FAADgQH/A60B/wOBAf8B2QGjAYEF/wH2AbkBgQX/A70B/wO7Af8DtwH/Af4B+gH3Af8DtQH/ + AfwB8wHtAf8DsQH/A7AB/wOtBf8BugGFAYEB/wQAA8YB/wPyAf8B3QHYAdIB/wHrAeUB3wH/A/wB/wPM + Af8DyQH/A70B/wP5Af8BhAKBAf8BkQKBAf8DFgEeA20B9wNfAdsLAAEBAyQBNgP8Af8D+QH/A8AB/wO6 + Af8DtAH/A68B/wOqAf8DpQH/A/wB/wP8Af8D/AH/A/wB/wMkATYDAAEBFAADjAH/A84B/wOBAf8UAAOB + Af8DrQH/A4EB/wHbAaQBgTX/Ab0BhwGBAf8EAAP4Af8B2QHGAbEB/wNhAeYDYQHmAdcBxAGvAf8D+QH/ + A/kB/wP5Af8D+QH/AYsCgQH/AZcCgQH/AxYBHgNYAbsDBAEGCwABAQMkATYD/AH/A/cB/wP5Af8D9wH/ + A/cB/wPzAf8D8AH/A+oB/wP8Af8D9gH/A/QB/wNNAZEDFwEgGAADlAH/A84B/wOEAf8DgQH/A4EB/wOB + Af8DgQH/A4EB/wOBAf8DtQH/A4EB/wHcAacBgQH/AdwBpwGBAf8B3AGnAYEB/wHcAacBgQH/AdwBpwGB + Af8B3AGnAYEB/wHcAacBgQH/AdwBpwGBAf8B3AGnAYEB/wHcAacBgQH/AdwBpwGBAf8B3AGnAYEB/wHc + AacBgQH/AdwBpwGBAf8BwAGLAYEB/wQAAcsBpgGBAf8BxgGgAYEB/wM9AWcDPQFnAbsBlQGBAf8BuAGU + AYEB/wG0AZABgQH/Aa8BiwGBAf8BqwGHAYEB/wGSAoEB/wMjATMDBAEGFAADJAE2AagCQAH9A/QB/wP1 + Af8D9QH/A/UB/wPxAf8D7wH/A+kB/wP8Af8D5wH/A00BkQMXASADAQECGAADlAH/A9YB/wPOAf8DzgH/ + A84B/wPGAf8DxgH/A70B/wO9Af8DvQH/A4EB/wGoAkAB/QHoAbkBkgH/AegBuQGSAf8B6AG5AZIB/wHo + AbkBkgH/AegBuQGSAf8B6AG5AZIB/wHoAbkBkgH/AegBuQGSAf8B6AG5AZIB/wHoAbkBkgH/AegBuQGS + Af8B6AG5AZIB/wHoAbkBkgH/AagCQAH9BAADIwEzAyMBMwgAAyMBMwMjATMDIwEzAyMBMwMjATMDIwEz + HAADIwEzA10B8AGoAkAB/QP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP4Af8DTQGRAxcBIAMBAQIcAAOc + Af8DnAH/A5QB/wOUAf8DjAH/A4wB/wOEAf8DgQH/A4EB/wOBAf8DgQH/Az4BawNlAfQB3AGnAYEB/wHc + AaYBgQH/AdoBpAGBAf8B2AGiAYEB/wHVAaABgQH/AdQBngGBAf8B0gGdAYEB/wHPAZoBgQH/Ac4BmQGB + Af8BywGWAYEB/wHJAZQBgQH/A2UB9AM+AWtIAAMUARwDIwEzAyQBNgMkATYDJAE2AyQBNgMkATYDJAE2 + AyQBNgMkATYDFwEgAwEBAtQAAxUBHQMjATQDJAE2AyQBNgMkATYDJAE2AyQBNgMkATYDJAE2AyQBNgMk + ATYDJAE2AyMBMwMVAR0gAAMmATgDFgEeMAADgQH/KAADWwHEA2UB9AHRAZYBgQH/Ac4BkgGBAf8BywGO + AYEB/wHJAYoBgQH/AccBhwGBAf8BwwGEAYEB/wHDAYQBgQH/AcMBhAGBAf8BwwGEAYEB/wHDAYQBgQH/ + AcMBhAGBAf8DVQGwDAADIwE0A1kB9QGoAkAB/QP8Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/ + AagCQAH9A18B8wMjATMcAAMmATgDYgH2A7YB/wMYASEoAAGBAYMBgQH/A4EB/ygAAdcBoQGBAf8B+AHy + Ae0B/wH3AfAB6gH/AfYB7QHmAf8B9AHqAeIB/wHzAecB3gH/AfEB5AHbAf8B8AHiAdgB/wHwAeIB2AH/ + AfAB4gHYAf8B8AHiAdgB/wHwAeIB2AH/AfAB4gHYAf8BqAJAAf0LAAEBAyQBNgN/Af4D/AH/A/wB/wP8 + Af8D/AH/A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/AagCQAH9AyQBNhgAAyYBOANiAfYD4AH/A9gB/wO2 + Af8DGAEhIAABgQGNAYEB/wGBAaEBgQH/AYEBngGBAf8DgQH/A4EB/wNaAdMDPgFrGAAB2QGkAYEB/wH5 + AfMB7gH/AesB0gG+Bf8B6wHTAb8N/wHqAccBrQ3/AfAB4gHYAf8BxgGMAYEB/wsAAQEDJAE2A/wB/wP8 + Af8D/AH/A/wB/wP8Af8D+wH/A/sB/wP7Af8D+wH/A/sB/wP7Af8D/AH/AyQBNhQAAycBOwNiAfYD4gH/ + A/0B/wP9Af8D2QH/A7YB/wMYASEYAAGBAZgBgQH/AYEBqQGBAf8BgwHGAYsB/wGBAcUBhwH/AYEBwwGF + Af8BgQHCAYIB/wGBAbUBgQH/AYEBlQGBAf8DWwHLAxMBGhAAAd0BqAGBAf8B+QHzAe8B/wHrAdABugH/ + AesB0AG7Af8B6wHQAbsB/wHrAdABuwH/AesB0AG7Af8B6wHRAb0B/wHqAc0BtQH/AeoBzQG1Af8B6gHN + AbUB/wHqAc0BtQH/AfAB4gHYAf8BxgGKAYEB/wgAAzgBXgNRAaAB7AH4AfwB/wHsAfgB+wH/AYUB1wH3 + Af8BqgHiAfgB/wP7Af8D+wH/A/oB/wP6Af8D+gH/A/oB/wP6Af8D/AH/AyQBNhAAAycBOwNiAfYD5AH/ + A/0B/wH7AewB1QH/Af0B0gGXAf8B/QH7AfcB/wPZAf8DtwH/AxgBIRQAAYEBnQGBAf8BgQGtAYEB/wGH + AckBjwH/AYUBxwGLAf8BggHGAYkB/wGBAcQBhwH/AYEBvwGBAf8BgQHCAYIB/wGBAZ0BgQH/A1sByxAA + Ad8BqgGCAf8B+QHzAe8B/wHqAc4BtwX/AesB0AG7Df8B6gHPAboB/wH7AfYB8gn/AfAB4gHYAf8ByAGN + AYEB/wgAAy8BSQGBAc8B9wH/AYEBxwH1Af8BgQHDAfMB/wGBAcMB9AH/AbwB6AH5Af8D+wH/A/sB/wP7 + Af8D+gH/A/oB/wP4Af8D+AH/A/wB/wMkATYMAAMnATsDYgH2A+UB/wP9Af8B+wHsAdYB/wH9Ac0BhgL/ + Ac8BjAH/AfoB0QGXAf8B+QH3AfMB/wPZAf8DtwH/AxgBIRQAAYEBngGBAf8BgQGuAYEB/wGBAaoBgQH/ + AYEBkAGBAf8BgQGjAYEB/wGBAcUBhwH/AYEBxAGFAf8BgQHCAYIB/wGBAZYBgQH/Az4BawwAAeEBrgGH + Af8B+gH0AfAB/wHqAcsBsgH/AeoBzAGzAf8B6gHMAbMB/wHqAcwBswH/AeoBzAGzAf8B6gHOAbcB/wHo + AccBrAH/AegBxwGsAf8B6AHIAbAB/wHoAcgBrgH/AfAB4gHYAf8BxAGGAYEB/wgAAyABLgGBAdAB9wH/ + AYMB3gH5Af8BgQHZAfkB/wGBAcEB9AH/AdQB8AH6Af8D/AH/A/wB/wP8Af8D+wH/A/kB/wP5Af8D+AH/ + A/wB/wMkATYIAAMnATsDYgH2A+cB/wP9Af8B+wHsAdYB/wH9Ac0BiAL/AdUBmAL/AdYBnAL/AdABjgH/ + AfcBzAGNAf8B/QH8AfsB/wPZAf8DtwH/AxYBHxQAAYEBnwGBAf8BgQGaAYEB/wQAA1IBpgNAAf0BgQHB + AYQB/wGBAcEBgQH/AYEBtwGBAf8DXgHYDAAB4wGxAYwB/wH6AfYB8QH/AeoByQGuBf8B6gHJAbAN/wHo + AccBrA3/AfEB5QHbAf8BxgGGAYEB/wQAAwIBAwNWAbEBgQHaAfgB/wGMAeAB+gH/AYMB3QH5Af8BgQHQ + AfYB/wGBAc8B9gH/AfkB+wH8Af8D/AH/A/wB/wP6Af8D+QH/A/YB/wP2Af8D/AH/AyQBNgQAAycBOwNi + AfYD6AH/A/0B/wH7AewB1gH/Af0BzQGIAv8B1QGYAv8B1wGeAv8B1gGbAv8BzgGJAf8B/QHhAboB/wP9 + Af8D1wH/A7UB/wMQARUYAAGBAaABgQH/CAADRwGCAYEBqwGBAf8BgwHHAYoB/wGCAcUBiAH/A4EB/wwA + AeUBtAGPAf8B+gH2AfIB/wHpAcYBqgH/AekBxgGsAf8B6gHHAawB/wHpAccBrQH/AekByQGuAf8B6QHJ + AbAB/wHoAccBrAH/AekByQGwAf8B6AHIAbAB/wHoAcwBtQH/AfIB5wHeAf8ByAGKAYEB/wQAA04BmQGB + AdgB+QH/AYEB2gH4Af8BiwHgAfoB/wGCAd0B+QH/AYEBzgH3Af8BgQHDAfQB/wGBAdMB9wH/A/wB/wP7 + Af8D+AH/A/YB/wPzAf8D8gH/A/wB/wMkATYDKAE9A2IB9gPpAf8D/QH/AfsB6wHTAv8BzAGDAv8B1AGY + Av8B1wGeAv8B1gGbAv8BzwGLAf8B/QHiAbwB/wP9Af8D2QH/A7kB/wMQARUMAAGBAbYBgQH/AYEBsgGB + Af8BgQGuAYEB/wGBAakBgQH/A0MBdwgAA0cBggGBAa8BgQH/AYgByQGPAf8BhQHIAYwB/wGBAYQBgQH/ + DAAB5wG3AZQB/wH7AfcB9AH/AekBwwGmBf8B6AHEAakN/wHoAccBrA3/AfcB8QHrAf8BywGPAYEB/wQA + AwcBCQMqAUADUAGeAYEB1wH4Af8BgQHSAfgB/wGLAd4B+AH/AcUB7QH6Af8B9AH6AfwB/wP7Af8D+AH/ + A/UB/wPyAf8D7wH/A+0B/wP8Af8DJAE2A2IB9gPrAf8D/QH/A/oB/wH7AfMB5wH/Af4BzgGJAv8B1AGW + Av8B1QGaAv8BzwGLAf8B/QHiAbwB/wP9Af8D2gH/A70B/wMQARUQAANeAdgBlAHRAZ0B/wGdAdUBpgH/ + AZMBzwGbAf8DQAH9A0cBggNHAYIDQAH9AYkByQGQAf8BhQHIAYwB/wGBAcABhAH/A14B2AwAAekBugGY + Af8B+wH3AfQB/wHpAcMBpgH/AekBwwGmAf8B6QHDAaYB/wHpAcMBpgH/AekBwwGmAf8B6QHDAaYB/wHp + AcMBpgH/AekBwwGmAf8B6QHDAaYB/wHpAcMBpgH/AfsB9wH0Af8BzgGTAYEB/wsAAQEDJAE2AYEB2QH3 + Af8BgQHWAfcB/wP8Af8D/AH/A/sB/wP4Af8D9QH/A/EB/wPsAf8D6gH/A+YB/wP8Af8DJAE2A80B/wP9 + Af8D/QH/A/wB/wP3Af8B/QH1AeoB/wH+Ac8BigL/AcwBgwH/Af0B4gG8Af8D/QH/A9wB/wPAAf8DEAEV + FAADPgFrAYEBxwGFAf8BoAHXAakB/wGeAdYBpwH/AZcB0QGgAf8BgQHAAYEB/wGBAbwBgQH/AY4BzAGX + Af8BkwHPAZsB/wGQAc4BmAH/AYEBqwGBAf8DPgFrDAAB6wG9AZsB/wH7AfcB9Cn/AfsB9wH0Af8B0QGX + AYEB/wsAAQEDJAE2AdAB8QH7Af8BywHuAfgB/wP5Af8D+QH/A/cB/wP2Af8D8gH/A+sB/wP8Af8D/AH/ + A/wB/wP8Af8DJAE2A84B/wP9Af8D4AH/A2oB+QFhAl0B4gP3Af8B+wHzAegB/wH9AeMBvQH/A/0B/wPe + Af8DwwH/AxABFRwAA1sBywGCAcoBjAH/AaEB2AGrAf8BmQHUAaIB/wGcAdUBpgH/AZsB1AGkAf8BkgHQ + AZsB/wGWAdEBngH/AYEBuAGBAf8DWwHLEAAB7AG/AZ4B/wH7AfcB9AH/AZwB1QGlAf8BmAHTAaEB/wGU + AdABnQH/AZABzgGYAf8BiwHLAZMB/wGHAckBjgH/AYIBxgGJAf8BgQHDAYQB/wGBAcEBgQH/AYEBvgGB + Af8B+wH3AfQB/wHUAZsBgQH/CwABAQMkATYD/AH/A/cB/wP5Af8D9wH/A/cB/wPzAf8D8AH/A+oB/wP8 + Af8D9gH/A/QB/wNNAZEDFwEgA9AB/wP9Af8DzQH/BAABXAJaAc0D8wH/A/sB/wP9Af8D4AH/A8cB/wMQ + ARUgAAMTARoDWwHLAYEByAGHAf8BlgHTAaEB/wGgAdgBqgH/AZ4B1gGnAf8BkAHOAZkB/wGBAboBgQH/ + A1sBywMTARoQAANgAesB+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0 + Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8B+wH3AfQB/wH7AfcB9AH/AfsB9wH0Af8DXAH4DAADJAE2 + AagCQAH9A/QB/wP1Af8D9QH/A/UB/wPxAf8D7wH/A+kB/wP8Af8D5wH/A00BkQMXASADAQECA9IB/wP9 + Af8D4gH/A84B/wPgAf8D/QH/A/0B/wPiAf8DywH/AxABFSwAAz4BawNeAdgBgQG5AYEB/wGBAbUBgQH/ + A14B2AM+AWsYAANGAX4DXwHjAe0BwAGfAf8B6wG+AZ0B/wHrAbwBmgH/AekBugGWAf8B5wG3AZMB/wHm + AbUBkAH/AeQBsgGMAf8B4gGvAYgB/wHgAawBhAH/Ad0BqQGBAf8B3AGlAYEB/wNdAcoMAAMjATMDXQHw + AagCQAH9A/wB/wP8Af8D/AH/A/wB/wP8Af8D/AH/A/gB/wNNAZEDFwEgAwEBAgQAA9MB/wP9Af8D/QH/ + A/0B/wP9Af8D/QH/A+QB/wPNAf8DEAEVpAADFAEcAyMBMwMkATYDJAE2AyQBNgMkATYDJAE2AyQBNgMk + ATYDJAE2AxcBIAMBAQIIAAPVAf8D1AH/A9IB/wPRAf8D0AH/A84B/wPNAf8DEAEVYAADgQH/A4EF/wOB + Af8DgQH/A4EB/wG1AaUBnAH/AYwCgQH/A4EB/wLnAd4B/wOBBf8DgQH/A4EB/wOBAf8DgQH/EAADEwEa + AyQBNQMvAUoDNgFZAzYBWQMvAUoDJAE1AxMBGiAAAycBOwNOAZQDXgHOA2AB6wNgAesDXgHOA04BlAMn + ATscAAM4AVwgAAM1AVcDBAEGCAADgQH/A4EF/wOBAf8DgQH/A4EB/wHOAcYBvQX/Ac4BxgG9Af8C5wHe + Af8DgQX/A4EB/wOBAf8DgQH/A4EB/wgAAwkBDAMjATQDPgFrA04BlQNaAcUDXQHtA10B7QNaAcUDTgGV + Az4BawMjATQDCQEMEAADCgENA04BlgNNAfoB4QHNAbkB/wHxAeABzgH/AfkB5wHWAf8B+AHmAdQB/wHw + Ad0BzAH/AeEBzQG5Af8DTQH6A04BlgMKAQ0UAANlAfQDZQH0A0oBjBAAA0ABbgNkAewDfwH+DAADgQH/ + A4EF/wGMAoEF/wOBAf8C5wHeDf8DgQX/AYwCgQX/A4EB/wOBAf8EAAMJAQwDKAE9A0cBgQFeAlsB2QHc + AdEByAH/AfEB6AHhAf8B+wH2AfIB/wH6AfMB7gH/Ae0B4AHWAf8B0gHDAbgB/wFeAlsB2QNHAYEDKAE9 + AwkBDAgAAwoBDQNXAbgB2QG/AaUB/wH+AfIB5AL/AecBzwH/AfwB2wG7Af8B+QHTAbIB/wH2AdABrAH/ + AfQB0QGuAf8B9gHYAbsB/wH6AekB1wH/AdkBvwGlAf8DVwG4AwoBDRAAA2EB4QGBAcsB7wH/AYEBtwHl + Af8DagH5A1IBowNMAZADWQH1AYEBowHdAf8BgQGuAeUB/wNiAfYMAAOBAf8DgQX/A4EF/wOBAf8C9wHv + Af8BnAGMAYEB/wGlAZwBjAX/A4EF/wOBBf8DgQH/A4EB/wQAAyMBNANHAYEDXwHzAfQB7gHoAv8C/gH/ + AfsB9AHvAf8B9wHrAeEB/wH2AeoB4AH/AfoB8wHtAf8B/gH8AfoB/wHuAeQB2wH/A18B8wNHAYEDIwE0 + CAADTgGWAdkBvwGlAv8B+AHrAv8B6wHRAv8B4QHDAf8B/gHbAboB/wH7AdYBsgH/AfcB0QGsAf8B9gHO + AakB/wHzAcsBpQH/AfIBzQGoAf8B+gHhAcoB/wHZAb8BpQH/A04BlhAAA1kBxwGBAcoB7gH/AYEBzgHx + Af8BgQHIAe8B/wGBAboB6AH/AYEBtQHlAf8BgQG9AesB/wGBAboB6wH/AYEBswHoAf8DZAHnDAADgQH/ + A4EB/wLnAd4F/wGlAZwBjAH/A4EF/wOBAf8DgQX/A4EB/wLnAd4F/wGlAZwBjAH/A4EB/wOBAf8DEwEa + Az4BawFeAlsB2QH3AfEB7QX/AfYB6AHdAf8B9gHoAd0B/wH5AfEB6wH/AfUB5gHbAf8B9QHlAdoB/wH0 + AeUB2AL/Av4B/wHvAeQB3AH/AV4CWwHZAz4BawMTARoDJgE4AagCQAH9Av4B+gL/AfkB6QL/Ae8B2AL/ + AecBzQL/AeEBwgH/AfwB3AG7Af8B+QHVAbMB/wH2AdABrAH/AfYBzQGoAf8B8wHLAaUB/wHzAc0BqQH/ + AfoB6gHZAf8BqAJAAf0DJgE4DAADUgGjAYEByAHsAf8BgQHMAfEB/wGBAcMB7QH/AYEBxAHuAf8BgQHD + Ae0B/wGBAbYB6gH/AYEBsQHoAf8BgQG0AecB/wNcAc8MAAOBAf8ChAG1Af8BhAGMAbUB/wKEAbUB/wKE + AbUB/wKBAa0B/wKBAbUB/wKBAbUB/wKBAbUB/wKBAbUB/wKBAbUB/wKBAbUB/wGEAYEBtQH/AoEBtQH/ + AoQBtQH/AaUBnAHOAf8DJAE1A04BlQHsAeUB4QL/Av4B/wH3AesB4QH/AfYB6gHgAf8B9gHpAd8B/wG9 + AoEB/wH5AfEB6wH/AfUB5wHcAf8B9QHmAdsB/wH1AeUB2gH/Af4B/AH6Af8B1gHJAb8B/wNOAZUDJAE1 + A04BlgHiAc4BugH/AfcB3gHGAv8B6QHRAv8B9gHiAv8B8gHdAv8B8AHdAf8B+AHqAdkB/wH4AeYB1AH/ + AfsB3wHHAf8B9QHPAasB/wHyAcoBpAH/AfMBzAGoAf8B+AHdAcMB/wHiAc4BugH/A04BlgwAA0UBfAGB + AcYB6gH/AYEB0gHyAf8BgQHCAe0B/wGBAb0B7AH/AYEBtwHqAf8BgQGzAekB/wGBAbkB6gH/AYEBswHm + Af8DVAGuDAADgQH/AfcBvQGBAv8BxgGBAf8B9wG9AYEC/wHGAYEB/wH3Ab0BgQH/AfcBvQGBAv8BxgGB + Av8BvQGBAv8BvQGBAf8B9wG9AYEC/wG9AYEC/wG9AYEB/wHvAb0BgQH/AfcBxgGBAf8B1gGUAYEB/wMv + AUoDWgHFAfgB8wHvAf8B+wH2AfIB/wH3AewB4wH/AfcB7AHjAf8B9wHrAeEB/wHFAYYBgQH/Ab4CgQH/ + AfkB8QHrAf8B9gHoAd0B/wH1AecB3AH/AfoB8wHuAf8B7wHkAdsB/wNaAcUDLwFKA14B0AHvAeAB0AH/ + AecBvgGWAf8B5wG8AZQB/wHwAcwBqQH/Af4B7AHaAf8B3QHEAawB/wNkAecDZAHnAd0BwwGqAf8B+AHd + AcQB/wH0AdIBsgH/AfYB1AGzAf8B9QHWAbgB/wHxAd8BzwH/A14B0AgAAwkBDANiAekBgQHVAfIB/wGB + AdAB8gH/AYEBxAHuAf8BgQHAAe0B/wGBAbsB6wH/AYEBtgHpAf8BgQG0AekB/wGBAb0B6wH/A20B9wMV + AR0IAAOBAf8BvQGUAYQB/wHGAZQBgQH/Ab0BlAGEAf8BxgGUAYQB/wG9AZQBhAH/AYwCgQH/AZQCgQH/ + Ac4BlAGMAf8BvQGUAYEB/wHGAZQBgQH/AcYBlAGBAf8BvQGUAYEB/wHOAZQBjAH/AbUBlAGBAf8BrQKB + Af8DNgFZA10B7QH+Af0B+wH/AfkB8QHrAf8B+AHuAeUB/wH3Ae0B5AH/AfcB7AHjAf8BwAGDAYEB/wHG + AYYBgQH/Ab4CgQH/AfkB8QHrAf8B9gHoAd0B/wH3AesB4QH/AfoB9QHxAf8DXQHtAzYBWQNkAewB9QHk + AdUB/wHpAb4BlwH/AegBvAGWAf8B5wG8AZQB/wHzAd4BygH/A2EB5gMUARsDFAEbA2EB5gH2AeMB0wH/ + AfEBzQGrAf8B7wHJAaQB/wHtAckBpAH/AfYB5AHSAf8DZAHsBAADBAEGA18B4wGBAc4B7QH/AYEB2wH1 + Af8BgQHNAfEB/wGBAcgB7wH/AYEBwwHuAf8BgQG+AewB/wGBAbgB6gH/AYEBtAHpAf8BgQHAAewB/wGB + AbIB5AH/A2UB9AMRARcEAAOBAf8B5wG1AYEB/wHvAb0BgQL/Ac4BhAL/Ac4BhAH/AfcBxgGBAf8BvQKB + Af8BrQKBAf8BrQKBAf8B7wG1AYEB/wHvAbUBgQH/Ae8BtQGBAf8B7wG9AYEB/wGtAoEB/wH3AbUBgQH/ + Ac4BnAGBAf8DNgFZA10B7QH+Af0B+wH/AfoB8gHsAf8B+QHvAegB/wH4Ae4B5QH/AfcB7QHkAf8BwAGD + AYEB/wHAAYMBgQH/AcgBhwGBAf8B7QHTAb4B/wH2AekB3wH/AfcB7AHjAf8B/QH7AfkB/wNdAe0DNgFZ + A2QB7AH1AeQB1QH/AekBvwGZAf8B6QHAAZkB/wHpAcEBmgH/AfMB3QHJAf8DYQHmAxQBGwMUARsDYQHm + AfgB6AHZAf8B8gHSAbIB/wHwAcwBqwH/AfABzAGrAf8B9gHkAdMB/wNkAewEAANeAd0BgQHSAe4B/wGB + AeEB9gH/AYEB1gH0Af8BgQHPAfIB/wGBAcsB8AH/AYEBxgHvAf8BgQHBAe0B/wGBAbwB6wH/AYEBtwHq + Af8BgQG0AekB/wGBAb4B7AH/AYEBsgHkAf8DWAHvAw0BEgOBAf8BrQGUAYwB/wGEAoEB/wOBAf8BhAKB + Af8DgQH/A4EB/wGEAoEB/wOBAf8DgQH/AYQCgQH/A4EB/wOBAf8BhAKBAf8DgQH/AaUCgQH/Ay8BSgNa + AcUB+gH2AfMB/wH8AfgB9AH/AfkB7wHoAf8B+QHvAegB/wH4Ae8B5gH/AcEBgwGBAf8BwQGDAYEB/wHo + AcgBrQH/AfcB7AHjAf8B9wHrAeEB/wH7AfUB8QH/AfYB7wHqAf8DWgHFAy8BSgNeAdAB8AHhAdEB/wHq + AcQBoQH/AekBwQGbAf8B6QHAAZcB/wHxAc4BsgH/AdwBwwGqAf8DZAHnA2QB5wHdAcQBrAL/AfwB8wL/ + AfQB5QL/Ae4B2gH/AfwB5QHRAf8B8QHhAdEB/wNeAdADYAHUAYEB1QHvAf8BgQHcAfIB/wGBAd4B9QH/ + AYEB3gH1Af8BgQHcAfYB/wGBAdQB8wH/AYEByQHvAf8BgQHEAe4B/wGBAcQB7QH/AYEByQHvAf8BgQHE + Ae4B/wGBAb4B6wH/AYEBtwHnAf8BgQGwAeIB/wNgAesDgQH/AecBxgGBAf8BrQKBAf8BrQKBAf8BrQKB + Af8BrQKBAf8BrQKBAf8BrQKBAf8BrQKBAf8B9wHOAZQB/wG1AoEB/wGtAoEB/wGtAoEB/wG9AoEB/wGl + AoEB/wHOAZQBgQH/AyQBNQNOAZUB8QHsAegF/wH5AfEB6wH/AfkB8AHpAf8B+QHvAegB/wHDAYQBgQH/ + AekByQGwAf8B+AHuAeUB/wH3AewB4wH/AfcB7AHjBf8B5wHfAdgB/wNOAZUDJAE1A04BlgHiAc4BugH/ + AfEB0gG2Af8B6wHBAZsB/wHrAb8BmQL/AeQB0wL/AegB2wH/AfMB2gHHAf8B+AHxAeQC/wH4Ae0C/wH3 + AekC/wH+AfEC/wH8Ae8C/wH8AfIB/wHiAc4BugH/A04BlgMJAQwDOgFhA1IBpwNgAdYDWQH1AYEBzgHt + Af8BgQHZAfQB/wGBAdMB8gH/AYEBywHwAf8BgQHPAfEB/wGBAb8B6AH/A2IB9gNfAdsDVAGuA0ABbgMV + AR0DgQH/Ab0BlAGcAf8BtQGUAaUB/wGUAoEB/wG1AZwBrQH/AbUBlAGlAf8BjAKBAf8BlAKBAf8BzgGl + Ab0B/wGtAZwBpQH/Aa0BlAGcAf8BrQGUAZwB/wHGAZwBrQH/AZQCgQH/AaUBjAGUAf8BlAKBAf8DEwEa + Az4BawFeAlsB2QH7AfgB9wX/AfkB8QHrAf8B+QHwAekB/wHpAcoBsQH/AfkB7wHoAf8B+AHvAeYB/wH4 + Ae4B5QX/AfkB9gHzAf8BXgJbAdkDPgFrAxMBGgMmATgBqAJAAf0B+gHtAeEB/wHsAccBowL/AewB2gX/ + AfcBzQG2Af8B7AG9AZcC/wH0AdwD/wH3Av8B9wHoA/8B8wP/AfkB/wL+AfkB/wGoAkAB/QMmATgUAANH + AYABgQHOAe0B/wGBAdkB9AH/AYEB1gHzAf8BgQHFAeoB/wNSAacUAAOBAf8B5wHGAZQB/wHnAb0BlAH/ + AecBxgGcAf8B3gG9AZwB/wHnAcYBnAH/Ad4BxgGcAf8BtQGMAYEB/wHnAb0BlAH/AecBvQGUAf8B5wHG + AZwB/wHnAcYBnAH/Ad4BvQGUAf8B9wHGAaUB/wHnAcYBnAH/Ab0BhAGBAf8EAAMjATQDRwGBA18B8wH7 + AfgB9wX/AfwB+QH2Af8B+gHzAe4B/wH6AfMB7QH/AfwB+AH0Bf8B+QH2AfMB/wNfAfMDRwGBAyMBNAgA + A04BlgHZAb8BpQL/AfoB8Ab/Af0B8wH/AfABwQGjAf8B7gHGAaAB/wH9AfAB0QP/Af4C/wH9AfAC/wH9 + AfEF/wHZAb8BpQH/A04BlhwAA2EB7gGBAdYB8gH/AYEB1gHzAf8DTQH6AwQBBhQAA4EB/wG9AaUBtQH/ + AbUBpQG9Af8BtQGcAb0B/wG1AZwBtQH/AbUBnAG1Af8BtQGcAbUB/wG1AaUBvQH/AbUBnAG1Af8BtQGc + AbUB/wG1AZwBtQH/AbUBnAG1Af8BtQGcAbUB/wG1AZwBvQH/Ab0BrQG1Af8BlAKBAf8EAAMJAQwDKAE9 + A0cBgQFeAlsB2QHxAewB6AH/AfoB9gHzAf8B/gH9AfsB/wH+Af0B+wH/AfkB9QHyAf8B7gHpAeUB/wFe + AlsB2QNHAYEDKAE9AwkBDAgAAwoBDQNXAbgB2QG/AaUB/wH+Af0B/AL/AfEB5AH/AfIBygGuAf8B8gHS + Aa0B/wH5Ae0ByAP/AfcD/wH9Af8B/gH9AfwB/wHZAb8BpQH/A1cBuAMKAQ0cAAM1AVcBgQHOAe0B/wGB + Ac0B7QH/A0cBgBgAA4EB/wG1AoEB/wG9AYQBgQH/Ab0CgQH/AbUBhAGBAf8BvQGEAYEB/wG9AYQBgQH/ + AbUCgQH/Ab0BhAGBAf8BvQGEAYEB/wG9AYQBgQH/Ab0BhAGBAf8BvQGEAYEB/wG9AYQBgQH/AbUBhAGB + Af8B1gGlAYQB/wgAAwkBDAMjATQDPgFrA04BlQNaAcUDXQHtA10B7QNaAcUDTgGVAz4BawMjATQDCQEM + EAADCgENA04BlgNNAfoB4QHNAbkB/wHxAeIB1AH/AfkB7AHfAf8B+wHzAeUB/wH0Ae0B5AH/AeIBzgG7 + Af8DTQH6A04BlgMKAQ0kAANfAdsDWAHvHAADgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOB + Af8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/EAADEwEaAyQBNQMvAUoDNgFZAzYBWQMvAUoDJAE1 + AxMBGiAAAycBOwNOAZQDXgHOA2AB6wFiAlgB7wFeAlsB2QNPAZcDJwE7LAADIwQzAVIcAAFCAU0BPgcA + AT4DAAEoAwABQAMAAXADAAEBAQABAQUAAYABAxYAA/8DAAHwAQ8B4AUAAcABAwHwBQABgAEBAfADAAHA + AQMBgAEBAQABAQIAAeABBwMAAQcCAAH8AT8DAAEPAgAB+AEfAgABgAEHAgAB+AEfAgAB4AEHAgAB8AEP + AgAB8AEHAgAB8AEPAgAB4AEHAgAB8AEPAgAB4AEHAgAB8AEPAgAB4AEHAgAB8AEPAYABAQHgAQ8CAAHw + AQ8BgAEBAeABfwIAAfABDwHAAQMB4AF/AgAB+AEfAfABDwHwAf8CAAT/AgAG/wIAAeABAwH/AX8C/wIA + AcABAwH+AT8C/wIAAYABAwH+AR8B/wHnAgABgAEDAeABDwH/AccCAAGBAf8B4AEHAYABAwIAAYMB/wHg + AQcBgAEBAgABgwHnAeABBwGAAQECAAGBAecB4AEPAYABAwIAAYABAwH+AR8B/wHnAgABgAEBAf4BPwH/ + AecCAAHAAQEE/wIAAeABAwT/AgAB/wHnBP8CAAH/AecE/wIAAv8BAAEfAfABBwL/AeEBhwEAAR8BwAED + AYABAQHBAYMBAAEfAYABAQGAAQEBwAEDAQABHwGAAQABgAEBAeABBwGAAR8CAAGAAQEB8AEPAcADAAGA + AQEB8AEPAcADAAGAAQECAAHAAwABgAEBAgABwAMAAYABAQIAAcADAAGAAQEBMAEMAeABAQIAAYABAQHw + AQ8B/gEDAgABgAEBAeABBwH+AQMBgAEBAYABAQHAAQMB/gEDAYABAQGAAQEBwQGDAf4BAwHAAQMBgAEB + AeEBhwH/AQMB8AEPBP8B/AE/Bv8B4AEHAcABAwHAAQMBwAEDAcABAwGAAQEBgAEBAYABAQHAAQMBgAEB + AYABAQGAAQEBwAEDAYABAQGAAQEBgAEBAcABAwGAAQEBgAEBAYABAQHAAQMBgAEBAYABAQGAAQEBwAED + AcABAwHAAQMBwAEDAcABAwHgAQcB4AEHAeABBwHAAQMB8AEPAfABDwHwAQ8BwAEDAfABBwHwAQcB8AEH + AcABAwHwAQcB8AEHAfABBwHAAQMB8AEPAfABDwHwAQ8BwAEDAfABDwHwAQ8B8AEPAeABBwH4AR8B+AEf + AfgBHwr/AYABAQT/AYcB/wGAAQEBgwP/AQABDwEAAgEBgwEAAQEBAAEPAwABAQEAAQEBAAEPAgABgAIA + AQEBAAEDAgABwAGAAQABAQEAAQMCAAH4AcABAAEBAQABAQIAAvgBAAEBBAAC+AEAAQEEAAL4AQABAQEA + AQECAAL4AQABAQEAAQMCAAL4AQABAQEAAQMBAAEBAfgCAAEBAQABDwGAAQEB+AIAAQEBMAE/AYABAwH4 + AgABAQL/AYABBwb/AcABAAH/AT8B/QH/AYABAQHAAQAB/gEfAfkB/wGAAQEBgAEAAfwBDwHwAR8BgAEB + AYABAAH4AQcB4AEHAYABAQGAAQAB8AEDAeABBwGAAQEBgAEAAeABAQHwAQMBgAEBAYABAAHAAQAB+QED + AYABAQIAAYABAAH9AYMBgAEBAwABAQHBAYMBgAEBAwABAwHAAQMBgAEBAYACAAEHAcABAwGAAQEBgAIA + AQ8B4AEHAYABAQGAAQABEAEfAeABBwGAAQEBwAIAAT8B+AEfAYABAQHAAQEBAAF/BP8BwAEDAQAD/wIA + AfABDwHwAQ8B7wHzAgABwAEDAcABAwHjAccCAAGAAQEBgAEBAeABBwIAAYABAQGAAQEB4AEHBgAB4AEH + BgAB4AEHBgABwAEDBgABgAEBBgABgBcAAfgBHwIAAYABAQGAAQEB/AEfAgABgAEBAYABAQH8AT8CAAHA + AQMBwAEDAf4BfwIAAfABDwHwAQ8B/gF/Cw== @@ -977,19 +977,19 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKlSURBVDhPhZLbSxRhGMbnNuh/6LqLiAitOyO66CKJCi2I - uikIwgsFKTJoNcyUQltnk1DMIAqyQCivUhK3rdb1sK6nddd1Xfe8a7U6s3PYOXxP78zujdnhhYdvmHmf - 33v4hvtbDH7Oo28ijd6xpK2GFwHp2sC8fMU1HT55x3PveNObfZXUP0ffpxQYgJxoIk/KCgYSBQ0ZQUPn - WB517XMfam4O76+k7w2eqv6QGL5EDXyL6XBHShhdlCCWGDxxDfzkd5y+6+Yr6XvDantbZpiNG5hNGPBu - aBgPKVjPSdgSNYiqjgsOB6uk7w0LsKMwLKQMBJIGZuI6POsljAVljAREJGmcS442xq2MHp3ffH9Gi/8m + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKkSURBVDhPhZLbSxRhGMbnNuh/6LqLiAitOyO66CKJCi2I + uikIwgsFKTJoLcyUQl1nk1DMIAqyQCivUhK3rdb1sK6nddd1Xfe8a7U6s3PYOXxP78zujdnhhYdvmHmf + 33v4hvtbDH7Oo28ijd6xpK2GFwHp2sC8fMU1HT55x3PveNObfZXUP0ffpxQYgJxoIk/KCgYSBQ0ZQUPH + WB51bXMfam4O76+k7w2eqv6QGL5EDXyL6XBHShhdlCCWGDxxDfzkd5y+6+Yr6XvDantbZpiNG5hNGPBu + aBgPKVjPSdgSNYiqjgsOB6uk7w0LsKMwLKQMBJIGZuI6POsljAVljAREJGmcS45Wxq2MHp3ffH9Gi/8m JwEEBVjOMFuBlAlfTIMnWrI7yQgmLloA78tqlTGT1kUrY5bKz86PNIINAFYyJpbSJuapk+mEtQ8NWZGh - 3gIsjVRtM0OBmuyGFHVCig1B3nyH8+0+GgEI5YC1fPkMEmwhBUzHYX+rc7QyLvz6iGBqO2R2oRjhkRu/ - jrCr6r8qyDoBqIPw2ypNl7PIjDciPlyH2KtabE05UaBNmzSOQROVxaAbjN4xG5AVCqhvmwN1cEyz5zdV - kkyZEpghopjsh5Luh5wegJQaJD1HMTUEVQzbgJ9FEzWtEyUu7KouMUOGmnKR+IqcyK3xSAR5RBZ4BOee - wO99jK+THVhZnbIBp5rd2XO3fDuct79aNTUBatoC9JKeQE08ghLvgLLpgBy9DTnSgGLoKsTls9CFGRtQ - +V04zt9zWGFaoVw52UPmLjLeh7LRQuYmSGs3UFy9DHGpFkLgBAF8uwGh7gO6th2hq3sGOfYU8kYv3Qhd - aaQLxfADqtwKMdhC1ZshLDZCzbt3A5Y7Dgb8nYcU/0OSdf5LlRzLU3Zz3C+D/8LvGNc3ywAAAABJRU5E - rkJggg== + 3gIsjVRtM0OBmuyCFHVCig1B3nyH820+GgEI5YC1fPkMEmwhBUzHYX+rs3YQfn1EMLUdMrtQjPDIjV9H + 2FX1XxVknQDUQfhtlabLWWTGGxEfrkPsVS22ppwo0KZNGsegicpi0A1G75gNyAoF1N+fA3VwTLPnN1WS + TJkSmCGimOyHku6HnB6AlBokPUcxNQRVDNuAn0UTNa0TJS7sqi4xQ4aacpH4ipzIrfFIBHlEFngE53rg + 9z7B18l2rKxO2YBTze7suVu+Hc7bX62amgA1bQF6ST1QE4+hxNuhbDogR29DjjSgGLoKcfksdGHGBlR+ + F47zdx9WmFYoV052k7mTjA+gbLSQuQnS2g0UVy9DXKqFEDhBAN9uQKjrgK5tR+jqnkGOPYW80Us3Qlca + 6UQx/JAqt0IMtlD1ZgiLjVDz7t2A5faDAX/HIcX/iGSd/1Ilx/KU3Rz3C3iIwup7jWv6AAAAAElFTkSu + QmCC @@ -1095,14 +1095,14 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGQSURBVDhPY6AKiJt0tjB+4tkJ8ZPPnQHi//H1Tf/iJ539 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGPSURBVDhPY6AKiJt0tjB+4tkJ8ZPPnQHi//H1jf/iJ539 Hz/hzP9EIJ08+ez/xImn/4e3H3+fMuHU6e51N/53r7n+H6qdgQGoePZ/EkHnqmsIA+Imn70BEpxz5C0c - z8aDQSC6/tgPqHYGhtgJZz/ADDh46wsYH7qNHcMMiKiv/wfVzsDgU7/pF0gQ3SZcGATCG/chvBBb3/AP - LEoCCK3bgzAgpucUWDCkfs9/n9pd/71qdoH5955+wIpBIKRmJ8KA6K4TYEGHsu3/m5dd/G9RvA3MByl+ - /f4bCoYZEFyxDWFAVMdxsKBx3pb/dUsv/NfL2QTmwwx49uYrGCMbEFi2CckFbUfBgjrp6/+XLjj7Xztl - LZiPz4CAojVILmg7AhYEGVAyH9MAZAwzwL9wBcIAUJyCBEEGgDQjG4ANg4BfPlI6CKzfew8sSgLwzVr0 - FqqdgSG0Zl9/SNXurUHVu98FV+76H1i+DRhIW/4HlGz871+0FujcVUAbV/z3zVsOxPX/fPOWfPTOW3YE - qp0SwMAAALtUiaS8nckQAAAAAElFTkSuQmCC + z8aDQSC6/tgPqHYGhtgJZz/ADDh46wsYH7qNHcMMiKiv/wfVzsDgU7/pF0gQ3SZcGATCG/chvBALNA0s + SgIIrduDMCCm5xRYMKR+z3+f2l3/vWp2gfn3nn7AikEgpGYnwoDorhNgQYey7f+bl138b1G8DcwHKX79 + /hsKhhkQXLENYUBUx3GwoHHelv91Sy/818vZBObDDHj25isYIxsQWLYJyQVtR8GCOunr/5cuOPtfO2Ut + mI/PgICiNUguaDsCFgQZUDIf0wBkDDPAv3AFwgBQnIIEQQaANCMbgA2DgF8+UjoIrN97DyxKAvDNWvQW + qp2BIbRmX39I1e6tQdW73wVX7vofWL4NGEhb/geUbPzvX7QW6NxVQBtX/PfNWw7E9f9885Z89M5bdgSq + nRLAwAAAtRmJoldod9MAAAAASUVORK5CYII= @@ -1201,7 +1201,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAADU - AQAAAk1TRnQBSQFMAwEBAAG8AQgBvAEIARABAAEQAQAE/wERAQAI/wFCAU0BNgcAATYDAAEoAwABQAMA + AQAAAk1TRnQBSQFMAwEBAAHEAQgBxAEIARABAAEQAQAE/wERAQAI/wFCAU0BNgcAATYDAAEoAwABQAMA ARADAAEBAQABEAYAAQj/ACMAAVkBZ3wAAQEBFgHgAQ0BNwFfegABAQEaAYgBLgECARoBFgFbcAABRAEq AUMBJgMiAR4BZwEuARABSwGIATIBAgEaARYBW24AAWUBLgEyAVcBMQFTAREBUwERAU8BzQFCARABSwGJ ATIBAgEWARUBW2wAAWYCMgFXAe0BRgHtAUYBzAFCAcwBQgHMAUIBEAFPAYcBLgEAARJsAAGGATYBMgFb diff --git a/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.Designer.cs b/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.Designer.cs index ab664afa..caedce1b 100644 --- a/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.Designer.cs +++ b/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.Designer.cs @@ -806,9 +806,6 @@ private void InitializeComponent() this.Name = "frmPlaylist"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "Playlist"; - this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.frmPlaylist_FormClosing); - this.Shown += new System.EventHandler(this.frmPlaylist_Shown); - this.VisibleChanged += new System.EventHandler(this.frmPlaylist_VisibleChanged); this.menuPlaylist.ResumeLayout(false); this.menuLoadPlaylist.ResumeLayout(false); this.flowToolbar.ResumeLayout(false); diff --git a/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.cs b/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.cs index 9e914057..c77c3631 100644 --- a/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.cs +++ b/MPfm/MPfm.Windows/Classes/Forms/frmPlaylist.cs @@ -16,8 +16,10 @@ // along with MPfm. If not, see . using System; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; +using System.Linq; using System.Windows.Forms; using MPfm.MVP.Views; using MPfm.Sound.AudioFiles; @@ -41,185 +43,6 @@ public frmPlaylist(Action onViewReady) ViewIsReady(); } - /// - /// Occurs when the form is first shown. - /// - /// Event sender - /// Event arguments - private void frmPlaylist_Shown(object sender, EventArgs e) - { - //RefreshPlaylist(); - //RefreshLibraryPlaylistsMenu(); - } - - /// - /// Occurs when the form is shown. - /// - /// Event sender - /// Event arguments - private void frmPlaylist_VisibleChanged(object sender, EventArgs e) - { - //if (Visible) - // RefreshLibraryPlaylistsMenu(); - } - - /// - /// Occurs when the user tries to close the form, using the X button or the - /// Close button. - /// - /// Event sender - /// Event arguments - private void frmPlaylist_FormClosing(object sender, FormClosingEventArgs e) - { - if (e.CloseReason == CloseReason.ApplicationExitCall || e.CloseReason == CloseReason.WindowsShutDown) - { - e.Cancel = false; - } - else - { - e.Cancel = true; - this.Hide(); - //Main.btnPlaylist.Checked = false; - } - } - - /// - /// Occurs when the user changes the order of the playlist songs in the ListView. - /// - /// Event arguments - public void viewSongs_ItemsReordered(EventArgs args) - { - //// Copy the original list of songs (we have to create a new list or the objects will stay as a reference) - //List playlistSongs = new List(); - //foreach (PlaylistSongDTO playlistSong in Main.Player.CurrentPlaylist.Songs) - //{ - // // Perform a deep clone of the object - // PlaylistSongDTO newPlaylistSong = new PlaylistSongDTO(); - // newPlaylistSong.PlaylistSongId = playlistSong.PlaylistSongId; - // newPlaylistSong.Song = playlistSong.Song; - // playlistSongs.Add(newPlaylistSong); - //} - - //// Loop through list view items - //for (int a = 0; a < viewSongs.Items.Count; a++ ) - //{ - // // Get the playlist song id - // Guid playlistSongId = new Guid(viewSongs.Items[a].Tag.ToString()); - - // // Check if the playlist song id matches - // if (playlistSongs[a].PlaylistSongId != playlistSongId) - // { - // // No match: we have to update this song - - // // We can't just update the properties of the PlaylistSongDTO or it will - // // change the values in the CurrentSong property. Thus we will create a new - // // PlaylistSongDTO instance with the same id. - // PlaylistSongDTO newPlaylistSong = new PlaylistSongDTO(); - - // // Update the playlist song id - // // Note; This changes the id of the CurrentSong too! it must not do that! - // newPlaylistSong.PlaylistSongId = playlistSongId; - - // // Find the SongDTO in the list of playlist songs - // foreach (PlaylistSongDTO playlistSong in playlistSongs) - // { - // // Is there a match? - // if (playlistSong.PlaylistSongId == playlistSongId) - // { - // // Update the SongDTO item - // newPlaylistSong.Song = playlistSong.Song; - - // // Get out of the loop - // break; - // } - - // } - - // // Replace object in array - // Main.Player.CurrentPlaylist.Songs[a] = newPlaylistSong; - // } - //} - } - - /// - /// Plays the selected song in the ListView displaying the songs in the current playlist. - /// - public void PlaySelectedSong() - { - //// Make sure there is a selected item - //if (viewSongs2.SelectedItems.Count == 0) - //{ - // return; - //} - - //// Get the playlist item identifier from the selected item - //Guid playlistItemId = viewSongs2.SelectedItems[0].PlaylistItemId; - //PlaylistItem item = Main.Player.Playlist.Items.FirstOrDefault(x => x.Id == playlistItemId); - //int index = Main.Player.Playlist.Items.IndexOf(item); - - //// Check if the player is playing - //if (Main.Player.IsPlaying) - //{ - // // Skip to new song - // Main.Player.GoTo(index); - //} - //else - //{ - // // Set playlist index - // Main.Player.Playlist.GoTo(index); - - // // Start playback - // Main.Play(); - //} - } - - #region Refresh Methods - - /// - /// Updates the title of the window with the playlist name. - /// - public void RefreshTitle() - { - //// Display playlist file path in form title if available - //if (!String.IsNullOrEmpty(Main.Player.Playlist.FilePath)) - //{ - // this.Text = Main.Player.Playlist.FilePath; - //} - //else - //{ - // this.Text = "Unsaved playlist"; - //} - } - - /// - /// Refreshes the list view showing the songs in the current playlist. - /// - public void RefreshPlaylist() - { - //// Make sure the playlist is valid - //if (Main.Player.Playlist == null) - //{ - // return; - //} - - //// Import songs into the control - //viewSongs2.ImportPlaylist(Main.Player.Playlist); - - //// Refresh window title - //RefreshTitle(); - } - - /// - /// Refreshes the play icon in the SongGridView displaying playlist items. - /// - /// Playlist item identifier - public void RefreshPlaylistPlayIcon(Guid newPlaylistItemId) - { - // Set currently playing song - viewSongs2.NowPlayingPlaylistItemId = newPlaylistItemId; - viewSongs2.Refresh(); - } - /// /// Refreshes the "Load playlist"/"Library playlists" menu item with library playlists. /// @@ -245,10 +68,6 @@ public void RefreshLibraryPlaylistsMenu() //} } - #endregion - - #region Toolbar Buttons Events - /// /// Occurs when the user clicks on the "New playlist" button in the toolbar. /// @@ -256,29 +75,8 @@ public void RefreshLibraryPlaylistsMenu() /// Event arguments private void btnNewPlaylist_Click(object sender, EventArgs e) { - //// Check if the playlist has at least one item - //if (viewSongs2.Items.Count > 0) - //{ - // // Warn user - // if (MessageBox.Show("Are you sure you wish to create a new playlist?\nYou will lose the contents of the current playlist. This will also stop playback.", "Create a new playlist", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == System.Windows.Forms.DialogResult.No) - // { - // // The user said no; exit method - // return; - // } - //} - - //// Is the player running? - //if (Main.Player.IsPlaying) - //{ - // // Stop playback - // Main.Stop(); - //} - - //// Empty current playlist - //Main.Player.Playlist.Clear(); - - //// Refresh playlist - //RefreshPlaylist(); + if (MessageBox.Show(this, "Are you sure you wish to create a new playlist? You will lose the current playlist if not saved.", "Playlist will be lost", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK) + OnNewPlaylist(); } /// @@ -288,7 +86,6 @@ private void btnNewPlaylist_Click(object sender, EventArgs e) /// Event arguments private void btnOpenPlaylist_Click(object sender, EventArgs e) { - // Display contextual menu menuLoadPlaylist.Show(btnOpenPlaylist, new Point(0, btnOpenPlaylist.Height)); } @@ -299,25 +96,7 @@ private void btnOpenPlaylist_Click(object sender, EventArgs e) /// Event arguments private void btnSavePlaylist_Click(object sender, EventArgs e) { - //// Check if playlist file is valid - //if (!String.IsNullOrEmpty(Main.Player.Playlist.FilePath)) - //{ - // // Check if file exists - // if (File.Exists(Main.Player.Playlist.FilePath)) - // { - // // Warn user is he wants to save - // if(MessageBox.Show("Warning: The playlist file already exists. Do you wish to overwrite this file?", "The playlist file already exists", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == System.Windows.Forms.DialogResult.Yes) - // { - // // The user wants to save - // SavePlaylist(Main.Player.Playlist.FilePath); - // } - // } - //} - //else - //{ - // // Ask user for file path - // SavePlaylistAs(); - //} + } /// @@ -328,7 +107,6 @@ private void btnSavePlaylist_Click(object sender, EventArgs e) /// Event arguments private void btnSavePlaylistAs_Click(object sender, EventArgs e) { - SavePlaylistAs(); } /// @@ -338,58 +116,19 @@ private void btnSavePlaylistAs_Click(object sender, EventArgs e) /// Event arguments private void btnRemoveSongs_Click(object sender, EventArgs e) { - //// Create a list of songs to remove - ////List removedSongs = new List(); - - //// Check if there is at least one item selected - //if (viewSongs2.SelectedItems.Count > 0) - //{ - // // Go through list view items (use a while loop so we can remove items from a collection we are iterating through) - // while (true) - // { - // // Are there selected items left? - // if (viewSongs2.SelectedItems.Count == 0) - // { - // // Exit loop - // break; - // } - - // // Get item - // SongGridViewItem item = viewSongs2.SelectedItems[0]; - - // // Check if the selected song is playing - // if (item.PlaylistItemId == Main.Player.Playlist.CurrentItem.Id) - // { - // // Warn the user - // MessageBox.Show("You cannot remove the current song from the playlist!", "Error removing song from playlist", MessageBoxButtons.OK, MessageBoxIcon.Warning); - // break; - // } - - // // Remove playlist item - // PlaylistItem playlistItem = Main.Player.Playlist.Items.FirstOrDefault(x => x.Id == item.PlaylistItemId); - // Main.Player.Playlist.Items.Remove(playlistItem); - - // // Remove grid view item - // viewSongs2.Items.Remove(item); - // } - - // // Set playlist as modified - // //Main.Player.CurrentPlaylist.PlaylistModified = true; - // //Main.Player.CurrentPlaylist.PlaylistType = PlaylistType.Custom; + if (viewSongs2.SelectedItems.Count == 0) + return; - // // Refresh the window title - // RefreshTitle(); + var ids = viewSongs2.SelectedItems.Select(x => x.PlaylistItemId).ToList(); - // // Refresh grid view - // viewSongs2.InvalidateSongCache(); - // viewSongs2.Refresh(); - //} + var ids2 = new List(); + foreach (var sel in viewSongs2.SelectedItems) + { + ids2.Add(sel.PlaylistItemId); + } + OnRemovePlaylistItems(ids); } - #endregion - - #region Contextual Menu Events - /// /// Occurs when the user right clicks on the Song Browser. /// @@ -412,7 +151,10 @@ private void menuPlaylist_Opening(object sender, CancelEventArgs e) /// Event arguments private void miPlaylistPlaySong_Click(object sender, EventArgs e) { - PlaySelectedSong(); + if (viewSongs2.SelectedItems.Count == 0) + return; + + OnSelectPlaylistItem(viewSongs2.SelectedItems[0].AudioFile.Id); } /// @@ -475,192 +217,6 @@ protected void miLoadPlaylistLibraryItem_Click(object sender, EventArgs e) //LoadPlaylist(item.Tag.ToString()); } - #endregion - - #region Load/Save Playlists - - /// - /// Loads a playlist. - /// - /// Playlist file path - public void LoadPlaylist(string playlistFilePath) - { - //try - //{ - // // Create window - // formLoadPlaylist = new frmLoadPlaylist(Main, playlistFilePath); - - // // Show Load playlist dialog (progress bar) - // DialogResult dialogResult = formLoadPlaylist.ShowDialog(this); - // if (dialogResult == System.Windows.Forms.DialogResult.OK) - // { - // // Get audio files - // List audioFiles = formLoadPlaylist.AudioFiles; - - // // Check if any audio files have failed loading - // List failedAudioFilePaths = formLoadPlaylist.FailedAudioFilePaths; - - // // Clear player playlist - // Main.Player.Playlist.Clear(); - // Main.Player.Playlist.FilePath = playlistFilePath; - - // // Make sure there are audio files to add to the player playlist - // if (audioFiles.Count > 0) - // { - // // Add audio files - // Main.Player.Playlist.AddItems(audioFiles); - // Main.Player.Playlist.First(); - // } - - // // Refresh song browser - // RefreshTitle(); - // RefreshPlaylist(); - // RefreshPlaylistPlayIcon(Guid.Empty); - - // // Check if any files could not be loaded - // if (failedAudioFilePaths != null && failedAudioFilePaths.Count > 0) - // { - // // Check if the user wants to see the list of files - // if (MessageBox.Show("Some files in the playlist could not be loaded. Do you wish to see the list of files in a text editor?", "Some files could not be loaded", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == System.Windows.Forms.DialogResult.Yes) - // { - // // Get temp file path - // string tempFilePath = Path.GetTempPath() + "MPfm_PlaylistLog_" + Conversion.DateTimeToUnixTimestamp(DateTime.Now).ToString("0.0000000").Replace(".", "") + ".txt"; - - // // Create temporary file - // TextWriter tw = null; - // try - // { - // // Open text writer - // tw = new StreamWriter(tempFilePath); - // foreach (string item in failedAudioFilePaths) - // { - // tw.WriteLine(item); - // } - // } - // catch (Exception ex) - // { - // // Display error - // MessageBox.Show("Failed to save the file to " + tempFilePath + "!\n\nException:\n" + ex.Message + "\n" + ex.StackTrace, "Failed to save the file", MessageBoxButtons.OK, MessageBoxIcon.Error); - // } - // finally - // { - // tw.Close(); - // } - - // // Start notepad - // Process.Start(tempFilePath); - // } - // } - - // // Check if the playlist is empty - // if (audioFiles.Count == 0) - // { - // // Warn user that the playlist is empty - // MessageBox.Show("The playlist file is empty or does not contain any valid file!", "Playlist is empty", MessageBoxButtons.OK, MessageBoxIcon.Information); - // } - // } - - // // Dispose form - // formLoadPlaylist.Dispose(); - // formLoadPlaylist = null; - //} - //catch (Exception ex) - //{ - // MessageBox.Show(ex.Message, "Error loading playlist", MessageBoxButtons.OK, MessageBoxIcon.Error); - //} - } - - /// - /// Saves the playlist under a different file path. - /// - public void SavePlaylistAs() - { - //// Check if the playlist is empty - //if (Main.Player.Playlist.Items.Count == 0) - //{ - // // Display error - // MessageBox.Show("Error: You cannot save an empty playlist!", "Error saving playlist", MessageBoxButtons.OK, MessageBoxIcon.Error); - // return; - //} - - //// Display dialog - //if (dialogSavePlaylist.ShowDialog() != System.Windows.Forms.DialogResult.OK) - //{ - // // The user has cancelled the operation - // return; - //} - - //// Save playlist - //SavePlaylist(dialogSavePlaylist.FileName); - } - - /// - /// Saves the playlist in the specified file path. - /// - /// Playlist file path - public void SavePlaylist(string playlistFilePath) - { - //bool relativePath = false; - - //// Change cursor - //Cursor.Current = Cursors.WaitCursor; - - //// Set playlist file path - //Main.Player.Playlist.FilePath = playlistFilePath; - - //// Check if the path is in the same - //DialogResult dialogResult = MessageBox.Show("Do you wish to use relative paths instead of absolute paths when possible?\n\nA full path or absolute path is a path that points to the same location on one file system regardless of the working directory or combined paths.\n\nA relative path is a path relative to the working directory of the user or application, so the full absolute path will not have to be given.", "Use relative paths", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); - - //// Check result - //if (dialogResult == System.Windows.Forms.DialogResult.Cancel) - //{ - // // Cancel - // return; - //} - //else if (dialogResult == System.Windows.Forms.DialogResult.Yes) - //{ - // // Save playlist - // //SavePlaylist(dialogSavePlaylist.FileName, true); - // relativePath = true; - //} - //else if (dialogResult == System.Windows.Forms.DialogResult.No) - //{ - // // Save playlist - // //SavePlaylist(dialogSavePlaylist.FileName, false); - // relativePath = false; - //} - - //// Determine what format the user has chosen - //if (dialogSavePlaylist.FileName.ToUpper().Contains(".M3U8")) - //{ - // // Save playlist - // PlaylistTools.SaveM3UPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath, true); - //} - //else if (dialogSavePlaylist.FileName.ToUpper().Contains(".M3U")) - //{ - // // Save playlist - // PlaylistTools.SaveM3UPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath, false); - //} - //else if (dialogSavePlaylist.FileName.ToUpper().Contains(".PLS")) - //{ - // // Save playlist - // PlaylistTools.SavePLSPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath); - //} - //else if (dialogSavePlaylist.FileName.ToUpper().Contains(".XSPF")) - //{ - // // Save playlist - // PlaylistTools.SaveXSPFPlaylist(dialogSavePlaylist.FileName, Main.Player.Playlist, relativePath); - //} - - //// Change cursor - //Cursor.Current = Cursors.Default; - - //// Refresh title - //RefreshTitle(); - } - - #endregion - /// /// Occurs when the user double clicks on one of the item in the playlist song view. /// @@ -668,14 +224,17 @@ public void SavePlaylist(string playlistFilePath) /// Event arguments private void viewSongs2_DoubleClick(object sender, EventArgs e) { - PlaySelectedSong(); + if (viewSongs2.SelectedItems.Count == 0) + return; + + OnSelectPlaylistItem(viewSongs2.SelectedItems[0].AudioFile.Id); } #region IPlaylistView implementation public Action OnChangePlaylistItemOrder { get; set; } public Action OnSelectPlaylistItem { get; set; } - public Action OnRemovePlaylistItem { get; set; } + public Action> OnRemovePlaylistItems { get; set; } public Action OnNewPlaylist { get; set; } public Action OnLoadPlaylist { get; set; } public Action OnSavePlaylist { get; set; } @@ -683,14 +242,42 @@ private void viewSongs2_DoubleClick(object sender, EventArgs e) public void PlaylistError(Exception ex) { + MethodInvoker methodUIUpdate = delegate + { + MessageBox.Show(string.Format("An error occured in Playlist: {0}", ex), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + }; + + if (InvokeRequired) + BeginInvoke(methodUIUpdate); + else + methodUIUpdate.Invoke(); } public void RefreshPlaylist(Playlist playlist) { + MethodInvoker methodUIUpdate = delegate { + viewSongs2.ImportAudioFiles(playlist.Items.Select(x => x.AudioFile).ToList()); + this.Text = !String.IsNullOrEmpty(playlist.FilePath) ? playlist.FilePath : "Unsaved playlist"; + }; + + if (InvokeRequired) + BeginInvoke(methodUIUpdate); + else + methodUIUpdate.Invoke(); } public void RefreshCurrentlyPlayingSong(int index, AudioFile audioFile) { + MethodInvoker methodUIUpdate = delegate + { + viewSongs2.NowPlayingPlaylistItemId = audioFile.Id; + viewSongs2.Refresh(); + }; + + if (InvokeRequired) + BeginInvoke(methodUIUpdate); + else + methodUIUpdate.Invoke(); } #endregion diff --git a/MPfm/MPfm.WindowsControls/Library/SongGridView.cs b/MPfm/MPfm.WindowsControls/Library/SongGridView.cs index 55e0b5d6..4d810e69 100644 --- a/MPfm/MPfm.WindowsControls/Library/SongGridView.cs +++ b/MPfm/MPfm.WindowsControls/Library/SongGridView.cs @@ -1,2924 +1,2745 @@ -// Copyright © 2011-2013 Yanick Castonguay -// -// This file is part of MPfm. -// -// MPfm is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// MPfm is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with MPfm. If not, see . - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Text; -using System.Drawing.Drawing2D; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Windows.Forms; -using MPfm.Sound.AudioFiles; -using MPfm.Sound.Playlists; - -namespace MPfm.WindowsControls -{ - /// - /// This custom grid view control displays the MPfm library. - /// - public partial class SongGridView : Control, IMessageFilter - { - #region Private Variables - - // Mode - private SongGridViewMode mode = SongGridViewMode.AudioFile; - - // Controls - private System.Windows.Forms.VScrollBar vScrollBar = null; - private System.Windows.Forms.HScrollBar hScrollBar = null; - private ContextMenuStrip menuColumns = null; - - // Background worker for updating album art - private int preloadLinesAlbumCover = 20; - private BackgroundWorker workerUpdateAlbumArt = null; - private List workerUpdateAlbumArtPile = null; - private System.Windows.Forms.Timer timerUpdateAlbumArt = null; - - // Cache - private SongGridViewCache songCache = null; - private List imageCache = new List(); - - // Private variables used for mouse events - private int columnMoveMarkerX = 0; - private int startLineNumber = 0; - private int numberOfLinesToDraw = 0; - private int minimumColumnWidth = 30; - private int dragStartX = -1; - private int dragOriginalColumnWidth = -1; - private bool isMouseOverControl = false; - private bool isUserHoldingLeftMouseButton = false; - private int lastItemIndexClicked = -1; - - // Animation timer and counter for currently playing song - private int timerAnimationNowPlayingCount = 0; - private Rectangle rectNowPlaying = new Rectangle(0, 0, 1, 1); - private System.Windows.Forms.Timer timerAnimationNowPlaying = null; - - #endregion - - #region Events - - /// - /// Delegate method for the OnSelectedItemChanged event. - /// - /// SelectedIndexChanged data - public delegate void SelectedIndexChanged(SongGridViewSelectedIndexChangedData data); - /// - /// The OnSelectedIndexChanged event is triggered when the selected item(s) have changed. - /// - public event SelectedIndexChanged OnSelectedIndexChanged; - - /// - /// Delegate method for the OnColumnClick event. - /// - /// SongGridViewColumnClickData data - public delegate void ColumnClick(SongGridViewColumnClickData data); - /// - /// The ColumnClick event is triggered when the user has clicked on one of the columns. - /// - public event ColumnClick OnColumnClick; - - #endregion - - #region Properties - - /// - /// Private value for the Theme property. - /// - private SongGridViewTheme theme = null; - /// - /// Defines the current theme used for rendering the control. - /// - public SongGridViewTheme Theme - { - get - { - return theme; - } - set - { - theme = value; - } - } - - #region Other Properties (Items, Columns, Menus, etc.) - - /// - /// Private value for the Items property. - /// - private List items = null; - /// - /// List of grid view items (representing songs). - /// - [Browsable(false)] - public List Items - { - get - { - return items; - } - } - - /// - /// Returns the list of selected items. - /// - [Browsable(false)] - public List SelectedItems - { - get - { - // Check if the item list exists - if (items != null) - { - return items.Where(x => x.IsSelected).ToList(); - } - - return null; - } - } - - /// - /// Private value for the Columns property. - /// - private List columns = null; - /// - /// List of grid view columns. - /// - [Browsable(false)] - public List Columns - { - get - { - return columns; - } - } - - - /// - /// Private value for the NowPlayingAudioFileId property. - /// - private Guid nowPlayingAudioFileId = Guid.Empty; - /// - /// Defines the currently playing audio file identifier. - /// - [Browsable(false)] - public Guid NowPlayingAudioFileId - { - get - { - return nowPlayingAudioFileId; - } - set - { - nowPlayingAudioFileId = value; - } - } - - /// - /// Private value for the NowPlayingPlaylistItemId property. - /// - private Guid nowPlayingPlaylistItemId = Guid.Empty; - /// - /// Defines the currently playing playlist item identifier. - /// - [Browsable(false)] - public Guid NowPlayingPlaylistItemId - { - get - { - return nowPlayingPlaylistItemId; - } - set - { - nowPlayingPlaylistItemId = value; - } - } - - /// - /// Private value for the ContextMenuStrip property. - /// - private ContextMenuStrip contextMenuStrip = null; - /// - /// ContextMenuStrip related to the grid. This context menu - /// opens when the user right clicks an item. - /// - [Category("Misc"), Browsable(true), Description("Stuff.")] - public override ContextMenuStrip ContextMenuStrip - { - get - { - return contextMenuStrip; - } - set - { - contextMenuStrip = value; - } - } - - #endregion - - #region Filter / OrderBy Properties - - /// - /// Private value for the OrderByFieldName property. - /// - private string orderByFieldName = string.Empty; - /// - /// Indicates which field should be used for ordering songs. - /// - public string OrderByFieldName - { - get - { - return orderByFieldName; - } - set - { - orderByFieldName = value; - - // Invalidate item list and cache - items = null; - songCache = null; - - // Refresh whole control - Refresh(); - } - } - - /// - /// Private value for the OrderByAscending property. - /// - private bool orderByAscending = true; - /// - /// Indicates if the order should be ascending (true) or descending (false). - /// - public bool OrderByAscending - { - get - { - return orderByAscending; - } - set - { - orderByAscending = value; - } - } - - #endregion - - #region Settings Properties - - /// - /// Private value for the DisplayDebugInformation property. - /// - private bool displayDebugInformation = false; - /// - /// If true, the debug information will be shown over the first column. - /// - public bool DisplayDebugInformation - { - get - { - return displayDebugInformation; - } - set - { - displayDebugInformation = value; - } - } - - /// - /// Private value for the CanResizeColumns property. - /// - private bool canResizeColumns = true; - /// - /// Indicates if the user can resize the columns or not. - /// - public bool CanResizeColumns - { - get - { - return canResizeColumns; - } - set - { - canResizeColumns = value; - } - } - - /// - /// Private value for the CanMoveColumns property. - /// - private bool canMoveColumns = true; - /// - /// Indicates if the user can move the columns or not. - /// - public bool CanMoveColumns - { - get - { - return canMoveColumns; - } - set - { - canMoveColumns = value; - } - } - - /// - /// Private value for the CanChangeOrderBy property. - /// - private bool canChangeOrderBy = true; - /// - /// Indicates if the user can change the order by or not. - /// - public bool CanChangeOrderBy - { - get - { - return canChangeOrderBy; - } - set - { - canChangeOrderBy = value; - } - } - - /// - /// Private value for the CanReorderItems property. - /// - private bool canReorderItems = true; - /// - /// Indicates if the user can reorder the items or not. - /// - public bool CanReorderItems - { - get - { - return canReorderItems; - } - set - { - canReorderItems = value; - } - } - - /// - /// Private value for the ImageCacheSize property. - /// - private int imageCacheSize = 10; - /// - /// Defines the size of the album art image cache (10 by default). - /// - public int ImageCacheSize - { - get - { - return imageCacheSize; - } - set - { - imageCacheSize = value; - } - } - - #endregion - - /// - /// Indicates if a column is currently moving. - /// - public bool IsColumnMoving - { - get - { - // Loop through columns - foreach (SongGridViewColumn column in columns) - { - // Check if column is moving - if (column.IsUserMovingColumn) - { - return true; - } - } - - return false; - } - } - - /// - /// Indicates if a column is currently resizing. - /// - public bool IsColumnResizing - { - get - { - // Loop through columns - foreach (SongGridViewColumn column in columns) - { - // Check if column is moving - if (column.IsUserResizingColumn) - { - return true; - } - } - - return false; - } - } - - #endregion - - #region Constructors - - /// - /// Default constructor for SongGridView. - /// - public SongGridView() - { - // Set default styles - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | - ControlStyles.Opaque | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); - - // Create default theme - theme = new SongGridViewTheme(); - - // Get embedded font collection - embeddedFonts = EmbeddedFontHelper.GetEmbeddedFonts(); - - // Create timer for animation - timerAnimationNowPlaying = new System.Windows.Forms.Timer(); - timerAnimationNowPlaying.Interval = 50; - timerAnimationNowPlaying.Tick += new EventHandler(timerAnimationNowPlaying_Tick); - timerAnimationNowPlaying.Enabled = true; - - // Create vertical scrollbar - vScrollBar = new System.Windows.Forms.VScrollBar(); - vScrollBar.Width = 16; - vScrollBar.Scroll += new ScrollEventHandler(vScrollBar_Scroll); - Controls.Add(vScrollBar); - - // Create horizontal scrollbar - hScrollBar = new System.Windows.Forms.HScrollBar(); - hScrollBar.Width = ClientRectangle.Width; - hScrollBar.Height = 16; - hScrollBar.Top = ClientRectangle.Height - hScrollBar.Height; - hScrollBar.Scroll += new ScrollEventHandler(hScrollBar_Scroll); - Controls.Add(hScrollBar); - - // Override mouse messages for mouse wheel (get mouse wheel events out of control) - Application.AddMessageFilter(this); - - // Create background worker for updating album art - workerUpdateAlbumArtPile = new List(); - workerUpdateAlbumArt = new BackgroundWorker(); - workerUpdateAlbumArt.DoWork += new DoWorkEventHandler(workerUpdateAlbumArt_DoWork); - workerUpdateAlbumArt.RunWorkerCompleted += new RunWorkerCompletedEventHandler(workerUpdateAlbumArt_RunWorkerCompleted); - - // Create timer for updating album art - timerUpdateAlbumArt = new System.Windows.Forms.Timer(); - timerUpdateAlbumArt.Interval = 10; - timerUpdateAlbumArt.Tick += new EventHandler(timerUpdateAlbumArt_Tick); - timerUpdateAlbumArt.Enabled = true; - - // Create columns - SongGridViewColumn columnSongAlbumCover = new SongGridViewColumn("Album Cover", string.Empty, true, 0); - SongGridViewColumn columnSongNowPlaying = new SongGridViewColumn("Now Playing", string.Empty, true, 1); - SongGridViewColumn columnSongFileType = new SongGridViewColumn("Type", "FileType", false, 2); - SongGridViewColumn columnSongTrackNumber = new SongGridViewColumn("Tr#", "DiscTrackNumber", true, 3); - SongGridViewColumn columnSongTrackCount = new SongGridViewColumn("Track Count", "TrackCount", false, 4); - SongGridViewColumn columnSongFilePath = new SongGridViewColumn("File Path", "FilePath", false, 5); - SongGridViewColumn columnSongTitle = new SongGridViewColumn("Song Title", "Title", true, 6); - SongGridViewColumn columnSongLength = new SongGridViewColumn("Length", "Length", true, 7); - SongGridViewColumn columnSongArtistName = new SongGridViewColumn("Artist Name", "ArtistName", true, 8); - SongGridViewColumn columnSongAlbumTitle = new SongGridViewColumn("Album Title", "AlbumTitle", true, 9); - SongGridViewColumn columnSongGenre = new SongGridViewColumn("Genre", "Genre", false, 10); - SongGridViewColumn columnSongPlayCount = new SongGridViewColumn("Play Count", "PlayCount", true, 11); - SongGridViewColumn columnSongLastPlayed = new SongGridViewColumn("Last Played", "LastPlayed", true, 12); - SongGridViewColumn columnSongBitrate = new SongGridViewColumn("Bitrate", "Bitrate", false, 13); - SongGridViewColumn columnSongSampleRate = new SongGridViewColumn("Sample Rate", "SampleRate", false, 14); - SongGridViewColumn columnSongTempo = new SongGridViewColumn("Tempo", "Tempo", false, 15); - SongGridViewColumn columnSongYear = new SongGridViewColumn("Year", "Year", false, 16); - - // Set visible column titles - columnSongAlbumCover.IsHeaderTitleVisible = false; - columnSongNowPlaying.IsHeaderTitleVisible = false; - - // Set additional flags - columnSongAlbumCover.CanBeReordered = false; - columnSongNowPlaying.CanBeReordered = false; - columnSongNowPlaying.CanBeResized = false; - - // Set default widths - columnSongAlbumCover.Width = 200; - columnSongNowPlaying.Width = 20; - columnSongFileType.Width = 40; - columnSongTrackNumber.Width = 30; - columnSongTrackCount.Width = 80; - columnSongFilePath.Width = 200; - columnSongTitle.Width = 200; - columnSongLength.Width = 70; - columnSongArtistName.Width = 140; - columnSongAlbumTitle.Width = 140; - columnSongGenre.Width = 140; - columnSongPlayCount.Width = 50; - columnSongLastPlayed.Width = 80; - columnSongBitrate.Width = 50; - columnSongSampleRate.Width = 50; - columnSongTempo.Width = 40; - columnSongYear.Width = 40; - - // Add columns to list - columns = new List(); - columns.Add(columnSongAlbumCover); - columns.Add(columnSongAlbumTitle); - columns.Add(columnSongArtistName); - columns.Add(columnSongBitrate); - columns.Add(columnSongFilePath); - columns.Add(columnSongGenre); - columns.Add(columnSongLastPlayed); - columns.Add(columnSongLength); - columns.Add(columnSongNowPlaying); - columns.Add(columnSongPlayCount); - columns.Add(columnSongSampleRate); - columns.Add(columnSongTitle); // Song title - columns.Add(columnSongTempo); - columns.Add(columnSongTrackNumber); - columns.Add(columnSongTrackCount); - columns.Add(columnSongFileType); // Type - columns.Add(columnSongYear); - - // Create contextual menu - menuColumns = new System.Windows.Forms.ContextMenuStrip(); - - // Loop through columns - foreach (SongGridViewColumn column in columns) - { - // Add menu item - ToolStripMenuItem menuItem = (ToolStripMenuItem)menuColumns.Items.Add(column.Title); - menuItem.Tag = column.Title; - menuItem.Checked = column.Visible; - menuItem.Click += new EventHandler(menuItemColumns_Click); - } - } - - /// - /// Occurs when the Update Album Art timer expires. - /// Checks if there are more album art covers to load and starts the background - /// thread if it is not running already. - /// - /// Event sender - /// Event arguments - public void timerUpdateAlbumArt_Tick(object sender, EventArgs e) - { - // Stop timer - timerUpdateAlbumArt.Enabled = false; - - // Check for the next album art to fetch - if (workerUpdateAlbumArtPile.Count > 0 && !workerUpdateAlbumArt.IsBusy) - { - // Do some cleanup: clean items that are not visible anymore - bool cleanUpDone = false; - while (!cleanUpDone) - { - int indexToDelete = -1; - for (int a = 0; a < workerUpdateAlbumArtPile.Count; a++) - { - // Get argument - SongGridViewBackgroundWorkerArgument arg = workerUpdateAlbumArtPile[a]; - - // Check if this album is still visible (cancel if it is out of display). - if (arg.LineIndex < startLineNumber || arg.LineIndex > startLineNumber + numberOfLinesToDraw + preloadLinesAlbumCover) - { - indexToDelete = a; - break; - } - } - - if (indexToDelete >= 0) - { - workerUpdateAlbumArtPile.RemoveAt(indexToDelete); - } - else - { - cleanUpDone = true; - } - } - // There must be more album art to fetch.. right? - if (workerUpdateAlbumArtPile.Count > 0) - { - // Start background worker - SongGridViewBackgroundWorkerArgument arg = workerUpdateAlbumArtPile[0]; - workerUpdateAlbumArt.RunWorkerAsync(arg); - } - } - - // Restart timer - timerUpdateAlbumArt.Enabled = true; - } - - /// - /// Occurs when the Update Album Art background worker starts its work. - /// Fetches the album cover in another thread and returns the image once done. - /// - /// Event sender - /// Event arguments - public void workerUpdateAlbumArt_DoWork(object sender, DoWorkEventArgs e) - { - // Make sure the argument is valid - if (e.Argument == null) - { - return; - } - - // Cast argument - SongGridViewBackgroundWorkerArgument arg = (SongGridViewBackgroundWorkerArgument)e.Argument; - - // Create result - SongGridViewBackgroundWorkerResult result = new SongGridViewBackgroundWorkerResult(); - result.AudioFile = arg.AudioFile; - - // Check if this album is still visible (cancel if it is out of display). - if (arg.LineIndex < startLineNumber || arg.LineIndex > startLineNumber + numberOfLinesToDraw + preloadLinesAlbumCover) - { - // Set result with empty image - e.Result = result; - return; - } - - // Extract image from file - Image imageAlbumCover = AudioFile.ExtractImageForAudioFile(arg.AudioFile.FilePath); - - // Set album art in return data - result.AlbumArt = imageAlbumCover; - e.Result = result; - } - - /// - /// Occurs when the Update Album Art background worker has finished its work. - /// - /// Event sender - /// Event arguments - public void workerUpdateAlbumArt_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) - { - // Check if the result was OK - if (e.Result == null) - { - return; - } - - // Get album art - SongGridViewBackgroundWorkerResult result = (SongGridViewBackgroundWorkerResult)e.Result; - - // Create cover art cache (even if the albumart is null, just to make sure the grid doesn't refetch the album art endlessly) - SongGridViewImageCache cache = new SongGridViewImageCache(); - cache.Key = result.AudioFile.ArtistName + "_" + result.AudioFile.AlbumTitle; - cache.Image = result.AlbumArt; - - // We found cover art! Add to cache and get out of the loop - imageCache.Add(cache); - - // Check if the cache size has been reached - if (imageCache.Count > imageCacheSize) - { - // Check if the image needs to be disposed - if (imageCache[0].Image != null) - { - // Dispose image - Image imageTemp = imageCache[0].Image; - imageTemp.Dispose(); - imageTemp = null; - } - - // Remove the oldest item - imageCache.RemoveAt(0); - } - - // Remove song from list - int indexRemove = -1; - for (int a = 0; a < workerUpdateAlbumArtPile.Count; a++) - { - if (workerUpdateAlbumArtPile[a].AudioFile.FilePath.ToUpper() == result.AudioFile.FilePath.ToUpper()) - { - indexRemove = a; - } - } - if (indexRemove >= 0) - { - workerUpdateAlbumArtPile.RemoveAt(indexRemove); - } - - // Refresh control (TODO: Invalidate instead) - Refresh(); - } - - /// - /// Clears the currently selected items. - /// - public void ClearSelectedItems() - { - // Loop through items - foreach (SongGridViewItem item in items) - { - // Check if item is selected - if (item.IsSelected) - { - // Unselect item - item.IsSelected = false; - } - } - - // Refresh control - Refresh(); - } - - /// - /// Imports audio files as SongGridViewItems. - /// - /// List of AudioFiles - public void ImportAudioFiles(List audioFiles) - { - // Set mode - mode = SongGridViewMode.AudioFile; - - // Create list of items - items = new List(); - foreach (AudioFile audioFile in audioFiles) - { - // Create item and add to list - SongGridViewItem item = new SongGridViewItem(); - item.AudioFile = audioFile; - item.PlaylistItemId = Guid.NewGuid(); - items.Add(item); - } - - // Reset scrollbar position - vScrollBar.Value = 0; - songCache = null; - - // Refresh control - Refresh(); - } - - /// - /// Imports playlist items as SongGridViewItems. - /// - /// Playlist - public void ImportPlaylist(Playlist playlist) - { - // Set mode - mode = SongGridViewMode.Playlist; - - // Create list of items - items = new List(); - foreach (PlaylistItem playlistItem in playlist.Items) - { - // Create item and add to list - SongGridViewItem item = new SongGridViewItem(); - item.AudioFile = playlistItem.AudioFile; - item.PlaylistItemId = playlistItem.Id; - items.Add(item); - } - - // Reset scrollbar position - vScrollBar.Value = 0; - songCache = null; - - // Refresh control - Refresh(); - } - - #endregion - - #region Paint Events - - /// - /// Update a specific line (if visible) by its audio file unique identifier. - /// - /// Audio file unique identifier - public void UpdateAudioFileLine(Guid audioFileId) - { - // Find the position of the line - for (int a = startLineNumber; a < startLineNumber + numberOfLinesToDraw; a++) - { - // Calculate offset - int offsetY = (a * songCache.LineHeight) - vScrollBar.Value + songCache.LineHeight; - - // Check if the line matches - if (items[a].AudioFile.Id == audioFileId) - { - // Invalidate this line - Invalidate(new Rectangle(columns[0].Width - hScrollBar.Value, offsetY, ClientRectangle.Width - columns[0].Width + hScrollBar.Value, songCache.LineHeight)); - - // Update and exit loop - Update(); - break; - } - } - } - - /// - /// Occurs when the kernel sends message to the control. - /// Intercepts WM_ERASEBKGND and cancels the message to prevent flickering. - /// - /// - protected override void OnNotifyMessage(Message m) - { - // Do not let WM_ERASEBKGND pass (prevent flickering) - if (m.Msg != 0x14) - { - base.OnNotifyMessage(m); - } - } - - /// - /// Occurs when the control needs to be painted. - /// - /// Paint Event arguments - protected override void OnPaint(PaintEventArgs e) - { - // Check if the library is valid - if (items == null) - { - return; - } - - // Draw bitmap for control - Graphics g = e.Graphics; - - // Set text anti-aliasing to ClearType (best looking AA) - g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; - - // Set smoothing mode for paths - g.SmoothingMode = SmoothingMode.AntiAlias; - - // Paint songs - PaintSongs(ref g); - - base.OnPaint(e); - } - - /// - /// Paints a grid view displaying a list of songs. - /// - /// Graphics object to write to - public void PaintSongs(ref Graphics g) - { - // Declare variables - Rectangle rect = new Rectangle(); - RectangleF rectF = new RectangleF(); - Pen pen = null; - SolidBrush brush = null; - LinearGradientBrush brushGradient = null; - Color colorNowPlaying1 = theme.RowNowPlayingTextGradient.Color1; - Color colorNowPlaying2 = theme.RowNowPlayingTextGradient.Color2; - int offsetX = 0; - int offsetY = 0; - int albumCoverStartIndex = 0; - int albumCoverEndIndex = 0; - string currentAlbumTitle = string.Empty; - //bool regenerateItems = true; - bool nowPlayingSongFound = false; - - //// Load custom font - //Font fontDefault = Tools.LoadCustomFont(FontCollection, CustomFontName, Font.Size, FontStyle.Regular); - //Font fontDefaultBold = Tools.LoadCustomFont(FontCollection, CustomFontName, Font.Size, FontStyle.Bold); - Font fontDefault = null; - Font fontDefaultBold = null; - - // Make sure the embedded font name needs to be loaded and is valid - if (theme.RowTextGradient.Font.UseEmbeddedFont && !String.IsNullOrEmpty(theme.RowTextGradient.Font.EmbeddedFontName)) - { - try - { - // Get embedded fonts - fontDefault = Tools.LoadEmbeddedFont(embeddedFonts, theme.RowTextGradient.Font.EmbeddedFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle()); - fontDefaultBold = Tools.LoadEmbeddedFont(embeddedFonts, theme.RowTextGradient.Font.EmbeddedFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); - } - catch - { - // Use default font instead - fontDefault = this.Font; - fontDefaultBold = new Font(this.Font, FontStyle.Bold); - } - } - - // Check if font is null - if (fontDefault == null) - { - try - { - // Try to get standard font - fontDefault = new Font(theme.RowTextGradient.Font.StandardFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle()); - fontDefaultBold = new Font(theme.RowTextGradient.Font.StandardFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); - } - catch - { - // Use default font instead - fontDefault = this.Font; - fontDefaultBold = new Font(this.Font, FontStyle.Bold); - } - } - - // Set string format - StringFormat stringFormat = new StringFormat(); - - // If there are no items.., - if (items == null) - { - // Do not do anything. - return; - } - - // Check if a cache exists, or if the cache needs to be refreshed - if (songCache == null) - { - // Create song cache - InvalidateSongCache(); - } - - // Calculate how many lines must be skipped because of the scrollbar position - startLineNumber = (int)Math.Floor((double)vScrollBar.Value / (double)(songCache.LineHeight)); - - // Check if the total number of lines exceeds the number of icons fitting in height - numberOfLinesToDraw = 0; - if (startLineNumber + songCache.NumberOfLinesFittingInControl > items.Count) - { - // There aren't enough lines to fill the screen - numberOfLinesToDraw = items.Count - startLineNumber; - } - else - { - // Fill up screen - numberOfLinesToDraw = songCache.NumberOfLinesFittingInControl; - } - - // Add one line for overflow; however, make sure we aren't adding a line without content - if (startLineNumber + numberOfLinesToDraw + 1 <= items.Count) - { - // Add one line for overflow - numberOfLinesToDraw++; - } - - // Loop through lines - for (int a = startLineNumber; a < startLineNumber + numberOfLinesToDraw; a++) - { - // Get audio file - AudioFile audioFile = items[a].AudioFile; - - // Calculate Y offset (compensate for scrollbar position) - offsetY = (a * songCache.LineHeight) - vScrollBar.Value + songCache.LineHeight; - - // Calculate album art cover column width - int albumArtColumnWidth = columns[0].Visible ? columns[0].Width : 0; - - // Calculate line background width - int lineBackgroundWidth = ClientRectangle.Width + hScrollBar.Value - albumArtColumnWidth; - - // Reduce width from scrollbar if visible - if (vScrollBar.Visible) - { - lineBackgroundWidth -= vScrollBar.Width; - } - - // Set rectangle - Rectangle rectBackground = new Rectangle(albumArtColumnWidth - hScrollBar.Value, offsetY, lineBackgroundWidth, songCache.LineHeight); - - // Set default line background color - Color colorBackground1 = theme.RowTextGradient.Color1; - Color colorBackground2 = theme.RowTextGradient.Color2; - - // Check conditions to determine background color - if ((mode == SongGridViewMode.AudioFile && audioFile.Id == nowPlayingAudioFileId) || - (mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == nowPlayingPlaylistItemId)) - { - // Set color - colorBackground1 = theme.RowNowPlayingTextGradient.Color1; - colorBackground2 = theme.RowNowPlayingTextGradient.Color2; - } - - // Check if item is selected - if (items[a].IsSelected) - { - // Use darker color - int diff = 40; - colorBackground1 = Color.FromArgb(255, - (colorBackground1.R - diff < 0) ? 0 : colorBackground1.R - diff, - (colorBackground1.G - diff < 0) ? 0 : colorBackground1.G - diff, - (colorBackground1.B - diff < 0) ? 0 : colorBackground1.B - diff); - colorBackground2 = Color.FromArgb(255, - (colorBackground2.R - diff < 0) ? 0 : colorBackground2.R - diff, - (colorBackground2.G - diff < 0) ? 0 : colorBackground2.G - diff, - (colorBackground2.B - diff < 0) ? 0 : colorBackground2.B - diff); - } - - // Check if mouse is over item - if (items[a].IsMouseOverItem) - { - // Use lighter color - int diff = 20; - colorBackground1 = Color.FromArgb(255, - (colorBackground1.R + diff > 255) ? 255 : colorBackground1.R + diff, - (colorBackground1.G + diff > 255) ? 255 : colorBackground1.G + diff, - (colorBackground1.B + diff > 255) ? 255 : colorBackground1.B + diff); - colorBackground2 = Color.FromArgb(255, - (colorBackground2.R + diff > 255) ? 255 : colorBackground2.R + diff, - (colorBackground2.G + diff > 255) ? 255 : colorBackground2.G + diff, - (colorBackground2.B + diff > 255) ? 255 : colorBackground2.B + diff); - } - - // Check conditions to determine background color - if ((mode == SongGridViewMode.AudioFile && audioFile.Id == nowPlayingAudioFileId) || - (mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == nowPlayingPlaylistItemId)) - { - // Set color - colorNowPlaying1 = colorBackground1; - colorNowPlaying2 = colorBackground2; - } - - // Create gradient - brushGradient = new LinearGradientBrush(rectBackground, colorBackground1, colorBackground2, 90); - g.FillRectangle(brushGradient, rectBackground); - brushGradient.Dispose(); - brushGradient = null; - - // Loop through columns - offsetX = 0; - for (int b = 0; b < songCache.ActiveColumns.Count; b++) - { - // Get current column - SongGridViewColumn column = songCache.ActiveColumns[b]; - - // Check if the column is visible - if (column.Visible) - { - // Check if this is the "Now playing" column - if (column.Title == "Now Playing") - { - // Draw now playing icon - if ((mode == SongGridViewMode.AudioFile && audioFile.Id == nowPlayingAudioFileId) || - (mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == nowPlayingPlaylistItemId)) - { - // Which size is the minimum? Width or height? - int availableWidthHeight = column.Width - 4; - if (songCache.LineHeight <= column.Width) - { - availableWidthHeight = songCache.LineHeight - 4; - } - else - { - availableWidthHeight = column.Width - 4; - } - - // Calculate the icon position - float iconNowPlayingX = ((column.Width - availableWidthHeight) / 2) + offsetX - hScrollBar.Value; - float iconNowPlayingY = offsetY + ((songCache.LineHeight - availableWidthHeight) / 2); - - // Create NowPlaying rect (MUST be in integer) - rectNowPlaying = new Rectangle((int)iconNowPlayingX, (int)iconNowPlayingY, availableWidthHeight, availableWidthHeight); - nowPlayingSongFound = true; - - // Draw outer circle - brushGradient = new LinearGradientBrush(rectNowPlaying, Color.FromArgb(50, theme.IconNowPlayingGradient.Color1.R, theme.IconNowPlayingGradient.Color1.G, theme.IconNowPlayingGradient.Color1.B), theme.IconNowPlayingGradient.Color2, timerAnimationNowPlayingCount % 360); - g.FillEllipse(brushGradient, rectNowPlaying); - brushGradient.Dispose(); - brushGradient = null; - - // Draw inner circle - rect = new Rectangle((int)iconNowPlayingX + 4, (int)iconNowPlayingY + 4, availableWidthHeight - 8, availableWidthHeight - 8); - brush = new SolidBrush(colorNowPlaying1); - g.FillEllipse(brush, rect); - brush.Dispose(); - brush = null; - } - } - else if (column.Title == "Album Cover") - { - #region Album Cover Zone - - // Check for an album title change (or the last item of the grid) - if (currentAlbumTitle != audioFile.AlbumTitle) - { - // Set the new current album title - currentAlbumTitle = audioFile.AlbumTitle; - - // For displaying the album cover, we need to know how many songs of the same album are bundled together - // Start by getting the start index - for (int c = a; c > 0; c--) - { - // Get audio file - AudioFile previousAudioFile = items[c].AudioFile; - - // Check if the album title matches - if (previousAudioFile.AlbumTitle != audioFile.AlbumTitle) - { - // Set album cover start index (+1 because the last song was the sound found in the previous loop iteration) - albumCoverStartIndex = c + 1; - break; - } - } - // Find the end index - for (int c = a + 1; c < items.Count; c++) - { - // Get audio file - AudioFile nextAudioFile = items[c].AudioFile; - - // If the album title is different, this means we found the next album title - if (nextAudioFile.AlbumTitle != audioFile.AlbumTitle) - { - // Set album cover end index (-1 because the last song was the song found in the previous loop iteration) - albumCoverEndIndex = c - 1; - break; - } - // If this is the last item of the grid... - else if (c == items.Count - 1) - { - // Set album cover end index as the last item of the grid - albumCoverEndIndex = c; - break; - } - } - - // Calculate y and height - int scrollbarOffsetY = (startLineNumber * songCache.LineHeight) - vScrollBar.Value; - int y = ((albumCoverStartIndex - startLineNumber) * songCache.LineHeight) + songCache.LineHeight + scrollbarOffsetY; - - // Calculate the height of the album cover zone (+1 on end index because the array is zero-based) - int albumCoverZoneHeight = (albumCoverEndIndex + 1 - albumCoverStartIndex) * songCache.LineHeight; - - int heightWithPadding = albumCoverZoneHeight - (theme.Padding * 2); - if (heightWithPadding > songCache.ActiveColumns[0].Width - (theme.Padding * 2)) - { - heightWithPadding = songCache.ActiveColumns[0].Width - (theme.Padding * 2); - } - - // Make sure the height is at least zero (not necessary to draw anything!) - if (albumCoverZoneHeight > 0) - { - // Draw album cover background - Rectangle rectAlbumCover = new Rectangle(0 - hScrollBar.Value, y, songCache.ActiveColumns[0].Width, albumCoverZoneHeight); - brushGradient = new LinearGradientBrush(rectAlbumCover, theme.AlbumCoverBackgroundGradient.Color1, theme.AlbumCoverBackgroundGradient.Color2, theme.AlbumCoverBackgroundGradient.GradientMode); - g.FillRectangle(brushGradient, rectAlbumCover); - brushGradient.Dispose(); - brushGradient = null; - - // Try to extract image from cache - Image imageAlbumCover = null; - SongGridViewImageCache cachedImage = imageCache.FirstOrDefault(x => x.Key == audioFile.ArtistName + "_" + audioFile.AlbumTitle); - if (cachedImage != null) - { - // Set image - imageAlbumCover = cachedImage.Image; - } - - // Album art not found in cache; try to find an album cover in one of the file - if (cachedImage == null) - { - // Check if the album cover is already in the pile - bool albumCoverFound = false; - foreach (SongGridViewBackgroundWorkerArgument arg in workerUpdateAlbumArtPile) - { - // Match by file path - if (arg.AudioFile.FilePath.ToUpper() == audioFile.FilePath.ToUpper()) - { - // We found the album cover - albumCoverFound = true; - } - } - - // Add to the pile only if the album cover isn't already in it - if (!albumCoverFound) - { - // Add item to update album art worker pile - SongGridViewBackgroundWorkerArgument arg = new SongGridViewBackgroundWorkerArgument(); - arg.AudioFile = audioFile; - arg.LineIndex = a; - workerUpdateAlbumArtPile.Add(arg); - } - } - - // Measure available width for text - int widthAvailableForText = columns[0].Width - (theme.Padding * 2); - - // Display titles depending on if an album art was found - RectangleF rectAlbumCoverArt = new RectangleF(); - RectangleF rectAlbumTitleText = new RectangleF(); - RectangleF rectArtistNameText = new RectangleF(); - SizeF sizeAlbumTitle = new SizeF(); - SizeF sizeArtistName = new SizeF(); - bool useAlbumArtOverlay = false; - - // If there's only one line, we need to do place the artist name and album title on one line - if (albumCoverEndIndex - albumCoverStartIndex == 0) - { - // Set string format - stringFormat.Alignment = StringAlignment.Near; - stringFormat.Trimming = StringTrimming.EllipsisCharacter; - - // Measure strings - sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, widthAvailableForText, stringFormat); - sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, widthAvailableForText - (int)sizeArtistName.Width, stringFormat); - - // Display artist name at full width first, then album name - rectArtistNameText = new RectangleF(theme.Padding - hScrollBar.Value, y + (theme.Padding / 2), widthAvailableForText, songCache.LineHeight); - rectAlbumTitleText = new RectangleF(theme.Padding - hScrollBar.Value + sizeArtistName.Width + theme.Padding, y + (theme.Padding / 2), widthAvailableForText - sizeArtistName.Width, songCache.LineHeight); - } - else - { - // There are at least two lines; is there an album cover to display? - if (imageAlbumCover == null) - { - // There is no album cover to display; display only text. - // Set string format - stringFormat.Alignment = StringAlignment.Center; - stringFormat.Trimming = StringTrimming.EllipsisWord; - - // Measure strings - sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, widthAvailableForText, stringFormat); - sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, widthAvailableForText, stringFormat); - - // Display the album title at the top of the zome - rectArtistNameText = new RectangleF(theme.Padding - hScrollBar.Value, y + theme.Padding, widthAvailableForText, heightWithPadding); - rectAlbumTitleText = new RectangleF(theme.Padding - hScrollBar.Value, y + theme.Padding + sizeArtistName.Height, widthAvailableForText, heightWithPadding); - } - else - { - // There is an album cover to display with more than 2 lines. - // Set string format - stringFormat.Alignment = StringAlignment.Near; - stringFormat.Trimming = StringTrimming.EllipsisWord; - - // Measure strings - sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, widthAvailableForText, stringFormat); - sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, widthAvailableForText, stringFormat); - - // If there's only two lines, display text on only two lines - if (albumCoverEndIndex - albumCoverStartIndex == 1) - { - // Display artist name on first line; display album title on second line - rectArtistNameText = new RectangleF(((columns[0].Width - sizeArtistName.Width) / 2) - hScrollBar.Value, y, widthAvailableForText, heightWithPadding); - rectAlbumTitleText = new RectangleF(((columns[0].Width - sizeAlbumTitle.Width) / 2) - hScrollBar.Value, y + songCache.LineHeight, widthAvailableForText, heightWithPadding); - } - // There is an album cover to display; between 2 and 6 lines AND the width of the column is at least 50 pixels (or - // it will try to display text in a too thin area) - else if (albumCoverEndIndex - albumCoverStartIndex <= 5 && columns[0].Width > 175) - { - // There is no album cover to display; display only text. - // Set string format - stringFormat.Alignment = StringAlignment.Near; - stringFormat.Trimming = StringTrimming.EllipsisWord; - - float widthRemainingForText = columns[0].Width - theme.Padding - heightWithPadding; - - // Measure strings - sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, new SizeF(widthRemainingForText, heightWithPadding), stringFormat); - sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, new SizeF(widthRemainingForText, heightWithPadding), stringFormat); - - // Try to center the cover art + padding + max text width - float maxWidth = sizeArtistName.Width; - if (sizeAlbumTitle.Width > maxWidth) - { - // Set new maximal width - maxWidth = sizeAlbumTitle.Width; - } - - useAlbumArtOverlay = true; - - float albumCoverX = (columns[0].Width - heightWithPadding - theme.Padding - theme.Padding - maxWidth) / 2; - float artistNameY = (albumCoverZoneHeight - sizeArtistName.Height - sizeAlbumTitle.Height) / 2; - - // Display the album title at the top of the zome - rectArtistNameText = new RectangleF(albumCoverX + heightWithPadding + theme.Padding - hScrollBar.Value, y + artistNameY, widthRemainingForText, heightWithPadding); - rectAlbumTitleText = new RectangleF(albumCoverX + heightWithPadding + theme.Padding - hScrollBar.Value, y + artistNameY + sizeArtistName.Height, widthRemainingForText, heightWithPadding); - - // Set cover art rectangle - rectAlbumCoverArt = new RectangleF(albumCoverX - hScrollBar.Value, y + theme.Padding, heightWithPadding, heightWithPadding); - } - // 7 and more lines - else - { - // Display artist name at the top of the album cover; display album title at the bottom of the album cover - rectArtistNameText = new RectangleF(((columns[0].Width - sizeArtistName.Width) / 2) - hScrollBar.Value, y + (theme.Padding * 2), widthAvailableForText, heightWithPadding); - rectAlbumTitleText = new RectangleF(((columns[0].Width - sizeAlbumTitle.Width) / 2) - hScrollBar.Value, y + heightWithPadding - sizeAlbumTitle.Height, widthAvailableForText, heightWithPadding); - - // Draw background overlay behind text - useAlbumArtOverlay = true; - - // Try to horizontally center the album cover if it's not taking the whole width (less padding) - float albumCoverX = theme.Padding; - if (columns[0].Width > heightWithPadding) - { - // Get position - albumCoverX = ((float)(columns[0].Width - heightWithPadding) / 2) - hScrollBar.Value; - } - - // Set cover art rectangle - rectAlbumCoverArt = new RectangleF(albumCoverX, y + theme.Padding, heightWithPadding, heightWithPadding); - } - - } - } - - // Display album cover - if (imageAlbumCover != null) - { - // Draw album cover - g.DrawImage(imageAlbumCover, rectAlbumCoverArt); - } - - if (useAlbumArtOverlay) - { - // Draw artist name and album title background - RectangleF rectArtistNameBackground = new RectangleF(rectArtistNameText.X - (theme.Padding / 2), rectArtistNameText.Y - (theme.Padding / 2), sizeArtistName.Width + theme.Padding, sizeArtistName.Height + theme.Padding); - RectangleF rectAlbumTitleBackground = new RectangleF(rectAlbumTitleText.X - (theme.Padding / 2), rectAlbumTitleText.Y - (theme.Padding / 2), sizeAlbumTitle.Width + theme.Padding, sizeAlbumTitle.Height + theme.Padding); - brush = new SolidBrush(Color.FromArgb(190, 0, 0, 0)); - g.FillRectangle(brush, rectArtistNameBackground); - g.FillRectangle(brush, rectAlbumTitleBackground); - brush.Dispose(); - brush = null; - } - - // Check if this is the artist name column (set font to bold) - g.DrawString(audioFile.ArtistName, fontDefaultBold, Brushes.White, rectArtistNameText, stringFormat); - g.DrawString(currentAlbumTitle, fontDefault, Brushes.White, rectAlbumTitleText, stringFormat); - - // Draw horizontal line to distinguish albums - // Part 1: Draw line over grid - pen = new Pen(theme.AlbumCoverBackgroundGradient.Color1); - g.DrawLine(pen, new Point(columns[0].Width, y), new Point(ClientRectangle.Width, y)); - pen.Dispose(); - pen = null; - - // Part 2: Draw line over album art zone, in a lighter color - pen = new Pen(Color.FromArgb(115, 115, 115)); - g.DrawLine(pen, new Point(0, y), new Point(columns[0].Width, y)); - pen.Dispose(); - pen = null; - } - } - - #endregion - } - else - { - // Get property through reflection - PropertyInfo propertyInfo = audioFile.GetType().GetProperty(column.FieldName); - if (propertyInfo != null) - { - // Get property value - string value = string.Empty; - try - { - // Determine the type of value - if (propertyInfo.PropertyType.FullName == "System.String") - { - // Try to get the value - value = propertyInfo.GetValue(audioFile, null).ToString(); - } - // Nullable Int64 - else if (propertyInfo.PropertyType.FullName.Contains("Int64") && - propertyInfo.PropertyType.FullName.Contains("Nullable")) - { - // Try to get the value - long? longValue = (long?)propertyInfo.GetValue(audioFile, null); - - // Check if null - if (longValue.HasValue) - { - // Render to string - value = longValue.Value.ToString(); - } - } - // Nullable DateTime - else if (propertyInfo.PropertyType.FullName.Contains("DateTime") && - propertyInfo.PropertyType.FullName.Contains("Nullable")) - { - // Try to get the value - DateTime? dateTimeValue = (DateTime?)propertyInfo.GetValue(audioFile, null); - - // Check if null - if (dateTimeValue.HasValue) - { - // Render to string - value = dateTimeValue.Value.ToShortDateString() + " " + dateTimeValue.Value.ToShortTimeString(); - } - } - else if (propertyInfo.PropertyType.FullName.Contains("System.UInt32")) - { - // Try to get the value - uint uintValue = (uint)propertyInfo.GetValue(audioFile, null); - - // Render to string - value = uintValue.ToString(); - } - else if (propertyInfo.PropertyType.FullName.Contains("System.Int32")) - { - // Try to get the value - int intValue = (int)propertyInfo.GetValue(audioFile, null); - - // Render to string - value = intValue.ToString(); - } - else if (propertyInfo.PropertyType.FullName.Contains("MPfm.Sound.AudioFileFormat")) - { - // Try to get the value - AudioFileFormat theValue = (AudioFileFormat)propertyInfo.GetValue(audioFile, null); - - // Render to string - value = theValue.ToString(); - } - else - { - // If the type of unknown, leave the value empty - } - } - catch - { - // Do nothing - } - - // The last column always take the remaining width - int columnWidth = column.Width; - if (b == songCache.ActiveColumns.Count - 1) - { - // Calculate the remaining width - int columnsWidth = 0; - for (int c = 0; c < songCache.ActiveColumns.Count - 1; c++) - { - columnsWidth += songCache.ActiveColumns[c].Width; - } - columnWidth = ClientRectangle.Width - columnsWidth + hScrollBar.Value; - } - - // Display text - rect = new Rectangle(offsetX - hScrollBar.Value, offsetY + (theme.Padding / 2), songCache.ActiveColumns[b].Width, songCache.LineHeight - theme.Padding + 2); - stringFormat.Trimming = StringTrimming.EllipsisCharacter; - stringFormat.Alignment = StringAlignment.Near; - - // Check if this is the artist name column - brush = new SolidBrush(theme.RowTextGradient.Font.Color); - if (column.FieldName == "ArtistName" || column.FieldName == "DiscTrackNumber") - { - // Use bold for artist name - g.DrawString(value, fontDefaultBold, brush, rect, stringFormat); - } - else - { - // Use default font for the other columns - g.DrawString(value, fontDefault, brush, rect, stringFormat); - } - brush.Dispose(); - brush = null; - } - } - - // Increment offset by the column width - offsetX += column.Width; - } - } - } - - // If no songs are playing... - if (!nowPlayingSongFound) - { - // Set the current now playing rectangle as "empty" - rectNowPlaying = new Rectangle(0, 0, 1, 1); - } - - // Draw header (for some reason, the Y must be set -1 to cover an area which isn't supposed to be displayed) - Rectangle rectBackgroundHeader = new Rectangle(0, -1, ClientRectangle.Width, songCache.LineHeight + 1); - brushGradient = new LinearGradientBrush(rectBackgroundHeader, theme.HeaderTextGradient.Color1, theme.HeaderTextGradient.Color2, 90); - g.FillRectangle(brushGradient, rectBackgroundHeader); - brushGradient.Dispose(); - brushGradient = null; - - // Loop through columns - offsetX = 0; - for (int b = 0; b < songCache.ActiveColumns.Count; b++) - { - // Get current column - SongGridViewColumn column = songCache.ActiveColumns[b]; - - // Check if the column is visible - if (column.Visible) - { - // The last column always take the remaining width - int columnWidth = column.Width; - if (b == songCache.ActiveColumns.Count - 1) - { - // Calculate the remaining width - int columnsWidth = 0; - for (int c = 0; c < songCache.ActiveColumns.Count - 1; c++) - { - columnsWidth += songCache.ActiveColumns[c].Width; - } - columnWidth = ClientRectangle.Width - columnsWidth + hScrollBar.Value; - } - - // Check if mouse is over this column header - if (column.IsMouseOverColumnHeader) - { - // Draw header (for some reason, the Y must be set -1 to cover an area which isn't supposed to be displayed) - rect = new Rectangle(offsetX - hScrollBar.Value, -1, column.Width, songCache.LineHeight + 1); - brushGradient = new LinearGradientBrush(rect, theme.HeaderHoverTextGradient.Color1, theme.HeaderHoverTextGradient.Color2, 90); - g.FillRectangle(brushGradient, rect); - brushGradient.Dispose(); - brushGradient = null; - } - else if (column.IsUserMovingColumn) - { - // Draw header (for some reason, the Y must be set -1 to cover an area which isn't supposed to be displayed) - rect = new Rectangle(offsetX - hScrollBar.Value, -1, column.Width, songCache.LineHeight + 1); - brushGradient = new LinearGradientBrush(rect, Color.Blue, Color.Green, 90); - g.FillRectangle(brushGradient, rect); - brushGradient.Dispose(); - brushGradient = null; - } - - // Check if the header title must be displayed - if (songCache.ActiveColumns[b].IsHeaderTitleVisible) - { - // Display title - Rectangle rectTitle = new Rectangle(offsetX - hScrollBar.Value, theme.Padding / 2, column.Width, songCache.LineHeight - theme.Padding + 2); - stringFormat.Trimming = StringTrimming.EllipsisCharacter; - brush = new SolidBrush(theme.HeaderTextGradient.Font.Color); - g.DrawString(column.Title, fontDefaultBold, brush, rectTitle, stringFormat); - brush.Dispose(); - brush = null; - } - - // Draw column separator line; determine the height of the line - int columnHeight = ClientRectangle.Height; - - // Determine the height of the line; if the items don't fit the control height... - if (items.Count < songCache.NumberOfLinesFittingInControl) - { - // Set height as the number of items (plus header) - columnHeight = (items.Count + 1) * songCache.LineHeight; - } - - // Draw line - g.DrawLine(Pens.DarkGray, new Point(offsetX + column.Width - hScrollBar.Value, 0), new Point(offsetX + column.Width - hScrollBar.Value, columnHeight)); - - // Check if the column is ordered by - if (column.FieldName == orderByFieldName && !String.IsNullOrEmpty(column.FieldName)) - { - // Create triangle points,,, - PointF[] ptTriangle = new PointF[3]; - - // ... depending on the order by ascending value - int triangleWidthHeight = 8; - int trianglePadding = (songCache.LineHeight - triangleWidthHeight) / 2; - if (orderByAscending) - { - // Create points for ascending - ptTriangle[0] = new PointF(offsetX + column.Width - triangleWidthHeight - (triangleWidthHeight / 2) - hScrollBar.Value, trianglePadding); - ptTriangle[1] = new PointF(offsetX + column.Width - triangleWidthHeight - hScrollBar.Value, songCache.LineHeight - trianglePadding); - ptTriangle[2] = new PointF(offsetX + column.Width - triangleWidthHeight - triangleWidthHeight - hScrollBar.Value, songCache.LineHeight - trianglePadding); - } - else - { - // Create points for descending - ptTriangle[0] = new PointF(offsetX + column.Width - triangleWidthHeight - (triangleWidthHeight / 2) - hScrollBar.Value, songCache.LineHeight - trianglePadding); - ptTriangle[1] = new PointF(offsetX + column.Width - triangleWidthHeight - hScrollBar.Value, trianglePadding); - ptTriangle[2] = new PointF(offsetX + column.Width - triangleWidthHeight - triangleWidthHeight - hScrollBar.Value, trianglePadding); - } - - // Draw triangle - pen = new Pen(theme.HeaderTextGradient.Font.Color); - g.DrawPolygon(pen, ptTriangle); - pen.Dispose(); - pen = null; - } - - // Increment offset by the column width - offsetX += column.Width; - } - } - - // Display column move marker - if (IsColumnMoving) - { - // Draw marker - pen = new Pen(Color.Red); - g.DrawRectangle(pen, new Rectangle(columnMoveMarkerX - hScrollBar.Value, 0, 1, ClientRectangle.Height)); - pen.Dispose(); - pen = null; - } - - // Display debug information if enabled - if (displayDebugInformation) - { - // Build debug string - StringBuilder sbDebug = new StringBuilder(); - sbDebug.AppendLine("Line Count: " + items.Count.ToString()); - sbDebug.AppendLine("Line Height: " + songCache.LineHeight.ToString()); - sbDebug.AppendLine("Lines Fit In Height: " + songCache.NumberOfLinesFittingInControl.ToString()); - sbDebug.AppendLine("Total Width: " + songCache.TotalWidth); - sbDebug.AppendLine("Total Height: " + songCache.TotalHeight); - sbDebug.AppendLine("Scrollbar Offset Y: " + songCache.ScrollBarOffsetY); - sbDebug.AppendLine("HScrollbar Maximum: " + hScrollBar.Maximum.ToString()); - sbDebug.AppendLine("HScrollbar LargeChange: " + hScrollBar.LargeChange.ToString()); - sbDebug.AppendLine("HScrollbar Value: " + hScrollBar.Value.ToString()); - sbDebug.AppendLine("VScrollbar Maximum: " + vScrollBar.Maximum.ToString()); - sbDebug.AppendLine("VScrollbar LargeChange: " + vScrollBar.LargeChange.ToString()); - sbDebug.AppendLine("VScrollbar Value: " + vScrollBar.Value.ToString()); - - // Measure string - stringFormat.Trimming = StringTrimming.Word; - stringFormat.LineAlignment = StringAlignment.Near; - SizeF sizeDebugText = g.MeasureString(sbDebug.ToString(), fontDefault, columns[0].Width - 1, stringFormat); - rectF = new RectangleF(0, 0, columns[0].Width - 1, sizeDebugText.Height); - - // Draw background - brush = new SolidBrush(Color.FromArgb(200, 0, 0, 0)); - g.FillRectangle(brush, rectF); - brush.Dispose(); - brush = null; - - // Draw string - g.DrawString(sbDebug.ToString(), fontDefault, Brushes.White, rectF, stringFormat); - } - - // If both scrollbars are visible... - if (hScrollBar.Visible && vScrollBar.Visible) - { - // Draw a bit of control color over the 16x16 area in the lower right corner - brush = new SolidBrush(SystemColors.Control); - g.FillRectangle(brush, new Rectangle(ClientRectangle.Width - 16, ClientRectangle.Height - 16, 16, 16)); - brush.Dispose(); - brush = null; - } - } - - #endregion - - #region Other Events - - /// - /// Occurs when the control is resized. - /// Invalidates the cache. - /// - /// Event arguments - protected override void OnResize(EventArgs e) - { - // Bug when putting window in maximized mode: black area at the bottom - // it's because the maximum value is set later in OnPaint - // If the scrollY value is - if (vScrollBar.Maximum - vScrollBar.LargeChange < vScrollBar.Value) - { - // Set new scrollbar value - vScrollBar.Value = vScrollBar.Maximum - vScrollBar.LargeChange + 1; - } - - // Set horizontal scrollbar width and position - hScrollBar.Top = ClientRectangle.Height - hScrollBar.Height; - - // Invalidate cache - InvalidateSongCache(); - - base.OnResize(e); - } - - /// - /// Occurs when the user changes the horizontal scrollbar value. - /// - /// Event sender - /// Event arguments - protected void hScrollBar_Scroll(object sender, ScrollEventArgs e) - { - // Redraw control - Refresh(); - } - - /// - /// Occurs when the user changes the vertical scrollbar value. - /// - /// Event sender - /// Event arguments - protected void vScrollBar_Scroll(object sender, ScrollEventArgs e) - { - // Redraw control - Refresh(); - } - - #endregion - - #region Mouse Events - - /// - /// Occurs when the mouse cursor enters on the control. - /// - /// Event arguments - protected override void OnMouseEnter(EventArgs e) - { - // Set flags - isMouseOverControl = true; - - base.OnMouseEnter(e); - } - - /// - /// Occurs when the mouse cursor leaves the control. - /// - /// Event arguments - protected override void OnMouseLeave(EventArgs e) - { - // Set flags - bool controlNeedsToBeUpdated = false; - isMouseOverControl = false; - - // Check if all the data is valid - if (columns == null || songCache == null) - { - return; - } - - // Calculate scrollbar offset Y - int scrollbarOffsetY = (startLineNumber * songCache.LineHeight) - vScrollBar.Value; - - // Check if there's at least one item - if (items.Count > 0) - { - // Reset mouse over item flags - for (int b = startLineNumber; b < startLineNumber + numberOfLinesToDraw; b++) - { - // Check if the mouse was over this item - if (items[b].IsMouseOverItem) - { - // Reset flag and invalidate region - items[b].IsMouseOverItem = false; - Invalidate(new Rectangle(columns[0].Width - hScrollBar.Value, ((b - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - columns[0].Width + hScrollBar.Value, songCache.LineHeight)); - controlNeedsToBeUpdated = true; - - // Exit loop - break; - } - } - } - - // Reset column flags - int columnOffsetX2 = 0; - for (int b = 0; b < songCache.ActiveColumns.Count; b++) - { - // Make sure column is visible - if (songCache.ActiveColumns[b].Visible) - { - // Was mouse over this column header? - if (songCache.ActiveColumns[b].IsMouseOverColumnHeader) - { - // Reset flag - songCache.ActiveColumns[b].IsMouseOverColumnHeader = false; - - // Invalidate region - Invalidate(new Rectangle(columnOffsetX2 - hScrollBar.Value, 0, songCache.ActiveColumns[b].Width, songCache.LineHeight)); - controlNeedsToBeUpdated = true; - } - - // Increment offset - columnOffsetX2 += songCache.ActiveColumns[b].Width; - } - } - - // Check if control needs to be updated - if (controlNeedsToBeUpdated) - { - // Update control - Update(); - } - - base.OnMouseLeave(e); - } - - /// - /// Occurs when the user is pressing down a mouse button. - /// - /// Event arguments - protected override void OnMouseDown(MouseEventArgs e) - { - // Set flags - dragStartX = e.X; - - // Check if all the data is valid - if (columns == null || songCache == null) - { - return; - } - - // Loop through columns - foreach (SongGridViewColumn column in songCache.ActiveColumns) - { - // Check for resizing column - if (column.IsMouseCursorOverColumnLimit && column.CanBeResized && CanResizeColumns) - { - // Set resizing column flag - column.IsUserResizingColumn = true; - - // Save the original column width - dragOriginalColumnWidth = column.Width; - } - } - - // Check if the left mouse button is held - if(e.Button == System.Windows.Forms.MouseButtons.Left) - { - // Set holding button flag - isUserHoldingLeftMouseButton = true; - } - - base.OnMouseDown(e); - } - - /// - /// Occurs when the user releases a mouse button. - /// - /// Event arguments - protected override void OnMouseUp(MouseEventArgs e) - { - // Reset flags - dragStartX = -1; - bool updateControl = false; - isUserHoldingLeftMouseButton = false; - - // Check if all the data is valid - if (columns == null || songCache == null) - { - return; - } - - // Loop through columns - SongGridViewColumn columnMoving = null; - foreach (SongGridViewColumn column in songCache.ActiveColumns) - { - // Reset flags - column.IsUserResizingColumn = false; - - // Check if this column is moving - if(column.IsUserMovingColumn) - { - // Set column - columnMoving = column; - } - } - - // Check if the user is moving a column - if (columnMoving != null) - { - // Set flag - columnMoving.IsUserMovingColumn = false; - updateControl = true; - - // Find out on what column the mouse cursor is - SongGridViewColumn columnOver = null; - int x = 0; - bool isPastCurrentlyMovingColumn = false; - for (int a = 0; a < songCache.ActiveColumns.Count; a++ ) - { - // Set current column - SongGridViewColumn currentColumn = songCache.ActiveColumns[a]; - - // Check if current column - if (currentColumn.FieldName == columnMoving.FieldName) - { - // Set flag - isPastCurrentlyMovingColumn = true; - } - - // Check if the column is visible - if (currentColumn.Visible) - { - // Check if the cursor is over the left part of the column - if (e.X >= x - hScrollBar.Value && e.X <= x + (currentColumn.Width / 2) - hScrollBar.Value) - { - // Check flag - if (isPastCurrentlyMovingColumn && currentColumn.FieldName != columnMoving.FieldName) - { - // Set column - columnOver = songCache.ActiveColumns[a - 1]; - } - else - { - // Set column - columnOver = songCache.ActiveColumns[a]; - } - break; - } - // Check if the cursor is over the right part of the column - else if (e.X >= x + (currentColumn.Width / 2) - hScrollBar.Value && e.X <= x + currentColumn.Width - hScrollBar.Value) - { - // Check if there is a next item - if (a < songCache.ActiveColumns.Count - 1) - { - // Check flag - if (isPastCurrentlyMovingColumn) - { - // Set column - columnOver = songCache.ActiveColumns[a]; - } - else - { - // Set column - columnOver = songCache.ActiveColumns[a + 1]; - } - } - - break; - } - - // Increment x - x += currentColumn.Width; - } - } - - //// Check if the column was found (the cursor might be past the last column - //if (columnOver == null) - //{ - // return; - //} - - // Order columns by their current order - List columnsOrdered = columns.OrderBy(q => q.Order).ToList(); - - // Move column - int indexRemove = -1; - int indexAdd = -1; - for (int a = 0; a < columnsOrdered.Count; a++) - { - // Find the moving column index - if (columnsOrdered[a].FieldName == columnMoving.FieldName) - { - // Set index - indexRemove = a; - } - - // Find the column index with the mouse over - if(columnOver != null && columnsOrdered[a].FieldName == columnOver.FieldName) - { - // Set index - indexAdd = a; - } - } - - // Remove column - columnsOrdered.RemoveAt(indexRemove); - - // Check if the index is -1 - if (indexAdd == -1) - { - // Add column to the end - columnsOrdered.Insert(columnsOrdered.Count, columnMoving); - } - else - { - // Add column to the new position - columnsOrdered.Insert(indexAdd, columnMoving); - } - - // Loop through columns to change the order - for (int a = 0; a < columnsOrdered.Count; a++) - { - // Set order - columnsOrdered[a].Order = a; - } - } - - // Check if the control needs to be updated - if (updateControl) - { - // Invalidate control and refresh - InvalidateSongCache(); - Invalidate(); - Update(); - } - - base.OnMouseUp(e); - } - - /// - /// Occurs when the user clicks on the control. - /// Selects or unselects items, reorders columns. - /// - /// Event arguments - protected override void OnMouseClick(MouseEventArgs e) - { - // Check if all the data is valid - if (columns == null || songCache == null) - { - return; - } - - // Calculate album cover art width - int albumArtCoverWidth = columns[0].Visible ? columns[0].Width : 0; - - // Make sure the control is focused - if (!Focused) - { - // Set control focus - Focus(); - } - - // Show context menu strip if the button click is right and not the album art column - if (e.Button == System.Windows.Forms.MouseButtons.Right && - e.X > columns[0].Width) - { - // Is there a context menu strip configured? - if (contextMenuStrip != null) - { - // Show context menu strip - contextMenuStrip.Show(Control.MousePosition.X, Control.MousePosition.Y); - } - } - - // Check if the user is resizing a column - SongGridViewColumn columnResizing = columns.FirstOrDefault(x => x.IsUserResizingColumn == true); - - // Calculate scrollbar offset Y - int scrollbarOffsetY = (startLineNumber * songCache.LineHeight) - vScrollBar.Value; - - // Check if the user has clicked on the header (for orderBy) - if (e.Y >= 0 && - e.Y <= songCache.LineHeight && - columnResizing == null && - !IsColumnMoving) - { - // Check on what column the user has clicked - int offsetX = 0; - for (int a = 0; a < songCache.ActiveColumns.Count; a++) - { - // Get current column - SongGridViewColumn column = songCache.ActiveColumns[a]; - - // Make sure column is visible - if (column.Visible) - { - // Check if the mouse pointer is over this column - if (e.X >= offsetX - hScrollBar.Value && e.X <= offsetX + column.Width - hScrollBar.Value) - { - // Check mouse button - if (e.Button == System.Windows.Forms.MouseButtons.Left && CanChangeOrderBy) - { - // Check if the column order was already set - if (orderByFieldName == column.FieldName) - { - // Reverse ascending/descending - orderByAscending = !orderByAscending; - } - else - { - // Set order by field name - orderByFieldName = column.FieldName; - - // By default, the order is ascending - orderByAscending = true; - } - - // Invalidate cache and song items - items = null; - songCache = null; - - // Raise column click event (if an event is subscribed) - if (OnColumnClick != null) - { - // Create data - SongGridViewColumnClickData data = new SongGridViewColumnClickData(); - data.ColumnIndex = a; - - // Raise event - OnColumnClick(data); - } - - // Refresh control - Refresh(); - return; - } - else if (e.Button == System.Windows.Forms.MouseButtons.Right) - { - // Refresh column visibility in menu before opening - foreach(ToolStripMenuItem menuItem in menuColumns.Items) - { - // Get column - SongGridViewColumn menuItemColumn = columns.FirstOrDefault(x => x.Title == menuItem.Tag.ToString()); - if (menuItemColumn != null) - { - menuItem.Checked = menuItemColumn.Visible; - } - } - - // Display columns contextual menu - menuColumns.Show(this, e.X, e.Y); - } - } - - // Increment X offset - offsetX += column.Width; - } - } - } - - // Loop through visible lines to find the original selected items - int startIndex = -1; - int endIndex = -1; - for (int a = startLineNumber; a < startLineNumber + numberOfLinesToDraw; a++) - { - // Check if the item is selected - if (items[a].IsSelected) - { - // Check if the start index was set - if (startIndex == -1) - { - // Set start index - startIndex = a; - } - // Check if the end index is set or if it needs to be updated - if (endIndex == -1 || endIndex < a) - { - // Set end index - endIndex = a; - } - } - } - - // Make sure the indexes are set - if (startIndex > -1 && endIndex > -1) - { - // Invalidate the original selected lines - int startY = ((startIndex - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY; - int endY = ((endIndex - startLineNumber + 2) * songCache.LineHeight) + scrollbarOffsetY; - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, startY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, endY - startY)); - } - - // Reset selection (make sure SHIFT or CTRL isn't held down) - if ((Control.ModifierKeys & Keys.Shift) == 0 && - (Control.ModifierKeys & Keys.Control) == 0) - { - SongGridViewItem mouseOverItem = items.FirstOrDefault(x => x.IsMouseOverItem == true); - if (mouseOverItem != null) - { - // Reset selection, unless the CTRL key is held (TODO) - List selectedItems = items.Where(x => x.IsSelected == true).ToList(); - foreach (SongGridViewItem item in selectedItems) - { - // Reset flag - item.IsSelected = false; - } - } - } - - // Loop through visible lines to update the new selected item - bool invalidatedNewSelection = false; - for (int a = startLineNumber; a < startLineNumber + numberOfLinesToDraw; a++) - { - // Check if mouse is over this item - if (items[a].IsMouseOverItem) - { - // Set flag - invalidatedNewSelection = true; - - // Check if SHIFT is held - if ((Control.ModifierKeys & Keys.Shift) != 0) - { - // Find the start index of the selection - int startIndexSelection = lastItemIndexClicked; - if (a < startIndexSelection) - { - startIndexSelection = a; - } - - // Find the end index of the selection - int endIndexSelection = lastItemIndexClicked; - if (a > endIndexSelection) - { - endIndexSelection = a + 1; - } - - // Loop through items to selected - for (int b = startIndexSelection; b < endIndexSelection; b++) - { - // Set items as selected - items[b].IsSelected = true; - } - - // Invalidate region - Invalidate(); - } - // Check if CTRL is held - else if ((Control.ModifierKeys & Keys.Control) != 0) - { - // Invert selection - items[a].IsSelected = !items[a].IsSelected; - - // Invalidate region - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, ((a - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - } - else - { - // Set this item as the new selected item - items[a].IsSelected = true; - - // Invalidate region - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, ((a - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - } - - // Set the last item clicked index - lastItemIndexClicked = a; - - // Exit loop - break; - } - } - - // Raise selected item changed event (if an event is subscribed) - if (invalidatedNewSelection && OnSelectedIndexChanged != null) - { - // Create data - SongGridViewSelectedIndexChangedData data = new SongGridViewSelectedIndexChangedData(); - - // Raise event - OnSelectedIndexChanged(data); - } - - // Update invalid regions - Update(); - - base.OnMouseClick(e); - } - - /// - /// Occurs when the user double-clicks on the control. - /// Starts the playback of a new song. - /// - /// Event arguments - protected override void OnMouseDoubleClick(MouseEventArgs e) - { - // Check if all the data is valid - if (columns == null || songCache == null) - { - return; - } - - // Calculate album cover art width - int albumArtCoverWidth = columns[0].Visible ? columns[0].Width : 0; - - // Calculate scrollbar offset Y - int scrollbarOffsetY = (startLineNumber * songCache.LineHeight) - vScrollBar.Value; - - // Keep original songId in case the now playing value is set before invalidating the older value - Guid originalId = Guid.Empty; - - // Check mode - if (mode == SongGridViewMode.AudioFile) - { - // Set original id - originalId = nowPlayingAudioFileId; - } - else if (mode == SongGridViewMode.Playlist) - { - // Set original id - originalId = nowPlayingPlaylistItemId; - } - - // Loop through visible lines - for (int a = startLineNumber; a < startLineNumber + numberOfLinesToDraw; a++) - { - // Check if mouse is over this item - if (items[a].IsMouseOverItem) - { - // Set this item as the new now playing - nowPlayingAudioFileId = items[a].AudioFile.Id; - nowPlayingPlaylistItemId = items[a].PlaylistItemId; - - // Invalidate region - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, ((a - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - } - else if (mode == SongGridViewMode.AudioFile && items[a].AudioFile.Id == originalId) - { - // Invalidate region - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, ((a - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - } - else if (mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == originalId) - { - // Invalidate region - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, ((a - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - } - } - - // Update invalid regions - Update(); - - base.OnMouseDoubleClick(e); - } - - /// - /// Occurs when the mouse pointer is moving over the control. - /// Manages the display of mouse on/off visual effects. - /// - /// Event arguments - protected override void OnMouseMove(MouseEventArgs e) - { - // Declare variables - bool controlNeedsToBeUpdated = false; - - // Check if all the data is valid - if (columns == null || songCache == null) - { - return; - } - - // Calculate album cover art width - int albumArtCoverWidth = columns[0].Visible ? columns[0].Width : 0; - - // Check if the user is currently resizing a column (loop through columns) - foreach (SongGridViewColumn column in songCache.ActiveColumns) - { - // Check if the user is currently resizing this column - if (column.IsUserResizingColumn && column.Visible) - { - // Calculate the new column width - int newWidth = dragOriginalColumnWidth - (dragStartX - e.X); - - // Make sure the width isn't lower than the minimum width - if (newWidth < minimumColumnWidth) - { - // Set width as minimum column width - newWidth = minimumColumnWidth; - } - - // Set column width - column.Width = newWidth; - - // Refresh control (invalidate whole control) - controlNeedsToBeUpdated = true; - Invalidate(); - InvalidateSongCache(); - - // Auto adjust horizontal scrollbar value if it exceeds the value range (i.e. do not show empty column) - if (hScrollBar.Value > hScrollBar.Maximum - hScrollBar.LargeChange) - { - // Set new value - int tempValue = hScrollBar.Maximum - hScrollBar.LargeChange; - if (tempValue < 0) - { - tempValue = 0; - } - hScrollBar.Value = tempValue; - } - } - - // Check if the user is moving the column - if (column.IsMouseOverColumnHeader && column.CanBeMoved && CanMoveColumns && isUserHoldingLeftMouseButton && !IsColumnResizing) - { - // Check if the X position has changed by at least 2 pixels (i.e. dragging) - if (dragStartX >= e.X + 2 || - dragStartX <= e.X - 2) - { - // Set resizing column flag - column.IsUserMovingColumn = true; - } - } - - // Check if the user is currently moving this column - if (column.IsUserMovingColumn) - { - // Loop through columns - int x = 0; - foreach (SongGridViewColumn columnOver in songCache.ActiveColumns) - { - // Check if column is visible - if (columnOver.Visible) - { - // Check if the cursor is over the left part of the column - if (e.X >= x - hScrollBar.Value && e.X <= x + (columnOver.Width / 2) - hScrollBar.Value) - { - // Set marker - columnMoveMarkerX = x; - } - // Check if the cursor is over the right part of the column - else if (e.X >= x + (columnOver.Width / 2) - hScrollBar.Value && e.X <= x + columnOver.Width - hScrollBar.Value) - { - // Set marker - columnMoveMarkerX = x + columnOver.Width; - } - - // Increment x - x += columnOver.Width; - } - } - - // Invalidate control - Invalidate(); - - // Set flags - controlNeedsToBeUpdated = true; - } - } - - // Check if a column is moving - if (!IsColumnMoving) - { - - // Check if the cursor needs to be changed - int offsetX = 0; - bool mousePointerIsOverColumnLimit = false; - foreach (SongGridViewColumn column in songCache.ActiveColumns) - { - // Check if column is visible - if (column.Visible) - { - // Increment offset by the column width - offsetX += column.Width; - - // Check if the column can be resized - if (column.CanBeResized) - { - // Check if the mouse pointer is over a column (add 1 pixel so it's easier to select) - if (e.X >= offsetX - hScrollBar.Value && e.X <= offsetX + 1 - hScrollBar.Value) - { - // Set flag - mousePointerIsOverColumnLimit = true; - column.IsMouseCursorOverColumnLimit = true; - - // Change the cursor if it's not the right cursor - if (Cursor != Cursors.VSplit) - { - // Change cursor - Cursor = Cursors.VSplit; - } - } - else - { - // Reset flag - column.IsMouseCursorOverColumnLimit = false; - } - } - } - } - - // Check if the default cursor needs to be restored - if (!mousePointerIsOverColumnLimit && Cursor != Cursors.Default) - { - // Restore the default cursor - Cursor = Cursors.Default; - } - - // Reset flags - int columnOffsetX2 = 0; - for (int b = 0; b < songCache.ActiveColumns.Count; b++) - { - // Get current column - SongGridViewColumn column = songCache.ActiveColumns[b]; - - // Check if column is visible - if (column.Visible) - { - // Was mouse over this column header? - if (column.IsMouseOverColumnHeader) - { - // Reset flag - column.IsMouseOverColumnHeader = false; - - // Invalidate region - Invalidate(new Rectangle(columnOffsetX2 - hScrollBar.Value, 0, column.Width, songCache.LineHeight)); - controlNeedsToBeUpdated = true; - } - - // Increment offset - columnOffsetX2 += column.Width; - } - } - - // Check if the mouse pointer is over the header - if (e.Y >= 0 && - e.Y <= songCache.LineHeight) - { - // Check on what column the user has clicked - int columnOffsetX = 0; - for (int a = 0; a < songCache.ActiveColumns.Count; a++) - { - // Get current column - SongGridViewColumn column = songCache.ActiveColumns[a]; - - // Make sure column is visible - if (column.Visible) - { - // Check if the mouse pointer is over this column - if (e.X >= columnOffsetX - hScrollBar.Value && e.X <= columnOffsetX + column.Width - hScrollBar.Value) - { - // Set flag - column.IsMouseOverColumnHeader = true; - - // Invalidate region - Invalidate(new Rectangle(columnOffsetX - hScrollBar.Value, 0, column.Width, songCache.LineHeight)); - - // Exit loop - controlNeedsToBeUpdated = true; - break; - } - - // Increment X offset - columnOffsetX += column.Width; - } - } - } - - // Check if the mouse cursor is over a line (loop through lines) - int offsetY = 0; - int scrollbarOffsetY = (startLineNumber * songCache.LineHeight) - vScrollBar.Value; - - // Check if there's at least one item - if (items.Count > 0) - { - // Reset mouse over item flags - for (int b = startLineNumber; b < startLineNumber + numberOfLinesToDraw; b++) - { - // Check if the mouse was over this item - if (items[b].IsMouseOverItem) - { - // Reset flag and invalidate region - items[b].IsMouseOverItem = false; - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, ((b - startLineNumber + 1) * songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - - // Exit loop - break; - } - } - - // Put new mouse over flag - for (int a = startLineNumber; a < startLineNumber + numberOfLinesToDraw; a++) - { - // Get audio file - AudioFile audioFile = items[a].AudioFile; - - // Calculate offset - offsetY = (a * songCache.LineHeight) - vScrollBar.Value + songCache.LineHeight; - - // Check if the mouse cursor is over this line (and not already mouse over) - if (e.X >= albumArtCoverWidth - hScrollBar.Value && - e.Y >= offsetY && - e.Y <= offsetY + songCache.LineHeight && - !items[a].IsMouseOverItem) - { - // Set item as mouse over - items[a].IsMouseOverItem = true; - - // Invalidate region - Invalidate(new Rectangle(albumArtCoverWidth - hScrollBar.Value, offsetY, ClientRectangle.Width - albumArtCoverWidth + hScrollBar.Value, songCache.LineHeight)); - - // Update control - controlNeedsToBeUpdated = true; - - // Exit loop - break; - } - } - } - } - - // Check if the control needs to be refreshed - if (controlNeedsToBeUpdated) - { - // Refresh control - Update(); - } - - base.OnMouseMove(e); - } - - /// - /// Occurs when the user clicks on one of the menu items of the Columns contextual menu. - /// - /// Event sender - /// Event arguments - protected void menuItemColumns_Click(object sender, EventArgs e) - { - // Make sure the sender is the menu item - if (sender is ToolStripMenuItem) - { - // Get the reference to the menu item - ToolStripMenuItem menuItem = (ToolStripMenuItem)sender; - - // Inverse selection - menuItem.Checked = !menuItem.Checked; - - // Get column - SongGridViewColumn column = columns.FirstOrDefault(x => x.Title == menuItem.Tag.ToString()); - if (column != null) - { - // Set visibility - column.Visible = menuItem.Checked; - } - - // Reset cache - songCache = null; - Refresh(); - } - } - - /// - /// Handles the global messages for the mouse wheel. - /// - /// Message - /// Nothing interesting - public bool PreFilterMessage(ref Message m) - { - // Check message type - if (m.Msg == Win32.WM_MOUSEWHEEL) - { - // Get mouse wheel delta - int delta = Win32.GetWheelDeltaWParam((int)m.WParam); - - // Check if all the data is valid - if (columns == null || songCache == null) - { - return false; - } - - // Make sure the mouse cursor is over the control, and that the vertical scrollbar is enabled - if (!isMouseOverControl || !vScrollBar.Enabled) - { - return false; - } - - // Get relative value - int value = delta / SystemInformation.MouseWheelScrollDelta; - - // Set new value - int newValue = vScrollBar.Value + (-value * songCache.LineHeight); - - // Check for maximum - if (newValue > vScrollBar.Maximum - vScrollBar.LargeChange) - { - newValue = vScrollBar.Maximum - vScrollBar.LargeChange; - } - // Check for minimum - if (newValue < 0) - { - newValue = 0; - } - - // Set scrollbar value - vScrollBar.Value = newValue; - - // Invalidate the whole control and refresh - Refresh(); - } - - return false; - } - - #endregion - - /// - /// Creates a cache of values used for rendering the song grid view. - /// Also sets scrollbar position, height, value, maximum, etc. - /// - public void InvalidateSongCache() - { - // Check if columns have been created - if (columns == null || columns.Count == 0 || items == null) - { - return; - } - - // Create cache - songCache = new SongGridViewCache(); - - // Get active columns and order them - songCache.ActiveColumns = columns.Where(x => x.Order >= 0).OrderBy(x => x.Order).ToList(); - - // Load custom font - //Font fontDefaultBold = Tools.LoadCustomFont(FontCollection, CustomFontName, Font.Size, FontStyle.Bold); - Font fontDefaultBold = null; - - // Make sure the embedded font name needs to be loaded and is valid - if (theme.RowTextGradient.Font.UseEmbeddedFont && !String.IsNullOrEmpty(theme.RowTextGradient.Font.EmbeddedFontName)) - { - try - { - // Get embedded font collection - EmbeddedFontCollection embeddedFonts = EmbeddedFontHelper.GetEmbeddedFonts(); - - // Get embedded fonts - fontDefaultBold = Tools.LoadEmbeddedFont(embeddedFonts, theme.RowTextGradient.Font.EmbeddedFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); - } - catch - { - // Use default font instead - fontDefaultBold = new Font(this.Font, FontStyle.Bold); - } - } - - // Check if font is null - if (fontDefaultBold == null) - { - try - { - // Try to get standard font - fontDefaultBold = new Font(theme.RowTextGradient.Font.StandardFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); - } - catch - { - // Use default font instead - fontDefaultBold = new Font(this.Font, FontStyle.Bold); - } - } - - //// Load default fonts if custom fonts were not found - //if (fontDefaultBold == null) - //{ - // // Load default font - // fontDefaultBold = new Font(Font.FontFamily.Name, Font.Size, FontStyle.Bold); - //} - - // Create temporary bitmap/graphics objects to measure a string (to determine line height) - Bitmap bmpTemp = new Bitmap(200, 100); - Graphics g = Graphics.FromImage(bmpTemp); - SizeF sizeFont = g.MeasureString("QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm!@#$%^&*()", fontDefaultBold); - g.Dispose(); - g = null; - bmpTemp.Dispose(); - bmpTemp = null; - - // Calculate the line height (try to measure the total possible height of characters using the custom or default font) - songCache.LineHeight = (int)sizeFont.Height + theme.Padding; - songCache.TotalHeight = songCache.LineHeight * items.Count; - - // Check if the total active columns width exceed the width available in the control - songCache.TotalWidth = 0; - for (int a = 0; a < songCache.ActiveColumns.Count; a++) - { - // Check if column is visible - if (songCache.ActiveColumns[a].Visible) - { - // Increment total width - songCache.TotalWidth += songCache.ActiveColumns[a].Width; - } - } - - // Calculate the number of lines visible (count out the header, which is one line height) - songCache.NumberOfLinesFittingInControl = (int)Math.Floor((double)(ClientRectangle.Height) / (double)(songCache.LineHeight)); - - // Set vertical scrollbar dimensions - vScrollBar.Top = songCache.LineHeight; - vScrollBar.Left = ClientRectangle.Width - vScrollBar.Width; - vScrollBar.Minimum = 0; - - // Scrollbar maximum is the number of lines fitting in the screen + the remaining line which might be cut - // by the control height because it's not a multiple of line height (i.e. the last line is only partly visible) - int lastLineHeight = ClientRectangle.Height - (songCache.LineHeight * songCache.NumberOfLinesFittingInControl); - - // Check width - if (songCache.TotalWidth > ClientRectangle.Width - vScrollBar.Width) - { - // Set scrollbar values - hScrollBar.Maximum = songCache.TotalWidth; - hScrollBar.SmallChange = 5; - hScrollBar.LargeChange = ClientRectangle.Width; - - // Show scrollbar - hScrollBar.Visible = true; - } - - // Check if the horizontal scrollbar needs to be turned off - if (songCache.TotalWidth <= ClientRectangle.Width - vScrollBar.Width && hScrollBar.Visible) - { - // Hide the horizontal scrollbar - hScrollBar.Visible = false; - } - - // If there are less items than items fitting on screen... - if (((songCache.NumberOfLinesFittingInControl - 1) * songCache.LineHeight) - hScrollBar.Height >= songCache.TotalHeight) - { - // Disable the scrollbar - vScrollBar.Enabled = false; - vScrollBar.Value = 0; - } - else - { - // Set scrollbar values - vScrollBar.Enabled = true; - - // The real large change needs to be added to the LargeChange and Maximum property in order to work. - int realLargeChange = songCache.LineHeight * 5; - - // Calculate the vertical scrollbar maximum - int vMax = songCache.LineHeight * (items.Count - songCache.NumberOfLinesFittingInControl + 1) - lastLineHeight + realLargeChange; - - // Add the horizontal scrollbar height if visible - if (hScrollBar.Visible) - { - // Add height - vMax += hScrollBar.Height; - } - - // Compensate for the header, and for the last line which might be truncated by the control height - vScrollBar.Maximum = vMax; - vScrollBar.SmallChange = songCache.LineHeight; - vScrollBar.LargeChange = 1 + realLargeChange; - } - - // Calculate the scrollbar offset Y - songCache.ScrollBarOffsetY = (startLineNumber * songCache.LineHeight) - vScrollBar.Value; - - // If both scrollbars need to be visible, the width and height must be changed - if (hScrollBar.Visible && vScrollBar.Visible) - { - // Cut 16 pixels - hScrollBar.Width = ClientRectangle.Width - 16; - vScrollBar.Height = ClientRectangle.Height - songCache.LineHeight - 16; - } - else - { - vScrollBar.Height = ClientRectangle.Height - songCache.LineHeight; - } - } - - /// - /// This timer triggers the animation of the currently playing song. - /// - /// Event sender - /// Event arguments - protected void timerAnimationNowPlaying_Tick(object sender, EventArgs e) - { - // If the rectangle is "empty", do not trigger invalidation - if (rectNowPlaying.X == 0 && - rectNowPlaying.Y == 0 && - rectNowPlaying.Width == 1 && - rectNowPlaying.Height == 1) - { - return; - } - - // Increment counter - timerAnimationNowPlayingCount += 10; - - // Invalidate region for now playing - Invalidate(rectNowPlaying); - Update(); // This is necessary after an invalidate. - } - } - - /// - /// Result data structure used for the SongGridView background worker. - /// - public class SongGridViewBackgroundWorkerResult - { - /// - /// Audio file. - /// - public AudioFile AudioFile { get; set; } - /// - /// Album cover. - /// - public Image AlbumArt { get; set; } - } - - /// - /// Argument data structure used for the SongGridView background worker. - /// - public class SongGridViewBackgroundWorkerArgument - { - /// - /// Audio file. - /// - public AudioFile AudioFile { get; set; } - /// - /// Line number (item index in the SongGridView control). - /// - public int LineIndex { get; set; } - } - - /// - /// Data structure used for the SelectedIndexChanged event. - /// - public class SongGridViewSelectedIndexChangedData - { - } - - /// - /// Data structure used for the ColumnClick event. - /// - public class SongGridViewColumnClickData - { - /// - /// Column index. - /// - public int ColumnIndex { get; set; } - } - -} +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Text; +using System.Drawing.Drawing2D; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows.Forms; +using MPfm.Sound.AudioFiles; +using MPfm.Sound.Playlists; + +namespace MPfm.WindowsControls +{ + /// + /// This custom grid view control displays the MPfm library. + /// + public partial class SongGridView : Control, IMessageFilter + { + #region Private Variables + + private SongGridViewMode _mode = SongGridViewMode.AudioFile; + + // Controls + private System.Windows.Forms.VScrollBar _vScrollBar = null; + private System.Windows.Forms.HScrollBar _hScrollBar = null; + private ContextMenuStrip _menuColumns = null; + + // Background worker for updating album art + private int _preloadLinesAlbumCover = 20; + private BackgroundWorker _workerUpdateAlbumArt = null; + private List _workerUpdateAlbumArtPile = null; + private System.Windows.Forms.Timer _timerUpdateAlbumArt = null; + private SongGridViewCache _songCache = null; + private List _imageCache = new List(); + + // Private variables used for mouse events + private int _columnMoveMarkerX = 0; + private int _startLineNumber = 0; + private int numberOfLinesToDraw = 0; + private int minimumColumnWidth = 30; + private int dragStartX = -1; + private int dragOriginalColumnWidth = -1; + private bool isMouseOverControl = false; + private bool isUserHoldingLeftMouseButton = false; + private int lastItemIndexClicked = -1; + + // Animation timer and counter for currently playing song + private int timerAnimationNowPlayingCount = 0; + private Rectangle rectNowPlaying = new Rectangle(0, 0, 1, 1); + private System.Windows.Forms.Timer timerAnimationNowPlaying = null; + + #endregion + + #region Events + + /// + /// Delegate method for the OnSelectedItemChanged event. + /// + /// SelectedIndexChanged data + public delegate void SelectedIndexChanged(SongGridViewSelectedIndexChangedData data); + /// + /// The OnSelectedIndexChanged event is triggered when the selected item(s) have changed. + /// + public event SelectedIndexChanged OnSelectedIndexChanged; + + /// + /// Delegate method for the OnColumnClick event. + /// + /// SongGridViewColumnClickData data + public delegate void ColumnClick(SongGridViewColumnClickData data); + /// + /// The ColumnClick event is triggered when the user has clicked on one of the columns. + /// + public event ColumnClick OnColumnClick; + + #endregion + + #region Properties + + /// + /// Private value for the Theme property. + /// + private SongGridViewTheme theme = null; + /// + /// Defines the current theme used for rendering the control. + /// + public SongGridViewTheme Theme + { + get + { + return theme; + } + set + { + theme = value; + } + } + + #region Other Properties (Items, Columns, Menus, etc.) + + /// + /// Private value for the Items property. + /// + private List items = null; + /// + /// List of grid view items (representing songs). + /// + [Browsable(false)] + public List Items + { + get + { + return items; + } + } + + /// + /// Returns the list of selected items. + /// + [Browsable(false)] + public List SelectedItems + { + get + { + // Check if the item list exists + if (items != null) + { + return items.Where(x => x.IsSelected).ToList(); + } + + return null; + } + } + + /// + /// Private value for the Columns property. + /// + private List columns = null; + /// + /// List of grid view columns. + /// + [Browsable(false)] + public List Columns + { + get + { + return columns; + } + } + + + /// + /// Private value for the NowPlayingAudioFileId property. + /// + private Guid nowPlayingAudioFileId = Guid.Empty; + /// + /// Defines the currently playing audio file identifier. + /// + [Browsable(false)] + public Guid NowPlayingAudioFileId + { + get + { + return nowPlayingAudioFileId; + } + set + { + nowPlayingAudioFileId = value; + } + } + + /// + /// Private value for the NowPlayingPlaylistItemId property. + /// + private Guid nowPlayingPlaylistItemId = Guid.Empty; + /// + /// Defines the currently playing playlist item identifier. + /// + [Browsable(false)] + public Guid NowPlayingPlaylistItemId + { + get + { + return nowPlayingPlaylistItemId; + } + set + { + nowPlayingPlaylistItemId = value; + } + } + + /// + /// Private value for the ContextMenuStrip property. + /// + private ContextMenuStrip contextMenuStrip = null; + /// + /// ContextMenuStrip related to the grid. This context menu + /// opens when the user right clicks an item. + /// + [Category("Misc"), Browsable(true), Description("Stuff.")] + public override ContextMenuStrip ContextMenuStrip + { + get + { + return contextMenuStrip; + } + set + { + contextMenuStrip = value; + } + } + + #endregion + + #region Filter / OrderBy Properties + + /// + /// Private value for the OrderByFieldName property. + /// + private string orderByFieldName = string.Empty; + /// + /// Indicates which field should be used for ordering songs. + /// + public string OrderByFieldName + { + get + { + return orderByFieldName; + } + set + { + orderByFieldName = value; + + // Invalidate item list and cache + items = null; + _songCache = null; + + // Refresh whole control + Refresh(); + } + } + + /// + /// Private value for the OrderByAscending property. + /// + private bool orderByAscending = true; + /// + /// Indicates if the order should be ascending (true) or descending (false). + /// + public bool OrderByAscending + { + get + { + return orderByAscending; + } + set + { + orderByAscending = value; + } + } + + #endregion + + #region Settings Properties + + /// + /// Private value for the DisplayDebugInformation property. + /// + private bool displayDebugInformation = false; + /// + /// If true, the debug information will be shown over the first column. + /// + public bool DisplayDebugInformation + { + get + { + return displayDebugInformation; + } + set + { + displayDebugInformation = value; + } + } + + /// + /// Private value for the CanResizeColumns property. + /// + private bool canResizeColumns = true; + /// + /// Indicates if the user can resize the columns or not. + /// + public bool CanResizeColumns + { + get + { + return canResizeColumns; + } + set + { + canResizeColumns = value; + } + } + + /// + /// Private value for the CanMoveColumns property. + /// + private bool canMoveColumns = true; + /// + /// Indicates if the user can move the columns or not. + /// + public bool CanMoveColumns + { + get + { + return canMoveColumns; + } + set + { + canMoveColumns = value; + } + } + + /// + /// Private value for the CanChangeOrderBy property. + /// + private bool canChangeOrderBy = true; + /// + /// Indicates if the user can change the order by or not. + /// + public bool CanChangeOrderBy + { + get + { + return canChangeOrderBy; + } + set + { + canChangeOrderBy = value; + } + } + + /// + /// Private value for the CanReorderItems property. + /// + private bool canReorderItems = true; + /// + /// Indicates if the user can reorder the items or not. + /// + public bool CanReorderItems + { + get + { + return canReorderItems; + } + set + { + canReorderItems = value; + } + } + + /// + /// Private value for the ImageCacheSize property. + /// + private int imageCacheSize = 10; + /// + /// Defines the size of the album art image cache (10 by default). + /// + public int ImageCacheSize + { + get + { + return imageCacheSize; + } + set + { + imageCacheSize = value; + } + } + + #endregion + + /// + /// Indicates if a column is currently moving. + /// + public bool IsColumnMoving + { + get + { + // Loop through columns + foreach (SongGridViewColumn column in columns) + { + // Check if column is moving + if (column.IsUserMovingColumn) + return true; + } + + return false; + } + } + + /// + /// Indicates if a column is currently resizing. + /// + public bool IsColumnResizing + { + get + { + // Loop through columns + foreach (SongGridViewColumn column in columns) + { + // Check if column is moving + if (column.IsUserResizingColumn) + return true; + } + + return false; + } + } + + #endregion + + #region Constructors + + /// + /// Default constructor for SongGridView. + /// + public SongGridView() + { + // Set default styles + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | + ControlStyles.Opaque | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true); + + // Create default theme + theme = new SongGridViewTheme(); + + // Get embedded font collection + embeddedFonts = EmbeddedFontHelper.GetEmbeddedFonts(); + + // Create timer for animation + timerAnimationNowPlaying = new System.Windows.Forms.Timer(); + timerAnimationNowPlaying.Interval = 50; + timerAnimationNowPlaying.Tick += new EventHandler(timerAnimationNowPlaying_Tick); + timerAnimationNowPlaying.Enabled = true; + + // Create vertical scrollbar + _vScrollBar = new System.Windows.Forms.VScrollBar(); + _vScrollBar.Width = 16; + _vScrollBar.Scroll += new ScrollEventHandler(vScrollBar_Scroll); + Controls.Add(_vScrollBar); + + // Create horizontal scrollbar + _hScrollBar = new System.Windows.Forms.HScrollBar(); + _hScrollBar.Width = ClientRectangle.Width; + _hScrollBar.Height = 16; + _hScrollBar.Top = ClientRectangle.Height - _hScrollBar.Height; + _hScrollBar.Scroll += new ScrollEventHandler(hScrollBar_Scroll); + Controls.Add(_hScrollBar); + + // Override mouse messages for mouse wheel (get mouse wheel events out of control) + Application.AddMessageFilter(this); + + // Create background worker for updating album art + _workerUpdateAlbumArtPile = new List(); + _workerUpdateAlbumArt = new BackgroundWorker(); + _workerUpdateAlbumArt.DoWork += new DoWorkEventHandler(workerUpdateAlbumArt_DoWork); + _workerUpdateAlbumArt.RunWorkerCompleted += new RunWorkerCompletedEventHandler(workerUpdateAlbumArt_RunWorkerCompleted); + + // Create timer for updating album art + _timerUpdateAlbumArt = new System.Windows.Forms.Timer(); + _timerUpdateAlbumArt.Interval = 10; + _timerUpdateAlbumArt.Tick += new EventHandler(timerUpdateAlbumArt_Tick); + _timerUpdateAlbumArt.Enabled = true; + + // Create columns + SongGridViewColumn columnSongAlbumCover = new SongGridViewColumn("Album Cover", string.Empty, true, 0); + SongGridViewColumn columnSongNowPlaying = new SongGridViewColumn("Now Playing", string.Empty, true, 1); + SongGridViewColumn columnSongFileType = new SongGridViewColumn("Type", "FileType", false, 2); + SongGridViewColumn columnSongTrackNumber = new SongGridViewColumn("Tr#", "DiscTrackNumber", true, 3); + SongGridViewColumn columnSongTrackCount = new SongGridViewColumn("Track Count", "TrackCount", false, 4); + SongGridViewColumn columnSongFilePath = new SongGridViewColumn("File Path", "FilePath", false, 5); + SongGridViewColumn columnSongTitle = new SongGridViewColumn("Song Title", "Title", true, 6); + SongGridViewColumn columnSongLength = new SongGridViewColumn("Length", "Length", true, 7); + SongGridViewColumn columnSongArtistName = new SongGridViewColumn("Artist Name", "ArtistName", true, 8); + SongGridViewColumn columnSongAlbumTitle = new SongGridViewColumn("Album Title", "AlbumTitle", true, 9); + SongGridViewColumn columnSongGenre = new SongGridViewColumn("Genre", "Genre", false, 10); + SongGridViewColumn columnSongPlayCount = new SongGridViewColumn("Play Count", "PlayCount", true, 11); + SongGridViewColumn columnSongLastPlayed = new SongGridViewColumn("Last Played", "LastPlayed", true, 12); + SongGridViewColumn columnSongBitrate = new SongGridViewColumn("Bitrate", "Bitrate", false, 13); + SongGridViewColumn columnSongSampleRate = new SongGridViewColumn("Sample Rate", "SampleRate", false, 14); + SongGridViewColumn columnSongTempo = new SongGridViewColumn("Tempo", "Tempo", false, 15); + SongGridViewColumn columnSongYear = new SongGridViewColumn("Year", "Year", false, 16); + + // Set visible column titles + columnSongAlbumCover.IsHeaderTitleVisible = false; + columnSongNowPlaying.IsHeaderTitleVisible = false; + + // Set additional flags + columnSongAlbumCover.CanBeReordered = false; + columnSongNowPlaying.CanBeReordered = false; + columnSongNowPlaying.CanBeResized = false; + + // Set default widths + columnSongAlbumCover.Width = 200; + columnSongNowPlaying.Width = 20; + columnSongFileType.Width = 40; + columnSongTrackNumber.Width = 30; + columnSongTrackCount.Width = 80; + columnSongFilePath.Width = 200; + columnSongTitle.Width = 200; + columnSongLength.Width = 70; + columnSongArtistName.Width = 140; + columnSongAlbumTitle.Width = 140; + columnSongGenre.Width = 140; + columnSongPlayCount.Width = 50; + columnSongLastPlayed.Width = 80; + columnSongBitrate.Width = 50; + columnSongSampleRate.Width = 50; + columnSongTempo.Width = 40; + columnSongYear.Width = 40; + + // Add columns to list + columns = new List(); + columns.Add(columnSongAlbumCover); + columns.Add(columnSongAlbumTitle); + columns.Add(columnSongArtistName); + columns.Add(columnSongBitrate); + columns.Add(columnSongFilePath); + columns.Add(columnSongGenre); + columns.Add(columnSongLastPlayed); + columns.Add(columnSongLength); + columns.Add(columnSongNowPlaying); + columns.Add(columnSongPlayCount); + columns.Add(columnSongSampleRate); + columns.Add(columnSongTitle); // Song title + columns.Add(columnSongTempo); + columns.Add(columnSongTrackNumber); + columns.Add(columnSongTrackCount); + columns.Add(columnSongFileType); // Type + columns.Add(columnSongYear); + + // Create contextual menu + _menuColumns = new System.Windows.Forms.ContextMenuStrip(); + + // Loop through columns + foreach (SongGridViewColumn column in columns) + { + // Add menu item + ToolStripMenuItem menuItem = (ToolStripMenuItem)_menuColumns.Items.Add(column.Title); + menuItem.Tag = column.Title; + menuItem.Checked = column.Visible; + menuItem.Click += new EventHandler(menuItemColumns_Click); + } + } + + /// + /// Occurs when the Update Album Art timer expires. + /// Checks if there are more album art covers to load and starts the background + /// thread if it is not running already. + /// + /// Event sender + /// Event arguments + public void timerUpdateAlbumArt_Tick(object sender, EventArgs e) + { + // Stop timer + _timerUpdateAlbumArt.Enabled = false; + + // Check for the next album art to fetch + if (_workerUpdateAlbumArtPile.Count > 0 && !_workerUpdateAlbumArt.IsBusy) + { + // Do some cleanup: clean items that are not visible anymore + bool cleanUpDone = false; + while (!cleanUpDone) + { + int indexToDelete = -1; + for (int a = 0; a < _workerUpdateAlbumArtPile.Count; a++) + { + // Get argument + SongGridViewBackgroundWorkerArgument arg = _workerUpdateAlbumArtPile[a]; + + // Check if this album is still visible (cancel if it is out of display). + if (arg.LineIndex < _startLineNumber || arg.LineIndex > _startLineNumber + numberOfLinesToDraw + _preloadLinesAlbumCover) + { + indexToDelete = a; + break; + } + } + + if (indexToDelete >= 0) + { + _workerUpdateAlbumArtPile.RemoveAt(indexToDelete); + } + else + { + cleanUpDone = true; + } + } + // There must be more album art to fetch.. right? + if (_workerUpdateAlbumArtPile.Count > 0) + { + // Start background worker + SongGridViewBackgroundWorkerArgument arg = _workerUpdateAlbumArtPile[0]; + _workerUpdateAlbumArt.RunWorkerAsync(arg); + } + } + + // Restart timer + _timerUpdateAlbumArt.Enabled = true; + } + + /// + /// Occurs when the Update Album Art background worker starts its work. + /// Fetches the album cover in another thread and returns the image once done. + /// + /// Event sender + /// Event arguments + public void workerUpdateAlbumArt_DoWork(object sender, DoWorkEventArgs e) + { + if (e.Argument == null) + return; + + // Cast argument + SongGridViewBackgroundWorkerArgument arg = (SongGridViewBackgroundWorkerArgument)e.Argument; + + // Create result + SongGridViewBackgroundWorkerResult result = new SongGridViewBackgroundWorkerResult(); + result.AudioFile = arg.AudioFile; + + // Check if this album is still visible (cancel if it is out of display). + if (arg.LineIndex < _startLineNumber || arg.LineIndex > _startLineNumber + numberOfLinesToDraw + _preloadLinesAlbumCover) + { + // Set result with empty image + e.Result = result; + return; + } + + // Extract image from file + Image imageAlbumCover = AudioFile.ExtractImageForAudioFile(arg.AudioFile.FilePath); + + // Set album art in return data + result.AlbumArt = imageAlbumCover; + e.Result = result; + } + + /// + /// Occurs when the Update Album Art background worker has finished its work. + /// + /// Event sender + /// Event arguments + public void workerUpdateAlbumArt_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + if (e.Result == null) + return; + + // Get album art + SongGridViewBackgroundWorkerResult result = (SongGridViewBackgroundWorkerResult)e.Result; + + // Create cover art cache (even if the albumart is null, just to make sure the grid doesn't refetch the album art endlessly) + SongGridViewImageCache cache = new SongGridViewImageCache(); + cache.Key = result.AudioFile.ArtistName + "_" + result.AudioFile.AlbumTitle; + cache.Image = result.AlbumArt; + + // We found cover art! Add to cache and get out of the loop + _imageCache.Add(cache); + + // Check if the cache size has been reached + if (_imageCache.Count > imageCacheSize) + { + // Check if the image needs to be disposed + if (_imageCache[0].Image != null) + { + Image imageTemp = _imageCache[0].Image; + imageTemp.Dispose(); + imageTemp = null; + } + + // Remove the oldest item + _imageCache.RemoveAt(0); + } + + // Remove song from list + int indexRemove = -1; + for (int a = 0; a < _workerUpdateAlbumArtPile.Count; a++) + if (_workerUpdateAlbumArtPile[a].AudioFile.FilePath.ToUpper() == result.AudioFile.FilePath.ToUpper()) + indexRemove = a; + if (indexRemove >= 0) + { + _workerUpdateAlbumArtPile.RemoveAt(indexRemove); + } + + // Refresh control (TODO: Invalidate instead) + Refresh(); + } + + /// + /// Clears the currently selected items. + /// + public void ClearSelectedItems() + { + // Loop through items + foreach (SongGridViewItem item in items) + { + // Check if item is selected; unselect item + if (item.IsSelected) + item.IsSelected = false; + } + + Refresh(); + } + + /// + /// Imports audio files as SongGridViewItems. + /// + /// List of AudioFiles + public void ImportAudioFiles(List audioFiles) + { + // Set mode + _mode = SongGridViewMode.AudioFile; + + // Create list of items + items = new List(); + foreach (AudioFile audioFile in audioFiles) + { + // Create item and add to list + SongGridViewItem item = new SongGridViewItem(); + item.AudioFile = audioFile; + item.PlaylistItemId = Guid.NewGuid(); + items.Add(item); + } + + // Reset scrollbar position + _vScrollBar.Value = 0; + _songCache = null; + + // Refresh control + Refresh(); + } + + /// + /// Imports playlist items as SongGridViewItems. + /// + /// Playlist + public void ImportPlaylist(Playlist playlist) + { + // Set mode + _mode = SongGridViewMode.Playlist; + + // Create list of items + items = new List(); + foreach (PlaylistItem playlistItem in playlist.Items) + { + // Create item and add to list + SongGridViewItem item = new SongGridViewItem(); + item.AudioFile = playlistItem.AudioFile; + item.PlaylistItemId = playlistItem.Id; + items.Add(item); + } + + // Reset scrollbar position + _vScrollBar.Value = 0; + _songCache = null; + + // Refresh control + Refresh(); + } + + #endregion + + #region Paint Events + + /// + /// Update a specific line (if visible) by its audio file unique identifier. + /// + /// Audio file unique identifier + public void UpdateAudioFileLine(Guid audioFileId) + { + // Find the position of the line + for (int a = _startLineNumber; a < _startLineNumber + numberOfLinesToDraw; a++) + { + // Calculate offset + int offsetY = (a * _songCache.LineHeight) - _vScrollBar.Value + _songCache.LineHeight; + if (items[a].AudioFile.Id == audioFileId) + { + Invalidate(new Rectangle(columns[0].Width - _hScrollBar.Value, offsetY, ClientRectangle.Width - columns[0].Width + _hScrollBar.Value, _songCache.LineHeight)); + Update(); + break; + } + } + } + + /// + /// Occurs when the kernel sends message to the control. + /// Intercepts WM_ERASEBKGND and cancels the message to prevent flickering. + /// + /// + protected override void OnNotifyMessage(Message m) + { + // Do not let WM_ERASEBKGND pass (prevent flickering) + if (m.Msg != 0x14) + base.OnNotifyMessage(m); + } + + /// + /// Occurs when the control needs to be painted. + /// + /// Paint Event arguments + protected override void OnPaint(PaintEventArgs e) + { + if (items == null) + return; + + Graphics g = e.Graphics; + g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; + g.SmoothingMode = SmoothingMode.AntiAlias; + PaintSongs(ref g); + + base.OnPaint(e); + } + + /// + /// Paints a grid view displaying a list of songs. + /// + /// Graphics object to write to + public void PaintSongs(ref Graphics g) + { + Rectangle rect = new Rectangle(); + RectangleF rectF = new RectangleF(); + Pen pen = null; + SolidBrush brush = null; + LinearGradientBrush brushGradient = null; + Color colorNowPlaying1 = theme.RowNowPlayingTextGradient.Color1; + Color colorNowPlaying2 = theme.RowNowPlayingTextGradient.Color2; + int offsetX = 0; + int offsetY = 0; + int albumCoverStartIndex = 0; + int albumCoverEndIndex = 0; + string currentAlbumTitle = string.Empty; + bool nowPlayingSongFound = false; + Font fontDefault = null; + Font fontDefaultBold = null; + + // Make sure the embedded font name needs to be loaded and is valid + if (theme.RowTextGradient.Font.UseEmbeddedFont && !String.IsNullOrEmpty(theme.RowTextGradient.Font.EmbeddedFontName)) + { + try + { + // Get embedded fonts + fontDefault = Tools.LoadEmbeddedFont(embeddedFonts, theme.RowTextGradient.Font.EmbeddedFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle()); + fontDefaultBold = Tools.LoadEmbeddedFont(embeddedFonts, theme.RowTextGradient.Font.EmbeddedFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); + } + catch + { + // Use default font instead + fontDefault = this.Font; + fontDefaultBold = new Font(this.Font, FontStyle.Bold); + } + } + + // Check if font is null + if (fontDefault == null) + { + try + { + // Try to get standard font + fontDefault = new Font(theme.RowTextGradient.Font.StandardFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle()); + fontDefaultBold = new Font(theme.RowTextGradient.Font.StandardFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); + } + catch + { + // Use default font instead + fontDefault = this.Font; + fontDefaultBold = new Font(this.Font, FontStyle.Bold); + } + } + + // Set string format + StringFormat stringFormat = new StringFormat(); + + // If there are no items, no need to draw this control. + if (items == null) + return; + + g.FillRectangle(Brushes.MediumVioletRed, Bounds); + + // Check if a cache exists, or if the cache needs to be refreshed + if (_songCache == null) + InvalidateSongCache(); + + // Calculate how many lines must be skipped because of the scrollbar position + _startLineNumber = (int)Math.Floor((double)_vScrollBar.Value / (double)(_songCache.LineHeight)); + + // Check if the total number of lines exceeds the number of icons fitting in height + numberOfLinesToDraw = 0; + if (_startLineNumber + _songCache.NumberOfLinesFittingInControl > items.Count) + { + // There aren't enough lines to fill the screen + numberOfLinesToDraw = items.Count - _startLineNumber; + } + else + { + // Fill up screen + numberOfLinesToDraw = _songCache.NumberOfLinesFittingInControl; + } + + // Add one line for overflow; however, make sure we aren't adding a line without content + if (_startLineNumber + numberOfLinesToDraw + 1 <= items.Count) + numberOfLinesToDraw++; + + // Loop through lines + for (int a = _startLineNumber; a < _startLineNumber + numberOfLinesToDraw; a++) + { + // Get audio file + AudioFile audioFile = items[a].AudioFile; + + // Calculate Y offset (compensate for scrollbar position) + offsetY = (a * _songCache.LineHeight) - _vScrollBar.Value + _songCache.LineHeight; + + // Calculate album art cover column width + int albumArtColumnWidth = columns[0].Visible ? columns[0].Width : 0; + + // Calculate line background width + int lineBackgroundWidth = ClientRectangle.Width + _hScrollBar.Value - albumArtColumnWidth; + + // Reduce width from scrollbar if visible + if (_vScrollBar.Visible) + lineBackgroundWidth -= _vScrollBar.Width; + + // Set rectangle + //Rectangle rectBackground = new Rectangle(albumArtColumnWidth - _hScrollBar.Value, offsetY, lineBackgroundWidth, _songCache.LineHeight); + Rectangle rectBackground = new Rectangle(albumArtColumnWidth - _hScrollBar.Value, offsetY, lineBackgroundWidth, _songCache.LineHeight + 1); + + // Set default line background color + Color colorBackground1 = theme.RowTextGradient.Color1; + Color colorBackground2 = theme.RowTextGradient.Color2; + + // Check conditions to determine background color + if ((_mode == SongGridViewMode.AudioFile && audioFile.Id == nowPlayingAudioFileId) || + (_mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == nowPlayingPlaylistItemId)) + { + colorBackground1 = theme.RowNowPlayingTextGradient.Color1; + colorBackground2 = theme.RowNowPlayingTextGradient.Color2; + } + + // Check if item is selected + if (items[a].IsSelected) + { + // Use darker color + int diff = 40; + colorBackground1 = Color.FromArgb(255, + (colorBackground1.R - diff < 0) ? 0 : colorBackground1.R - diff, + (colorBackground1.G - diff < 0) ? 0 : colorBackground1.G - diff, + (colorBackground1.B - diff < 0) ? 0 : colorBackground1.B - diff); + colorBackground2 = Color.FromArgb(255, + (colorBackground2.R - diff < 0) ? 0 : colorBackground2.R - diff, + (colorBackground2.G - diff < 0) ? 0 : colorBackground2.G - diff, + (colorBackground2.B - diff < 0) ? 0 : colorBackground2.B - diff); + } + + //// Check if mouse is over item + //if (items[a].IsMouseOverItem) + //{ + // // Use lighter color + // int diff = 20; + // colorBackground1 = Color.FromArgb(255, + // (colorBackground1.R + diff > 255) ? 255 : colorBackground1.R + diff, + // (colorBackground1.G + diff > 255) ? 255 : colorBackground1.G + diff, + // (colorBackground1.B + diff > 255) ? 255 : colorBackground1.B + diff); + // colorBackground2 = Color.FromArgb(255, + // (colorBackground2.R + diff > 255) ? 255 : colorBackground2.R + diff, + // (colorBackground2.G + diff > 255) ? 255 : colorBackground2.G + diff, + // (colorBackground2.B + diff > 255) ? 255 : colorBackground2.B + diff); + //} + + // Check conditions to determine background color + if ((_mode == SongGridViewMode.AudioFile && audioFile.Id == nowPlayingAudioFileId) || + (_mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == nowPlayingPlaylistItemId)) + { + colorNowPlaying1 = colorBackground1; + colorNowPlaying2 = colorBackground2; + } + + // Create gradient + brushGradient = new LinearGradientBrush(rectBackground, colorBackground1, colorBackground2, 90); + g.FillRectangle(brushGradient, rectBackground); + brushGradient.Dispose(); + brushGradient = null; + + // Loop through columns + offsetX = 0; + for (int b = 0; b < _songCache.ActiveColumns.Count; b++) + { + // Get current column + SongGridViewColumn column = _songCache.ActiveColumns[b]; + + // Check if the column is visible + if (column.Visible) + { + // Check if this is the "Now playing" column + if (column.Title == "Now Playing") + { + // Draw now playing icon + if ((_mode == SongGridViewMode.AudioFile && audioFile.Id == nowPlayingAudioFileId) || + (_mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == nowPlayingPlaylistItemId)) + { + // Which size is the minimum? Width or height? + int availableWidthHeight = column.Width - 4; + if (_songCache.LineHeight <= column.Width) + { + availableWidthHeight = _songCache.LineHeight - 4; + } + else + { + availableWidthHeight = column.Width - 4; + } + + // Calculate the icon position + float iconNowPlayingX = ((column.Width - availableWidthHeight) / 2) + offsetX - _hScrollBar.Value; + float iconNowPlayingY = offsetY + ((_songCache.LineHeight - availableWidthHeight) / 2); + + // Create NowPlaying rect (MUST be in integer) + rectNowPlaying = new Rectangle((int)iconNowPlayingX, (int)iconNowPlayingY, availableWidthHeight, availableWidthHeight); + nowPlayingSongFound = true; + + // Draw outer circle + brushGradient = new LinearGradientBrush(rectNowPlaying, Color.FromArgb(50, theme.IconNowPlayingGradient.Color1.R, theme.IconNowPlayingGradient.Color1.G, theme.IconNowPlayingGradient.Color1.B), theme.IconNowPlayingGradient.Color2, timerAnimationNowPlayingCount % 360); + g.FillEllipse(brushGradient, rectNowPlaying); + brushGradient.Dispose(); + brushGradient = null; + + // Draw inner circle + rect = new Rectangle((int)iconNowPlayingX + 4, (int)iconNowPlayingY + 4, availableWidthHeight - 8, availableWidthHeight - 8); + brush = new SolidBrush(colorNowPlaying1); + g.FillEllipse(brush, rect); + brush.Dispose(); + brush = null; + } + } + else if (column.Title == "Album Cover") + { + #region Album Cover Zone + + // Check for an album title change (or the last item of the grid) + if (currentAlbumTitle != audioFile.AlbumTitle) + { + // Set the new current album title + currentAlbumTitle = audioFile.AlbumTitle; + + // For displaying the album cover, we need to know how many songs of the same album are bundled together + // Start by getting the start index + for (int c = a; c > 0; c--) + { + // Get audio file + AudioFile previousAudioFile = items[c].AudioFile; + + // Check if the album title matches + if (previousAudioFile.AlbumTitle != audioFile.AlbumTitle) + { + // Set album cover start index (+1 because the last song was the sound found in the previous loop iteration) + albumCoverStartIndex = c + 1; + break; + } + } + // Find the end index + for (int c = a + 1; c < items.Count; c++) + { + // Get audio file + AudioFile nextAudioFile = items[c].AudioFile; + + // If the album title is different, this means we found the next album title + if (nextAudioFile.AlbumTitle != audioFile.AlbumTitle) + { + // Set album cover end index (-1 because the last song was the song found in the previous loop iteration) + albumCoverEndIndex = c - 1; + break; + } + // If this is the last item of the grid... + else if (c == items.Count - 1) + { + // Set album cover end index as the last item of the grid + albumCoverEndIndex = c; + break; + } + } + + // Calculate y and height + int scrollbarOffsetY = (_startLineNumber * _songCache.LineHeight) - _vScrollBar.Value; + int y = ((albumCoverStartIndex - _startLineNumber) * _songCache.LineHeight) + _songCache.LineHeight + scrollbarOffsetY; + + // Calculate the height of the album cover zone (+1 on end index because the array is zero-based) + int albumCoverZoneHeight = (albumCoverEndIndex + 1 - albumCoverStartIndex) * _songCache.LineHeight; + + int heightWithPadding = albumCoverZoneHeight - (theme.Padding * 2); + if (heightWithPadding > _songCache.ActiveColumns[0].Width - (theme.Padding * 2)) + { + heightWithPadding = _songCache.ActiveColumns[0].Width - (theme.Padding * 2); + } + + // Make sure the height is at least zero (not necessary to draw anything!) + if (albumCoverZoneHeight > 0) + { + // Draw album cover background + Rectangle rectAlbumCover = new Rectangle(0 - _hScrollBar.Value, y, _songCache.ActiveColumns[0].Width, albumCoverZoneHeight); + brushGradient = new LinearGradientBrush(rectAlbumCover, theme.AlbumCoverBackgroundGradient.Color1, theme.AlbumCoverBackgroundGradient.Color2, theme.AlbumCoverBackgroundGradient.GradientMode); + g.FillRectangle(brushGradient, rectAlbumCover); + brushGradient.Dispose(); + brushGradient = null; + + // Try to extract image from cache + Image imageAlbumCover = null; + SongGridViewImageCache cachedImage = _imageCache.FirstOrDefault(x => x.Key == audioFile.ArtistName + "_" + audioFile.AlbumTitle); + if (cachedImage != null) + { + // Set image + imageAlbumCover = cachedImage.Image; + } + + // Album art not found in cache; try to find an album cover in one of the file + if (cachedImage == null) + { + // Check if the album cover is already in the pile + bool albumCoverFound = false; + foreach (SongGridViewBackgroundWorkerArgument arg in _workerUpdateAlbumArtPile) + { + // Match by file path + if (arg.AudioFile.FilePath.ToUpper() == audioFile.FilePath.ToUpper()) + { + // We found the album cover + albumCoverFound = true; + } + } + + // Add to the pile only if the album cover isn't already in it + if (!albumCoverFound) + { + // Add item to update album art worker pile + SongGridViewBackgroundWorkerArgument arg = new SongGridViewBackgroundWorkerArgument(); + arg.AudioFile = audioFile; + arg.LineIndex = a; + _workerUpdateAlbumArtPile.Add(arg); + } + } + + // Measure available width for text + int widthAvailableForText = columns[0].Width - (theme.Padding * 2); + + // Display titles depending on if an album art was found + RectangleF rectAlbumCoverArt = new RectangleF(); + RectangleF rectAlbumTitleText = new RectangleF(); + RectangleF rectArtistNameText = new RectangleF(); + SizeF sizeAlbumTitle = new SizeF(); + SizeF sizeArtistName = new SizeF(); + bool useAlbumArtOverlay = false; + + // If there's only one line, we need to do place the artist name and album title on one line + if (albumCoverEndIndex - albumCoverStartIndex == 0) + { + // Set string format + stringFormat.Alignment = StringAlignment.Near; + stringFormat.Trimming = StringTrimming.EllipsisCharacter; + + // Measure strings + sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, widthAvailableForText, stringFormat); + sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, widthAvailableForText - (int)sizeArtistName.Width, stringFormat); + + // Display artist name at full width first, then album name + rectArtistNameText = new RectangleF(theme.Padding - _hScrollBar.Value, y + (theme.Padding / 2), widthAvailableForText, _songCache.LineHeight); + rectAlbumTitleText = new RectangleF(theme.Padding - _hScrollBar.Value + sizeArtistName.Width + theme.Padding, y + (theme.Padding / 2), widthAvailableForText - sizeArtistName.Width, _songCache.LineHeight); + } + else + { + // There are at least two lines; is there an album cover to display? + if (imageAlbumCover == null) + { + // There is no album cover to display; display only text. + // Set string format + stringFormat.Alignment = StringAlignment.Center; + stringFormat.Trimming = StringTrimming.EllipsisWord; + + // Measure strings + sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, widthAvailableForText, stringFormat); + sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, widthAvailableForText, stringFormat); + + // Display the album title at the top of the zome + rectArtistNameText = new RectangleF(theme.Padding - _hScrollBar.Value, y + theme.Padding, widthAvailableForText, heightWithPadding); + rectAlbumTitleText = new RectangleF(theme.Padding - _hScrollBar.Value, y + theme.Padding + sizeArtistName.Height, widthAvailableForText, heightWithPadding); + } + else + { + // There is an album cover to display with more than 2 lines. + // Set string format + stringFormat.Alignment = StringAlignment.Near; + stringFormat.Trimming = StringTrimming.EllipsisWord; + + // Measure strings + sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, widthAvailableForText, stringFormat); + sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, widthAvailableForText, stringFormat); + + // If there's only two lines, display text on only two lines + if (albumCoverEndIndex - albumCoverStartIndex == 1) + { + // Display artist name on first line; display album title on second line + rectArtistNameText = new RectangleF(((columns[0].Width - sizeArtistName.Width) / 2) - _hScrollBar.Value, y, widthAvailableForText, heightWithPadding); + rectAlbumTitleText = new RectangleF(((columns[0].Width - sizeAlbumTitle.Width) / 2) - _hScrollBar.Value, y + _songCache.LineHeight, widthAvailableForText, heightWithPadding); + } + // There is an album cover to display; between 2 and 6 lines AND the width of the column is at least 50 pixels (or + // it will try to display text in a too thin area) + else if (albumCoverEndIndex - albumCoverStartIndex <= 5 && columns[0].Width > 175) + { + // There is no album cover to display; display only text. + // Set string format + stringFormat.Alignment = StringAlignment.Near; + stringFormat.Trimming = StringTrimming.EllipsisWord; + + float widthRemainingForText = columns[0].Width - theme.Padding - heightWithPadding; + + // Measure strings + sizeArtistName = g.MeasureString(audioFile.ArtistName, fontDefaultBold, new SizeF(widthRemainingForText, heightWithPadding), stringFormat); + sizeAlbumTitle = g.MeasureString(currentAlbumTitle, fontDefault, new SizeF(widthRemainingForText, heightWithPadding), stringFormat); + + // Try to center the cover art + padding + max text width + float maxWidth = sizeArtistName.Width; + if (sizeAlbumTitle.Width > maxWidth) + { + // Set new maximal width + maxWidth = sizeAlbumTitle.Width; + } + + useAlbumArtOverlay = true; + + float albumCoverX = (columns[0].Width - heightWithPadding - theme.Padding - theme.Padding - maxWidth) / 2; + float artistNameY = (albumCoverZoneHeight - sizeArtistName.Height - sizeAlbumTitle.Height) / 2; + + // Display the album title at the top of the zome + rectArtistNameText = new RectangleF(albumCoverX + heightWithPadding + theme.Padding - _hScrollBar.Value, y + artistNameY, widthRemainingForText, heightWithPadding); + rectAlbumTitleText = new RectangleF(albumCoverX + heightWithPadding + theme.Padding - _hScrollBar.Value, y + artistNameY + sizeArtistName.Height, widthRemainingForText, heightWithPadding); + + // Set cover art rectangle + rectAlbumCoverArt = new RectangleF(albumCoverX - _hScrollBar.Value, y + theme.Padding, heightWithPadding, heightWithPadding); + } + // 7 and more lines + else + { + // Display artist name at the top of the album cover; display album title at the bottom of the album cover + rectArtistNameText = new RectangleF(((columns[0].Width - sizeArtistName.Width) / 2) - _hScrollBar.Value, y + (theme.Padding * 2), widthAvailableForText, heightWithPadding); + rectAlbumTitleText = new RectangleF(((columns[0].Width - sizeAlbumTitle.Width) / 2) - _hScrollBar.Value, y + heightWithPadding - sizeAlbumTitle.Height, widthAvailableForText, heightWithPadding); + + // Draw background overlay behind text + useAlbumArtOverlay = true; + + // Try to horizontally center the album cover if it's not taking the whole width (less padding) + float albumCoverX = theme.Padding; + if (columns[0].Width > heightWithPadding) + { + // Get position + albumCoverX = ((float)(columns[0].Width - heightWithPadding) / 2) - _hScrollBar.Value; + } + + // Set cover art rectangle + rectAlbumCoverArt = new RectangleF(albumCoverX, y + theme.Padding, heightWithPadding, heightWithPadding); + } + + } + } + + // Display album cover + if (imageAlbumCover != null) + g.DrawImage(imageAlbumCover, rectAlbumCoverArt); + + if (useAlbumArtOverlay) + { + //// Draw artist name and album title background + //RectangleF rectArtistNameBackground = new RectangleF(rectArtistNameText.X - (theme.Padding / 2), rectArtistNameText.Y - (theme.Padding / 2), sizeArtistName.Width + theme.Padding, sizeArtistName.Height + theme.Padding); + //RectangleF rectAlbumTitleBackground = new RectangleF(rectAlbumTitleText.X - (theme.Padding / 2), rectAlbumTitleText.Y - (theme.Padding / 2), sizeAlbumTitle.Width + theme.Padding, sizeAlbumTitle.Height + theme.Padding); + //brush = new SolidBrush(Color.FromArgb(190, 0, 0, 0)); + //g.FillRectangle(brush, rectArtistNameBackground); + //g.FillRectangle(brush, rectAlbumTitleBackground); + //brush.Dispose(); + //brush = null; + } + + // Check if this is the artist name column (set font to bold) + //g.DrawString(audioFile.ArtistName, fontDefaultBold, Brushes.White, rectArtistNameText, stringFormat); + //g.DrawString(currentAlbumTitle, fontDefault, Brushes.White, rectAlbumTitleText, stringFormat); + + // Draw horizontal line to distinguish albums + // Part 1: Draw line over grid + //pen = new Pen(theme.AlbumCoverBackgroundGradient.Color1); + pen = new Pen(Color.FromArgb(180, 180, 180)); + g.DrawLine(pen, new Point(columns[0].Width, y), new Point(ClientRectangle.Width, y)); + pen.Dispose(); + pen = null; + + // Part 2: Draw line over album art zone, in a lighter color + pen = new Pen(Color.FromArgb(115, 115, 115)); + g.DrawLine(pen, new Point(0, y), new Point(columns[0].Width, y)); + pen.Dispose(); + pen = null; + } + } + + #endregion + } + else + { + // Get property through reflection + PropertyInfo propertyInfo = audioFile.GetType().GetProperty(column.FieldName); + if (propertyInfo != null) + { + // Get property value + string value = string.Empty; + try + { + // Determine the type of value + if (propertyInfo.PropertyType.FullName == "System.String") + { + // Try to get the value + value = propertyInfo.GetValue(audioFile, null).ToString(); + } + // Nullable Int64 + else if (propertyInfo.PropertyType.FullName.Contains("Int64") && + propertyInfo.PropertyType.FullName.Contains("Nullable")) + { + // Try to get the value + long? longValue = (long?)propertyInfo.GetValue(audioFile, null); + + // Check if null + if (longValue.HasValue) + { + // Render to string + value = longValue.Value.ToString(); + } + } + // Nullable DateTime + else if (propertyInfo.PropertyType.FullName.Contains("DateTime") && + propertyInfo.PropertyType.FullName.Contains("Nullable")) + { + // Try to get the value + DateTime? dateTimeValue = (DateTime?)propertyInfo.GetValue(audioFile, null); + + // Check if null + if (dateTimeValue.HasValue) + { + // Render to string + value = dateTimeValue.Value.ToShortDateString() + " " + dateTimeValue.Value.ToShortTimeString(); + } + } + else if (propertyInfo.PropertyType.FullName.Contains("System.UInt32")) + { + // Try to get the value + uint uintValue = (uint)propertyInfo.GetValue(audioFile, null); + + // Render to string + value = uintValue.ToString(); + } + else if (propertyInfo.PropertyType.FullName.Contains("System.Int32")) + { + // Try to get the value + int intValue = (int)propertyInfo.GetValue(audioFile, null); + + // Render to string + value = intValue.ToString(); + } + else if (propertyInfo.PropertyType.FullName.Contains("MPfm.Sound.AudioFileFormat")) + { + // Try to get the value + AudioFileFormat theValue = (AudioFileFormat)propertyInfo.GetValue(audioFile, null); + + // Render to string + value = theValue.ToString(); + } + else + { + // If the type of unknown, leave the value empty + } + } + catch + { + // Do nothing + } + + // The last column always take the remaining width + int columnWidth = column.Width; + if (b == _songCache.ActiveColumns.Count - 1) + { + // Calculate the remaining width + int columnsWidth = 0; + for (int c = 0; c < _songCache.ActiveColumns.Count - 1; c++) + { + columnsWidth += _songCache.ActiveColumns[c].Width; + } + columnWidth = ClientRectangle.Width - columnsWidth + _hScrollBar.Value; + } + + // Display text + rect = new Rectangle(offsetX - _hScrollBar.Value, offsetY + (theme.Padding / 2), _songCache.ActiveColumns[b].Width, _songCache.LineHeight - theme.Padding + 2); + stringFormat.Trimming = StringTrimming.EllipsisCharacter; + stringFormat.Alignment = StringAlignment.Near; + + // Check if this is the artist name column + brush = new SolidBrush(theme.RowTextGradient.Font.Color); + if (column.FieldName == "ArtistName" || column.FieldName == "DiscTrackNumber") + { + // Use bold for artist name + g.DrawString(value, fontDefaultBold, brush, rect, stringFormat); + } + else + { + // Use default font for the other columns + g.DrawString(value, fontDefault, brush, rect, stringFormat); + } + brush.Dispose(); + brush = null; + } + } + + // Increment offset by the column width + offsetX += column.Width; + } + } + } + + // If no songs are playing, set the current now playing rectangle as "empty" + if (!nowPlayingSongFound) + rectNowPlaying = new Rectangle(0, 0, 1, 1); + + // Draw header (for some reason, the Y must be set -1 to cover an area which isn't supposed to be displayed) + Rectangle rectBackgroundHeader = new Rectangle(0, -1, ClientRectangle.Width, _songCache.LineHeight + 1); + brushGradient = new LinearGradientBrush(rectBackgroundHeader, theme.HeaderTextGradient.Color1, theme.HeaderTextGradient.Color2, 90); + g.FillRectangle(brushGradient, rectBackgroundHeader); + brushGradient.Dispose(); + brushGradient = null; + + // Loop through columns + offsetX = 0; + for (int b = 0; b < _songCache.ActiveColumns.Count; b++) + { + SongGridViewColumn column = _songCache.ActiveColumns[b]; + if (column.Visible) + { + // The last column always take the remaining width + int columnWidth = column.Width; + if (b == _songCache.ActiveColumns.Count - 1) + { + // Calculate the remaining width + int columnsWidth = 0; + for (int c = 0; c < _songCache.ActiveColumns.Count - 1; c++) + columnsWidth += _songCache.ActiveColumns[c].Width; + columnWidth = ClientRectangle.Width - columnsWidth + _hScrollBar.Value; + } + + // Check if mouse is over this column header + if (column.IsMouseOverColumnHeader) + { + // Draw header (for some reason, the Y must be set -1 to cover an area which isn't supposed to be displayed) + rect = new Rectangle(offsetX - _hScrollBar.Value, -1, column.Width, _songCache.LineHeight + 1); + brushGradient = new LinearGradientBrush(rect, theme.HeaderHoverTextGradient.Color1, theme.HeaderHoverTextGradient.Color2, 90); + g.FillRectangle(brushGradient, rect); + brushGradient.Dispose(); + brushGradient = null; + } + else if (column.IsUserMovingColumn) + { + // Draw header (for some reason, the Y must be set -1 to cover an area which isn't supposed to be displayed) + rect = new Rectangle(offsetX - _hScrollBar.Value, -1, column.Width, _songCache.LineHeight + 1); + brushGradient = new LinearGradientBrush(rect, Color.Blue, Color.Green, 90); + g.FillRectangle(brushGradient, rect); + brushGradient.Dispose(); + brushGradient = null; + } + + // Check if the header title must be displayed + if (_songCache.ActiveColumns[b].IsHeaderTitleVisible) + { + // Display title + Rectangle rectTitle = new Rectangle(offsetX - _hScrollBar.Value, theme.Padding / 2, column.Width, _songCache.LineHeight - theme.Padding + 2); + stringFormat.Trimming = StringTrimming.EllipsisCharacter; + brush = new SolidBrush(theme.HeaderTextGradient.Font.Color); + g.DrawString(column.Title, fontDefaultBold, brush, rectTitle, stringFormat); + brush.Dispose(); + brush = null; + } + + // Draw column separator line; determine the height of the line + int columnHeight = ClientRectangle.Height; + + // Determine the height of the line; if the items don't fit the control height... + if (items.Count < _songCache.NumberOfLinesFittingInControl) + { + // Set height as the number of items (plus header) + columnHeight = (items.Count + 1) * _songCache.LineHeight; + } + + // Draw column line + //g.DrawLine(Pens.DarkGray, new Point(offsetX + column.Width - _hScrollBar.Value, 0), new Point(offsetX + column.Width - _hScrollBar.Value, columnHeight)); + + // Check if the column is ordered by + if (column.FieldName == orderByFieldName && !String.IsNullOrEmpty(column.FieldName)) + { + // Create triangle points,,, + PointF[] ptTriangle = new PointF[3]; + + // ... depending on the order by ascending value + int triangleWidthHeight = 8; + int trianglePadding = (_songCache.LineHeight - triangleWidthHeight) / 2; + if (orderByAscending) + { + // Create points for ascending + ptTriangle[0] = new PointF(offsetX + column.Width - triangleWidthHeight - (triangleWidthHeight / 2) - _hScrollBar.Value, trianglePadding); + ptTriangle[1] = new PointF(offsetX + column.Width - triangleWidthHeight - _hScrollBar.Value, _songCache.LineHeight - trianglePadding); + ptTriangle[2] = new PointF(offsetX + column.Width - triangleWidthHeight - triangleWidthHeight - _hScrollBar.Value, _songCache.LineHeight - trianglePadding); + } + else + { + // Create points for descending + ptTriangle[0] = new PointF(offsetX + column.Width - triangleWidthHeight - (triangleWidthHeight / 2) - _hScrollBar.Value, _songCache.LineHeight - trianglePadding); + ptTriangle[1] = new PointF(offsetX + column.Width - triangleWidthHeight - _hScrollBar.Value, trianglePadding); + ptTriangle[2] = new PointF(offsetX + column.Width - triangleWidthHeight - triangleWidthHeight - _hScrollBar.Value, trianglePadding); + } + + // Draw triangle + pen = new Pen(theme.HeaderTextGradient.Font.Color); + g.DrawPolygon(pen, ptTriangle); + pen.Dispose(); + pen = null; + } + + // Increment offset by the column width + offsetX += column.Width; + } + } + + // Display column move marker + if (IsColumnMoving) + { + // Draw marker + pen = new Pen(Color.Red); + g.DrawRectangle(pen, new Rectangle(_columnMoveMarkerX - _hScrollBar.Value, 0, 1, ClientRectangle.Height)); + pen.Dispose(); + pen = null; + } + + // Display debug information if enabled + if (displayDebugInformation) + { + // Build debug string + StringBuilder sbDebug = new StringBuilder(); + sbDebug.AppendLine("Line Count: " + items.Count.ToString()); + sbDebug.AppendLine("Line Height: " + _songCache.LineHeight.ToString()); + sbDebug.AppendLine("Lines Fit In Height: " + _songCache.NumberOfLinesFittingInControl.ToString()); + sbDebug.AppendLine("Total Width: " + _songCache.TotalWidth); + sbDebug.AppendLine("Total Height: " + _songCache.TotalHeight); + sbDebug.AppendLine("Scrollbar Offset Y: " + _songCache.ScrollBarOffsetY); + sbDebug.AppendLine("HScrollbar Maximum: " + _hScrollBar.Maximum.ToString()); + sbDebug.AppendLine("HScrollbar LargeChange: " + _hScrollBar.LargeChange.ToString()); + sbDebug.AppendLine("HScrollbar Value: " + _hScrollBar.Value.ToString()); + sbDebug.AppendLine("VScrollbar Maximum: " + _vScrollBar.Maximum.ToString()); + sbDebug.AppendLine("VScrollbar LargeChange: " + _vScrollBar.LargeChange.ToString()); + sbDebug.AppendLine("VScrollbar Value: " + _vScrollBar.Value.ToString()); + + // Measure string + stringFormat.Trimming = StringTrimming.Word; + stringFormat.LineAlignment = StringAlignment.Near; + SizeF sizeDebugText = g.MeasureString(sbDebug.ToString(), fontDefault, columns[0].Width - 1, stringFormat); + rectF = new RectangleF(0, 0, columns[0].Width - 1, sizeDebugText.Height); + + // Draw background + brush = new SolidBrush(Color.FromArgb(200, 0, 0, 0)); + g.FillRectangle(brush, rectF); + brush.Dispose(); + brush = null; + + // Draw string + g.DrawString(sbDebug.ToString(), fontDefault, Brushes.White, rectF, stringFormat); + } + + // If both scrollbars are visible... + if (_hScrollBar.Visible && _vScrollBar.Visible) + { + // Draw a bit of control color over the 16x16 area in the lower right corner + brush = new SolidBrush(SystemColors.Control); + g.FillRectangle(brush, new Rectangle(ClientRectangle.Width - 16, ClientRectangle.Height - 16, 16, 16)); + brush.Dispose(); + brush = null; + } + } + + #endregion + + #region Other Events + + /// + /// Occurs when the control is resized. + /// Invalidates the cache. + /// + /// Event arguments + protected override void OnResize(EventArgs e) + { + // Bug when putting window in maximized mode: black area at the bottom + // it's because the maximum value is set later in OnPaint + // If the scrollY value is + if (_vScrollBar.Maximum - _vScrollBar.LargeChange < _vScrollBar.Value) + { + // Set new scrollbar value + _vScrollBar.Value = _vScrollBar.Maximum - _vScrollBar.LargeChange + 1; + } + + // Set horizontal scrollbar width and position + _hScrollBar.Top = ClientRectangle.Height - _hScrollBar.Height; + + InvalidateSongCache(); + + base.OnResize(e); + } + + /// + /// Occurs when the user changes the horizontal scrollbar value. + /// + /// Event sender + /// Event arguments + protected void hScrollBar_Scroll(object sender, ScrollEventArgs e) + { + Refresh(); + } + + /// + /// Occurs when the user changes the vertical scrollbar value. + /// + /// Event sender + /// Event arguments + protected void vScrollBar_Scroll(object sender, ScrollEventArgs e) + { + Refresh(); + } + + #endregion + + #region Mouse Events + + /// + /// Occurs when the mouse cursor enters on the control. + /// + /// Event arguments + protected override void OnMouseEnter(EventArgs e) + { + isMouseOverControl = true; + base.OnMouseEnter(e); + } + + /// + /// Occurs when the mouse cursor leaves the control. + /// + /// Event arguments + protected override void OnMouseLeave(EventArgs e) + { + bool controlNeedsToBeUpdated = false; + isMouseOverControl = false; + + if (columns == null || _songCache == null) + return; + + // Calculate scrollbar offset Y + int scrollbarOffsetY = (_startLineNumber * _songCache.LineHeight) - _vScrollBar.Value; + + // Check if there's at least one item + if (items.Count > 0) + { + // Reset mouse over item flags + for (int b = _startLineNumber; b < _startLineNumber + numberOfLinesToDraw; b++) + { + // Check if the mouse was over this item + if (items[b].IsMouseOverItem) + { + // Reset flag and invalidate region + items[b].IsMouseOverItem = false; + Invalidate(new Rectangle(columns[0].Width - _hScrollBar.Value, ((b - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - columns[0].Width + _hScrollBar.Value, _songCache.LineHeight)); + controlNeedsToBeUpdated = true; + + // Exit loop + break; + } + } + } + + // Reset column flags + int columnOffsetX2 = 0; + for (int b = 0; b < _songCache.ActiveColumns.Count; b++) + { + // Make sure column is visible + if (_songCache.ActiveColumns[b].Visible) + { + // Was mouse over this column header? + if (_songCache.ActiveColumns[b].IsMouseOverColumnHeader) + { + // Reset flag + _songCache.ActiveColumns[b].IsMouseOverColumnHeader = false; + + // Invalidate region + Invalidate(new Rectangle(columnOffsetX2 - _hScrollBar.Value, 0, _songCache.ActiveColumns[b].Width, _songCache.LineHeight)); + controlNeedsToBeUpdated = true; + } + + // Increment offset + columnOffsetX2 += _songCache.ActiveColumns[b].Width; + } + } + + // Check if control needs to be updated + if (controlNeedsToBeUpdated) + Update(); + + base.OnMouseLeave(e); + } + + /// + /// Occurs when the user is pressing down a mouse button. + /// + /// Event arguments + protected override void OnMouseDown(MouseEventArgs e) + { + // Set flags + dragStartX = e.X; + + // Check if all the data is valid + if (columns == null || _songCache == null) + { + return; + } + + // Loop through columns + foreach (SongGridViewColumn column in _songCache.ActiveColumns) + { + // Check for resizing column + if (column.IsMouseCursorOverColumnLimit && column.CanBeResized && CanResizeColumns) + { + // Set resizing column flag + column.IsUserResizingColumn = true; + + // Save the original column width + dragOriginalColumnWidth = column.Width; + } + } + + // Check if the left mouse button is held + if(e.Button == System.Windows.Forms.MouseButtons.Left) + { + // Set holding button flag + isUserHoldingLeftMouseButton = true; + } + + base.OnMouseDown(e); + } + + /// + /// Occurs when the user releases a mouse button. + /// + /// Event arguments + protected override void OnMouseUp(MouseEventArgs e) + { + // Reset flags + dragStartX = -1; + bool updateControl = false; + isUserHoldingLeftMouseButton = false; + + if (columns == null || _songCache == null) + return; + + // Loop through columns + SongGridViewColumn columnMoving = null; + foreach (SongGridViewColumn column in _songCache.ActiveColumns) + { + // Reset flags + column.IsUserResizingColumn = false; + + // Check if this column is moving + if(column.IsUserMovingColumn) + { + // Set column + columnMoving = column; + } + } + + // Check if the user is moving a column + if (columnMoving != null) + { + // Set flag + columnMoving.IsUserMovingColumn = false; + updateControl = true; + + // Find out on what column the mouse cursor is + SongGridViewColumn columnOver = null; + int x = 0; + bool isPastCurrentlyMovingColumn = false; + for (int a = 0; a < _songCache.ActiveColumns.Count; a++ ) + { + // Set current column + SongGridViewColumn currentColumn = _songCache.ActiveColumns[a]; + + // Check if current column + if (currentColumn.FieldName == columnMoving.FieldName) + { + // Set flag + isPastCurrentlyMovingColumn = true; + } + + // Check if the column is visible + if (currentColumn.Visible) + { + // Check if the cursor is over the left part of the column + if (e.X >= x - _hScrollBar.Value && e.X <= x + (currentColumn.Width / 2) - _hScrollBar.Value) + { + // Check flag + if (isPastCurrentlyMovingColumn && currentColumn.FieldName != columnMoving.FieldName) + { + // Set column + columnOver = _songCache.ActiveColumns[a - 1]; + } + else + { + // Set column + columnOver = _songCache.ActiveColumns[a]; + } + break; + } + // Check if the cursor is over the right part of the column + else if (e.X >= x + (currentColumn.Width / 2) - _hScrollBar.Value && e.X <= x + currentColumn.Width - _hScrollBar.Value) + { + // Check if there is a next item + if (a < _songCache.ActiveColumns.Count - 1) + { + // Check flag + if (isPastCurrentlyMovingColumn) + { + // Set column + columnOver = _songCache.ActiveColumns[a]; + } + else + { + // Set column + columnOver = _songCache.ActiveColumns[a + 1]; + } + } + + break; + } + + // Increment x + x += currentColumn.Width; + } + } + + //// Check if the column was found (the cursor might be past the last column + //if (columnOver == null) + //{ + // return; + //} + + // Order columns by their current order + List columnsOrdered = columns.OrderBy(q => q.Order).ToList(); + + // Move column + int indexRemove = -1; + int indexAdd = -1; + for (int a = 0; a < columnsOrdered.Count; a++) + { + // Find the moving column index + if (columnsOrdered[a].FieldName == columnMoving.FieldName) + { + // Set index + indexRemove = a; + } + + // Find the column index with the mouse over + if(columnOver != null && columnsOrdered[a].FieldName == columnOver.FieldName) + { + // Set index + indexAdd = a; + } + } + + // Remove column + columnsOrdered.RemoveAt(indexRemove); + + // Check if the index is -1 + if (indexAdd == -1) + { + // Add column to the end + columnsOrdered.Insert(columnsOrdered.Count, columnMoving); + } + else + { + // Add column to the new position + columnsOrdered.Insert(indexAdd, columnMoving); + } + + // Loop through columns to change the order + for (int a = 0; a < columnsOrdered.Count; a++) + { + // Set order + columnsOrdered[a].Order = a; + } + } + + // Check if the control needs to be updated + if (updateControl) + { + // Invalidate control and refresh + InvalidateSongCache(); + Invalidate(); + Update(); + } + + base.OnMouseUp(e); + } + + /// + /// Occurs when the user clicks on the control. + /// Selects or unselects items, reorders columns. + /// + /// Event arguments + protected override void OnMouseClick(MouseEventArgs e) + { + if (columns == null || _songCache == null) + return; + + // Calculate album cover art width + int albumArtCoverWidth = columns[0].Visible ? columns[0].Width : 0; + + // Make sure the control is focused + if (!Focused) + Focus(); + + // Show context menu strip if the button click is right and not the album art column + if (e.Button == System.Windows.Forms.MouseButtons.Right && + e.X > columns[0].Width) + { + // Is there a context menu strip configured? + if (contextMenuStrip != null) + contextMenuStrip.Show(Control.MousePosition.X, Control.MousePosition.Y); + } + + // Check if the user is resizing a column + SongGridViewColumn columnResizing = columns.FirstOrDefault(x => x.IsUserResizingColumn == true); + + // Calculate scrollbar offset Y + int scrollbarOffsetY = (_startLineNumber * _songCache.LineHeight) - _vScrollBar.Value; + + // Check if the user has clicked on the header (for orderBy) + if (e.Y >= 0 && + e.Y <= _songCache.LineHeight && + columnResizing == null && + !IsColumnMoving) + { + // Check on what column the user has clicked + int offsetX = 0; + for (int a = 0; a < _songCache.ActiveColumns.Count; a++) + { + SongGridViewColumn column = _songCache.ActiveColumns[a]; + if (column.Visible) + { + // Check if the mouse pointer is over this column + if (e.X >= offsetX - _hScrollBar.Value && e.X <= offsetX + column.Width - _hScrollBar.Value) + { + // Check mouse button + if (e.Button == System.Windows.Forms.MouseButtons.Left && CanChangeOrderBy) + { + // Check if the column order was already set + if (orderByFieldName == column.FieldName) + { + // Reverse ascending/descending + orderByAscending = !orderByAscending; + } + else + { + // Set order by field name + orderByFieldName = column.FieldName; + orderByAscending = true; + } + + // Invalidate cache and song items + items = null; + _songCache = null; + + // Raise column click event (if an event is subscribed) + if (OnColumnClick != null) + { + SongGridViewColumnClickData data = new SongGridViewColumnClickData(); + data.ColumnIndex = a; + OnColumnClick(data); + } + + Refresh(); + return; + } + else if (e.Button == System.Windows.Forms.MouseButtons.Right) + { + // Refresh column visibility in menu before opening + foreach(ToolStripMenuItem menuItem in _menuColumns.Items) + { + SongGridViewColumn menuItemColumn = columns.FirstOrDefault(x => x.Title == menuItem.Tag.ToString()); + if (menuItemColumn != null) + menuItem.Checked = menuItemColumn.Visible; + } + + // Display columns contextual menu + _menuColumns.Show(this, e.X, e.Y); + } + } + + // Increment X offset + offsetX += column.Width; + } + } + } + + // Loop through visible lines to find the original selected items + int startIndex = -1; + int endIndex = -1; + for (int a = _startLineNumber; a < _startLineNumber + numberOfLinesToDraw; a++) + { + // Check if the item is selected + if (items[a].IsSelected) + { + // Check if the start index was set + if (startIndex == -1) + { + // Set start index + startIndex = a; + } + // Check if the end index is set or if it needs to be updated + if (endIndex == -1 || endIndex < a) + { + // Set end index + endIndex = a; + } + } + } + + // Make sure the indexes are set + if (startIndex > -1 && endIndex > -1) + { + // Invalidate the original selected lines + int startY = ((startIndex - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY; + int endY = ((endIndex - _startLineNumber + 2) * _songCache.LineHeight) + scrollbarOffsetY; + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, startY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, endY - startY)); + } + + // Reset selection (make sure SHIFT or CTRL isn't held down) + if ((Control.ModifierKeys & Keys.Shift) == 0 && + (Control.ModifierKeys & Keys.Control) == 0) + { + SongGridViewItem mouseOverItem = items.FirstOrDefault(x => x.IsMouseOverItem == true); + if (mouseOverItem != null) + { + // Reset selection, unless the CTRL key is held (TODO) + List selectedItems = items.Where(x => x.IsSelected == true).ToList(); + foreach (SongGridViewItem item in selectedItems) + item.IsSelected = false; + } + } + + // Loop through visible lines to update the new selected item + bool invalidatedNewSelection = false; + for (int a = _startLineNumber; a < _startLineNumber + numberOfLinesToDraw; a++) + { + // Check if mouse is over this item + if (items[a].IsMouseOverItem) + { + invalidatedNewSelection = true; + + // Check if SHIFT is held + if ((Control.ModifierKeys & Keys.Shift) != 0) + { + // Find the start index of the selection + int startIndexSelection = lastItemIndexClicked; + if (a < startIndexSelection) + startIndexSelection = a; + + // Find the end index of the selection + int endIndexSelection = lastItemIndexClicked; + if (a > endIndexSelection) + endIndexSelection = a + 1; + + // Loop through items to selected + for (int b = startIndexSelection; b < endIndexSelection; b++) + items[b].IsSelected = true; + + // Invalidate region + Invalidate(); + } + // Check if CTRL is held + else if ((Control.ModifierKeys & Keys.Control) != 0) + { + // Invert selection + items[a].IsSelected = !items[a].IsSelected; + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, ((a - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + } + else + { + // Set this item as the new selected item + items[a].IsSelected = true; + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, ((a - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + } + + // Set the last item clicked index + lastItemIndexClicked = a; + + // Exit loop + break; + } + } + + // Raise selected item changed event (if an event is subscribed) + if (invalidatedNewSelection && OnSelectedIndexChanged != null) + { + SongGridViewSelectedIndexChangedData data = new SongGridViewSelectedIndexChangedData(); + OnSelectedIndexChanged(data); + } + + Update(); + + base.OnMouseClick(e); + } + + /// + /// Occurs when the user double-clicks on the control. + /// Starts the playback of a new song. + /// + /// Event arguments + protected override void OnMouseDoubleClick(MouseEventArgs e) + { + if (columns == null || _songCache == null) + return; + + // Calculate album cover art width + int albumArtCoverWidth = columns[0].Visible ? columns[0].Width : 0; + + // Calculate scrollbar offset Y + int scrollbarOffsetY = (_startLineNumber * _songCache.LineHeight) - _vScrollBar.Value; + + // Keep original songId in case the now playing value is set before invalidating the older value + Guid originalId = Guid.Empty; + + // Set original id + if (_mode == SongGridViewMode.AudioFile) + originalId = nowPlayingAudioFileId; + else if (_mode == SongGridViewMode.Playlist) + originalId = nowPlayingPlaylistItemId; + + // Loop through visible lines + for (int a = _startLineNumber; a < _startLineNumber + numberOfLinesToDraw; a++) + { + // Check if mouse is over this item + if (items[a].IsMouseOverItem) + { + // Set this item as the new now playing + nowPlayingAudioFileId = items[a].AudioFile.Id; + nowPlayingPlaylistItemId = items[a].PlaylistItemId; + + // Invalidate region + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, ((a - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + } + else if (_mode == SongGridViewMode.AudioFile && items[a].AudioFile.Id == originalId) + { + // Invalidate region + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, ((a - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + } + else if (_mode == SongGridViewMode.Playlist && items[a].PlaylistItemId == originalId) + { + // Invalidate region + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, ((a - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + } + } + + // Update invalid regions + Update(); + + base.OnMouseDoubleClick(e); + } + + /// + /// Occurs when the mouse pointer is moving over the control. + /// Manages the display of mouse on/off visual effects. + /// + /// Event arguments + protected override void OnMouseMove(MouseEventArgs e) + { + bool controlNeedsToBeUpdated = false; + + if (columns == null || _songCache == null) + return; + + // Calculate album cover art width + int albumArtCoverWidth = columns[0].Visible ? columns[0].Width : 0; + + // Check if the user is currently resizing a column (loop through columns) + foreach (SongGridViewColumn column in _songCache.ActiveColumns) + { + // Check if the user is currently resizing this column + if (column.IsUserResizingColumn && column.Visible) + { + // Calculate the new column width + int newWidth = dragOriginalColumnWidth - (dragStartX - e.X); + + // Make sure the width isn't lower than the minimum width + if (newWidth < minimumColumnWidth) + newWidth = minimumColumnWidth; + + // Set column width + column.Width = newWidth; + + // Refresh control (invalidate whole control) + controlNeedsToBeUpdated = true; + Invalidate(); + InvalidateSongCache(); + + // Auto adjust horizontal scrollbar value if it exceeds the value range (i.e. do not show empty column) + if (_hScrollBar.Value > _hScrollBar.Maximum - _hScrollBar.LargeChange) + { + // Set new value + int tempValue = _hScrollBar.Maximum - _hScrollBar.LargeChange; + if (tempValue < 0) + tempValue = 0; + _hScrollBar.Value = tempValue; + } + } + + // Check if the user is moving the column + if (column.IsMouseOverColumnHeader && column.CanBeMoved && CanMoveColumns && isUserHoldingLeftMouseButton && !IsColumnResizing) + { + // Check if the X position has changed by at least 2 pixels (i.e. dragging) + if (dragStartX >= e.X + 2 || + dragStartX <= e.X - 2) + { + // Set resizing column flag + column.IsUserMovingColumn = true; + } + } + + // Check if the user is currently moving this column + if (column.IsUserMovingColumn) + { + // Loop through columns + int x = 0; + foreach (SongGridViewColumn columnOver in _songCache.ActiveColumns) + { + // Check if column is visible + if (columnOver.Visible) + { + // Check if the cursor is over the left part of the column + if (e.X >= x - _hScrollBar.Value && e.X <= x + (columnOver.Width / 2) - _hScrollBar.Value) + _columnMoveMarkerX = x; + // Check if the cursor is over the right part of the column + else if (e.X >= x + (columnOver.Width / 2) - _hScrollBar.Value && e.X <= x + columnOver.Width - _hScrollBar.Value) + _columnMoveMarkerX = x + columnOver.Width; + + x += columnOver.Width; + } + } + + Invalidate(); + controlNeedsToBeUpdated = true; + } + } + + if (!IsColumnMoving) + { + // Check if the cursor needs to be changed + int offsetX = 0; + bool mousePointerIsOverColumnLimit = false; + foreach (SongGridViewColumn column in _songCache.ActiveColumns) + { + if (column.Visible) + { + // Increment offset by the column width + offsetX += column.Width; + if (column.CanBeResized) + { + // Check if the mouse pointer is over a column (add 1 pixel so it's easier to select) + if (e.X >= offsetX - _hScrollBar.Value && e.X <= offsetX + 1 - _hScrollBar.Value) + { + mousePointerIsOverColumnLimit = true; + column.IsMouseCursorOverColumnLimit = true; + + // Change the cursor if it's not the right cursor + if (Cursor != Cursors.VSplit) + Cursor = Cursors.VSplit; + } + else + { + column.IsMouseCursorOverColumnLimit = false; + } + } + } + } + + // Check if the default cursor needs to be restored + if (!mousePointerIsOverColumnLimit && Cursor != Cursors.Default) + Cursor = Cursors.Default; + + int columnOffsetX2 = 0; + for (int b = 0; b < _songCache.ActiveColumns.Count; b++) + { + SongGridViewColumn column = _songCache.ActiveColumns[b]; + if (column.Visible) + { + // Was mouse over this column header? + if (column.IsMouseOverColumnHeader) + { + // Invalidate region + column.IsMouseOverColumnHeader = false; + Invalidate(new Rectangle(columnOffsetX2 - _hScrollBar.Value, 0, column.Width, _songCache.LineHeight)); + controlNeedsToBeUpdated = true; + } + + // Increment offset + columnOffsetX2 += column.Width; + } + } + + // Check if the mouse pointer is over the header + if (e.Y >= 0 && + e.Y <= _songCache.LineHeight) + { + // Check on what column the user has clicked + int columnOffsetX = 0; + for (int a = 0; a < _songCache.ActiveColumns.Count; a++) + { + SongGridViewColumn column = _songCache.ActiveColumns[a]; + if (column.Visible) + { + // Check if the mouse pointer is over this column + if (e.X >= columnOffsetX - _hScrollBar.Value && e.X <= columnOffsetX + column.Width - _hScrollBar.Value) + { + // Invalidate region + column.IsMouseOverColumnHeader = true; + Invalidate(new Rectangle(columnOffsetX - _hScrollBar.Value, 0, column.Width, _songCache.LineHeight)); + + // Exit loop + controlNeedsToBeUpdated = true; + break; + } + + columnOffsetX += column.Width; + } + } + } + + // Check if the mouse cursor is over a line (loop through lines) + int offsetY = 0; + int scrollbarOffsetY = (_startLineNumber * _songCache.LineHeight) - _vScrollBar.Value; + + // Check if there's at least one item + if (items.Count > 0) + { + // Reset mouse over item flags + for (int b = _startLineNumber; b < _startLineNumber + numberOfLinesToDraw; b++) + { + // Check if the mouse was over this item + if (items[b].IsMouseOverItem) + { + // Reset flag and invalidate region + items[b].IsMouseOverItem = false; + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, ((b - _startLineNumber + 1) * _songCache.LineHeight) + scrollbarOffsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + + // Exit loop + break; + } + } + + // Put new mouse over flag + for (int a = _startLineNumber; a < _startLineNumber + numberOfLinesToDraw; a++) + { + AudioFile audioFile = items[a].AudioFile; + + // Calculate offset + offsetY = (a * _songCache.LineHeight) - _vScrollBar.Value + _songCache.LineHeight; + + // Check if the mouse cursor is over this line (and not already mouse over) + if (e.X >= albumArtCoverWidth - _hScrollBar.Value && + e.Y >= offsetY && + e.Y <= offsetY + _songCache.LineHeight && + !items[a].IsMouseOverItem) + { + // Set item as mouse over + items[a].IsMouseOverItem = true; + + // Invalidate region and update control + Invalidate(new Rectangle(albumArtCoverWidth - _hScrollBar.Value, offsetY, ClientRectangle.Width - albumArtCoverWidth + _hScrollBar.Value, _songCache.LineHeight)); + controlNeedsToBeUpdated = true; + + // Exit loop + break; + } + } + } + } + + // Check if the control needs to be refreshed + if (controlNeedsToBeUpdated) + Update(); + + base.OnMouseMove(e); + } + + /// + /// Occurs when the user clicks on one of the menu items of the Columns contextual menu. + /// + /// Event sender + /// Event arguments + protected void menuItemColumns_Click(object sender, EventArgs e) + { + // Make sure the sender is the menu item + if (sender is ToolStripMenuItem) + { + // Get the reference to the menu item + ToolStripMenuItem menuItem = (ToolStripMenuItem)sender; + + // Inverse selection + menuItem.Checked = !menuItem.Checked; + + // Get column + SongGridViewColumn column = columns.FirstOrDefault(x => x.Title == menuItem.Tag.ToString()); + if (column != null) + { + // Set visibility + column.Visible = menuItem.Checked; + } + + // Reset cache + _songCache = null; + Refresh(); + } + } + + /// + /// Handles the global messages for the mouse wheel. + /// + /// Message + /// Nothing interesting + public bool PreFilterMessage(ref Message m) + { + // Check message type + if (m.Msg == Win32.WM_MOUSEWHEEL) + { + // Get mouse wheel delta + int delta = Win32.GetWheelDeltaWParam((int)m.WParam); + + // Check if all the data is valid + if (columns == null || _songCache == null) + { + return false; + } + + // Make sure the mouse cursor is over the control, and that the vertical scrollbar is enabled + if (!isMouseOverControl || !_vScrollBar.Enabled) + { + return false; + } + + // Get relative value + int value = delta / SystemInformation.MouseWheelScrollDelta; + + // Set new value + int newValue = _vScrollBar.Value + (-value * _songCache.LineHeight); + + // Check for maximum + if (newValue > _vScrollBar.Maximum - _vScrollBar.LargeChange) + { + newValue = _vScrollBar.Maximum - _vScrollBar.LargeChange; + } + // Check for minimum + if (newValue < 0) + { + newValue = 0; + } + + // Set scrollbar value + _vScrollBar.Value = newValue; + + // Invalidate the whole control and refresh + Refresh(); + } + + return false; + } + + #endregion + + /// + /// Creates a cache of values used for rendering the song grid view. + /// Also sets scrollbar position, height, value, maximum, etc. + /// + public void InvalidateSongCache() + { + // Check if columns have been created + if (columns == null || columns.Count == 0 || items == null) + { + return; + } + + // Create cache + _songCache = new SongGridViewCache(); + + // Get active columns and order them + _songCache.ActiveColumns = columns.Where(x => x.Order >= 0).OrderBy(x => x.Order).ToList(); + + // Load custom font + //Font fontDefaultBold = Tools.LoadCustomFont(FontCollection, CustomFontName, Font.Size, FontStyle.Bold); + Font fontDefaultBold = null; + + // Make sure the embedded font name needs to be loaded and is valid + if (theme.RowTextGradient.Font.UseEmbeddedFont && !String.IsNullOrEmpty(theme.RowTextGradient.Font.EmbeddedFontName)) + { + try + { + // Get embedded font collection + EmbeddedFontCollection embeddedFonts = EmbeddedFontHelper.GetEmbeddedFonts(); + + // Get embedded fonts + fontDefaultBold = Tools.LoadEmbeddedFont(embeddedFonts, theme.RowTextGradient.Font.EmbeddedFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); + } + catch + { + // Use default font instead + fontDefaultBold = new Font(this.Font, FontStyle.Bold); + } + } + + // Check if font is null + if (fontDefaultBold == null) + { + try + { + // Try to get standard font + fontDefaultBold = new Font(theme.RowTextGradient.Font.StandardFontName, theme.RowTextGradient.Font.Size, theme.RowTextGradient.Font.ToFontStyle() | FontStyle.Bold); + } + catch + { + // Use default font instead + fontDefaultBold = new Font(this.Font, FontStyle.Bold); + } + } + + //// Load default fonts if custom fonts were not found + //if (fontDefaultBold == null) + //{ + // // Load default font + // fontDefaultBold = new Font(Font.FontFamily.Name, Font.Size, FontStyle.Bold); + //} + + // Create temporary bitmap/graphics objects to measure a string (to determine line height) + Bitmap bmpTemp = new Bitmap(200, 100); + Graphics g = Graphics.FromImage(bmpTemp); + SizeF sizeFont = g.MeasureString("QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm!@#$%^&*()", fontDefaultBold); + g.Dispose(); + g = null; + bmpTemp.Dispose(); + bmpTemp = null; + + // Calculate the line height (try to measure the total possible height of characters using the custom or default font) + _songCache.LineHeight = (int)sizeFont.Height + theme.Padding; + _songCache.TotalHeight = _songCache.LineHeight * items.Count; + + // Check if the total active columns width exceed the width available in the control + _songCache.TotalWidth = 0; + for (int a = 0; a < _songCache.ActiveColumns.Count; a++) + { + // Check if column is visible + if (_songCache.ActiveColumns[a].Visible) + { + // Increment total width + _songCache.TotalWidth += _songCache.ActiveColumns[a].Width; + } + } + + // Calculate the number of lines visible (count out the header, which is one line height) + _songCache.NumberOfLinesFittingInControl = (int)Math.Floor((double)(ClientRectangle.Height) / (double)(_songCache.LineHeight)); + + // Set vertical scrollbar dimensions + _vScrollBar.Top = _songCache.LineHeight; + _vScrollBar.Left = ClientRectangle.Width - _vScrollBar.Width; + _vScrollBar.Minimum = 0; + + // Scrollbar maximum is the number of lines fitting in the screen + the remaining line which might be cut + // by the control height because it's not a multiple of line height (i.e. the last line is only partly visible) + int lastLineHeight = ClientRectangle.Height - (_songCache.LineHeight * _songCache.NumberOfLinesFittingInControl); + + // Check width + if (_songCache.TotalWidth > ClientRectangle.Width - _vScrollBar.Width) + { + // Set scrollbar values + _hScrollBar.Maximum = _songCache.TotalWidth; + _hScrollBar.SmallChange = 5; + _hScrollBar.LargeChange = ClientRectangle.Width; + + // Show scrollbar + _hScrollBar.Visible = true; + } + + // Check if the horizontal scrollbar needs to be turned off + if (_songCache.TotalWidth <= ClientRectangle.Width - _vScrollBar.Width && _hScrollBar.Visible) + { + // Hide the horizontal scrollbar + _hScrollBar.Visible = false; + } + + // If there are less items than items fitting on screen... + if (((_songCache.NumberOfLinesFittingInControl - 1) * _songCache.LineHeight) - _hScrollBar.Height >= _songCache.TotalHeight) + { + // Disable the scrollbar + _vScrollBar.Enabled = false; + _vScrollBar.Value = 0; + } + else + { + // Set scrollbar values + _vScrollBar.Enabled = true; + + // The real large change needs to be added to the LargeChange and Maximum property in order to work. + int realLargeChange = _songCache.LineHeight * 5; + + // Calculate the vertical scrollbar maximum + int vMax = _songCache.LineHeight * (items.Count - _songCache.NumberOfLinesFittingInControl + 1) - lastLineHeight + realLargeChange; + + // Add the horizontal scrollbar height if visible + if (_hScrollBar.Visible) + { + // Add height + vMax += _hScrollBar.Height; + } + + // Compensate for the header, and for the last line which might be truncated by the control height + _vScrollBar.Maximum = vMax; + _vScrollBar.SmallChange = _songCache.LineHeight; + _vScrollBar.LargeChange = 1 + realLargeChange; + } + + // Calculate the scrollbar offset Y + _songCache.ScrollBarOffsetY = (_startLineNumber * _songCache.LineHeight) - _vScrollBar.Value; + + // If both scrollbars need to be visible, the width and height must be changed + if (_hScrollBar.Visible && _vScrollBar.Visible) + { + // Cut 16 pixels + _hScrollBar.Width = ClientRectangle.Width - 16; + _vScrollBar.Height = ClientRectangle.Height - _songCache.LineHeight - 16; + } + else + { + _vScrollBar.Height = ClientRectangle.Height - _songCache.LineHeight; + } + } + + /// + /// This timer triggers the animation of the currently playing song. + /// + /// Event sender + /// Event arguments + protected void timerAnimationNowPlaying_Tick(object sender, EventArgs e) + { + // If the rectangle is "empty", do not trigger invalidation + if (rectNowPlaying.X == 0 && + rectNowPlaying.Y == 0 && + rectNowPlaying.Width == 1 && + rectNowPlaying.Height == 1) + { + return; + } + + // Increment counter + timerAnimationNowPlayingCount += 10; + + // Invalidate region for now playing + Invalidate(rectNowPlaying); + Update(); // This is necessary after an invalidate. + } + } + + /// + /// Result data structure used for the SongGridView background worker. + /// + public class SongGridViewBackgroundWorkerResult + { + /// + /// Audio file. + /// + public AudioFile AudioFile { get; set; } + /// + /// Album cover. + /// + public Image AlbumArt { get; set; } + } + + /// + /// Argument data structure used for the SongGridView background worker. + /// + public class SongGridViewBackgroundWorkerArgument + { + /// + /// Audio file. + /// + public AudioFile AudioFile { get; set; } + /// + /// Line number (item index in the SongGridView control). + /// + public int LineIndex { get; set; } + } + + /// + /// Data structure used for the SelectedIndexChanged event. + /// + public class SongGridViewSelectedIndexChangedData + { + } + + /// + /// Data structure used for the ColumnClick event. + /// + public class SongGridViewColumnClickData + { + /// + /// Column index. + /// + public int ColumnIndex { get; set; } + } + +} diff --git a/MPfm/MPfm.WindowsControls/Misc/Gradient.cs b/MPfm/MPfm.WindowsControls/Misc/Gradient.cs index 9a47fce6..9d02cc2d 100644 --- a/MPfm/MPfm.WindowsControls/Misc/Gradient.cs +++ b/MPfm/MPfm.WindowsControls/Misc/Gradient.cs @@ -1,162 +1,162 @@ -// Copyright © 2011-2013 Yanick Castonguay -// -// This file is part of MPfm. -// -// MPfm is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// MPfm is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with MPfm. If not, see . - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Design; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Drawing.Text; -using System.IO; -using System.Reflection; -using System.Text; -using System.Windows.Forms; -using System.Xml; -using System.Xml.Linq; -using System.Xml.Serialization; -using System.ComponentModel; - -namespace MPfm.WindowsControls -{ - /// - /// The Gradient class groups different properties related to gradients into one object. - /// - [TypeConverter(typeof(ExpandableObjectConverter))] - public class Gradient - { - /// - /// Private value for the Color1 property. - /// - private Color color1 = Color.FromArgb(0, 0, 0); - /// - /// Defines the first color of the gradient. - /// - [XmlIgnore] - [RefreshProperties(RefreshProperties.Repaint)] - [Category("Gradient"), Browsable(true), Description("Defines the first color of the gradient.")] - public Color Color1 - { - get - { - return color1; - } - set - { - color1 = value; - } - } - /// - /// Gets/sets the first color of the gradient using a 32-bit integer (ARGB). - /// This is used for serializing the Color structure in XML. - /// - [Browsable(false)] - [XmlElement(ElementName = "Color1")] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public int Color1Int - { - get - { - return color1.ToArgb(); - } - set - { - color1 = Color.FromArgb(value); - } - } - - /// - /// Private value for the Color2 property. - /// - private Color color2 = Color.FromArgb(0, 0, 0); - /// - /// Defines the second color of the gradient. - /// - [XmlIgnore] - [RefreshProperties(RefreshProperties.Repaint)] - [Category("Gradient"), Browsable(true), Description("Defines the second color of the gradient.")] - public Color Color2 - { - get - { - return color2; - } - set - { - color2 = value; - } - } - /// - /// Gets/sets the second color of the gradient using a 32-bit integer (ARGB). - /// This is used for serializing the Color structure in XML. - /// - [Browsable(false)] - [XmlElement(ElementName = "Color2")] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public int Color2Int - { - get - { - return color2.ToArgb(); - } - set - { - color2 = Color.FromArgb(value); - } - } - - /// - /// Private value for the GradientMode property. - /// - private LinearGradientMode gradientMode = LinearGradientMode.Vertical; - /// - /// Defines the gradient mode. - /// - [Category("Gradient"), Browsable(true), Description("Defines the gradient mode.")] - public LinearGradientMode GradientMode - { - get - { - return gradientMode; - } - set - { - gradientMode = value; - } - } - - /// - /// Default constructor for the Gradient class. - /// - public Gradient() - { - } - - /// - /// Constructor for the Gradient class. - /// - /// Gradient first color - /// Gradient second color - /// Gradient mode - public Gradient(Color color1, Color color2, LinearGradientMode mode) - { - this.color1 = color1; - this.color2 = color2; - this.gradientMode = mode; - } - } -} +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Design; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Drawing.Text; +using System.IO; +using System.Reflection; +using System.Text; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using System.ComponentModel; + +namespace MPfm.WindowsControls +{ + /// + /// The Gradient class groups different properties related to gradients into one object. + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class Gradient + { + /// + /// Private value for the Color1 property. + /// + private Color color1 = Color.FromArgb(255, 0, 0); + /// + /// Defines the first color of the gradient. + /// + [XmlIgnore] + [RefreshProperties(RefreshProperties.Repaint)] + [Category("Gradient"), Browsable(true), Description("Defines the first color of the gradient.")] + public Color Color1 + { + get + { + return color1; + } + set + { + color1 = value; + } + } + /// + /// Gets/sets the first color of the gradient using a 32-bit integer (ARGB). + /// This is used for serializing the Color structure in XML. + /// + [Browsable(false)] + [XmlElement(ElementName = "Color1")] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int Color1Int + { + get + { + return color1.ToArgb(); + } + set + { + color1 = Color.FromArgb(value); + } + } + + /// + /// Private value for the Color2 property. + /// + private Color color2 = Color.FromArgb(0, 0, 0); + /// + /// Defines the second color of the gradient. + /// + [XmlIgnore] + [RefreshProperties(RefreshProperties.Repaint)] + [Category("Gradient"), Browsable(true), Description("Defines the second color of the gradient.")] + public Color Color2 + { + get + { + return color2; + } + set + { + color2 = value; + } + } + /// + /// Gets/sets the second color of the gradient using a 32-bit integer (ARGB). + /// This is used for serializing the Color structure in XML. + /// + [Browsable(false)] + [XmlElement(ElementName = "Color2")] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int Color2Int + { + get + { + return color2.ToArgb(); + } + set + { + color2 = Color.FromArgb(value); + } + } + + /// + /// Private value for the GradientMode property. + /// + private LinearGradientMode gradientMode = LinearGradientMode.Vertical; + /// + /// Defines the gradient mode. + /// + [Category("Gradient"), Browsable(true), Description("Defines the gradient mode.")] + public LinearGradientMode GradientMode + { + get + { + return gradientMode; + } + set + { + gradientMode = value; + } + } + + /// + /// Default constructor for the Gradient class. + /// + public Gradient() + { + } + + /// + /// Constructor for the Gradient class. + /// + /// Gradient first color + /// Gradient second color + /// Gradient mode + public Gradient(Color color1, Color color2, LinearGradientMode mode) + { + this.color1 = color1; + this.color2 = color2; + this.gradientMode = mode; + } + } +}