diff --git a/Application/App.xaml.cs b/Application/App.xaml.cs index 2e10bc0..8ac0960 100644 --- a/Application/App.xaml.cs +++ b/Application/App.xaml.cs @@ -85,8 +85,9 @@ namespace FeedCenter var splashWindow = new SplashWindow(); splashWindow.ShowDialog(); - // Set whether we should auto-start - Current.SetStartWithWindows(Settings.Default.StartWithWindows); + // Set whether we should auto-start (if not debugging) + if (!IsDebugBuild) + Current.SetStartWithWindows(Settings.Default.StartWithWindows); // Initialize the window mainWindow.Initialize(); diff --git a/Application/FeedCenter.csproj b/Application/FeedCenter.csproj index 13d466b..4e1e368 100644 --- a/Application/FeedCenter.csproj +++ b/Application/FeedCenter.csproj @@ -144,9 +144,6 @@ MinimumRecommendedRules.ruleset - - ..\..\Common.Wpf.MarkupExtensions\bin\Release\Common.Wpf.MarkupExtensions.dll - False ..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.dll @@ -161,6 +158,10 @@ ..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll True + + False + ..\..\..\Public\namebasedgrid\bin\Debug\NameBasedGrid.dll + @@ -190,7 +191,18 @@ FeedChooserWindow.xaml + + + + + + + + + + + MSBuild:Compile Designer @@ -277,7 +289,7 @@ Designer MSBuild:Compile - + MSBuild:Compile Designer @@ -293,7 +305,7 @@ - + MainWindow.xaml Code diff --git a/Application/FeedCenter.csproj.DotSettings b/Application/FeedCenter.csproj.DotSettings index 2a8adce..688c02c 100644 --- a/Application/FeedCenter.csproj.DotSettings +++ b/Application/FeedCenter.csproj.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Application/MainWindow.xaml.cs b/Application/MainWindow.xaml.cs deleted file mode 100644 index ef21f2c..0000000 --- a/Application/MainWindow.xaml.cs +++ /dev/null @@ -1,1418 +0,0 @@ -using Common.Debug; -using Common.Helpers; -using Common.IO; -using Common.Update; -using Common.Wpf.Extensions; -using FeedCenter.Options; -using FeedCenter.Properties; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Web.UI; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Interop; -using System.Windows.Media; -using Common.Internet; - -namespace FeedCenter -{ - public partial class MainWindow - { - private readonly string[] _chromeExtensions = { "chrome-extension://ehojfdcmnajoklleckniaifaijfnkpbi/subscribe.html?", "chrome-extension://nlbjncdgjeocebhnmkbbbdekmmmcbfjd/subscribe.html?" }; - - #region Member variables - - private int _feedIndex; - private Feed _currentFeed; - private DateTime _lastFeedDisplay; - private DateTime _lastFeedRead; - private System.Windows.Forms.Timer _mainTimer; - private FeedCenterEntities _database; - - private InterprocessMessageListener _commandLineListener; - private BackgroundWorker _feedReadWorker; - - #endregion - - #region Constructor - - public MainWindow() - { - InitializeComponent(); - } - - private bool _activated; - protected override void OnActivated(EventArgs e) - { - base.OnActivated(e); - - if (_activated) - return; - - _activated = true; - - // Load the lock state - HandleWindowLockState(); - - // Watch for size and location changes - SizeChanged += HandleWindowSizeChanged; - LocationChanged += HandleWindowLocationChanged; - - // Watch for setting changes - Settings.Default.PropertyChanged += HandlePropertyChanged; - } - - public void Initialize() - { - // Setup the update message properties and callbacks - UpdateCheck.ApplicationName = Properties.Resources.ApplicationDisplayName; - UpdateCheck.UpdateServer = Settings.Default.VersionLocation; - UpdateCheck.UpdateFile = Settings.Default.VersionFile; - UpdateCheck.ApplicationShutdown = ApplicationShutdown; - UpdateCheck.ApplicationCurrentMessage = ApplicationCurrentMessage; - UpdateCheck.ApplicationUpdateMessage = ApplicationUpdateMessage; - - // Show the notification icon - NotificationIcon.Initialize(this); - - // Load window settings - LoadWindowSettings(); - - // Set the foreground color to something that can be seen - LinkTextList.Foreground = (System.Drawing.SystemColors.Desktop.GetBrightness() < 0.5) ? Brushes.White : Brushes.Black; - HeaderLabel.Foreground = LinkTextList.Foreground; - - // Create the background worker that does the actual reading - _feedReadWorker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; - _feedReadWorker.DoWork += HandleFeedReadWorkerStart; - _feedReadWorker.ProgressChanged += HandleFeedReadWorkerProgressChanged; - _feedReadWorker.RunWorkerCompleted += HandleFeedReadWorkerCompleted; - - // Setup the database - _database = new FeedCenterEntities(); - - // Initialize the command line listener - _commandLineListener = new InterprocessMessageListener(Properties.Resources.ApplicationName); - _commandLineListener.MessageReceived += HandleCommandLine; - - // Handle any command line we were started with - HandleCommandLine(null, new InterprocessMessageListener.InterprocessMessageEventArgs(Environment.CommandLine)); - - // Create a timer to keep track of things we need to do - InitializeTimer(); - - // Initialize the feed display - InitializeFeed(); - - // Check for update - if (Settings.Default.CheckVersionAtStartup) - UpdateCheck.CheckForUpdate(); - - // Show the link if updates are available - if (UpdateCheck.UpdateAvailable) - NewVersionLink.Visibility = Visibility.Visible; - - Tracer.WriteLine("MainForm creation finished"); - } - - private static bool ApplicationUpdateMessage(string title, string message) - { - return MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes; - } - - private static void ApplicationCurrentMessage(string title, string message) - { - MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information); - } - - private static void ApplicationShutdown() - { - Application.Current.Shutdown(); - } - - #endregion - - #region Window overrides - - protected override void OnClosing(CancelEventArgs e) - { - base.OnClosing(e); - - // Ditch the worker - if (_feedReadWorker != null) - { - _feedReadWorker.CancelAsync(); - _feedReadWorker.Dispose(); - } - - // Get rid of the timer - TerminateTimer(); - - // Save current window settings - SaveWindowSettings(); - - // Save settings - Settings.Default.Save(); - - // Save options - _database.SaveChanges(); - - // Get rid of the notification icon - NotificationIcon.Dispose(); - } - - #endregion - - #region Setting events - - private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) - { - // Make sure we're on the right thread - if (!Dispatcher.CheckAccess()) - { - Dispatcher.Invoke(new EventHandler(HandlePropertyChanged), sender, e); - return; - } - - if (e.PropertyName == Reflection.GetPropertyName(() => Settings.Default.MultipleLineDisplay)) - { - // Update the current feed - DisplayFeed(); - } - else if (e.PropertyName == Reflection.GetPropertyName(() => Settings.Default.WindowLocked)) - { - // Update the window for the new window lock value - HandleWindowLockState(); - } - else if (e.PropertyName == Reflection.GetPropertyName(() => Settings.Default.ToolbarLocation)) - { - // Update the window for the toolbar location - switch (Settings.Default.ToolbarLocation) - { - case Dock.Top: - Grid.SetRow(NavigationToolbarTray, MainGrid.GetRowIndex(TopToolbarRow)); - - break; - case Dock.Bottom: - Grid.SetRow(NavigationToolbarTray, MainGrid.GetRowIndex(BottomToolbarRow)); - break; - } - } - } - - #endregion - - #region Window methods - - private void LoadWindowSettings() - { - // Get the last window location - var windowLocation = Settings.Default.WindowLocation; - - // Set the window into position - Left = windowLocation.X; - Top = windowLocation.Y; - - // Get the last window size - var windowSize = Settings.Default.WindowSize; - - // Set the window into the previous size if it is valid - if (!windowSize.Width.Equals(0) && !windowSize.Height.Equals(0)) - { - Width = windowSize.Width; - Height = windowSize.Height; - } - - // Set the location of the navigation tray - switch (Settings.Default.ToolbarLocation) - { - case Dock.Top: - Grid.SetRow(NavigationToolbarTray, MainGrid.GetRowIndex(TopToolbarRow)); - break; - case Dock.Bottom: - Grid.SetRow(NavigationToolbarTray, MainGrid.GetRowIndex(BottomToolbarRow)); - break; - } - - // Load the lock state - HandleWindowLockState(); - } - - private void SaveWindowSettings() - { - // Set the last window location - Settings.Default.WindowLocation = new Point(Left, Top); - - // Set the last window size - Settings.Default.WindowSize = new Size(Width, Height); - - // Save the dock on the navigation tray - Settings.Default.ToolbarLocation = Grid.GetRow(NavigationToolbarTray) == MainGrid.GetRowIndex(TopToolbarRow) ? Dock.Top : Dock.Bottom; - - // Save settings - Settings.Default.Save(); - } - - private void HandleWindowLockState() - { - // Set the resize mode for the window - ResizeMode = Settings.Default.WindowLocked ? ResizeMode.NoResize : ResizeMode.CanResize; - - // Show or hide the border - WindowBorder.BorderBrush = Settings.Default.WindowLocked ? SystemColors.ActiveBorderBrush : Brushes.Transparent; - - // Update the borders - UpdateBorder(); - } - - #endregion - - #region Header events - - private void HandleHeaderLabelMouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - // Ignore if the window is locked - if (Settings.Default.WindowLocked) - return; - - // Start dragging - DragMove(); - } - - private void HandleCloseButtonClick(object sender, RoutedEventArgs e) - { - // Close the window - Close(); - } - - #endregion - - #region Timer handling - - private void InitializeTimer() - { - _mainTimer = new System.Windows.Forms.Timer { Interval = 1000 }; - _mainTimer.Tick += HandleMainTimerTick; - } - - private void TerminateTimer() - { - StopTimer(); - - _mainTimer.Dispose(); - } - - private void StartTimer() - { - _mainTimer.Start(); - } - - private void StopTimer() - { - _mainTimer.Stop(); - } - - private void HandleMainTimerTick(object sender, EventArgs e) - { - // If the background worker is busy then don't do anything - if (_feedReadWorker.IsBusy) - return; - - // Stop the timer for now - StopTimer(); - - // Move to the next feed if the scroll interval has expired and the mouse isn't hovering - if (LinkTextList.IsMouseOver) - _lastFeedDisplay = DateTime.Now; - else if (DateTime.Now - _lastFeedDisplay >= Settings.Default.FeedScrollInterval) - NextFeed(); - - // Check to see if we should try to read the feeds - if (DateTime.Now - _lastFeedRead >= Settings.Default.FeedCheckInterval) - ReadFeeds(); - - // Get the timer going again - StartTimer(); - } - - #endregion - - #region Feed display - - private void UpdateToolbarButtonState() - { - // Cache the feed count to save (a little) time - var feedCount = _database.Feeds.Count(); - - // Set button states - PreviousToolbarButton.IsEnabled = (feedCount > 1); - NextToolbarButton.IsEnabled = (feedCount > 1); - RefreshToolbarButton.IsEnabled = (feedCount > 0); - FeedButton.IsEnabled = (feedCount > 0); - OpenAllToolbarButton.IsEnabled = (feedCount > 0); - MarkReadToolbarButton.IsEnabled = (feedCount > 0); - FeedLabel.Visibility = (feedCount == 0 ? Visibility.Hidden : Visibility.Visible); - FeedButton.Visibility = (feedCount > 1 ? Visibility.Hidden : Visibility.Visible); - } - - private void InitializeFeed() - { - UpdateToolbarButtonState(); - - // Cache the feed count to save (a little) time - var feedCount = _database.Feeds.Count(); - - // Clear the link list - LinkTextList.Items.Clear(); - - // Reset the feed index - _feedIndex = -1; - - // Start the timer - StartTimer(); - - // Don't go further if we have no feeds - if (feedCount == 0) - return; - - // Get the first feed - NextFeed(); - } - - private void NextFeed() - { - var feedCount = _database.Feeds.Count(); - - if (feedCount == 0) - return; - - if (Settings.Default.DisplayEmptyFeeds) - { - // Increment the index and adjust if we've gone around the end - _feedIndex = (_feedIndex + 1) % feedCount; - - // Get the feed - _currentFeed = _database.Feeds.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex); - } - else - { - // Keep track if we found something - var found = false; - - // Remember our starting position - var startIndex = (_feedIndex == -1 ? 0 : _feedIndex); - - // Increment the index and adjust if we've gone around the end - _feedIndex = (_feedIndex + 1) % feedCount; - - // Loop until we come back to the start index - do - { - // Get the feed - _currentFeed = _database.Feeds.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex); - - // If the current feed has unread items then we can display it - if (_currentFeed.Items.Count(item => !item.BeenRead) > 0) - { - found = true; - break; - } - - // Increment the index and adjust if we've gone around the end - _feedIndex = (_feedIndex + 1) % feedCount; - } - while (_feedIndex != startIndex); - - // If nothing was found then clear the current feed - if (!found) - { - _feedIndex = -1; - _currentFeed = null; - } - } - - // Update the feed timestamp - _lastFeedDisplay = DateTime.Now; - - // Update the display - DisplayFeed(); - } - - private void PreviousFeed() - { - var feedCount = _database.Feeds.Count(); - - if (feedCount == 0) - return; - - if (Settings.Default.DisplayEmptyFeeds) - { - // Decrement the feed index - _feedIndex--; - - // If we've gone below the start of the list then reset to the end - if (_feedIndex < 0) - _feedIndex = feedCount - 1; - - // Get the feed - _currentFeed = _database.Feeds.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex); - } - else - { - // Keep track if we found something - var found = false; - - // Remember our starting position - var startIndex = (_feedIndex == -1 ? 0 : _feedIndex); - - // Decrement the feed index - _feedIndex--; - - // If we've gone below the start of the list then reset to the end - if (_feedIndex < 0) - _feedIndex = feedCount - 1; - - // Loop until we come back to the start index - do - { - // Get the feed - _currentFeed = _database.Feeds.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex); - - // If the current feed has unread items then we can display it - if (_currentFeed.Items.Count(item => !item.BeenRead) > 0) - { - found = true; - break; - } - - // Decrement the feed index - _feedIndex--; - - // If we've gone below the start of the list then reset to the end - if (_feedIndex < 0) - _feedIndex = feedCount - 1; - } - while (_feedIndex != startIndex); - - // If nothing was found then clear the current feed - if (!found) - { - _feedIndex = -1; - _currentFeed = null; - } - } - - // Update the feed timestamp - _lastFeedDisplay = DateTime.Now; - - // Update the display - DisplayFeed(); - } - - private void UpdateOpenAllButton() - { - var multipleOpenAction = _currentFeed.MultipleOpenAction; - - switch (multipleOpenAction) - { - case MultipleOpenAction.IndividualPages: - OpenAllToolbarButton.ToolTip = Properties.Resources.openAllMultipleToolbarButton; - break; - case MultipleOpenAction.SinglePage: - OpenAllToolbarButton.ToolTip = Properties.Resources.openAllSingleToolbarButton; - break; - } - } - - private void DisplayFeed() - { - // Just clear the display if we have no feed - if (_currentFeed == null) - { - FeedLabel.Text = string.Empty; - FeedButton.Visibility = Visibility.Hidden; - LinkTextList.Items.Clear(); - - return; - } - - // Set the header to the feed title - FeedLabel.Text = (_currentFeed.Name.Length > 0 ? _currentFeed.Name : _currentFeed.Title); - FeedButton.Visibility = _database.Feeds.Count() > 1 ? Visibility.Visible : Visibility.Hidden; - - // Clear the current list - LinkTextList.Items.Clear(); - - // Sort the items by sequence - var sortedItems = _currentFeed.Items.Where(item => !item.BeenRead).OrderBy(item => item.Sequence); - - // Loop over all items in the current feed - foreach (var feedItem in sortedItems) - { - // Add the list item - LinkTextList.Items.Add(feedItem); - } - - UpdateOpenAllButton(); - } - - private void MarkAllItemsAsRead() - { - // Loop over all items and mark them as read - foreach (FeedItem feedItem in LinkTextList.Items) - feedItem.BeenRead = true; - - // Save the changes - _database.SaveChanges(); - - // Clear the list - LinkTextList.Items.Clear(); - } - - #endregion - - #region Feed reading - - private class FeedReadWorkerInput - { - public bool ForceRead; - public Feed Feed; - } - - private void SetProgressMode(bool value, int feedCount) - { - // Reset the progress bar if we need it - if (value) - { - FeedReadProgress.Value = 0; - FeedReadProgress.Maximum = feedCount + 2; - FeedReadProgress.Visibility = Visibility.Visible; - } - else - { - FeedReadProgress.Visibility = Visibility.Collapsed; - } - } - - private void ReadCurrentFeed(bool forceRead = false) - { - // Don't read if we're already working - if (_feedReadWorker.IsBusy) - return; - - // Don't read if there is nothing to read - if (!_database.Feeds.Any()) - return; - - // Switch to progress mode - SetProgressMode(true, 1); - - // Create the input class - var workerInput = new FeedReadWorkerInput { ForceRead = forceRead, Feed = _currentFeed }; - - // Start the worker - _feedReadWorker.RunWorkerAsync(workerInput); - } - - private void ReadFeeds(bool forceRead = false) - { - // Don't read if we're already working - if (_feedReadWorker.IsBusy) - return; - - // Don't read if there is nothing to read - if (!_database.Feeds.Any()) - return; - - // Switch to progress mode - SetProgressMode(true, _database.Feeds.Count()); - - // Create the input class - var workerInput = new FeedReadWorkerInput { ForceRead = forceRead, Feed = null }; - - // Start the worker - _feedReadWorker.RunWorkerAsync(workerInput); - } - - private void HandleFeedReadWorkerProgressChanged(object sender, ProgressChangedEventArgs e) - { - // Set progress - FeedReadProgress.Value = e.ProgressPercentage; - } - - private void HandleFeedReadWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) - { - // Reset the database to current settings - ResetDatabase(); - - // Save settings - Settings.Default.Save(); - - // Set the read timestamp - _lastFeedRead = DateTime.Now; - - // Update the current feed - DisplayFeed(); - - // Switch to normal mode - SetProgressMode(false, 0); - - // Check for update - if (UpdateCheck.UpdateAvailable) - NewVersionLink.Visibility = Visibility.Visible; - - UpdateErrorLink(); - } - - private void UpdateErrorLink() - { - var feedErrorCount = _database.Feeds.Count(f => f.LastReadResult != FeedReadResult.Success); - - // Set the visibility of the error link - FeedErrorsLink.Visibility = feedErrorCount == 0 ? Visibility.Collapsed : Visibility.Visible; - - // Set the text to match the number of errors - FeedErrorsLink.Text = feedErrorCount == 1 - ? Properties.Resources.FeedErrorLink - : string.Format(Properties.Resources.FeedErrorsLink, feedErrorCount); - } - - private static void HandleFeedReadWorkerStart(object sender, DoWorkEventArgs e) - { - // Create a new database instance for just this thread - var database = new FeedCenterEntities(); - - // Get the worker - var worker = (BackgroundWorker) sender; - - // Get the input information - var workerInput = (FeedReadWorkerInput) e.Argument; - - // Setup for progress - var currentProgress = 0; - - // Create the list of feeds to read - var feedsToRead = new List(); - - // If we have a single feed then add it to the list - otherwise add them all - if (workerInput.Feed != null) - feedsToRead.Add(database.Feeds.First(feed => feed.ID == workerInput.Feed.ID)); - else - feedsToRead.AddRange(database.Feeds); - - // Loop over each feed and read it - foreach (var feed in feedsToRead) - { - // Read the feed - feed.Read(database, workerInput.ForceRead); - - // Increment progress - currentProgress += 1; - - // Report progress - worker.ReportProgress(currentProgress); - } - - // Save the changes - database.SaveChanges(); - - // Increment progress - currentProgress += 1; - - // Report progress - worker.ReportProgress(currentProgress); - - // See if we're due for a version check - if (DateTime.Now - Settings.Default.LastVersionCheck >= Settings.Default.VersionCheckInterval) - { - // Get the update information - UpdateCheck.CheckForUpdate(); - - // Update the last check time - Settings.Default.LastVersionCheck = DateTime.Now; - } - - // Increment progress - currentProgress += 1; - - // Report progress - worker.ReportProgress(currentProgress); - - // Sleep for a little bit so the user can see the update - Thread.Sleep(Settings.Default.ProgressSleepInterval * 3); - } - - #endregion - - #region Remote command line handling - - private void HandleCommandLine(object sender, InterprocessMessageListener.InterprocessMessageEventArgs e) - { - // If the command line is blank then ignore it - if (e.Message.Length == 0) - return; - - // Pad the command line with a trailing space just to be lazy in parsing - var commandLine = e.Message + " "; - - // Look for the feed URL in the command line - var startPosition = commandLine.IndexOf("feed://", StringComparison.Ordinal); - - // If we found one then we should extract and process it - if (startPosition > 0) - { - // Advance past the protocol - startPosition += 7; - - // Starting at the URL position look for the next space - var endPosition = commandLine.IndexOf(" ", startPosition, StringComparison.Ordinal); - - // Extract the feed URL - var feedUrl = commandLine.Substring(startPosition, endPosition - startPosition); - - // Add the HTTP protocol by default - feedUrl = "http://" + feedUrl; - - // Create a new feed using the URL - HandleNewFeed(feedUrl); - } - } - - private delegate void NewFeedDelegate(string feedUrl); - private void HandleNewFeed(string feedUrl) - { - // Create and configure the new feed - var feed = Feed.Create(_database); - feed.Source = feedUrl; - feed.Category = _database.DefaultCategory; - - // Try to detect the feed type - var feedTypeResult = feed.DetectFeedType(); - - // If we can't figure it out it could be an HTML page - if (feedTypeResult.Item1 == FeedType.Unknown) - { - // Only check if the feed was able to be read - otherwise fall through and show the dialog - if (feedTypeResult.Item2.Length > 0) - { - // Create and load an HTML document with the text - var htmlDocument = new HtmlAgilityPack.HtmlDocument(); - htmlDocument.LoadHtml(feedTypeResult.Item2); - - // Look for all RSS or atom links in the document - var rssLinks = htmlDocument.DocumentNode.Descendants("link") - .Where(n => n.Attributes["type"] != null && (n.Attributes["type"].Value == "application/rss+xml" || n.Attributes["type"].Value == "application/atom+xml")) - .Select(n => new Tuple(UrlHelper.GetAbsoluteUrlString(feed.Source, n.Attributes["href"].Value), WebUtility.HtmlDecode(n.Attributes["title"]?.Value ?? string.Empty))) - .ToList(); - - // If there was only one link found then switch to feed to it - if (rssLinks.Count == 1) - { - feed.Source = rssLinks[0].Item1; - } - else - { - var feedChooserWindow = new FeedChooserWindow(); - var feedLink = feedChooserWindow.Display(this, rssLinks); - - if (string.IsNullOrEmpty(feedLink)) - return; - - feed.Source = feedLink; - } - } - } - - // Read the feed for the first time - var feedReadResult = feed.Read(_database); - - // See if we read the feed okay - if (feedReadResult == FeedReadResult.Success) - { - // Update the feed name to be the title - feed.Name = feed.Title; - - // Add the feed to the feed table - _database.Feeds.Add(feed); - - // Save the changes - _database.SaveChanges(); - - // Show a tip - NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), System.Windows.Forms.ToolTipIcon.Info); - - // Re-initialize the feed display - DisplayFeed(); - } - else - { - // Feed read failed - ceate a new feed window - var feedForm = new FeedWindow(); - - var dialogResult = feedForm.Display(_database, feed, this); - - // Display the new feed form - if (dialogResult.HasValue && dialogResult.Value) - { - // Add the feed to the feed table - _database.Feeds.Add(feed); - - // Save the changes - _database.SaveChanges(); - - // Re-initialize the feed display - DisplayFeed(); - } - } - } - - #endregion - - #region Database helpers - - private void ResetDatabase() - { - // Get the ID of the current feed - var currentId = _currentFeed?.ID ?? Guid.Empty; - - // Create a new database object - _database = new FeedCenterEntities(); - - UpdateToolbarButtonState(); - - // Get a list of feeds ordered by name - var feedList = _database.Feeds.OrderBy(f => f.Name).ToList(); - - // First try to find the current feed by ID to see if it is still there - var newIndex = feedList.FindIndex(f => f.ID == currentId); - - if (newIndex == -1) - { - // The current feed isn't there anymore so see if we can find a feed at the old index - if (feedList.ElementAtOrDefault(_feedIndex) != null) - newIndex = _feedIndex; - - // If there is no feed at the old location then give up and go back to the start - if (newIndex == -1 && feedList.Count > 0) - newIndex = 0; - } - - // Set the current index to the new index - _feedIndex = newIndex; - - // Re-get the current feed - _currentFeed = (_feedIndex == -1 ? null : _database.Feeds.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex)); - } - - #endregion - - #region Drag and drop - - private void HandleDragOver(object sender, DragEventArgs e) - { - // Default to not allowed - e.Effects = DragDropEffects.None; - e.Handled = true; - - // If there isn't any text in the data then it is not allowed - if (!e.Data.GetDataPresent(DataFormats.Text)) - return; - - // Get the data as a string - var data = (string) e.Data.GetData(DataFormats.Text); - - // If the data doesn't look like a URI then it is not allowed - if (!Uri.IsWellFormedUriString(data, UriKind.Absolute)) - return; - - // Allowed - e.Effects = DragDropEffects.Copy; - } - - private void HandleDragDrop(object sender, DragEventArgs e) - { - // Get the data as a string - var data = (string) e.Data.GetData(DataFormats.Text); - - // Check to see if the data starts with any known Chrome extension - var chromeExtension = _chromeExtensions.FirstOrDefault(c => data.StartsWith(c)); - - // Remove the Chrome extension URL and decode the URL - if (chromeExtension != null) - { - data = data.Substring(chromeExtension.Length); - data = WebUtility.UrlDecode(data); - } - - // Handle the new feed but allow the drag/drop to complete - Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data); - } - - #endregion - - #region Link list events - - private void HandleLinkTextListMouseUp(object sender, MouseButtonEventArgs e) - { - switch (e.ChangedButton) - { - case MouseButton.XButton1: - - PreviousFeed(); - break; - - case MouseButton.XButton2: - - NextFeed(); - break; - } - } - - private void HandleItemMouseUp(object sender, MouseButtonEventArgs e) - { - // Only handle the middle button - if (e.ChangedButton != MouseButton.Middle) - return; - - // Get the feed item - var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; - - // The feed item has been read and is no longer new - feedItem.BeenRead = true; - feedItem.New = false; - - // Remove the item from the list - LinkTextList.Items.Remove(feedItem); - - // Save the changes - _database.SaveChanges(); - - } - - private void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e) - { - // Get the feed item - var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; - - // Open the item link - if (BrowserCommon.OpenLink(feedItem.Link)) - { - // The feed item has been read and is no longer new - feedItem.BeenRead = true; - feedItem.New = false; - - // Remove the item from the list - LinkTextList.Items.Remove(feedItem); - - // Save the changes - _database.SaveChanges(); - } - } - - #endregion - - #region Feed list menu - - private void HandleFeedButtonClick(object sender, RoutedEventArgs e) - { - // Create a new context menu - var contextMenu = new ContextMenu(); - - // Loop over each feed - foreach (var feed in _database.Feeds.OrderBy(feed => feed.Name)) - { - // Build a string to display the feed name and the unread count - var display = $"{feed.Name} ({feed.Items.Count(item => !item.BeenRead):d})"; - - // Create a menu item - var menuItem = new MenuItem - { - Header = display, - Tag = feed, - - // Set the current item to bold - FontWeight = feed == _currentFeed ? FontWeights.Bold : FontWeights.Normal - }; - - - // Handle the click - menuItem.Click += HandleFeedMenuItemClick; - - // Add the item to the list - contextMenu.Items.Add(menuItem); - } - - // Set the context menu placement to this button - contextMenu.PlacementTarget = this; - - // Open the context menu - contextMenu.IsOpen = true; - } - - private void HandleFeedMenuItemClick(object sender, RoutedEventArgs e) - { - // Get the menu item clicked - var menuItem = (MenuItem) sender; - - // Get the feed from the menu item tab - var feed = (Feed) menuItem.Tag; - - // Loop over all feeds and look for the index of the new one - var feedIndex = 0; - foreach (var loopFeed in _database.Feeds.OrderBy(loopFeed => loopFeed.Name)) - { - if (loopFeed == feed) - { - _feedIndex = feedIndex; - break; - } - - feedIndex++; - } - - // Set the current feed - _currentFeed = feed; - - // Update the feed timestamp - _lastFeedDisplay = DateTime.Now; - - // Update the display - DisplayFeed(); - } - - #endregion - - #region Window border - - private void UpdateBorder() - { - var windowInteropHelper = new WindowInteropHelper(this); - - var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle); - - var rectangle = new System.Drawing.Rectangle - { - X = (int) Left, - Y = (int) Top, - Width = (int) Width, - Height = (int) Height - }; - - var borderThickness = new Thickness(); - - if (rectangle.Right != screen.WorkingArea.Right) - borderThickness.Right = 1; - - if (rectangle.Left != screen.WorkingArea.Left) - borderThickness.Left = 1; - - if (rectangle.Top != screen.WorkingArea.Top) - borderThickness.Top = 1; - - if (rectangle.Bottom != screen.WorkingArea.Bottom) - borderThickness.Bottom = 1; - - WindowBorder.BorderThickness = borderThickness; - } - - private DelayedMethod _windowStateDelay; - - private void HandleWindowSizeChanged(object sender, SizeChangedEventArgs e) - { - if (_windowStateDelay == null) - _windowStateDelay = new DelayedMethod(500, UpdateWindowSettings); - - _windowStateDelay.Reset(); - } - - private void HandleWindowLocationChanged(object sender, EventArgs e) - { - if (_windowStateDelay == null) - _windowStateDelay = new DelayedMethod(500, UpdateWindowSettings); - - _windowStateDelay.Reset(); - } - - private void UpdateWindowSettings() - { - // Save current window settings - SaveWindowSettings(); - - // Update the border - UpdateBorder(); - } - - #endregion - - #region Feed header - - private void HandleFeedLabelMouseDown(object sender, MouseButtonEventArgs e) - { - // Open the link for the current feed on a left double click - if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) - BrowserCommon.OpenLink(_currentFeed.Link); - } - - #endregion - - #region Navigation toolbar - - private void HandlePreviousToolbarButtonClick(object sender, RoutedEventArgs e) - { - PreviousFeed(); - } - - private void HandleNextToolbarButtonClick(object sender, RoutedEventArgs e) - { - NextFeed(); - } - - private void OpenAllFeedItemsIndividually() - { - // Create a new list of feed items - var feedItems = (from FeedItem feedItem in LinkTextList.Items select feedItem).ToList(); - - // Get the browser - var browser = BrowserCommon.FindBrowser(Settings.Default.Browser); - - // Cache the settings object - var settings = Settings.Default; - - // Start with a longer sleep interval to give time for the browser to come up - var sleepInterval = settings.OpenAllSleepIntervalFirst; - - // Loop over all items - foreach (var feedItem in feedItems) - { - // Try to open the link - if (BrowserCommon.OpenLink(browser, feedItem.Link)) - { - // Mark the feed as read - feedItem.BeenRead = true; - - // Remove the item - LinkTextList.Items.Remove(feedItem); - } - - // Wait a little bit - Thread.Sleep(sleepInterval); - - // Switch to the normal sleep interval - sleepInterval = settings.OpenAllSleepInterval; - } - - // Save the changes - _database.SaveChanges(); - } - - private void HandleOptionsToolbarButtonClick(object sender, RoutedEventArgs e) - { - // Create the options form - var optionsWindow = new OptionsWindow { Owner = this }; - - // Show the options form and get the result - var result = optionsWindow.ShowDialog(); - - // If okay was selected - if (result.HasValue && result.Value) - { - // Reset the database to current settings - ResetDatabase(); - - // Re-initialize the feed display - DisplayFeed(); - } - } - - private void HandleMarkReadToolbarButtonClick(object sender, RoutedEventArgs e) - { - MarkAllItemsAsRead(); - } - - private void HandleShowErrorsButtonClick(object sender, RoutedEventArgs e) - { - // Create the feed error window - var feedErrorWindow = new FeedErrorWindow(); - - // Display the window - var result = feedErrorWindow.Display(this); - - // If okay was selected - if (result.GetValueOrDefault()) - { - // Reset the database to current settings - ResetDatabase(); - - // Re-initialize the feed display - DisplayFeed(); - - UpdateErrorLink(); - } - } - - private void HandleRefreshMenuItemClick(object sender, RoutedEventArgs e) - { - var menuItem = (MenuItem) e.Source; - - if (Equals(menuItem, MenuRefresh)) - ReadCurrentFeed(true); - else if (Equals(menuItem, MenuRefreshAll)) - ReadFeeds(true); - } - - private void HandleRefreshToolbarButtonClick(object sender, RoutedEventArgs e) - { - ReadFeeds(true); - } - - private void HandleOpenAllMenuItemClick(object sender, RoutedEventArgs e) - { - var menuItem = (MenuItem) e.Source; - - if (Equals(menuItem, MenuOpenAllSinglePage)) - OpenAllFeedItemsOnSinglePage(); - else if (Equals(menuItem, MenuOpenAllMultiplePages)) - OpenAllFeedItemsIndividually(); - } - - private void HandleOpenAllToolbarButtonClick(object sender, RoutedEventArgs e) - { - var multipleOpenAction = _currentFeed.MultipleOpenAction; - - switch (multipleOpenAction) - { - case MultipleOpenAction.IndividualPages: - OpenAllFeedItemsIndividually(); - break; - case MultipleOpenAction.SinglePage: - OpenAllFeedItemsOnSinglePage(); - break; - } - } - - private void HandleEditCurrentFeedMenuItemClick(object sender, RoutedEventArgs e) - { - // Create a new feed window - var feedWindow = new FeedWindow(); - - // Display the feed window and get the result - var result = feedWindow.Display(_database, _currentFeed, this); - - // If OK was clicked... - if (result.HasValue && result.Value) - { - // Save - _database.SaveChanges(); - - // Update feed - DisplayFeed(); - } - } - - private void HandleDeleteCurrentFeedMenuItemClick(object sender, RoutedEventArgs e) - { - // Confirm this delete since it is for real - if (MessageBox.Show(this, Properties.Resources.ConfirmDelete, string.Empty, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No) - return; - - // Get the current feed - var feedToDelete = _currentFeed; - - // Move to the next feed - NextFeed(); - - // Delete all items - foreach (var item in feedToDelete.Items.ToList()) - _database.FeedItems.Remove(item); - - // Delete the feed - _database.Feeds.Remove(feedToDelete); - - // Save - _database.SaveChanges(); - } - - #endregion - - #region Single page reading - - private void OpenAllFeedItemsOnSinglePage() - { - var fileName = Path.GetTempFileName() + ".html"; - TextWriter textWriter = new StreamWriter(fileName); - - using (var htmlTextWriter = new HtmlTextWriter(textWriter)) - { - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Html); - - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Head); - - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Title); - htmlTextWriter.Write(_currentFeed.Title); - htmlTextWriter.RenderEndTag(); - - htmlTextWriter.AddAttribute("http-equiv", "Content-Type"); - htmlTextWriter.AddAttribute("content", "text/html; charset=utf-8"); - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Meta); - htmlTextWriter.RenderEndTag(); - - htmlTextWriter.RenderEndTag(); - - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Body); - - var sortedItems = from item in _currentFeed.Items where !item.BeenRead orderby item.Sequence ascending select item; - - var firstItem = true; - - foreach (var item in sortedItems) - { - if (!firstItem) - { - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Hr); - htmlTextWriter.RenderEndTag(); - } - - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Div); - - htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, item.Link); - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.A); - htmlTextWriter.Write(item.Title.Length == 0 ? item.Link : item.Title); - htmlTextWriter.RenderEndTag(); - - htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Br); - htmlTextWriter.RenderEndTag(); - - htmlTextWriter.Write(item.Description); - - htmlTextWriter.RenderEndTag(); - - firstItem = false; - } - - htmlTextWriter.RenderEndTag(); - htmlTextWriter.RenderEndTag(); - } - - textWriter.Flush(); - textWriter.Close(); - - BrowserCommon.OpenLink(fileName); - - MarkAllItemsAsRead(); - } - - #endregion - - private void HandleNewVersionLinkClick(object sender, RoutedEventArgs e) - { - // Display update information - UpdateCheck.DisplayUpdateInformation(true); - } - } -} diff --git a/Application/MainWindow/CategoryList.cs b/Application/MainWindow/CategoryList.cs new file mode 100644 index 0000000..31c1416 --- /dev/null +++ b/Application/MainWindow/CategoryList.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; + +namespace FeedCenter +{ + public partial class MainWindow + { + private void DisplayCategory() + { + CategoryLabel.Text = string.Format(Properties.Resources.CategoryFilterHeader, _currentCategory == null ? Properties.Resources.AllCategory : _currentCategory.Name); + } + + private void HandleCategoryButtonClick(object sender, RoutedEventArgs e) + { + // Create a new context menu + var contextMenu = new ContextMenu(); + + // Create the "all" menu item + var menuItem = new MenuItem + { + Header = Properties.Resources.AllCategory, + Tag = null, + + // Set the current item to bold + FontWeight = _currentCategory == null ? FontWeights.Bold : FontWeights.Normal + }; + + // Handle the click + menuItem.Click += HandleCategoryMenuItemClick; + + // Add the item to the list + contextMenu.Items.Add(menuItem); + + // Loop over each feed + foreach (var category in _database.Categories.OrderBy(category => category.Name)) + { + // Create a menu item + menuItem = new MenuItem + { + Header = category.Name, + Tag = category, + + // Set the current item to bold + FontWeight = category.ID == _currentCategory?.ID ? FontWeights.Bold : FontWeights.Normal + }; + + // Handle the click + menuItem.Click += HandleCategoryMenuItemClick; + + // Add the item to the list + contextMenu.Items.Add(menuItem); + } + + // Set the context menu placement to this button + contextMenu.PlacementTarget = this; + + // Open the context menu + contextMenu.IsOpen = true; + } + + private void HandleCategoryMenuItemClick(object sender, RoutedEventArgs e) + { + // Get the menu item clicked + var menuItem = (MenuItem) sender; + + // Get the category from the menu item tab + var category = (Category) menuItem.Tag; + + // If the category changed then reset the current feed to the first in the category + if (_currentCategory?.ID != category?.ID) + { + _currentFeed = category == null ? _database.Feeds.FirstOrDefault() : category.Feeds.FirstOrDefault(); + } + + // Set the current category + _currentCategory = category; + + // Get the current feed list to match the category + _feedList = _currentCategory?.Feeds.ToList() ?? _database.Feeds.ToList(); + + // Reset the feed index + _feedIndex = -1; + + // Get the first feed + NextFeed(); + + // Update the feed timestamp + _lastFeedDisplay = DateTime.Now; + + // Update the display + DisplayCategory(); + DisplayFeed(); + } + } +} diff --git a/Application/MainWindow/CommandLine.cs b/Application/MainWindow/CommandLine.cs new file mode 100644 index 0000000..6756e1b --- /dev/null +++ b/Application/MainWindow/CommandLine.cs @@ -0,0 +1,42 @@ +using Common.IO; +using System; + +namespace FeedCenter +{ + public partial class MainWindow + { + private InterprocessMessageListener _commandLineListener; + + private void HandleCommandLine(object sender, InterprocessMessageListener.InterprocessMessageEventArgs e) + { + // If the command line is blank then ignore it + if (e.Message.Length == 0) + return; + + // Pad the command line with a trailing space just to be lazy in parsing + var commandLine = e.Message + " "; + + // Look for the feed URL in the command line + var startPosition = commandLine.IndexOf("feed://", StringComparison.Ordinal); + + // If we found one then we should extract and process it + if (startPosition > 0) + { + // Advance past the protocol + startPosition += 7; + + // Starting at the URL position look for the next space + var endPosition = commandLine.IndexOf(" ", startPosition, StringComparison.Ordinal); + + // Extract the feed URL + var feedUrl = commandLine.Substring(startPosition, endPosition - startPosition); + + // Add the HTTP protocol by default + feedUrl = "http://" + feedUrl; + + // Create a new feed using the URL + HandleNewFeed(feedUrl); + } + } + } +} diff --git a/Application/MainWindow/DragDrop.cs b/Application/MainWindow/DragDrop.cs new file mode 100644 index 0000000..e628440 --- /dev/null +++ b/Application/MainWindow/DragDrop.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Net; +using System.Windows; + +namespace FeedCenter +{ + public partial class MainWindow + { + private readonly string[] _chromeExtensions = { "chrome-extension://ehojfdcmnajoklleckniaifaijfnkpbi/subscribe.html?", "chrome-extension://nlbjncdgjeocebhnmkbbbdekmmmcbfjd/subscribe.html?" }; + + private void HandleDragOver(object sender, DragEventArgs e) + { + // Default to not allowed + e.Effects = DragDropEffects.None; + e.Handled = true; + + // If there isn't any text in the data then it is not allowed + if (!e.Data.GetDataPresent(DataFormats.Text)) + return; + + // Get the data as a string + var data = (string) e.Data.GetData(DataFormats.Text); + + // If the data doesn't look like a URI then it is not allowed + if (!Uri.IsWellFormedUriString(data, UriKind.Absolute)) + return; + + // Allowed + e.Effects = DragDropEffects.Copy; + } + + private void HandleDragDrop(object sender, DragEventArgs e) + { + // Get the data as a string + var data = (string) e.Data.GetData(DataFormats.Text); + + // Check to see if the data starts with any known Chrome extension + var chromeExtension = _chromeExtensions.FirstOrDefault(c => data.StartsWith(c)); + + // Remove the Chrome extension URL and decode the URL + if (chromeExtension != null) + { + data = data.Substring(chromeExtension.Length); + data = WebUtility.UrlDecode(data); + } + + // Handle the new feed but allow the drag/drop to complete + Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data); + } + } +} diff --git a/Application/MainWindow/FeedCreation.cs b/Application/MainWindow/FeedCreation.cs new file mode 100644 index 0000000..cb151a1 --- /dev/null +++ b/Application/MainWindow/FeedCreation.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Net; +using Common.Internet; +using FeedCenter.Options; + +namespace FeedCenter +{ + public partial class MainWindow + { + private delegate void NewFeedDelegate(string feedUrl); + private void HandleNewFeed(string feedUrl) + { + // Create and configure the new feed + var feed = Feed.Create(_database); + feed.Source = feedUrl; + feed.Category = _database.DefaultCategory; + + // Try to detect the feed type + var feedTypeResult = feed.DetectFeedType(); + + // If we can't figure it out it could be an HTML page + if (feedTypeResult.Item1 == FeedType.Unknown) + { + // Only check if the feed was able to be read - otherwise fall through and show the dialog + if (feedTypeResult.Item2.Length > 0) + { + // Create and load an HTML document with the text + var htmlDocument = new HtmlAgilityPack.HtmlDocument(); + htmlDocument.LoadHtml(feedTypeResult.Item2); + + // Look for all RSS or atom links in the document + var rssLinks = htmlDocument.DocumentNode.Descendants("link") + .Where(n => n.Attributes["type"] != null && (n.Attributes["type"].Value == "application/rss+xml" || n.Attributes["type"].Value == "application/atom+xml")) + .Select(n => new Tuple(UrlHelper.GetAbsoluteUrlString(feed.Source, n.Attributes["href"].Value), WebUtility.HtmlDecode(n.Attributes["title"]?.Value ?? string.Empty))) + .ToList(); + + // If there was only one link found then switch to feed to it + if (rssLinks.Count == 1) + { + feed.Source = rssLinks[0].Item1; + } + else + { + var feedChooserWindow = new FeedChooserWindow(); + var feedLink = feedChooserWindow.Display(this, rssLinks); + + if (string.IsNullOrEmpty(feedLink)) + return; + + feed.Source = feedLink; + } + } + } + + // Read the feed for the first time + var feedReadResult = feed.Read(_database); + + // See if we read the feed okay + if (feedReadResult == FeedReadResult.Success) + { + // Update the feed name to be the title + feed.Name = feed.Title; + + // Add the feed to the feed table + _database.Feeds.Add(feed); + + // Save the changes + _database.SaveChanges(); + + // Show a tip + NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), System.Windows.Forms.ToolTipIcon.Info); + + // Re-initialize the feed display + DisplayFeed(); + } + else + { + // Feed read failed - ceate a new feed window + var feedForm = new FeedWindow(); + + var dialogResult = feedForm.Display(_database, feed, this); + + // Display the new feed form + if (dialogResult.HasValue && dialogResult.Value) + { + // Add the feed to the feed table + _database.Feeds.Add(feed); + + // Save the changes + _database.SaveChanges(); + + // Re-initialize the feed display + DisplayFeed(); + } + } + } + } +} diff --git a/Application/MainWindow/FeedList.cs b/Application/MainWindow/FeedList.cs new file mode 100644 index 0000000..11e906f --- /dev/null +++ b/Application/MainWindow/FeedList.cs @@ -0,0 +1,135 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace FeedCenter +{ + public partial class MainWindow + { + private void HandleLinkTextListMouseUp(object sender, MouseButtonEventArgs e) + { + switch (e.ChangedButton) + { + case MouseButton.XButton1: + + PreviousFeed(); + break; + + case MouseButton.XButton2: + + NextFeed(); + break; + } + } + + private void HandleItemMouseUp(object sender, MouseButtonEventArgs e) + { + // Only handle the middle button + if (e.ChangedButton != MouseButton.Middle) + return; + + // Get the feed item + var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; + + // The feed item has been read and is no longer new + feedItem.BeenRead = true; + feedItem.New = false; + + // Remove the item from the list + LinkTextList.Items.Remove(feedItem); + + // Save the changes + _database.SaveChanges(); + + } + + private void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e) + { + // Get the feed item + var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; + + // Open the item link + if (BrowserCommon.OpenLink(feedItem.Link)) + { + // The feed item has been read and is no longer new + feedItem.BeenRead = true; + feedItem.New = false; + + // Remove the item from the list + LinkTextList.Items.Remove(feedItem); + + // Save the changes + _database.SaveChanges(); + } + } + + private void HandleFeedButtonClick(object sender, RoutedEventArgs e) + { + // Create a new context menu + var contextMenu = new ContextMenu(); + + // Loop over each feed + foreach (var feed in _feedList.OrderBy(feed => feed.Name)) + { + // Build a string to display the feed name and the unread count + var display = $"{feed.Name} ({feed.Items.Count(item => !item.BeenRead):d})"; + + // Create a menu item + var menuItem = new MenuItem + { + Header = display, + Tag = feed, + + // Set the current item to bold + FontWeight = feed == _currentFeed ? FontWeights.Bold : FontWeights.Normal + }; + + + // Handle the click + menuItem.Click += HandleFeedMenuItemClick; + + // Add the item to the list + contextMenu.Items.Add(menuItem); + } + + // Set the context menu placement to this button + contextMenu.PlacementTarget = this; + + // Open the context menu + contextMenu.IsOpen = true; + } + + private void HandleFeedMenuItemClick(object sender, RoutedEventArgs e) + { + // Get the menu item clicked + var menuItem = (MenuItem) sender; + + // Get the feed from the menu item tab + var feed = (Feed) menuItem.Tag; + + // Loop over all feeds and look for the index of the new one + var feedIndex = 0; + foreach (var loopFeed in _feedList.OrderBy(loopFeed => loopFeed.Name)) + { + if (loopFeed == feed) + { + _feedIndex = feedIndex; + break; + } + + feedIndex++; + } + + // Set the current feed + _currentFeed = feed; + + // Update the feed timestamp + _lastFeedDisplay = DateTime.Now; + + // Update the display + DisplayFeed(); + } + } +} diff --git a/Application/MainWindow/FeedReading.cs b/Application/MainWindow/FeedReading.cs new file mode 100644 index 0000000..e47c7aa --- /dev/null +++ b/Application/MainWindow/FeedReading.cs @@ -0,0 +1,185 @@ +using Common.Update; +using FeedCenter.Properties; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Windows; + +namespace FeedCenter +{ + public partial class MainWindow + { + private BackgroundWorker _feedReadWorker; + + private class FeedReadWorkerInput + { + public bool ForceRead; + public Feed Feed; + } + + private void SetProgressMode(bool value, int feedCount) + { + // Reset the progress bar if we need it + if (value) + { + FeedReadProgress.Value = 0; + FeedReadProgress.Maximum = feedCount + 2; + FeedReadProgress.Visibility = Visibility.Visible; + } + else + { + FeedReadProgress.Visibility = Visibility.Collapsed; + } + } + + private void ReadCurrentFeed(bool forceRead = false) + { + // Don't read if we're already working + if (_feedReadWorker.IsBusy) + return; + + // Don't read if there is nothing to read + if (!_database.Feeds.Any()) + return; + + // Switch to progress mode + SetProgressMode(true, 1); + + // Create the input class + var workerInput = new FeedReadWorkerInput { ForceRead = forceRead, Feed = _currentFeed }; + + // Start the worker + _feedReadWorker.RunWorkerAsync(workerInput); + } + + private void ReadFeeds(bool forceRead = false) + { + // Don't read if we're already working + if (_feedReadWorker.IsBusy) + return; + + // Don't read if there is nothing to read + if (!_database.Feeds.Any()) + return; + + // Switch to progress mode + SetProgressMode(true, _database.Feeds.Count()); + + // Create the input class + var workerInput = new FeedReadWorkerInput { ForceRead = forceRead, Feed = null }; + + // Start the worker + _feedReadWorker.RunWorkerAsync(workerInput); + } + + private void HandleFeedReadWorkerProgressChanged(object sender, ProgressChangedEventArgs e) + { + // Set progress + FeedReadProgress.Value = e.ProgressPercentage; + } + + private void HandleFeedReadWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + // Reset the database to current settings + ResetDatabase(); + + // Save settings + Settings.Default.Save(); + + // Set the read timestamp + _lastFeedRead = DateTime.Now; + + // Update the current feed + DisplayFeed(); + + // Switch to normal mode + SetProgressMode(false, 0); + + // Check for update + if (UpdateCheck.UpdateAvailable) + NewVersionLink.Visibility = Visibility.Visible; + + UpdateErrorLink(); + } + + private void UpdateErrorLink() + { + var feedErrorCount = _database.Feeds.Count(f => f.LastReadResult != FeedReadResult.Success); + + // Set the visibility of the error link + FeedErrorsLink.Visibility = feedErrorCount == 0 ? Visibility.Collapsed : Visibility.Visible; + + // Set the text to match the number of errors + FeedErrorsLink.Text = feedErrorCount == 1 + ? Properties.Resources.FeedErrorLink + : string.Format(Properties.Resources.FeedErrorsLink, feedErrorCount); + } + + private static void HandleFeedReadWorkerStart(object sender, DoWorkEventArgs e) + { + // Create a new database instance for just this thread + var database = new FeedCenterEntities(); + + // Get the worker + var worker = (BackgroundWorker) sender; + + // Get the input information + var workerInput = (FeedReadWorkerInput) e.Argument; + + // Setup for progress + var currentProgress = 0; + + // Create the list of feeds to read + var feedsToRead = new List(); + + // If we have a single feed then add it to the list - otherwise add them all + if (workerInput.Feed != null) + feedsToRead.Add(database.Feeds.First(feed => feed.ID == workerInput.Feed.ID)); + else + feedsToRead.AddRange(database.Feeds); + + // Loop over each feed and read it + foreach (var feed in feedsToRead) + { + // Read the feed + feed.Read(database, workerInput.ForceRead); + + // Increment progress + currentProgress += 1; + + // Report progress + worker.ReportProgress(currentProgress); + } + + // Save the changes + database.SaveChanges(); + + // Increment progress + currentProgress += 1; + + // Report progress + worker.ReportProgress(currentProgress); + + // See if we're due for a version check + if (DateTime.Now - Settings.Default.LastVersionCheck >= Settings.Default.VersionCheckInterval) + { + // Get the update information + UpdateCheck.CheckForUpdate(); + + // Update the last check time + Settings.Default.LastVersionCheck = DateTime.Now; + } + + // Increment progress + currentProgress += 1; + + // Report progress + worker.ReportProgress(currentProgress); + + // Sleep for a little bit so the user can see the update + Thread.Sleep(Settings.Default.ProgressSleepInterval * 3); + } + } +} diff --git a/Application/MainWindow/Header.cs b/Application/MainWindow/Header.cs new file mode 100644 index 0000000..9ea5af5 --- /dev/null +++ b/Application/MainWindow/Header.cs @@ -0,0 +1,32 @@ +using FeedCenter.Properties; +using System.Windows; +using System.Windows.Input; + +namespace FeedCenter +{ + public partial class MainWindow + { + private void HandleHeaderLabelMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + // Ignore if the window is locked + if (Settings.Default.WindowLocked) + return; + + // Start dragging + DragMove(); + } + + private void HandleCloseButtonClick(object sender, RoutedEventArgs e) + { + // Close the window + Close(); + } + + private void HandleFeedLabelMouseDown(object sender, MouseButtonEventArgs e) + { + // Open the link for the current feed on a left double click + if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) + BrowserCommon.OpenLink(_currentFeed.Link); + } + } +} diff --git a/Application/MainWindow.xaml b/Application/MainWindow/MainWindow.xaml similarity index 81% rename from Application/MainWindow.xaml rename to Application/MainWindow/MainWindow.xaml index e495b80..b091efb 100644 --- a/Application/MainWindow.xaml +++ b/Application/MainWindow/MainWindow.xaml @@ -5,10 +5,10 @@ xmlns:windows="clr-namespace:Common.Wpf.Windows;assembly=Common.Wpf" xmlns:toolbar="clr-namespace:Common.Wpf.Toolbar;assembly=Common.Wpf" xmlns:splitButton="clr-namespace:Common.Wpf.Toolbar.SplitButton;assembly=Common.Wpf" - xmlns:markup="clr-namespace:Common.Wpf.MarkupExtensions;assembly=Common.Wpf.MarkupExtensions" xmlns:linkControl="clr-namespace:Common.Wpf.LinkControl;assembly=Common.Wpf" xmlns:htmlTextBlock="clr-namespace:Common.Wpf.HtmlTextBlock;assembly=Common.Wpf" xmlns:system="clr-namespace:System;assembly=mscorlib" + xmlns:nameBasedGrid="clr-namespace:NameBasedGrid;assembly=NameBasedGrid" Title="MainWindow" Height="360" Width="252" @@ -37,29 +37,31 @@ Name="WindowBorder" Padding="0" Background="{x:Static SystemColors.DesktopBrush}"> - - + + - - - - - - - - + + + + + + + + + - + + nameBasedGrid:NameBasedGrid.Row="HeaderRow"> @@ -82,7 +84,7 @@ + + + + + + +