More UI updates

This commit is contained in:
2023-04-16 12:57:17 -04:00
parent 5c0c84a068
commit d6a2fd5a46
47 changed files with 3695 additions and 3768 deletions

View File

@@ -8,121 +8,120 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Threading; using System.Windows.Threading;
namespace FeedCenter namespace FeedCenter;
public partial class App
{ {
public partial class App // ReSharper disable ConvertPropertyToExpressionBody
private static bool IsDebugBuild
{ {
// ReSharper disable ConvertPropertyToExpressionBody get
private static bool IsDebugBuild
{ {
get
{
#if DEBUG #if DEBUG
return true; return true;
#else #else
return false; return false;
#endif #endif
}
}
// ReSharper restore ConvertPropertyToExpressionBody
public static string Name => FeedCenter.Properties.Resources.ApplicationName;
[STAThread]
public static void Main()
{
// Create and initialize the app object
var app = new App();
app.InitializeComponent();
// Create an single instance handle to see if we are already running
var isolationHandle = SingleInstance.GetSingleInstanceHandleAsync(Name).Result;
// If there is another copy then pass it the command line and exit
if (isolationHandle == null)
return;
// Use the handle over the lifetime of the application
using (isolationHandle)
{
// Set the path
LegacyDatabase.DatabasePath = SystemConfiguration.DataDirectory;
LegacyDatabase.DatabaseFile = Path.Combine(SystemConfiguration.DataDirectory,
Settings.Default.DatabaseFile_Legacy);
Database.DatabasePath = SystemConfiguration.DataDirectory;
Database.DatabaseFile = Path.Combine(SystemConfiguration.DataDirectory, Settings.Default.DatabaseFile);
// Get the generic provider
var genericProvider =
(GenericSettingsProvider) Settings.Default.Providers[nameof(GenericSettingsProvider)];
if (genericProvider == null)
return;
// Set the callbacks into the provider
genericProvider.OpenDataStore = SettingsStore.OpenDataStore;
genericProvider.GetSettingValue = SettingsStore.GetSettingValue;
genericProvider.SetSettingValue = SettingsStore.SetSettingValue;
Log.Logger = new LoggerConfiguration()
.Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.File(
Path.Join(SystemConfiguration.UserSettingsPath,
$"{FeedCenter.Properties.Resources.ApplicationName}_.txt"),
rollingInterval: RollingInterval.Day, retainedFileCountLimit: 5,
outputTemplate: "[{Timestamp:u} - {ThreadId} - {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
Log.Logger.Information("---");
Log.Logger.Information("Application started");
Log.Logger.Information("Command line arguments:");
foreach (var arg in Environment.GetCommandLineArgs()
.Select((value, index) => (Value: value, Index: index)))
Log.Logger.Information("\tArg {0}: {1}", arg.Index, arg.Value);
Current.DispatcherUnhandledException += HandleCurrentDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += HandleCurrentDomainUnhandledException;
// Check if we need to upgrade settings from a previous version
if (Settings.Default.FirstRun)
{
Settings.Default.Upgrade();
Settings.Default.FirstRun = false;
Settings.Default.Save();
}
// Create the main window before the splash otherwise WPF gets messed up
var mainWindow = new MainWindow();
// Show the splash window
var splashWindow = new SplashWindow();
splashWindow.ShowDialog();
// Set whether we should auto-start (if not debugging)
if (!IsDebugBuild)
Current.SetStartWithWindows(Settings.Default.StartWithWindows);
// Initialize the window
mainWindow.Initialize();
// Run the app
app.Run(mainWindow);
}
}
private static void HandleCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Log.Logger.Error((Exception) e.ExceptionObject, "Exception");
}
private static void HandleCurrentDispatcherUnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{
Log.Logger.Error(e.Exception, "Exception");
} }
} }
// ReSharper restore ConvertPropertyToExpressionBody
public static string Name => FeedCenter.Properties.Resources.ApplicationName;
[STAThread]
public static void Main()
{
// Create and initialize the app object
var app = new App();
app.InitializeComponent();
// Create an single instance handle to see if we are already running
var isolationHandle = SingleInstance.GetSingleInstanceHandleAsync(Name).Result;
// If there is another copy then pass it the command line and exit
if (isolationHandle == null)
return;
// Use the handle over the lifetime of the application
using (isolationHandle)
{
// Set the path
LegacyDatabase.DatabasePath = SystemConfiguration.DataDirectory;
LegacyDatabase.DatabaseFile = Path.Combine(SystemConfiguration.DataDirectory,
Settings.Default.DatabaseFile_Legacy);
Database.DatabasePath = SystemConfiguration.DataDirectory;
Database.DatabaseFile = Path.Combine(SystemConfiguration.DataDirectory, Settings.Default.DatabaseFile);
// Get the generic provider
var genericProvider =
(GenericSettingsProvider) Settings.Default.Providers[nameof(GenericSettingsProvider)];
if (genericProvider == null)
return;
// Set the callbacks into the provider
genericProvider.OpenDataStore = SettingsStore.OpenDataStore;
genericProvider.GetSettingValue = SettingsStore.GetSettingValue;
genericProvider.SetSettingValue = SettingsStore.SetSettingValue;
Log.Logger = new LoggerConfiguration()
.Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.File(
Path.Join(SystemConfiguration.UserSettingsPath,
$"{FeedCenter.Properties.Resources.ApplicationName}_.txt"),
rollingInterval: RollingInterval.Day, retainedFileCountLimit: 5,
outputTemplate: "[{Timestamp:u} - {ThreadId} - {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
Log.Logger.Information("---");
Log.Logger.Information("Application started");
Log.Logger.Information("Command line arguments:");
foreach (var arg in Environment.GetCommandLineArgs()
.Select((value, index) => (Value: value, Index: index)))
Log.Logger.Information("\tArg {0}: {1}", arg.Index, arg.Value);
Current.DispatcherUnhandledException += HandleCurrentDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += HandleCurrentDomainUnhandledException;
// Check if we need to upgrade settings from a previous version
if (Settings.Default.FirstRun)
{
Settings.Default.Upgrade();
Settings.Default.FirstRun = false;
Settings.Default.Save();
}
// Create the main window before the splash otherwise WPF gets messed up
var mainWindow = new MainWindow();
// Show the splash window
var splashWindow = new SplashWindow();
splashWindow.ShowDialog();
// Set whether we should auto-start (if not debugging)
if (!IsDebugBuild)
Current.SetStartWithWindows(Settings.Default.StartWithWindows);
// Initialize the window
mainWindow.Initialize();
// Run the app
app.Run(mainWindow);
}
}
private static void HandleCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Log.Logger.Error((Exception) e.ExceptionObject, "Exception");
}
private static void HandleCurrentDispatcherUnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{
Log.Logger.Error(e.Exception, "Exception");
}
} }

View File

@@ -1,25 +1,24 @@
using System.IO; using System.IO;
namespace FeedCenter.Data namespace FeedCenter.Data;
public static class Database
{ {
public static class Database public static string DatabaseFile { get; set; }
public static string DatabasePath { get; set; }
public static FeedCenterEntities Entities { get; set; }
public static bool Exists => File.Exists(DatabaseFile);
public static bool Loaded { get; set; }
public static void Load()
{ {
public static string DatabaseFile { get; set; } if (Loaded) return;
public static string DatabasePath { get; set; }
public static FeedCenterEntities Entities { get; set; } Entities = new FeedCenterEntities();
public static bool Exists => File.Exists(DatabaseFile); Loaded = true;
public static bool Loaded { get; set; }
public static void Load()
{
if (Loaded) return;
Entities = new FeedCenterEntities();
Loaded = true;
}
} }
} }

View File

@@ -2,28 +2,27 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
namespace FeedCenter.Data namespace FeedCenter.Data;
public class RealmObservableCollection<T> : ObservableCollection<T> where T : IRealmObject
{ {
public class RealmObservableCollection<T> : ObservableCollection<T> where T : IRealmObject private readonly Realm _realm;
public RealmObservableCollection(Realm realm) : base(realm.All<T>())
{ {
private readonly Realm _realm; _realm = realm;
}
public RealmObservableCollection(Realm realm) : base(realm.All<T>()) protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
_realm = realm; if (e.OldItems != null)
} foreach (T item in e.OldItems)
_realm.Remove(item);
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) if (e.NewItems != null)
{ foreach (T item in e.NewItems)
if (e.OldItems != null) _realm.Add(item);
foreach (T item in e.OldItems)
_realm.Remove(item);
if (e.NewItems != null) base.OnCollectionChanged(e);
foreach (T item in e.NewItems)
_realm.Add(item);
base.OnCollectionChanged(e);
}
} }
} }

View File

@@ -4,50 +4,49 @@ using Realms;
using System; using System;
using System.Linq; using System.Linq;
namespace FeedCenter namespace FeedCenter;
public class FeedCenterEntities
{ {
public class FeedCenterEntities public Realm RealmInstance { get; }
public RealmObservableCollection<Category> Categories { get; }
public RealmObservableCollection<Feed> Feeds { get; private set; }
public RealmObservableCollection<Setting> Settings { get; private set; }
public FeedCenterEntities()
{ {
public Realm RealmInstance { get; } var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}");
public RealmObservableCollection<Category> Categories { get; } RealmInstance = Realm.GetInstance(realmConfiguration);
public RealmObservableCollection<Feed> Feeds { get; private set; }
public RealmObservableCollection<Setting> Settings { get; private set; }
public FeedCenterEntities() Settings = new RealmObservableCollection<Setting>(RealmInstance);
Feeds = new RealmObservableCollection<Feed>(RealmInstance);
Categories = new RealmObservableCollection<Category>(RealmInstance);
if (!Categories.Any())
{ {
var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}"); RealmInstance.Write(() => Categories.Add(Category.CreateDefault()));
RealmInstance = Realm.GetInstance(realmConfiguration);
Settings = new RealmObservableCollection<Setting>(RealmInstance);
Feeds = new RealmObservableCollection<Feed>(RealmInstance);
Categories = new RealmObservableCollection<Category>(RealmInstance);
if (!Categories.Any())
{
RealmInstance.Write(() => Categories.Add(Category.CreateDefault()));
}
}
public void Refresh()
{
RealmInstance.Refresh();
}
public void SaveChanges(Action action)
{
RealmInstance.Write(action);
}
public Transaction BeginTransaction()
{
return RealmInstance.BeginWrite();
}
public Category DefaultCategory
{
get { return Categories.First(c => c.IsDefault); }
} }
} }
public void Refresh()
{
RealmInstance.Refresh();
}
public void SaveChanges(Action action)
{
RealmInstance.Write(action);
}
public Transaction BeginTransaction()
{
return RealmInstance.BeginWrite();
}
public Category DefaultCategory
{
get { return Categories.First(c => c.IsDefault); }
}
} }

View File

@@ -132,7 +132,7 @@
<PackageReference Include="ChrisKaczor.Wpf.Controls.HtmlTextBlock" Version="1.0.2" /> <PackageReference Include="ChrisKaczor.Wpf.Controls.HtmlTextBlock" Version="1.0.2" />
<PackageReference Include="ChrisKaczor.Wpf.Controls.Link" Version="1.0.3" /> <PackageReference Include="ChrisKaczor.Wpf.Controls.Link" Version="1.0.3" />
<PackageReference Include="ChrisKaczor.Wpf.Controls.Toolbar" Version="1.0.2" /> <PackageReference Include="ChrisKaczor.Wpf.Controls.Toolbar" Version="1.0.2" />
<PackageReference Include="ChrisKaczor.Wpf.Validation" Version="1.0.2" /> <PackageReference Include="ChrisKaczor.Wpf.Validation" Version="1.0.3" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.ControlBox" Version="1.0.2" /> <PackageReference Include="ChrisKaczor.Wpf.Windows.ControlBox" Version="1.0.2" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.SnappingWindow" Version="1.0.2" /> <PackageReference Include="ChrisKaczor.Wpf.Windows.SnappingWindow" Version="1.0.2" />
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />

View File

@@ -14,12 +14,24 @@
FocusManager.FocusedElement="{Binding ElementName=FeedDataGrid}" FocusManager.FocusedElement="{Binding ElementName=FeedDataGrid}"
controlBox:ControlBox.HasMaximizeButton="False" controlBox:ControlBox.HasMaximizeButton="False"
controlBox:ControlBox.HasMinimizeButton="False"> controlBox:ControlBox.HasMinimizeButton="False">
<Grid> <Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="6">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DataGrid AutoGenerateColumns="False" <DataGrid Grid.Row="0"
Grid.Column="0"
AutoGenerateColumns="False"
x:Name="FeedDataGrid" x:Name="FeedDataGrid"
CanUserReorderColumns="False" CanUserReorderColumns="False"
GridLinesVisibility="None" GridLinesVisibility="None"
@@ -27,16 +39,11 @@
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
HeadersVisibility="Column" HeadersVisibility="Column"
Margin="6" BorderThickness="1,1,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Background="{x:Null}" Background="{x:Null}"
CanUserSortColumns="True" CanUserSortColumns="True"
MouseDoubleClick="HandleMouseDoubleClick"> MouseDoubleClick="HandleMouseDoubleClick">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness"
Value="0" />
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="{x:Static my:Resources.FeedNameColumnHeader}" <DataGridTextColumn Header="{x:Static my:Resources.FeedNameColumnHeader}"
Binding="{Binding Item2}" Binding="{Binding Item2}"
@@ -44,22 +51,24 @@
SortDirection="Ascending" /> SortDirection="Ascending" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<Button Content="{x:Static my:Resources.OkayButton}" <StackPanel
Height="23" Grid.Column="0"
IsDefault="True" Grid.Row="1"
Width="75" Orientation="Horizontal"
Click="HandleOkayButtonClick" Margin="0,5,0,0"
Margin="0,0,90,10" HorizontalAlignment="Right">
Grid.Row="1" <Button Content="{x:Static my:Resources.OkayButton}"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
HorizontalAlignment="Right" /> VerticalAlignment="Bottom"
<Button Content="{x:Static my:Resources.CancelButton}" Width="75"
Height="23" Margin="0,0,5,0"
IsCancel="True" IsDefault="True"
Width="75" Click="HandleOkayButtonClick" />
Margin="0,0,10,10" <Button Content="{x:Static my:Resources.CancelButton}"
Grid.Row="1" HorizontalAlignment="Right"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
HorizontalAlignment="Right" /> Width="75"
IsCancel="True" />
</StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -2,51 +2,50 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
public partial class FeedChooserWindow
{ {
public partial class FeedChooserWindow private string _returnLink;
public FeedChooserWindow()
{ {
private string _returnLink; InitializeComponent();
}
public FeedChooserWindow() public string Display(Window owner, List<Tuple<string, string>> rssLinks)
{ {
InitializeComponent(); // Bind to the list
} FeedDataGrid.ItemsSource = rssLinks;
FeedDataGrid.SelectedIndex = 0;
public string Display(Window owner, List<Tuple<string, string>> rssLinks) // Set the window owner
{ Owner = owner;
// Bind to the list
FeedDataGrid.ItemsSource = rssLinks;
FeedDataGrid.SelectedIndex = 0;
// Set the window owner ShowDialog();
Owner = owner;
ShowDialog(); return _returnLink;
}
return _returnLink; private void Save()
} {
var selectedItem = (Tuple<string, string>) FeedDataGrid.SelectedItem;
private void Save() _returnLink = selectedItem.Item1;
{
var selectedItem = (Tuple<string, string>) FeedDataGrid.SelectedItem;
_returnLink = selectedItem.Item1; Close();
}
Close(); private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
} {
Save();
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e) private void HandleMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (FeedDataGrid.SelectedItem != null)
{ {
Save(); Save();
} }
private void HandleMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (FeedDataGrid.SelectedItem != null)
{
Save();
}
}
} }
} }

View File

@@ -2,7 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:FeedCenter.Properties" xmlns:my="clr-namespace:FeedCenter.Properties"
xmlns:linkControl="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.Link" xmlns:linkControl="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.Link"
xmlns:controlBox="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.ControlBox" xmlns:controlBox="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.ControlBox" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:feedCenter="clr-namespace:FeedCenter"
mc:Ignorable="d"
x:Class="FeedCenter.FeedErrorWindow" x:Class="FeedCenter.FeedErrorWindow"
Title="{x:Static my:Resources.FeedErrorWindow}" Title="{x:Static my:Resources.FeedErrorWindow}"
Height="300" Height="300"
@@ -11,29 +13,40 @@
Icon="/FeedCenter;component/Resources/Application.ico" Icon="/FeedCenter;component/Resources/Application.ico"
controlBox:ControlBox.HasMaximizeButton="False" controlBox:ControlBox.HasMaximizeButton="False"
controlBox:ControlBox.HasMinimizeButton="False"> controlBox:ControlBox.HasMinimizeButton="False">
<Grid> <Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="6">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="225*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DataGrid AutoGenerateColumns="False" <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<DataGrid Grid.Row="0"
Grid.Column="0"
AutoGenerateColumns="False"
x:Name="FeedDataGrid" x:Name="FeedDataGrid"
CanUserReorderColumns="False" CanUserReorderColumns="False"
GridLinesVisibility="None" GridLinesVisibility="None"
SelectionMode="Single" SelectionMode="Single"
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
BorderThickness="1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
HeadersVisibility="Column" HeadersVisibility="Column"
Margin="6,6,6,0"
Background="{x:Null}" Background="{x:Null}"
CanUserSortColumns="True"> CanUserSortColumns="True"
<DataGrid.CellStyle> d:DataContext="{d:DesignInstance Type=feedCenter:Feed}" SelectionChanged="FeedDataGrid_SelectionChanged">
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness"
Value="0" />
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="{x:Static my:Resources.FeedNameColumnHeader}" <DataGridTextColumn Header="{x:Static my:Resources.FeedNameColumnHeader}"
Binding="{Binding Name}" Binding="{Binding Name}"
@@ -49,50 +62,42 @@
</DataGrid> </DataGrid>
<Border Grid.Row="1" <Border Grid.Row="1"
BorderThickness="1,0,1,1" BorderThickness="1,0,1,1"
Margin="6,0,6,3"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<linkControl:Link x:Name="EditFeedButton" <linkControl:Link x:Name="EditFeedButton"
Margin="2" Margin="2"
Click="HandleEditFeedButtonClick" Click="HandleEditFeedButtonClick"
Text="{x:Static my:Resources.EditLink}" Text="{x:Static my:Resources.EditLink}"
ToolTip="{x:Static my:Resources.EditFeedButton}" /> ToolTip="{x:Static my:Resources.EditFeedButton}" />
<linkControl:Link x:Name="DeleteFeedButton" <linkControl:Link x:Name="DeleteFeedButton"
Margin="2" Margin="2"
Click="HandleDeleteFeedButtonClick" Click="HandleDeleteFeedButtonClick"
Text="{x:Static my:Resources.DeleteLink}" Text="{x:Static my:Resources.DeleteLink}"
ToolTip="{x:Static my:Resources.DeleteFeedButton}" /> ToolTip="{x:Static my:Resources.DeleteFeedButton}" />
<linkControl:Link x:Name="RefreshCurrent" <linkControl:Link x:Name="RefreshCurrent"
Margin="2" Margin="2"
Click="HandleRefreshCurrentButtonClick" Click="HandleRefreshCurrentButtonClick"
Text="{x:Static my:Resources.RefreshCurrent}" Text="{x:Static my:Resources.RefreshCurrent}"
ToolTip="{x:Static my:Resources.RefreshCurrent}" /> ToolTip="{x:Static my:Resources.RefreshCurrent}" />
<linkControl:Link x:Name="OpenPage" <linkControl:Link x:Name="OpenPage"
Margin="6,2,2,2" Margin="6,2,2,2"
Click="HandleOpenPageButtonClick" Click="HandleOpenPageButtonClick"
Text="{x:Static my:Resources.OpenPage}" Text="{x:Static my:Resources.OpenPage}"
ToolTip="{x:Static my:Resources.OpenPage}" /> ToolTip="{x:Static my:Resources.OpenPage}" />
<linkControl:Link x:Name="OpenFeed" <linkControl:Link x:Name="OpenFeed"
Margin="2" Margin="2"
Click="HandleOpenFeedButtonClick" Click="HandleOpenFeedButtonClick"
Text="{x:Static my:Resources.OpenFeed}" Text="{x:Static my:Resources.OpenFeed}"
ToolTip="{x:Static my:Resources.OpenFeed}" /> ToolTip="{x:Static my:Resources.OpenFeed}" />
</StackPanel> </StackPanel>
</Border> </Border>
<Grid DockPanel.Dock="Right" <Button
Grid.Row="2" Grid.Row="2"
Margin="6,3,6,6"> Grid.Column="0"
<Grid.ColumnDefinitions> Margin="0,6,0,0"
<ColumnDefinition Width="*" /> Content="{x:Static my:Resources.CloseButton}"
<ColumnDefinition Width="Auto" /> HorizontalAlignment="Right"
<ColumnDefinition Width="Auto" /> IsCancel="True" />
</Grid.ColumnDefinitions>
<Button Content="{x:Static my:Resources.CloseButton}"
Height="23"
IsCancel="True"
Width="75"
Grid.Column="2" />
</Grid>
</Grid> </Grid>
</Window> </Window>

View File

@@ -9,128 +9,134 @@ using FeedCenter.Data;
using FeedCenter.Options; using FeedCenter.Options;
using FeedCenter.Properties; using FeedCenter.Properties;
namespace FeedCenter namespace FeedCenter;
public partial class FeedErrorWindow
{ {
public partial class FeedErrorWindow private CollectionViewSource _collectionViewSource;
public FeedErrorWindow()
{ {
private CollectionViewSource _collectionViewSource; InitializeComponent();
}
public FeedErrorWindow() public void Display(Window owner)
{
// Create a view and sort it by name
_collectionViewSource = new CollectionViewSource { Source = Database.Entities.Feeds };
_collectionViewSource.Filter += HandleCollectionViewSourceFilter;
_collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
// Bind to the list
FeedDataGrid.ItemsSource = _collectionViewSource.View;
FeedDataGrid.SelectedIndex = 0;
// Set the window owner
Owner = owner;
// Show the dialog and result the result
ShowDialog();
}
private static void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{
var feed = (Feed) e.Item;
e.Accepted = feed.LastReadResult != FeedReadResult.Success;
}
private void HandleEditFeedButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedFeed();
}
private void HandleDeleteFeedButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedFeed();
}
private void EditSelectedFeed()
{
if (FeedDataGrid.SelectedItem == null)
return;
var feed = (Feed) FeedDataGrid.SelectedItem;
var feedWindow = new FeedWindow();
feedWindow.Display(feed, GetWindow(this));
}
private void DeleteSelectedFeed()
{
if (MessageBox.Show(this, Properties.Resources.ConfirmDeleteFeed, Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var feed = (Feed) FeedDataGrid.SelectedItem;
Database.Entities.SaveChanges(() => Database.Entities.Feeds.Remove(feed));
SetFeedButtonStates();
}
private void SetFeedButtonStates()
{
var feed = FeedDataGrid.SelectedItem as Feed;
EditFeedButton.IsEnabled = feed != null;
DeleteFeedButton.IsEnabled = feed != null;
RefreshCurrent.IsEnabled = feed != null;
OpenPage.IsEnabled = feed != null && !string.IsNullOrEmpty(feed.Link);
OpenFeed.IsEnabled = FeedDataGrid.SelectedItem != null;
}
private void HandleOpenPageButtonClick(object sender, RoutedEventArgs e)
{
var feed = (Feed) FeedDataGrid.SelectedItem;
InstalledBrowser.OpenLink(Settings.Default.Browser, feed.Link);
}
private void HandleOpenFeedButtonClick(object sender, RoutedEventArgs e)
{
var feed = (Feed) FeedDataGrid.SelectedItem;
InstalledBrowser.OpenLink(Settings.Default.Browser, feed.Source);
}
private async void HandleRefreshCurrentButtonClick(object sender, RoutedEventArgs e)
{
IsEnabled = false;
Mouse.OverrideCursor = Cursors.Wait;
var feedId = ((Feed) FeedDataGrid.SelectedItem).Id;
await Task.Run(() =>
{ {
InitializeComponent(); var entities = new FeedCenterEntities();
}
public void Display(Window owner) var feed = entities.Feeds.First(f => f.Id == feedId);
{
// Create a view and sort it by name
_collectionViewSource = new CollectionViewSource { Source = Database.Entities.Feeds };
_collectionViewSource.Filter += HandleCollectionViewSourceFilter;
_collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
// Bind to the list entities.SaveChanges(() => feed.Read(true));
FeedDataGrid.ItemsSource = _collectionViewSource.View; });
FeedDataGrid.SelectedIndex = 0;
// Set the window owner Database.Entities.Refresh();
Owner = owner;
// Show the dialog and result the result var selectedIndex = FeedDataGrid.SelectedIndex;
ShowDialog();
}
private static void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e) _collectionViewSource.View.Refresh();
{
var feed = (Feed) e.Item;
e.Accepted = feed.LastReadResult != FeedReadResult.Success; if (selectedIndex >= FeedDataGrid.Items.Count)
} FeedDataGrid.SelectedIndex = FeedDataGrid.Items.Count - 1;
else
FeedDataGrid.SelectedIndex = selectedIndex;
private void HandleEditFeedButtonClick(object sender, RoutedEventArgs e) SetFeedButtonStates();
{
EditSelectedFeed();
}
private void HandleDeleteFeedButtonClick(object sender, RoutedEventArgs e) Mouse.OverrideCursor = null;
{ IsEnabled = true;
DeleteSelectedFeed(); }
}
private void EditSelectedFeed() private void FeedDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{ {
if (FeedDataGrid.SelectedItem == null) SetFeedButtonStates();
return;
var feed = (Feed) FeedDataGrid.SelectedItem;
var feedWindow = new FeedWindow();
feedWindow.Display(feed, GetWindow(this));
}
private void DeleteSelectedFeed()
{
if (MessageBox.Show(this, Properties.Resources.ConfirmDeleteFeed, Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var feed = (Feed) FeedDataGrid.SelectedItem;
Database.Entities.SaveChanges(() => Database.Entities.Feeds.Remove(feed));
SetFeedButtonStates();
}
private void SetFeedButtonStates()
{
EditFeedButton.IsEnabled = FeedDataGrid.SelectedItem != null;
DeleteFeedButton.IsEnabled = FeedDataGrid.SelectedItem != null;
RefreshCurrent.IsEnabled = FeedDataGrid.SelectedItem != null;
OpenPage.IsEnabled = FeedDataGrid.SelectedItem != null;
OpenFeed.IsEnabled = FeedDataGrid.SelectedItem != null;
}
private void HandleOpenPageButtonClick(object sender, RoutedEventArgs e)
{
var feed = (Feed) FeedDataGrid.SelectedItem;
InstalledBrowser.OpenLink(Settings.Default.Browser, feed.Link);
}
private void HandleOpenFeedButtonClick(object sender, RoutedEventArgs e)
{
var feed = (Feed) FeedDataGrid.SelectedItem;
InstalledBrowser.OpenLink(Settings.Default.Browser, feed.Source);
}
private async void HandleRefreshCurrentButtonClick(object sender, RoutedEventArgs e)
{
IsEnabled = false;
Mouse.OverrideCursor = Cursors.Wait;
var feedId = ((Feed) FeedDataGrid.SelectedItem).Id;
await Task.Run(() =>
{
var entities = new FeedCenterEntities();
var feed = entities.Feeds.First(f => f.Id == feedId);
entities.SaveChanges(() => feed.Read(true));
});
Database.Entities.Refresh();
var selectedIndex = FeedDataGrid.SelectedIndex;
_collectionViewSource.View.Refresh();
if (selectedIndex >= FeedDataGrid.Items.Count)
FeedDataGrid.SelectedIndex = FeedDataGrid.Items.Count - 1;
else
FeedDataGrid.SelectedIndex = selectedIndex;
SetFeedButtonStates();
Mouse.OverrideCursor = null;
IsEnabled = true;
}
} }
} }

View File

@@ -2,142 +2,141 @@
using System; using System;
using System.Xml; using System.Xml;
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
internal class AtomParser : FeedParserBase
{ {
internal class AtomParser : FeedParserBase public AtomParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
{ {
public AtomParser(Feed feed) : base(feed) { } try
public override FeedReadResult ParseFeed(string feedText)
{ {
try // Create the XML document
{ var document = new XmlDocument { XmlResolver = null };
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text // Load the XML document from the text
document.LoadXml(feedText); document.LoadXml(feedText);
// Get the root node // Get the root node
XmlNode rootNode = document.DocumentElement; XmlNode rootNode = document.DocumentElement;
// If we didn't find a root node then bail // If we didn't find a root node then bail
if (rootNode == null) if (rootNode == null)
return FeedReadResult.UnknownError; return FeedReadResult.UnknownError;
// Initialize the sequence number for items // Initialize the sequence number for items
var sequence = 0; var sequence = 0;
// Loop over all nodes in the root node // Loop over all nodes in the root node
foreach (XmlNode node in rootNode.ChildNodes) foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "title":
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
string rel = null;
if (node.Attributes == null)
break;
XmlNode relNode = GetAttribute(node, "rel");
if (relNode != null)
rel = relNode.InnerText;
if (string.IsNullOrEmpty(rel) || rel == "alternate")
Feed.Link = GetAttribute(node, "href").InnerText.Trim();
break;
case "subtitle":
Feed.Description = node.InnerText;
break;
case "entry":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml;
}
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{ {
// Handle each node that we find // Handle each node that we find
switch (childNode.Name.ToLower()) switch (node.Name)
{ {
case "title": case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim(); Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "id":
feedItem.Guid = childNode.InnerText;
break;
case "content":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break; break;
case "link": case "link":
string rel = null; string rel = null;
if (childNode.Attributes == null) if (node.Attributes == null)
break; break;
XmlNode relNode = GetAttribute(childNode, "rel"); XmlNode relNode = GetAttribute(node, "rel");
if (relNode != null) if (relNode != null)
rel = relNode.InnerText.Trim(); rel = relNode.InnerText;
if (string.IsNullOrEmpty(rel) || rel == "alternate") if (string.IsNullOrEmpty(rel) || rel == "alternate")
{ Feed.Link = GetAttribute(node, "href").InnerText.Trim();
var link = GetAttribute(childNode, "href").InnerText;
if (link.StartsWith("/")) break;
{
var uri = new Uri(Feed.Link);
link = uri.Scheme + "://" + uri.Host + link; case "subtitle":
} Feed.Description = node.InnerText;
break;
feedItem.Link = link;
}
case "entry":
HandleFeedItem(node, ref sequence);
break; break;
} }
} }
if (string.IsNullOrWhiteSpace(feedItem.Guid)) return FeedReadResult.Success;
feedItem.Guid = feedItem.Link;
return feedItem;
} }
catch (XmlException xmlException)
private static XmlAttribute GetAttribute(XmlNode node, string attributeName)
{ {
if (node?.Attributes == null) Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return null;
return node.Attributes[attributeName, node.NamespaceURI] ?? node.Attributes[attributeName]; return FeedReadResult.InvalidXml;
} }
} }
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "id":
feedItem.Guid = childNode.InnerText;
break;
case "content":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break;
case "link":
string rel = null;
if (childNode.Attributes == null)
break;
XmlNode relNode = GetAttribute(childNode, "rel");
if (relNode != null)
rel = relNode.InnerText.Trim();
if (string.IsNullOrEmpty(rel) || rel == "alternate")
{
var link = GetAttribute(childNode, "href").InnerText;
if (link.StartsWith("/"))
{
var uri = new Uri(Feed.Link);
link = uri.Scheme + "://" + uri.Host + link;
}
feedItem.Link = link;
}
break;
}
}
if (string.IsNullOrWhiteSpace(feedItem.Guid))
feedItem.Guid = feedItem.Link;
return feedItem;
}
private static XmlAttribute GetAttribute(XmlNode node, string attributeName)
{
if (node?.Attributes == null)
return null;
return node.Attributes[attributeName, node.NamespaceURI] ?? node.Attributes[attributeName];
}
}

View File

@@ -1,8 +1,7 @@
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
internal enum FeedParseError
{ {
internal enum FeedParseError Unknown = 0,
{ InvalidXml = 1
Unknown = 0,
InvalidXml = 1
}
} }

View File

@@ -1,14 +1,13 @@
using System; using System;
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
{
internal class FeedParseException : ApplicationException
{
public FeedParseException(FeedParseError feedParseError)
{
ParseError = feedParseError;
}
public FeedParseError ParseError { get; set; } internal class FeedParseException : ApplicationException
{
public FeedParseException(FeedParseError feedParseError)
{
ParseError = feedParseError;
} }
public FeedParseError ParseError { get; set; }
} }

View File

@@ -3,159 +3,158 @@ using System;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
[Serializable]
internal class InvalidFeedFormatException : ApplicationException
{ {
[Serializable] internal InvalidFeedFormatException(Exception exception)
internal class InvalidFeedFormatException : ApplicationException : base(string.Empty, exception)
{ {
internal InvalidFeedFormatException(Exception exception)
: base(string.Empty, exception)
{
}
}
internal abstract class FeedParserBase
{
#region Member variables
protected readonly Feed Feed;
#endregion
#region Constructor
protected FeedParserBase(Feed feed)
{
Feed = feed;
}
#endregion
#region Methods
public abstract FeedReadResult ParseFeed(string feedText);
protected abstract FeedItem ParseFeedItem(XmlNode node);
protected void HandleFeedItem(XmlNode node, ref int sequence)
{
// Build a feed item from the node
var newFeedItem = ParseFeedItem(node);
if (newFeedItem == null)
return;
// Check for feed items with no guid or link
if (string.IsNullOrWhiteSpace(newFeedItem.Guid) && string.IsNullOrWhiteSpace(newFeedItem.Link))
return;
// Look for an item that has the same guid
var existingFeedItem = Feed.Items.FirstOrDefault(item => item.Guid == newFeedItem.Guid && item.Id != newFeedItem.Id);
// Check to see if we already have this feed item
if (existingFeedItem == null)
{
Log.Logger.Information("New link: " + newFeedItem.Link);
// Associate the new item with the right feed
newFeedItem.Feed = Feed;
// Set the item as new
newFeedItem.New = true;
// Add the item to the list
Feed.Items.Add(newFeedItem);
// Feed was updated
Feed.LastUpdated = DateTime.Now;
}
else
{
Log.Logger.Information("Existing link: " + newFeedItem.Link);
// Update the fields in the existing item
existingFeedItem.Link = newFeedItem.Link;
existingFeedItem.Title = newFeedItem.Title;
existingFeedItem.Guid = newFeedItem.Guid;
existingFeedItem.Description = newFeedItem.Description;
// Item is no longer new
existingFeedItem.New = false;
// Switch over to the existing item for the rest
newFeedItem = existingFeedItem;
}
// Item was last seen now
newFeedItem.LastFound = Feed.LastChecked;
// Set the sequence
newFeedItem.Sequence = sequence;
// Increment the sequence
sequence++;
}
#endregion
#region Parser creation and detection
public static FeedParserBase CreateFeedParser(Feed feed, string feedText)
{
var feedType = DetectFeedType(feedText);
return feedType switch
{
FeedType.Rss => new RssParser(feed),
FeedType.Rdf => new RdfParser(feed),
FeedType.Atom => new AtomParser(feed),
_ => throw new ArgumentException($"Feed type {feedType} is not supported")
};
}
public static FeedType DetectFeedType(string feedText)
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Loop over all child nodes
foreach (XmlNode node in document.ChildNodes)
{
switch (node.Name)
{
case "rss":
return FeedType.Rss;
case "rdf:RDF":
return FeedType.Rdf;
case "feed":
return FeedType.Atom;
}
}
// No clue!
return FeedType.Unknown;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
}
#endregion
} }
} }
internal abstract class FeedParserBase
{
#region Member variables
protected readonly Feed Feed;
#endregion
#region Constructor
protected FeedParserBase(Feed feed)
{
Feed = feed;
}
#endregion
#region Methods
public abstract FeedReadResult ParseFeed(string feedText);
protected abstract FeedItem ParseFeedItem(XmlNode node);
protected void HandleFeedItem(XmlNode node, ref int sequence)
{
// Build a feed item from the node
var newFeedItem = ParseFeedItem(node);
if (newFeedItem == null)
return;
// Check for feed items with no guid or link
if (string.IsNullOrWhiteSpace(newFeedItem.Guid) && string.IsNullOrWhiteSpace(newFeedItem.Link))
return;
// Look for an item that has the same guid
var existingFeedItem = Feed.Items.FirstOrDefault(item => item.Guid == newFeedItem.Guid && item.Id != newFeedItem.Id);
// Check to see if we already have this feed item
if (existingFeedItem == null)
{
Log.Logger.Information("New link: " + newFeedItem.Link);
// Associate the new item with the right feed
newFeedItem.Feed = Feed;
// Set the item as new
newFeedItem.New = true;
// Add the item to the list
Feed.Items.Add(newFeedItem);
// Feed was updated
Feed.LastUpdated = DateTime.Now;
}
else
{
Log.Logger.Information("Existing link: " + newFeedItem.Link);
// Update the fields in the existing item
existingFeedItem.Link = newFeedItem.Link;
existingFeedItem.Title = newFeedItem.Title;
existingFeedItem.Guid = newFeedItem.Guid;
existingFeedItem.Description = newFeedItem.Description;
// Item is no longer new
existingFeedItem.New = false;
// Switch over to the existing item for the rest
newFeedItem = existingFeedItem;
}
// Item was last seen now
newFeedItem.LastFound = Feed.LastChecked;
// Set the sequence
newFeedItem.Sequence = sequence;
// Increment the sequence
sequence++;
}
#endregion
#region Parser creation and detection
public static FeedParserBase CreateFeedParser(Feed feed, string feedText)
{
var feedType = DetectFeedType(feedText);
return feedType switch
{
FeedType.Rss => new RssParser(feed),
FeedType.Rdf => new RdfParser(feed),
FeedType.Atom => new AtomParser(feed),
_ => throw new ArgumentException($"Feed type {feedType} is not supported")
};
}
public static FeedType DetectFeedType(string feedText)
{
try
{
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text
document.LoadXml(feedText);
// Loop over all child nodes
foreach (XmlNode node in document.ChildNodes)
{
switch (node.Name)
{
case "rss":
return FeedType.Rss;
case "rdf:RDF":
return FeedType.Rdf;
case "feed":
return FeedType.Atom;
}
}
// No clue!
return FeedType.Unknown;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
}
#endregion
}

View File

@@ -2,113 +2,112 @@
using Serilog; using Serilog;
using System.Xml; using System.Xml;
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
internal class RdfParser : FeedParserBase
{ {
internal class RdfParser : FeedParserBase public RdfParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
{ {
public RdfParser(Feed feed) : base(feed) { } try
public override FeedReadResult ParseFeed(string feedText)
{ {
try // Create the XML document
{ var document = new XmlDocument { XmlResolver = null };
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text // Load the XML document from the text
document.LoadXml(feedText); document.LoadXml(feedText);
// Create the namespace manager // Create the namespace manager
var namespaceManager = document.GetAllNamespaces(); var namespaceManager = document.GetAllNamespaces();
// Get the root node // Get the root node
XmlNode rootNode = document.DocumentElement; XmlNode rootNode = document.DocumentElement;
// If we didn't find a root node then bail // If we didn't find a root node then bail
if (rootNode == null) if (rootNode == null)
return FeedReadResult.UnknownError; return FeedReadResult.UnknownError;
// Get the channel node // Get the channel node
var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager); var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager);
if (channelNode == null)
return FeedReadResult.InvalidXml;
// Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "title":
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
Feed.Link = node.InnerText.Trim();
break;
case "description":
Feed.Description = node.InnerText;
break;
}
}
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the channel node
foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
if (channelNode == null)
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
}
}
protected override FeedItem ParseFeedItem(XmlNode node) // Loop over all nodes in the channel node
{ foreach (XmlNode node in channelNode.ChildNodes)
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{ {
// Handle each node that we find // Handle each node that we find
switch (childNode.Name.ToLower()) switch (node.Name)
{ {
case "title": case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim(); Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break; break;
case "link": case "link":
feedItem.Link = childNode.InnerText.Trim(); Feed.Link = node.InnerText.Trim();
// RDF doesn't have a GUID node so we'll just use the link
feedItem.Guid = feedItem.Link;
break; break;
case "description": case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText); Feed.Description = node.InnerText;
break; break;
} }
} }
return feedItem; // Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the channel node
foreach (XmlNode node in rootNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml;
} }
} }
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "link":
feedItem.Link = childNode.InnerText.Trim();
// RDF doesn't have a GUID node so we'll just use the link
feedItem.Guid = feedItem.Link;
break;
case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break;
}
}
return feedItem;
}
}

View File

@@ -3,121 +3,120 @@ using Serilog;
using System; using System;
using System.Xml; using System.Xml;
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
internal class RssParser : FeedParserBase
{ {
internal class RssParser : FeedParserBase public RssParser(Feed feed) : base(feed) { }
public override FeedReadResult ParseFeed(string feedText)
{ {
public RssParser(Feed feed) : base(feed) { } try
public override FeedReadResult ParseFeed(string feedText)
{ {
try // Create the XML document
{ var document = new XmlDocument { XmlResolver = null };
// Create the XML document
var document = new XmlDocument { XmlResolver = null };
// Load the XML document from the text // Load the XML document from the text
document.LoadXml(feedText); document.LoadXml(feedText);
// Create the namespace manager // Create the namespace manager
var namespaceManager = document.GetAllNamespaces(); var namespaceManager = document.GetAllNamespaces();
// Get the root node // Get the root node
XmlNode rootNode = document.DocumentElement; XmlNode rootNode = document.DocumentElement;
// If we didn't find a root node then bail // If we didn't find a root node then bail
if (rootNode == null) if (rootNode == null)
return FeedReadResult.UnknownError; return FeedReadResult.UnknownError;
// Get the channel node // Get the channel node
var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager) ?? var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager) ??
rootNode.SelectSingleNode("channel", namespaceManager); rootNode.SelectSingleNode("channel", namespaceManager);
if (channelNode == null)
return FeedReadResult.InvalidXml;
// Initialize the sequence number for items
var sequence = 0;
// Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes)
{
// Handle each node that we find
switch (node.Name)
{
case "title":
Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break;
case "link":
Feed.Link = node.InnerText.Trim();
break;
case "description":
Feed.Description = node.InnerText;
break;
case "item":
HandleFeedItem(node, ref sequence);
break;
}
}
return FeedReadResult.Success;
}
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
if (channelNode == null)
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
}
}
protected override FeedItem ParseFeedItem(XmlNode node) // Initialize the sequence number for items
{ var sequence = 0;
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node // Loop over all nodes in the channel node
foreach (XmlNode childNode in node.ChildNodes) foreach (XmlNode node in channelNode.ChildNodes)
{ {
// Handle each node that we find // Handle each node that we find
switch (childNode.Name.ToLower()) switch (node.Name)
{ {
case "title": case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim(); Feed.Title = System.Net.WebUtility.HtmlDecode(node.InnerText).Trim();
break; break;
case "link": case "link":
feedItem.Link = childNode.InnerText.Trim(); Feed.Link = node.InnerText.Trim();
break;
case "guid":
feedItem.Guid = childNode.InnerText.Trim();
var permaLink = true;
if (childNode.Attributes != null)
{
var permaLinkNode = childNode.Attributes.GetNamedItem("isPermaLink");
permaLink = permaLinkNode == null || permaLinkNode.Value == "true";
}
if (permaLink && Uri.IsWellFormedUriString(feedItem.Guid, UriKind.Absolute))
feedItem.Link = feedItem.Guid;
break; break;
case "description": case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText); Feed.Description = node.InnerText;
break;
case "item":
HandleFeedItem(node, ref sequence);
break; break;
} }
} }
if (string.IsNullOrWhiteSpace(feedItem.Guid)) return FeedReadResult.Success;
feedItem.Guid = feedItem.Link; }
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return feedItem; return FeedReadResult.InvalidXml;
} }
} }
}
protected override FeedItem ParseFeedItem(XmlNode node)
{
// Create a new feed item
var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes)
{
// Handle each node that we find
switch (childNode.Name.ToLower())
{
case "title":
feedItem.Title = System.Net.WebUtility.HtmlDecode(childNode.InnerText).Trim();
break;
case "link":
feedItem.Link = childNode.InnerText.Trim();
break;
case "guid":
feedItem.Guid = childNode.InnerText.Trim();
var permaLink = true;
if (childNode.Attributes != null)
{
var permaLinkNode = childNode.Attributes.GetNamedItem("isPermaLink");
permaLink = permaLinkNode == null || permaLinkNode.Value == "true";
}
if (permaLink && Uri.IsWellFormedUriString(feedItem.Guid, UriKind.Absolute))
feedItem.Link = feedItem.Guid;
break;
case "description":
feedItem.Description = System.Net.WebUtility.HtmlDecode(childNode.InnerText);
break;
}
}
if (string.IsNullOrWhiteSpace(feedItem.Guid))
feedItem.Guid = feedItem.Link;
return feedItem;
}
}

View File

@@ -6,71 +6,70 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Realms; using Realms;
namespace FeedCenter namespace FeedCenter;
public class Category : RealmObject, INotifyDataErrorInfo
{ {
public class Category : RealmObject, INotifyDataErrorInfo public const string DefaultName = "< default >";
private readonly DataErrorDictionary _dataErrorDictionary;
public Category()
{ {
public const string DefaultName = "< default >"; _dataErrorDictionary = new DataErrorDictionary();
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
}
private readonly DataErrorDictionary _dataErrorDictionary; [Ignored]
public ICollection<Feed> Feeds { get; set; }
public Category() [PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
public bool IsDefault { get; internal set; }
public string Name
{
get => RawName;
set
{ {
_dataErrorDictionary = new DataErrorDictionary(); RawName = value;
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
}
[Ignored] ValidateName();
public ICollection<Feed> Feeds { get; set; } RaisePropertyChanged();
[PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
public bool IsDefault { get; internal set; }
public string Name
{
get => RawName;
set
{
RawName = value;
ValidateName();
RaisePropertyChanged();
}
}
[MapTo("Name")]
private string RawName { get; set; } = string.Empty;
[UsedImplicitly]
public int SortKey => IsDefault ? 0 : 1;
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
{
return _dataErrorDictionary.GetErrors(propertyName);
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
public static Category CreateDefault()
{
return new Category { Name = DefaultName, IsDefault = true };
}
private void ValidateName()
{
_dataErrorDictionary.ClearErrors(nameof(Name));
if (string.IsNullOrWhiteSpace(Name))
_dataErrorDictionary.AddError(nameof(Name), "Name cannot be empty");
} }
} }
[MapTo("Name")]
private string RawName { get; set; } = string.Empty;
[UsedImplicitly]
public int SortKey => IsDefault ? 0 : 1;
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
{
return _dataErrorDictionary.GetErrors(propertyName);
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
public static Category CreateDefault()
{
return new Category { Name = DefaultName, IsDefault = true };
}
private void ValidateName()
{
_dataErrorDictionary.ClearErrors(nameof(Name));
if (string.IsNullOrWhiteSpace(Name))
_dataErrorDictionary.AddError(nameof(Name), "Name cannot be empty");
}
} }

View File

@@ -3,41 +3,40 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
namespace FeedCenter namespace FeedCenter;
internal class DataErrorDictionary : Dictionary<string, List<string>>
{ {
internal class DataErrorDictionary : Dictionary<string, List<string>> public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void OnErrorsChanged(string propertyName)
{ {
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
private void OnErrorsChanged(string propertyName) public IEnumerable GetErrors(string propertyName)
{ {
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); return TryGetValue(propertyName, out var value) ? value : null;
} }
public IEnumerable GetErrors(string propertyName) public void AddError(string propertyName, string error)
{ {
return TryGetValue(propertyName, out var value) ? value : null; if (!ContainsKey(propertyName))
} this[propertyName] = new List<string>();
public void AddError(string propertyName, string error) if (this[propertyName].Contains(error))
{ return;
if (!ContainsKey(propertyName))
this[propertyName] = new List<string>();
if (this[propertyName].Contains(error)) this[propertyName].Add(error);
return; OnErrorsChanged(propertyName);
}
this[propertyName].Add(error); public void ClearErrors(string propertyName)
OnErrorsChanged(propertyName); {
} if (!ContainsKey(propertyName))
return;
public void ClearErrors(string propertyName) Remove(propertyName);
{ OnErrorsChanged(propertyName);
if (!ContainsKey(propertyName))
return;
Remove(propertyName);
OnErrorsChanged(propertyName);
}
} }
} }

View File

@@ -20,431 +20,480 @@ using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace FeedCenter namespace FeedCenter;
#region Enumerations
public enum MultipleOpenAction
{ {
#region Enumerations IndividualPages,
SinglePage
}
public enum MultipleOpenAction public enum FeedType
{
Unknown,
Rss,
Rdf,
Atom
}
public enum FeedReadResult
{
Success,
NotModified,
NotDue,
UnknownError,
InvalidXml,
NotEnabled,
Unauthorized,
NoResponse,
NotFound,
Timeout,
ConnectionFailed,
ServerError,
Moved,
TemporarilyUnavailable
}
#endregion
public partial class Feed : RealmObject, INotifyDataErrorInfo
{
private static HttpClient _httpClient;
private readonly DataErrorDictionary _dataErrorDictionary;
public Feed()
{ {
IndividualPages, _dataErrorDictionary = new DataErrorDictionary();
SinglePage _dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
} }
public enum FeedType public bool Authenticate { get; set; }
public Guid CategoryId { get; set; }
public int CheckInterval { get; set; } = 60;
public string Description { get; set; }
public bool Enabled { get; set; } = true;
[PrimaryKey]
public Guid Id { get; set; }
[UsedImplicitly]
public IList<FeedItem> Items { get; }
public DateTimeOffset LastChecked { get; set; }
public FeedReadResult LastReadResult
{ {
Unknown, get => Enum.TryParse(LastReadResultRaw, out FeedReadResult result) ? result : FeedReadResult.Success;
Rss, set => LastReadResultRaw = value.ToString();
Rdf,
Atom
} }
public enum FeedReadResult // ReSharper disable once UnusedMember.Global
public string LastReadResultDescription
{ {
Success, get
NotModified, {
NotDue, // Cast the last read result to the proper enum
UnknownError, var lastReadResult = LastReadResult;
InvalidXml,
NotEnabled, // Build the name of the resource using the enum name and the value
Unauthorized, var resourceName = $"{nameof(FeedReadResult)}_{lastReadResult}";
NoResponse,
NotFound, // Try to get the value from the resources
Timeout, var resourceValue = Resources.ResourceManager.GetString(resourceName);
ConnectionFailed,
ServerError, // Return the value or just the enum value if not found
Moved return resourceValue ?? lastReadResult.ToString();
}
} }
#endregion private string LastReadResultRaw { get; set; }
public partial class Feed : RealmObject, INotifyDataErrorInfo public DateTimeOffset LastUpdated { get; set; }
public string Link { get; set; }
public MultipleOpenAction MultipleOpenAction
{ {
private static HttpClient _httpClient; get => Enum.TryParse(MultipleOpenActionRaw, out MultipleOpenAction result) ? result : MultipleOpenAction.IndividualPages;
set => MultipleOpenActionRaw = value.ToString();
}
private readonly DataErrorDictionary _dataErrorDictionary; private string MultipleOpenActionRaw { get; set; }
public Feed() public string Name
{
get => RawName;
set
{ {
_dataErrorDictionary = new DataErrorDictionary(); RawName = value;
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
ValidateString(nameof(Name), RawName);
RaisePropertyChanged();
} }
}
public bool Authenticate { get; set; } [MapTo("Password")]
public string RawPassword { get; set; }
public Guid CategoryId { get; set; } public string Password
{
public int CheckInterval { get; set; } = 60; get => RawPassword;
public string Description { get; set; } set
public bool Enabled { get; set; } = true;
[PrimaryKey]
public Guid Id { get; set; }
[UsedImplicitly]
public IList<FeedItem> Items { get; }
public DateTimeOffset LastChecked { get; set; }
public FeedReadResult LastReadResult
{ {
get => Enum.TryParse(LastReadResultRaw, out FeedReadResult result) ? result : FeedReadResult.Success; RawPassword = value;
set => LastReadResultRaw = value.ToString();
}
// ReSharper disable once UnusedMember.Global if (!Authenticate)
public string LastReadResultDescription
{
get
{ {
// Cast the last read result to the proper enum _dataErrorDictionary.ClearErrors(nameof(Password));
var lastReadResult = LastReadResult; return;
// Build the name of the resource using the enum name and the value
var resourceName = $"{nameof(FeedReadResult)}_{lastReadResult}";
// Try to get the value from the resources
var resourceValue = Resources.ResourceManager.GetString(resourceName);
// Return the value or just the enum value if not found
return resourceValue ?? lastReadResult.ToString();
} }
ValidateString(nameof(Password), RawPassword);
RaisePropertyChanged();
} }
}
private string LastReadResultRaw { get; set; } [MapTo("Name")]
private string RawName { get; set; } = string.Empty;
public DateTimeOffset LastUpdated { get; set; } [MapTo("Source")]
public string Link { get; set; } private string RawSource { get; set; } = string.Empty;
public MultipleOpenAction MultipleOpenAction public string Source
{
get => RawSource;
set
{ {
get => Enum.TryParse(MultipleOpenActionRaw, out MultipleOpenAction result) ? result : MultipleOpenAction.IndividualPages; RawSource = value;
set => MultipleOpenActionRaw = value.ToString();
ValidateString(nameof(Source), RawSource);
RaisePropertyChanged();
} }
}
private string MultipleOpenActionRaw { get; set; } public string Title { get; set; }
public string Name [MapTo("Username")]
public string RawUsername { get; set; }
public string Username
{
get => RawUsername;
set
{ {
get => RawName; RawUsername = value;
set
if (!Authenticate)
{ {
RawName = value; _dataErrorDictionary.ClearErrors(nameof(Username));
return;
ValidateString(nameof(Name), RawName);
RaisePropertyChanged();
} }
ValidateString(nameof(Username), RawUsername);
RaisePropertyChanged();
}
}
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
{
return _dataErrorDictionary.GetErrors(propertyName);
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public static Feed Create()
{
return new Feed { Id = Guid.NewGuid(), CategoryId = Database.Entities.DefaultCategory.Id };
}
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
private void ValidateString(string propertyName, string value)
{
_dataErrorDictionary.ClearErrors(propertyName);
if (string.IsNullOrWhiteSpace(value))
_dataErrorDictionary.AddError(propertyName, $"{propertyName} cannot be empty");
}
#region Reading
public FeedReadResult Read(bool forceRead = false)
{
Log.Logger.Information("Reading feed: {0}", Source);
var result = ReadFeed(forceRead);
// Handle the result
switch (result)
{
case FeedReadResult.NotDue:
case FeedReadResult.NotEnabled:
case FeedReadResult.NotModified:
// Ignore
break;
default:
// Save as last result
LastReadResult = result;
break;
} }
public string Password { get; set; } // If the feed was successfully read and we have no last update timestamp - set the last update timestamp to now
if (result == FeedReadResult.Success && LastUpdated == default)
LastUpdated = DateTimeOffset.Now;
[MapTo("Name")] Log.Logger.Information("Done reading feed: {0}", result);
private string RawName { get; set; } = string.Empty;
[MapTo("Source")] return result;
private string RawSource { get; set; } = string.Empty; }
public string Source public Tuple<FeedType, string> DetectFeedType()
{
var retrieveResult = RetrieveFeed();
if (retrieveResult.Item1 != FeedReadResult.Success)
{ {
get => RawSource; return new Tuple<FeedType, string>(FeedType.Unknown, string.Empty);
set }
{
RawSource = value;
ValidateString(nameof(Source), RawSource); var feedType = FeedType.Unknown;
RaisePropertyChanged();
try
{
feedType = FeedParserBase.DetectFeedType(retrieveResult.Item2);
}
catch
{
// Ignore
}
return new Tuple<FeedType, string>(feedType, retrieveResult.Item2);
}
private Tuple<FeedReadResult, string> RetrieveFeed()
{
try
{
// Create and configure the HTTP client if needed
if (_httpClient == null)
{
var clientHandler = new HttpClientHandler
{
// Set that we'll accept compressed data
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
AllowAutoRedirect = true
};
_httpClient = new HttpClient(clientHandler);
// Set a user agent string
var userAgent = string.IsNullOrWhiteSpace(Settings.Default.DefaultUserAgent) ? "FeedCenter/" + UpdateCheck.LocalVersion : Settings.Default.DefaultUserAgent;
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
// Set a timeout
_httpClient.Timeout = TimeSpan.FromSeconds(10);
} }
// If we need to authenticate then set the credentials
_httpClient.DefaultRequestHeaders.Authorization = Authenticate ? new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))) : null;
// Attempt to get the response
var feedStream = _httpClient.GetStreamAsync(Source).Result;
// Create the text reader
using StreamReader textReader = new XmlSanitizingStream(feedStream, Encoding.UTF8);
// Get the feed text
var feedText = textReader.ReadToEnd();
if (string.IsNullOrEmpty(feedText))
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
// Get rid of any leading and trailing whitespace
feedText = feedText.Trim();
// Clean up common invalid XML characters
feedText = feedText.Replace("&nbsp;", "&#160;");
// Find ampersands that aren't properly escaped and replace them with escaped versions
var r = UnescapedAmpersandRegex();
feedText = r.Replace(feedText, "&amp;");
return Tuple.Create(FeedReadResult.Success, feedText);
} }
catch (IOException ioException)
public string Title { get; set; }
public string Username { get; set; }
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
{ {
return _dataErrorDictionary.GetErrors(propertyName); Log.Logger.Error(ioException, "Exception");
return Tuple.Create(FeedReadResult.ConnectionFailed, string.Empty);
} }
catch (AggregateException aggregateException)
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public static Feed Create()
{ {
return new Feed { Id = Guid.NewGuid(), CategoryId = Database.Entities.DefaultCategory.Id }; Log.Logger.Error(aggregateException, "Exception");
}
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e) var innerException = aggregateException.InnerException;
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
private void ValidateString(string propertyName, string value) if (innerException is not HttpRequestException httpRequestException)
{ return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
_dataErrorDictionary.ClearErrors(propertyName);
if (string.IsNullOrWhiteSpace(value)) switch (httpRequestException.StatusCode)
_dataErrorDictionary.AddError(propertyName, $"{propertyName} cannot be empty");
}
#region Reading
public FeedReadResult Read(bool forceRead = false)
{
Log.Logger.Information("Reading feed: {0}", Source);
var result = ReadFeed(forceRead);
// Handle the result
switch (result)
{ {
case FeedReadResult.NotDue: case HttpStatusCode.ServiceUnavailable:
case FeedReadResult.NotEnabled: return Tuple.Create(FeedReadResult.TemporarilyUnavailable, string.Empty);
case FeedReadResult.NotModified:
case HttpStatusCode.InternalServerError:
return Tuple.Create(FeedReadResult.ServerError, string.Empty);
case HttpStatusCode.NotModified:
return Tuple.Create(FeedReadResult.NotModified, string.Empty);
case HttpStatusCode.NotFound:
return Tuple.Create(FeedReadResult.NotFound, string.Empty);
case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden:
return Tuple.Create(FeedReadResult.Unauthorized, string.Empty);
case HttpStatusCode.Moved:
return Tuple.Create(FeedReadResult.Moved, string.Empty);
}
if (httpRequestException.InnerException is not SocketException socketException)
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
return socketException.SocketErrorCode switch
{
SocketError.NoData => Tuple.Create(FeedReadResult.NoResponse, string.Empty),
SocketError.HostNotFound => Tuple.Create(FeedReadResult.NotFound, string.Empty),
_ => Tuple.Create(FeedReadResult.UnknownError, string.Empty)
};
}
catch (WebException webException)
{
var result = FeedReadResult.UnknownError;
switch (webException.Status)
{
case WebExceptionStatus.ConnectFailure:
case WebExceptionStatus.NameResolutionFailure:
result = FeedReadResult.ConnectionFailed;
// Ignore
break; break;
default: case WebExceptionStatus.Timeout:
// Save as last result result = FeedReadResult.Timeout;
LastReadResult = result;
break; break;
} }
// If the feed was successfully read and we have no last update timestamp - set the last update timestamp to now Log.Logger.Error(webException, "Exception");
if (result == FeedReadResult.Success && LastUpdated == default)
LastUpdated = DateTimeOffset.Now;
Log.Logger.Information("Done reading feed: {0}", result); if (result == FeedReadResult.UnknownError)
Debug.Print("Unknown error");
return result; return Tuple.Create(result, string.Empty);
} }
catch (Exception exception)
public Tuple<FeedType, string> DetectFeedType()
{ {
Log.Logger.Error(exception, "Exception");
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
}
}
private FeedReadResult ReadFeed(bool forceRead)
{
try
{
// If not enabled then do nothing
if (!Enabled)
return FeedReadResult.NotEnabled;
// Check if we're forcing a read
if (!forceRead)
{
// Figure out how long since we last checked
var timeSpan = DateTimeOffset.Now - LastChecked;
// Check if we are due to read the feed
if (timeSpan.TotalMinutes < CheckInterval)
return FeedReadResult.NotDue;
}
// We're checking it now so update the time
LastChecked = DateTimeOffset.Now;
// Read the feed text
var retrieveResult = RetrieveFeed(); var retrieveResult = RetrieveFeed();
if (retrieveResult.Item1 != FeedReadResult.Success) // Get the information out of the async result
var result = retrieveResult.Item1;
var feedText = retrieveResult.Item2;
// If we didn't successfully retrieve the feed then stop
if (result != FeedReadResult.Success)
return result;
// Create a new RSS parser
var feedParser = FeedParserBase.CreateFeedParser(this, feedText);
// Parse the feed
result = feedParser.ParseFeed(feedText);
// If we didn't successfully parse the feed then stop
if (result != FeedReadResult.Success)
return result;
// Create the removed items list - if an item wasn't seen during this check then remove it
var removedItems = Items.Where(testItem => testItem.LastFound != LastChecked).ToList();
// If items were removed the feed was updated
if (removedItems.Count > 0)
LastUpdated = DateTime.Now;
// Loop over the items to be removed
foreach (var itemToRemove in removedItems)
{ {
return new Tuple<FeedType, string>(FeedType.Unknown, string.Empty); // Remove the item from the list
Items.Remove(itemToRemove);
} }
return new Tuple<FeedType, string>(FeedParserBase.DetectFeedType(retrieveResult.Item2), retrieveResult.Item2); return FeedReadResult.Success;
} }
catch (FeedParseException feedParseException)
private Tuple<FeedReadResult, string> RetrieveFeed()
{ {
try Log.Logger.Error(feedParseException, "Exception");
{
// Create and configure the HTTP client if needed
if (_httpClient == null)
{
var clientHandler = new HttpClientHandler
{
// Set that we'll accept compressed data
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
AllowAutoRedirect = true
};
_httpClient = new HttpClient(clientHandler); return FeedReadResult.InvalidXml;
// Set a user agent string
var userAgent = string.IsNullOrWhiteSpace(Settings.Default.DefaultUserAgent) ? "FeedCenter/" + UpdateCheck.LocalVersion : Settings.Default.DefaultUserAgent;
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
// Set a timeout
_httpClient.Timeout = TimeSpan.FromSeconds(10);
}
// If we need to authenticate then set the credentials
_httpClient.DefaultRequestHeaders.Authorization = Authenticate ? new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))) : null;
// Attempt to get the response
var feedStream = _httpClient.GetStreamAsync(Source).Result;
// Create the text reader
using StreamReader textReader = new XmlSanitizingStream(feedStream, Encoding.UTF8);
// Get the feed text
var feedText = textReader.ReadToEnd();
if (string.IsNullOrEmpty(feedText))
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
// Get rid of any leading and trailing whitespace
feedText = feedText.Trim();
// Clean up common invalid XML characters
feedText = feedText.Replace("&nbsp;", "&#160;");
// Find ampersands that aren't properly escaped and replace them with escaped versions
var r = UnescapedAmpersandRegex();
feedText = r.Replace(feedText, "&amp;");
return Tuple.Create(FeedReadResult.Success, feedText);
}
catch (IOException ioException)
{
Log.Logger.Error(ioException, "Exception");
return Tuple.Create(FeedReadResult.ConnectionFailed, string.Empty);
}
catch (AggregateException aggregateException)
{
Log.Logger.Error(aggregateException, "Exception");
var innerException = aggregateException.InnerException;
if (innerException is not HttpRequestException httpRequestException)
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
switch (httpRequestException.StatusCode)
{
case HttpStatusCode.InternalServerError:
return Tuple.Create(FeedReadResult.ServerError, string.Empty);
case HttpStatusCode.NotModified:
return Tuple.Create(FeedReadResult.NotModified, string.Empty);
case HttpStatusCode.NotFound:
return Tuple.Create(FeedReadResult.NotFound, string.Empty);
case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden:
return Tuple.Create(FeedReadResult.Unauthorized, string.Empty);
case HttpStatusCode.Moved:
return Tuple.Create(FeedReadResult.Moved, string.Empty);
}
if (httpRequestException.InnerException is not SocketException socketException)
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
switch (socketException.SocketErrorCode)
{
case SocketError.NoData:
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
case SocketError.HostNotFound:
return Tuple.Create(FeedReadResult.NotFound, string.Empty);
}
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
}
catch (WebException webException)
{
var result = FeedReadResult.UnknownError;
switch (webException.Status)
{
case WebExceptionStatus.ConnectFailure:
case WebExceptionStatus.NameResolutionFailure:
result = FeedReadResult.ConnectionFailed;
break;
case WebExceptionStatus.Timeout:
result = FeedReadResult.Timeout;
break;
}
Log.Logger.Error(webException, "Exception");
if (result == FeedReadResult.UnknownError)
Debug.Print("Unknown error");
return Tuple.Create(result, string.Empty);
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception");
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
}
} }
catch (InvalidFeedFormatException exception)
private FeedReadResult ReadFeed(bool forceRead)
{ {
try Log.Logger.Error(exception, "Exception");
{
// If not enabled then do nothing
if (!Enabled)
return FeedReadResult.NotEnabled;
// Check if we're forcing a read return FeedReadResult.InvalidXml;
if (!forceRead)
{
// Figure out how long since we last checked
var timeSpan = DateTimeOffset.Now - LastChecked;
// Check if we are due to read the feed
if (timeSpan.TotalMinutes < CheckInterval)
return FeedReadResult.NotDue;
}
// We're checking it now so update the time
LastChecked = DateTimeOffset.Now;
// Read the feed text
var retrieveResult = RetrieveFeed();
// Get the information out of the async result
var result = retrieveResult.Item1;
var feedText = retrieveResult.Item2;
// If we didn't successfully retrieve the feed then stop
if (result != FeedReadResult.Success)
return result;
// Create a new RSS parser
var feedParser = FeedParserBase.CreateFeedParser(this, feedText);
// Parse the feed
result = feedParser.ParseFeed(feedText);
// If we didn't successfully parse the feed then stop
if (result != FeedReadResult.Success)
return result;
// Create the removed items list - if an item wasn't seen during this check then remove it
var removedItems = Items.Where(testItem => testItem.LastFound != LastChecked).ToList();
// If items were removed the feed was updated
if (removedItems.Count > 0)
LastUpdated = DateTime.Now;
// Loop over the items to be removed
foreach (var itemToRemove in removedItems)
{
// Remove the item from the list
Items.Remove(itemToRemove);
}
return FeedReadResult.Success;
}
catch (FeedParseException feedParseException)
{
Log.Logger.Error(feedParseException, "Exception");
return FeedReadResult.InvalidXml;
}
catch (InvalidFeedFormatException exception)
{
Log.Logger.Error(exception, "Exception");
return FeedReadResult.InvalidXml;
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception");
return FeedReadResult.UnknownError;
}
} }
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception");
[GeneratedRegex("&(?!(?:[a-z]+|#[0-9]+|#x[0-9a-f]+);)")] return FeedReadResult.UnknownError;
private static partial Regex UnescapedAmpersandRegex(); }
#endregion
} }
[GeneratedRegex("&(?!(?:[a-z]+|#[0-9]+|#x[0-9a-f]+);)")]
private static partial Regex UnescapedAmpersandRegex();
#endregion
} }

View File

@@ -3,84 +3,83 @@ using System.Text.RegularExpressions;
using FeedCenter.Options; using FeedCenter.Options;
using Realms; using Realms;
namespace FeedCenter namespace FeedCenter;
public partial class FeedItem : RealmObject
{ {
public partial class FeedItem : RealmObject public bool BeenRead { get; set; }
public string Description { get; set; }
public Feed Feed { get; set; }
public Guid FeedId { get; set; }
public string Guid { get; set; }
[PrimaryKey]
public Guid Id { get; set; }
public DateTimeOffset LastFound { get; set; }
public string Link { get; set; }
public bool New { get; set; }
public int Sequence { get; set; }
public string Title { get; set; }
public static FeedItem Create()
{ {
public bool BeenRead { get; set; } return new FeedItem { Id = System.Guid.NewGuid() };
public string Description { get; set; }
public Feed Feed { get; set; }
public Guid FeedId { get; set; }
public string Guid { get; set; }
[PrimaryKey]
public Guid Id { get; set; }
public DateTimeOffset LastFound { get; set; }
public string Link { get; set; }
public bool New { get; set; }
public int Sequence { get; set; }
public string Title { get; set; }
public static FeedItem Create()
{
return new FeedItem { Id = System.Guid.NewGuid() };
}
public override string ToString()
{
var title = Title;
switch (Properties.Settings.Default.MultipleLineDisplay)
{
case MultipleLineDisplay.SingleLine:
// Strip any newlines from the title
title = NewlineRegex().Replace(title, " ");
break;
case MultipleLineDisplay.FirstLine:
// Find the first newline
var newlineIndex = title.IndexOf("\n", StringComparison.Ordinal);
// If a newline was found return everything before it
if (newlineIndex > -1)
title = title[..newlineIndex];
break;
case MultipleLineDisplay.Normal:
break;
default:
throw new ArgumentOutOfRangeException();
}
title ??= string.Empty;
// Condense multiple spaces to one space
title = MultipleSpaceRegex().Replace(title, " ");
// Condense tabs to one space
title = TabRegex().Replace(title, " ");
// If the title is blank then put in the "no title" title
if (title.Length == 0)
title = Properties.Resources.NoTitleText;
return title;
}
[GeneratedRegex("\\n")]
private static partial Regex NewlineRegex();
[GeneratedRegex("[ ]{2,}")]
private static partial Regex MultipleSpaceRegex();
[GeneratedRegex("\\t")]
private static partial Regex TabRegex();
} }
public override string ToString()
{
var title = Title;
switch (Properties.Settings.Default.MultipleLineDisplay)
{
case MultipleLineDisplay.SingleLine:
// Strip any newlines from the title
title = NewlineRegex().Replace(title, " ");
break;
case MultipleLineDisplay.FirstLine:
// Find the first newline
var newlineIndex = title.IndexOf("\n", StringComparison.Ordinal);
// If a newline was found return everything before it
if (newlineIndex > -1)
title = title[..newlineIndex];
break;
case MultipleLineDisplay.Normal:
break;
default:
throw new ArgumentOutOfRangeException();
}
title ??= string.Empty;
// Condense multiple spaces to one space
title = MultipleSpaceRegex().Replace(title, " ");
// Condense tabs to one space
title = TabRegex().Replace(title, " ");
// If the title is blank then put in the "no title" title
if (title.Length == 0)
title = Properties.Resources.NoTitleText;
return title;
}
[GeneratedRegex("\\n")]
private static partial Regex NewlineRegex();
[GeneratedRegex("[ ]{2,}")]
private static partial Regex MultipleSpaceRegex();
[GeneratedRegex("\\t")]
private static partial Regex TabRegex();
} }

View File

@@ -4,28 +4,47 @@ using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private void DisplayCategory()
{ {
private void DisplayCategory() CategoryLabel.Text = string.Format(Properties.Resources.CategoryFilterHeader, _currentCategory == null ? Properties.Resources.AllCategory : _currentCategory.Name);
{ }
CategoryLabel.Text = string.Format(Properties.Resources.CategoryFilterHeader, _currentCategory == null ? Properties.Resources.AllCategory : _currentCategory.Name);
}
private void HandleCategoryButtonClick(object sender, RoutedEventArgs e) private void HandleCategoryButtonClick(object sender, RoutedEventArgs e)
{ {
// Create a new context menu // Create a new context menu
var contextMenu = new ContextMenu(); var contextMenu = new ContextMenu();
// Create the "all" menu item // Create the "all" menu item
var menuItem = new MenuItem 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 = Properties.Resources.AllCategory, Header = category.Name,
Tag = null, Tag = category,
// Set the current item to bold // Set the current item to bold
FontWeight = _currentCategory == null ? FontWeights.Bold : FontWeights.Normal FontWeight = category.Id == _currentCategory?.Id ? FontWeights.Bold : FontWeights.Normal
}; };
// Handle the click // Handle the click
@@ -33,68 +52,48 @@ namespace FeedCenter
// Add the item to the list // Add the item to the list
contextMenu.Items.Add(menuItem); 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) // 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)
{ {
// Get the menu item clicked _currentFeed = category == null ? _database.Feeds.FirstOrDefault() : category.Feeds.FirstOrDefault();
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 == null ? _database.Feeds : _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id);
// Refresh the feed index
_feedIndex = -1;
// Get the first feed
NextFeed();
// Update the feed timestamp
_lastFeedDisplay = DateTime.Now;
// Update the display
DisplayCategory();
DisplayFeed();
Settings.Default.LastCategoryID = _currentCategory?.Id.ToString() ?? string.Empty;
} }
// Set the current category
_currentCategory = category;
// Get the current feed list to match the category
_feedList = _currentCategory == null ? _database.Feeds : _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id);
// Refresh the feed index
_feedIndex = -1;
// Get the first feed
NextFeed();
// Update the feed timestamp
_lastFeedDisplay = DateTime.Now;
// Update the display
DisplayCategory();
DisplayFeed();
Settings.Default.LastCategoryID = _currentCategory?.Id.ToString() ?? string.Empty;
} }
} }

View File

@@ -1,38 +1,37 @@
using System; using System;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private void HandleCommandLine(string commandLine)
{ {
private void HandleCommandLine(string commandLine) // If the command line is blank then ignore it
{ if (commandLine.Length == 0)
// If the command line is blank then ignore it return;
if (commandLine.Length == 0)
return;
// Pad the command line with a trailing space just to be lazy in parsing // Pad the command line with a trailing space just to be lazy in parsing
commandLine += " "; commandLine += " ";
// Look for the feed URL in the command line // Look for the feed URL in the command line
var startPosition = commandLine.IndexOf("feed://", StringComparison.Ordinal); var startPosition = commandLine.IndexOf("feed://", StringComparison.Ordinal);
// If nothing was found then exit // If nothing was found then exit
if (startPosition <= 0) return; if (startPosition <= 0) return;
// Advance past the protocol // Advance past the protocol
startPosition += 7; startPosition += 7;
// Starting at the URL position look for the next space // Starting at the URL position look for the next space
var endPosition = commandLine.IndexOf(" ", startPosition, StringComparison.Ordinal); var endPosition = commandLine.IndexOf(" ", startPosition, StringComparison.Ordinal);
// Extract the feed URL // Extract the feed URL
var feedUrl = commandLine[startPosition..endPosition]; var feedUrl = commandLine[startPosition..endPosition];
// Add the HTTP protocol by default // Add the HTTP protocol by default
feedUrl = "http://" + feedUrl; feedUrl = "http://" + feedUrl;
// Create a new feed using the URL // Create a new feed using the URL
HandleNewFeed(feedUrl); HandleNewFeed(feedUrl);
}
} }
} }

View File

@@ -3,53 +3,52 @@ using System.Linq;
using System.Net; using System.Net;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
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)
{ {
private readonly string[] _chromeExtensions = { "chrome-extension://ehojfdcmnajoklleckniaifaijfnkpbi/subscribe.html?", "chrome-extension://nlbjncdgjeocebhnmkbbbdekmmmcbfjd/subscribe.html?" }; // Default to not allowed
e.Effects = DragDropEffects.None;
e.Handled = true;
private void HandleDragOver(object sender, DragEventArgs e) // If there isn't any text in the data then it is not allowed
{ if (!e.Data.GetDataPresent(DataFormats.Text))
// Default to not allowed return;
e.Effects = DragDropEffects.None;
e.Handled = true;
// If there isn't any text in the data then it is not allowed // Get the data as a string
if (!e.Data.GetDataPresent(DataFormats.Text)) var data = (string) e.Data.GetData(DataFormats.Text);
return;
// Get the data as a string // If the data doesn't look like a URI then it is not allowed
var data = (string) e.Data.GetData(DataFormats.Text); if (!Uri.IsWellFormedUriString(data, UriKind.Absolute))
return;
// If the data doesn't look like a URI then it is not allowed // Allowed
if (!Uri.IsWellFormedUriString(data, UriKind.Absolute)) e.Effects = DragDropEffects.Copy;
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);
if (string.IsNullOrEmpty(data))
return;
// Check to see if the data starts with any known Chrome extension
var chromeExtension = _chromeExtensions.FirstOrDefault(data.StartsWith);
// Remove the Chrome extension URL and decode the URL
if (chromeExtension != null)
{
data = data[chromeExtension.Length..];
data = WebUtility.UrlDecode(data);
}
// Handle the new feed but allow the drag/drop to complete
Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data);
}
} }
}
private void HandleDragDrop(object sender, DragEventArgs e)
{
// Get the data as a string
var data = (string) e.Data.GetData(DataFormats.Text);
if (string.IsNullOrEmpty(data))
return;
// Check to see if the data starts with any known Chrome extension
var chromeExtension = _chromeExtensions.FirstOrDefault(data.StartsWith);
// Remove the Chrome extension URL and decode the URL
if (chromeExtension != null)
{
data = data[chromeExtension.Length..];
data = WebUtility.UrlDecode(data);
}
// Handle the new feed but allow the drag/drop to complete
Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data);
}
}

View File

@@ -2,101 +2,121 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private delegate void NewFeedDelegate(string feedUrl);
{
private delegate void NewFeedDelegate(string feedUrl);
private static string GetAbsoluteUrlString(string baseUrl, string url) private static string GetAbsoluteUrlString(string baseUrl, string url)
{
var uri = new Uri(url, UriKind.RelativeOrAbsolute);
if (!uri.IsAbsoluteUri)
uri = new Uri(new Uri(baseUrl), uri);
return uri.ToString();
}
private void HandleNewFeed(string feedUrl)
{
// Create and configure the new feed
var feed = Feed.Create();
feed.Source = feedUrl;
feed.CategoryId = _database.DefaultCategory.Id;
// 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)
{ {
var uri = new Uri(url, UriKind.RelativeOrAbsolute); // Only check if the feed was able to be read - otherwise fall through and show the dialog
if (!uri.IsAbsoluteUri) if (feedTypeResult.Item2.Length > 0)
uri = new Uri(new Uri(baseUrl), uri); {
return uri.ToString(); // 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<string, string>(GetAbsoluteUrlString(feed.Source, n.Attributes["href"].Value), WebUtility.HtmlDecode(n.Attributes["title"]?.Value ?? feedUrl)))
.Distinct()
.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;
}
}
} }
private void HandleNewFeed(string feedUrl) // Read the feed for the first time
var feedReadResult = feed.Read(true);
// Check to see if this might be rate limited
if (feedReadResult == FeedReadResult.TemporarilyUnavailable)
{ {
// Create and configure the new feed // Wait a second
var feed = Feed.Create(); Thread.Sleep(1000);
feed.Source = feedUrl;
feed.CategoryId = _database.DefaultCategory.Id;
// Try to detect the feed type // Try to read again
var feedTypeResult = feed.DetectFeedType(); feedReadResult = feed.Read(true);
}
// If we can't figure it out it could be an HTML page // See if we read the feed okay
if (feedTypeResult.Item1 == FeedType.Unknown) if (feedReadResult == FeedReadResult.Success)
{ {
// Only check if the feed was able to be read - otherwise fall through and show the dialog // Update the feed name to be the title
if (feedTypeResult.Item2.Length > 0) feed.Name = feed.Title;
{
// 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 // Add the feed to the feed table
var rssLinks = htmlDocument.DocumentNode.Descendants("link") _database.SaveChanges(() => _database.Feeds.Add(feed));
.Where(n => n.Attributes["type"] != null && (n.Attributes["type"].Value == "application/rss+xml" || n.Attributes["type"].Value == "application/atom+xml"))
.Select(n => new Tuple<string, string>(GetAbsoluteUrlString(feed.Source, n.Attributes["href"].Value), WebUtility.HtmlDecode(n.Attributes["title"]?.Value ?? feedUrl)))
.Distinct()
.ToList();
// If there was only one link found then switch to feed to it // Show a tip
if (rssLinks.Count == 1) NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), System.Windows.Forms.ToolTipIcon.Info);
{
feed.Source = rssLinks[0].Item1;
}
else
{
var feedChooserWindow = new FeedChooserWindow();
var feedLink = feedChooserWindow.Display(this, rssLinks);
if (string.IsNullOrEmpty(feedLink)) _currentFeed = feed;
return;
feed.Source = feedLink; // Refresh the database to current settings
} ResetDatabase();
}
}
// Read the feed for the first time // Re-initialize the feed display
var feedReadResult = feed.Read(); DisplayFeed();
}
else
{
// Feed read failed - create a new feed window
var feedForm = new FeedWindow();
// See if we read the feed okay var dialogResult = feedForm.Display(feed, this);
if (feedReadResult == FeedReadResult.Success)
{
// Update the feed name to be the title
feed.Name = feed.Title;
// Add the feed to the feed table // Display the new feed form
_database.SaveChanges(() => _database.Feeds.Add(feed)); if (!dialogResult.HasValue || !dialogResult.Value)
return;
// Show a tip // Add the feed to the feed table
NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), System.Windows.Forms.ToolTipIcon.Info); _database.SaveChanges(() => _database.Feeds.Add(feed));
// Re-initialize the feed display _currentFeed = feed;
DisplayFeed();
}
else
{
// Feed read failed - create a new feed window
var feedForm = new FeedWindow();
var dialogResult = feedForm.Display(feed, this); // Refresh the database to current settings
ResetDatabase();
// Display the new feed form // Re-initialize the feed display
if (dialogResult.HasValue && dialogResult.Value) DisplayFeed();
{
// Add the feed to the feed table
_database.SaveChanges(() => _database.Feeds.Add(feed));
// Re-initialize the feed display
DisplayFeed();
}
}
} }
} }
} }

View File

@@ -6,130 +6,129 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private void HandleLinkTextListMouseUp(object sender, MouseButtonEventArgs e)
{ {
private void HandleLinkTextListMouseUp(object sender, MouseButtonEventArgs e) switch (e.ChangedButton)
{ {
switch (e.ChangedButton) case MouseButton.XButton1:
{
case MouseButton.XButton1:
PreviousFeed(); PreviousFeed();
break; break;
case MouseButton.XButton2: case MouseButton.XButton2:
NextFeed(); NextFeed();
break; 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
_database.SaveChanges(() =>
{
feedItem.BeenRead = true;
feedItem.New = false;
});
// Remove the item from the list
LinkTextList.Items.Remove(feedItem);
}
private void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Get the feed item
var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext;
// Try to open the item link
if (!InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link))
return;
// The feed item has been read and is no longer new
_database.SaveChanges(() =>
{
feedItem.BeenRead = true;
feedItem.New = false;
});
// Remove the item from the list
LinkTextList.Items.Remove(feedItem);
}
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.Id == _currentFeed.Id ? 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.Id == feed.Id)
{
_feedIndex = feedIndex;
break;
}
feedIndex++;
}
// Set the current feed
_currentFeed = feed;
// Update the feed timestamp
_lastFeedDisplay = DateTime.Now;
// Update the display
DisplayFeed();
} }
} }
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
_database.SaveChanges(() =>
{
feedItem.BeenRead = true;
feedItem.New = false;
});
// Remove the item from the list
LinkTextList.Items.Remove(feedItem);
}
private void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Get the feed item
var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext;
// Try to open the item link
if (!InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link))
return;
// The feed item has been read and is no longer new
_database.SaveChanges(() =>
{
feedItem.BeenRead = true;
feedItem.New = false;
});
// Remove the item from the list
LinkTextList.Items.Remove(feedItem);
}
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.Id == _currentFeed.Id ? 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.Id == feed.Id)
{
_feedIndex = feedIndex;
break;
}
feedIndex++;
}
// Set the current feed
_currentFeed = feed;
// Update the feed timestamp
_lastFeedDisplay = DateTime.Now;
// Update the display
DisplayFeed();
}
} }

View File

@@ -7,191 +7,190 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private BackgroundWorker _feedReadWorker;
private class FeedReadWorkerInput
{ {
private BackgroundWorker _feedReadWorker; public bool ForceRead { get; }
public Guid? FeedId { get; }
private class FeedReadWorkerInput public FeedReadWorkerInput()
{ {
public bool ForceRead { get; }
public Guid? FeedId { get; }
public FeedReadWorkerInput()
{
}
public FeedReadWorkerInput(bool forceRead)
{
ForceRead = forceRead;
}
public FeedReadWorkerInput(bool forceRead, Guid? feedId)
{
ForceRead = forceRead;
FeedId = feedId;
}
} }
private void SetProgressMode(bool value, int feedCount) public FeedReadWorkerInput(bool forceRead)
{ {
// Refresh the progress bar if we need it ForceRead = forceRead;
if (value)
{
FeedReadProgress.Value = 0;
FeedReadProgress.Maximum = feedCount + 2;
FeedReadProgress.Visibility = Visibility.Visible;
}
else
{
FeedReadProgress.Visibility = Visibility.Collapsed;
}
} }
private void ReadCurrentFeed(bool forceRead = false) public FeedReadWorkerInput(bool forceRead, Guid? feedId)
{ {
// Don't read if we're already working ForceRead = forceRead;
if (_feedReadWorker.IsBusy) FeedId = feedId;
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, _currentFeed.Id);
// 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);
// 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)
{
// Refresh 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 ?? new FeedReadWorkerInput();
// Setup for progress
var currentProgress = 0;
// Create the list of feeds to read
var feedsToRead = new List<Feed>();
// If we have a single feed then add it to the list - otherwise add them all
if (workerInput.FeedId != null)
feedsToRead.Add(database.Feeds.First(feed => feed.Id == workerInput.FeedId));
else
feedsToRead.AddRange(database.Feeds);
// Loop over each feed and read it
foreach (var feed in feedsToRead)
{
// Read the feed
database.SaveChanges(() => feed.Read(workerInput.ForceRead));
// Increment progress
currentProgress += 1;
// Report progress
worker.ReportProgress(currentProgress);
}
// 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().Wait();
// 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);
} }
} }
private void SetProgressMode(bool value, int feedCount)
{
// Refresh 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, _currentFeed.Id);
// 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);
// 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)
{
// Refresh 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 ?? new FeedReadWorkerInput();
// Setup for progress
var currentProgress = 0;
// Create the list of feeds to read
var feedsToRead = new List<Feed>();
// If we have a single feed then add it to the list - otherwise add them all
if (workerInput.FeedId != null)
feedsToRead.Add(database.Feeds.First(feed => feed.Id == workerInput.FeedId));
else
feedsToRead.AddRange(database.Feeds);
// Loop over each feed and read it
foreach (var feed in feedsToRead)
{
// Read the feed
database.SaveChanges(() => feed.Read(workerInput.ForceRead));
// Increment progress
currentProgress += 1;
// Report progress
worker.ReportProgress(currentProgress);
}
// 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().Wait();
// 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);
}
} }

View File

@@ -3,31 +3,30 @@ using FeedCenter.Properties;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private void HandleHeaderLabelMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
private void HandleHeaderLabelMouseLeftButtonDown(object sender, MouseButtonEventArgs e) // Ignore if the window is locked
{ if (Settings.Default.WindowLocked)
// Ignore if the window is locked return;
if (Settings.Default.WindowLocked)
return;
// Start dragging // Start dragging
DragMove(); DragMove();
} }
private void HandleCloseButtonClick(object sender, RoutedEventArgs e) private void HandleCloseButtonClick(object sender, RoutedEventArgs e)
{ {
// Close the window // Close the window
Close(); Close();
} }
private void HandleFeedLabelMouseDown(object sender, MouseButtonEventArgs e) private void HandleFeedLabelMouseDown(object sender, MouseButtonEventArgs e)
{ {
// Open the link for the current feed on a left double click // Open the link for the current feed on a left double click
if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left)
InstalledBrowser.OpenLink(Settings.Default.Browser, _currentFeed.Link); InstalledBrowser.OpenLink(Settings.Default.Browser, _currentFeed.Link);
}
} }
} }

View File

@@ -11,419 +11,418 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow : IDisposable
{ {
public partial class MainWindow : IDisposable private Category _currentCategory;
private Feed _currentFeed;
private FeedCenterEntities _database;
private int _feedIndex;
private IEnumerable<Feed> _feedList;
public MainWindow()
{ {
private Category _currentCategory; InitializeComponent();
private Feed _currentFeed;
private FeedCenterEntities _database;
private int _feedIndex;
private IEnumerable<Feed> _feedList;
public MainWindow()
{
InitializeComponent();
}
public void Dispose()
{
_mainTimer?.Dispose();
_feedReadWorker?.Dispose();
GC.SuppressFinalize(this);
}
protected override async void OnClosed(EventArgs e)
{
base.OnClosed(e);
await SingleInstance.Stop();
}
public async void Initialize()
{
// Setup the update handler
InitializeUpdate();
// 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 = Database.Entities;
// Initialize the single instance listener
SingleInstance.MessageReceived += SingleInstance_MessageReceived;
await SingleInstance.StartAsync(App.Name);
// Handle any command line we were started with
HandleCommandLine(Environment.CommandLine);
// Create a timer to keep track of things we need to do
InitializeTimer();
// Initialize the feed display
InitializeDisplay();
// Check for update
if (Settings.Default.CheckVersionAtStartup)
await UpdateCheck.CheckForUpdate();
// Show the link if updates are available
if (UpdateCheck.UpdateAvailable)
NewVersionLink.Visibility = Visibility.Visible;
Log.Logger.Information("MainForm creation finished");
}
private void SingleInstance_MessageReceived(object sender, string commandLine)
{
HandleCommandLine(commandLine);
}
#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<PropertyChangedEventArgs>(HandlePropertyChanged), sender, e);
return;
}
switch (e.PropertyName)
{
case nameof(Settings.Default.MultipleLineDisplay):
// Update the current feed
DisplayFeed();
break;
case nameof(Settings.Default.WindowLocked):
// Update the window for the new window lock value
HandleWindowLockState();
break;
case nameof(Settings.Default.ToolbarLocation):
// Update the window for the toolbar location
switch (Settings.Default.ToolbarLocation)
{
case Dock.Top:
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "TopToolbarRow");
break;
case Dock.Bottom:
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "BottomToolbarRow");
break;
case Dock.Left:
case Dock.Right:
default:
throw new NotSupportedException();
}
break;
}
}
#endregion
#region Database helpers
private void ResetDatabase()
{
// Get the ID of the current feed
var currentId = _currentFeed?.IsValid ?? false ? _currentFeed.Id : Guid.Empty;
// Create a new database object
_database.Refresh();
_feedList = _currentCategory == null
? _database.Feeds.ToList()
: _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id).ToList();
UpdateToolbarButtonState();
// Get a list of feeds ordered by name
var feedList = _feedList.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
: _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
}
#endregion
#region Feed display
private void UpdateToolbarButtonState()
{
// Cache the feed count to save (a little) time
var feedCount = _feedList?.Count() ?? 0;
// 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;
CategoryGrid.Visibility = _database.Categories.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
}
private void InitializeDisplay()
{
// Get the last category (defaulting to none)
_currentCategory =
_database.Categories.FirstOrDefault(category =>
category.Id.ToString() == Settings.Default.LastCategoryID);
DisplayCategory();
// Get the current feed list to match the category
_feedList = _currentCategory == null
? _database.Feeds
: _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id);
UpdateToolbarButtonState();
// Clear the link list
LinkTextList.Items.Clear();
// Refresh the feed index
_feedIndex = -1;
// Start the timer
StartTimer();
// Don't go further if we have no feeds
if (!_feedList.Any())
return;
// Get the first feed
NextFeed();
}
private void NextFeed()
{
var feedCount = _feedList.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 = _feedList.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 = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
// If the current feed has unread items then we can display it
if (_currentFeed.Items.Any(item => !item.BeenRead))
{
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 = _feedList.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 = _feedList.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 = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
// If the current feed has unread items then we can display it
if (_currentFeed.Items.Any(item => !item.BeenRead))
{
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 = _feedList.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
_database.SaveChanges(() =>
{
foreach (FeedItem feedItem in LinkTextList.Items)
feedItem.BeenRead = true;
});
// Clear the list
LinkTextList.Items.Clear();
}
#endregion
} }
public void Dispose()
{
_mainTimer?.Dispose();
_feedReadWorker?.Dispose();
GC.SuppressFinalize(this);
}
protected override async void OnClosed(EventArgs e)
{
base.OnClosed(e);
await SingleInstance.Stop();
}
public async void Initialize()
{
// Setup the update handler
InitializeUpdate();
// 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 = Database.Entities;
// Initialize the single instance listener
SingleInstance.MessageReceived += SingleInstance_MessageReceived;
await SingleInstance.StartAsync(App.Name);
// Handle any command line we were started with
HandleCommandLine(Environment.CommandLine);
// Create a timer to keep track of things we need to do
InitializeTimer();
// Initialize the feed display
InitializeDisplay();
// Check for update
if (Settings.Default.CheckVersionAtStartup)
await UpdateCheck.CheckForUpdate();
// Show the link if updates are available
if (UpdateCheck.UpdateAvailable)
NewVersionLink.Visibility = Visibility.Visible;
Log.Logger.Information("MainForm creation finished");
}
private void SingleInstance_MessageReceived(object sender, string commandLine)
{
HandleCommandLine(commandLine);
}
#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<PropertyChangedEventArgs>(HandlePropertyChanged), sender, e);
return;
}
switch (e.PropertyName)
{
case nameof(Settings.Default.MultipleLineDisplay):
// Update the current feed
DisplayFeed();
break;
case nameof(Settings.Default.WindowLocked):
// Update the window for the new window lock value
HandleWindowLockState();
break;
case nameof(Settings.Default.ToolbarLocation):
// Update the window for the toolbar location
switch (Settings.Default.ToolbarLocation)
{
case Dock.Top:
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "TopToolbarRow");
break;
case Dock.Bottom:
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "BottomToolbarRow");
break;
case Dock.Left:
case Dock.Right:
default:
throw new NotSupportedException();
}
break;
}
}
#endregion
#region Database helpers
private void ResetDatabase()
{
// Get the ID of the current feed
var currentId = _currentFeed?.IsValid ?? false ? _currentFeed.Id : Guid.Empty;
// Create a new database object
_database.Refresh();
_feedList = _currentCategory == null
? _database.Feeds.ToList()
: _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id).ToList();
UpdateToolbarButtonState();
// Get a list of feeds ordered by name
var feedList = _feedList.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
: _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
}
#endregion
#region Feed display
private void UpdateToolbarButtonState()
{
// Cache the feed count to save (a little) time
var feedCount = _feedList?.Count() ?? 0;
// 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;
CategoryGrid.Visibility = _database.Categories.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
}
private void InitializeDisplay()
{
// Get the last category (defaulting to none)
_currentCategory =
_database.Categories.FirstOrDefault(category =>
category.Id.ToString() == Settings.Default.LastCategoryID);
DisplayCategory();
// Get the current feed list to match the category
_feedList = _currentCategory == null
? _database.Feeds
: _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id);
UpdateToolbarButtonState();
// Clear the link list
LinkTextList.Items.Clear();
// Refresh the feed index
_feedIndex = -1;
// Start the timer
StartTimer();
// Don't go further if we have no feeds
if (!_feedList.Any())
return;
// Get the first feed
NextFeed();
}
private void NextFeed()
{
var feedCount = _feedList.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 = _feedList.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 = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
// If the current feed has unread items then we can display it
if (_currentFeed.Items.Any(item => !item.BeenRead))
{
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 = _feedList.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 = _feedList.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 = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
// If the current feed has unread items then we can display it
if (_currentFeed.Items.Any(item => !item.BeenRead))
{
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 = _feedList.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
_database.SaveChanges(() =>
{
foreach (FeedItem feedItem in LinkTextList.Items)
feedItem.BeenRead = true;
});
// Clear the list
LinkTextList.Items.Clear();
}
#endregion
} }

View File

@@ -2,58 +2,57 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private Timer _mainTimer;
private DateTime _lastFeedRead;
private DateTime _lastFeedDisplay;
private void InitializeTimer()
{ {
private Timer _mainTimer; _mainTimer = new Timer { Interval = 1000 };
private DateTime _lastFeedRead; _mainTimer.Tick += HandleMainTimerTick;
private DateTime _lastFeedDisplay;
private void InitializeTimer()
{
_mainTimer = new 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();
}
} }
}
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();
}
}

View File

@@ -8,229 +8,228 @@ using ChrisKaczor.InstalledBrowsers;
using FeedCenter.Options; using FeedCenter.Options;
using FeedCenter.Properties; using FeedCenter.Properties;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private void HandlePreviousToolbarButtonClick(object sender, RoutedEventArgs e)
{ {
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();
// 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)
{ {
PreviousFeed(); // Try to open the link
} if (InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link))
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();
// 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 // Mark the feed as read
if (InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link)) _database.SaveChanges(() => feedItem.BeenRead = true);
{
// Mark the feed as read
_database.SaveChanges(() => feedItem.BeenRead = true);
// Remove the item // Remove the item
LinkTextList.Items.Remove(feedItem); LinkTextList.Items.Remove(feedItem);
}
// Wait a little bit
Thread.Sleep(sleepInterval);
// Switch to the normal sleep interval
sleepInterval = settings.OpenAllSleepInterval;
}
}
private void HandleOptionsToolbarButtonClick(object sender, RoutedEventArgs e)
{
// Create the options form
var optionsWindow = new OptionsWindow { Owner = this };
// Show the options window
optionsWindow.ShowDialog();
// Refresh the database to current settings
ResetDatabase();
// Re-initialize the feed display
DisplayFeed();
UpdateErrorLink();
}
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
feedErrorWindow.Display(this);
// Refresh 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(_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.ConfirmDeleteFeed, 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 the feed
_database.SaveChanges(() => _database.Feeds.Remove(feedToDelete));
}
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(); // Wait a little bit
textWriter.Close(); Thread.Sleep(sleepInterval);
InstalledBrowser.OpenLink(Settings.Default.Browser, fileName); // Switch to the normal sleep interval
sleepInterval = settings.OpenAllSleepInterval;
MarkAllItemsAsRead();
} }
} }
private void HandleOptionsToolbarButtonClick(object sender, RoutedEventArgs e)
{
// Create the options form
var optionsWindow = new OptionsWindow { Owner = this };
// Show the options window
optionsWindow.ShowDialog();
// Refresh the database to current settings
ResetDatabase();
// Re-initialize the feed display
DisplayFeed();
UpdateErrorLink();
}
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
feedErrorWindow.Display(this);
// Refresh 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(_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.ConfirmDeleteFeed, 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 the feed
_database.SaveChanges(() => _database.Feeds.Remove(feedToDelete));
}
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();
InstalledBrowser.OpenLink(Settings.Default.Browser, fileName);
MarkAllItemsAsRead();
}
} }

View File

@@ -2,39 +2,38 @@
using FeedCenter.Properties; using FeedCenter.Properties;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private static void InitializeUpdate()
{ {
private static void InitializeUpdate() UpdateCheck.Initialize(ServerType.GitHub,
{ Settings.Default.VersionLocation,
UpdateCheck.Initialize(ServerType.GitHub, string.Empty,
Settings.Default.VersionLocation, Properties.Resources.ApplicationDisplayName,
string.Empty, ApplicationShutdown,
Properties.Resources.ApplicationDisplayName, ApplicationCurrentMessage,
ApplicationShutdown, ApplicationUpdateMessage);
ApplicationCurrentMessage, }
ApplicationUpdateMessage);
}
private static bool ApplicationUpdateMessage(string title, string message) private static bool ApplicationUpdateMessage(string title, string message)
{ {
return MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes; return MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes;
} }
private static void ApplicationCurrentMessage(string title, string message) private static void ApplicationCurrentMessage(string title, string message)
{ {
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
} }
private static void ApplicationShutdown() private static void ApplicationShutdown()
{ {
Application.Current.Shutdown(); Application.Current.Shutdown();
} }
private void HandleNewVersionLinkClick(object sender, RoutedEventArgs e) private void HandleNewVersionLinkClick(object sender, RoutedEventArgs e)
{ {
UpdateCheck.DisplayUpdateInformation(true); UpdateCheck.DisplayUpdateInformation(true);
}
} }
} }

View File

@@ -7,167 +7,166 @@ using System.Windows.Controls;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
namespace FeedCenter namespace FeedCenter;
public partial class MainWindow
{ {
public partial class MainWindow private void LoadWindowSettings()
{ {
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))
{ {
// Get the last window location Width = windowSize.Width;
var windowLocation = Settings.Default.WindowLocation; Height = windowSize.Height;
// 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:
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "TopToolbarRow");
break;
case Dock.Bottom:
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "BottomToolbarRow");
break;
}
// Load the lock state
HandleWindowLockState();
} }
private void SaveWindowSettings() // Set the location of the navigation tray
switch (Settings.Default.ToolbarLocation)
{ {
// Set the last window location case Dock.Top:
Settings.Default.WindowLocation = new Point(Left, Top); NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "TopToolbarRow");
break;
// Set the last window size case Dock.Bottom:
Settings.Default.WindowSize = new Size(Width, Height); NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "BottomToolbarRow");
break;
// Save the dock on the navigation tray
Settings.Default.ToolbarLocation = NameBasedGrid.NameBasedGrid.GetRow(NavigationToolbarTray) == "TopToolbarRow" ? Dock.Top : Dock.Bottom;
// Save settings
Settings.Default.Save();
} }
private void HandleWindowLockState() // 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 = NameBasedGrid.NameBasedGrid.GetRow(NavigationToolbarTray) == "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();
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
// Ditch the worker
if (_feedReadWorker != null)
{ {
// Set the resize mode for the window _feedReadWorker.CancelAsync();
ResizeMode = Settings.Default.WindowLocked ? ResizeMode.NoResize : ResizeMode.CanResize; _feedReadWorker.Dispose();
// Show or hide the border
WindowBorder.BorderBrush = Settings.Default.WindowLocked ? SystemColors.ActiveBorderBrush : Brushes.Transparent;
// Update the borders
UpdateBorder();
} }
protected override void OnClosing(CancelEventArgs e) // Get rid of the timer
TerminateTimer();
// Save current window settings
SaveWindowSettings();
// Save settings
_database.SaveChanges(Settings.Default.Save);
// Get rid of the notification icon
NotificationIcon.Dispose();
}
private readonly DebounceDispatcher _updateWindowSettingsDispatcher = new(500);
private void HandleWindowSizeChanged(object sender, SizeChangedEventArgs e)
{
_updateWindowSettingsDispatcher.Debounce(() => Dispatcher.Invoke(UpdateWindowSettings));
}
private void HandleWindowLocationChanged(object sender, EventArgs e)
{
_updateWindowSettingsDispatcher.Debounce(() => Dispatcher.Invoke(UpdateWindowSettings));
}
private void UpdateBorder()
{
var windowInteropHelper = new WindowInteropHelper(this);
var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle);
var rectangle = new System.Drawing.Rectangle
{ {
base.OnClosing(e); X = (int) Left,
Y = (int) Top,
Width = (int) Width,
Height = (int) Height
};
// Ditch the worker var borderThickness = new Thickness();
if (_feedReadWorker != null)
{
_feedReadWorker.CancelAsync();
_feedReadWorker.Dispose();
}
// Get rid of the timer if (rectangle.Right != screen.WorkingArea.Right)
TerminateTimer(); borderThickness.Right = 1;
// Save current window settings if (rectangle.Left != screen.WorkingArea.Left)
SaveWindowSettings(); borderThickness.Left = 1;
// Save settings if (rectangle.Top != screen.WorkingArea.Top)
_database.SaveChanges(Settings.Default.Save); borderThickness.Top = 1;
// Get rid of the notification icon if (rectangle.Bottom != screen.WorkingArea.Bottom)
NotificationIcon.Dispose(); borderThickness.Bottom = 1;
}
private readonly DebounceDispatcher _updateWindowSettingsDispatcher = new(500); WindowBorder.BorderThickness = borderThickness;
}
private void HandleWindowSizeChanged(object sender, SizeChangedEventArgs e) private void UpdateWindowSettings()
{ {
_updateWindowSettingsDispatcher.Debounce(() => Dispatcher.Invoke(UpdateWindowSettings)); // Save current window settings
} SaveWindowSettings();
private void HandleWindowLocationChanged(object sender, EventArgs e) // Update the border
{ UpdateBorder();
_updateWindowSettingsDispatcher.Debounce(() => Dispatcher.Invoke(UpdateWindowSettings)); }
}
private void UpdateBorder() private bool _activated;
{
var windowInteropHelper = new WindowInteropHelper(this);
var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle); protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
var rectangle = new System.Drawing.Rectangle if (_activated)
{ return;
X = (int) Left,
Y = (int) Top,
Width = (int) Width,
Height = (int) Height
};
var borderThickness = new Thickness(); _activated = true;
if (rectangle.Right != screen.WorkingArea.Right) // Load the lock state
borderThickness.Right = 1; HandleWindowLockState();
if (rectangle.Left != screen.WorkingArea.Left) // Watch for size and location changes
borderThickness.Left = 1; SizeChanged += HandleWindowSizeChanged;
LocationChanged += HandleWindowLocationChanged;
if (rectangle.Top != screen.WorkingArea.Top) // Watch for setting changes
borderThickness.Top = 1; Settings.Default.PropertyChanged += HandlePropertyChanged;
if (rectangle.Bottom != screen.WorkingArea.Bottom)
borderThickness.Bottom = 1;
WindowBorder.BorderThickness = borderThickness;
}
private void UpdateWindowSettings()
{
// Save current window settings
SaveWindowSettings();
// Update the border
UpdateBorder();
}
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;
}
} }
} }

View File

@@ -1,78 +1,77 @@
using FeedCenter.Properties; using FeedCenter.Properties;
using System.Windows.Forms; using System.Windows.Forms;
namespace FeedCenter namespace FeedCenter;
internal static class NotificationIcon
{ {
internal static class NotificationIcon private static MainWindow _mainForm;
private static NotifyIcon _notificationIcon;
public static void Initialize(MainWindow mainForm)
{ {
private static MainWindow _mainForm; // Store the main window
private static NotifyIcon _notificationIcon; _mainForm = mainForm;
public static void Initialize(MainWindow mainForm) // Create the notification icon
{ _notificationIcon = new NotifyIcon { Icon = Resources.Application };
// Store the main window _notificationIcon.DoubleClick += HandleNotificationIconDoubleClick;
_mainForm = mainForm;
// Create the notification icon // Setup the menu
_notificationIcon = new NotifyIcon { Icon = Resources.Application }; var contextMenuStrip = new ContextMenuStrip();
_notificationIcon.DoubleClick += HandleNotificationIconDoubleClick;
// Setup the menu var toolStripMenuItem = new ToolStripMenuItem(Resources.NotificationIconContextMenuLocked, null, HandleLockWindowClicked) { Checked = Settings.Default.WindowLocked };
var contextMenuStrip = new ContextMenuStrip(); contextMenuStrip.Items.Add(toolStripMenuItem);
var toolStripMenuItem = new ToolStripMenuItem(Resources.NotificationIconContextMenuLocked, null, HandleLockWindowClicked) { Checked = Settings.Default.WindowLocked }; contextMenuStrip.Items.Add(new ToolStripSeparator());
contextMenuStrip.Items.Add(toolStripMenuItem);
contextMenuStrip.Items.Add(new ToolStripSeparator()); contextMenuStrip.Items.Add(Resources.NotificationIconContextMenuExit, null, HandleContextMenuExitClick);
contextMenuStrip.Items.Add(Resources.NotificationIconContextMenuExit, null, HandleContextMenuExitClick); // Set the menu into the icon
_notificationIcon.ContextMenuStrip = contextMenuStrip;
// Set the menu into the icon // Show the icon
_notificationIcon.ContextMenuStrip = contextMenuStrip; _notificationIcon.Visible = true;
// Show the icon
_notificationIcon.Visible = true;
}
private static void HandleNotificationIconDoubleClick(object sender, System.EventArgs e)
{
// Bring the main form to the front
_mainForm.Activate();
}
private static void HandleContextMenuExitClick(object sender, System.EventArgs e)
{
// Close the main form
_mainForm.Close();
}
private static void HandleLockWindowClicked(object sender, System.EventArgs e)
{
// Toggle the lock setting
Settings.Default.WindowLocked = !Settings.Default.WindowLocked;
// Refresh the menu choice
((ToolStripMenuItem) sender).Checked = Settings.Default.WindowLocked;
}
public static void Dispose()
{
// Get rid of the icon
_notificationIcon.Visible = false;
_notificationIcon.Dispose();
_notificationIcon = null;
_mainForm = null;
}
public static void ShowBalloonTip(string text, ToolTipIcon icon)
{
ShowBalloonTip(text, icon, Settings.Default.BalloonTipTimeout);
}
private static void ShowBalloonTip(string text, ToolTipIcon icon, int timeout)
{
_notificationIcon.ShowBalloonTip(timeout, Resources.ApplicationDisplayName, text, icon);
}
} }
}
private static void HandleNotificationIconDoubleClick(object sender, System.EventArgs e)
{
// Bring the main form to the front
_mainForm.Activate();
}
private static void HandleContextMenuExitClick(object sender, System.EventArgs e)
{
// Close the main form
_mainForm.Close();
}
private static void HandleLockWindowClicked(object sender, System.EventArgs e)
{
// Toggle the lock setting
Settings.Default.WindowLocked = !Settings.Default.WindowLocked;
// Refresh the menu choice
((ToolStripMenuItem) sender).Checked = Settings.Default.WindowLocked;
}
public static void Dispose()
{
// Get rid of the icon
_notificationIcon.Visible = false;
_notificationIcon.Dispose();
_notificationIcon = null;
_mainForm = null;
}
public static void ShowBalloonTip(string text, ToolTipIcon icon)
{
ShowBalloonTip(text, icon, Settings.Default.BalloonTipTimeout);
}
private static void ShowBalloonTip(string text, ToolTipIcon icon, int timeout)
{
_notificationIcon.ShowBalloonTip(timeout, Resources.ApplicationDisplayName, text, icon);
}
}

View File

@@ -6,96 +6,95 @@ using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using FeedCenter.Data; using FeedCenter.Data;
namespace FeedCenter.Options namespace FeedCenter.Options;
public partial class BulkFeedWindow
{ {
public partial class BulkFeedWindow private List<CheckedListItem<Feed>> _checkedListBoxItems;
private CollectionViewSource _collectionViewSource;
public BulkFeedWindow()
{ {
private List<CheckedListItem<Feed>> _checkedListBoxItems; InitializeComponent();
private CollectionViewSource _collectionViewSource; }
public BulkFeedWindow() public void Display(Window window)
{
_checkedListBoxItems = new List<CheckedListItem<Feed>>();
foreach (var feed in Database.Entities.Feeds)
_checkedListBoxItems.Add(new CheckedListItem<Feed> { Item = feed });
_collectionViewSource = new CollectionViewSource { Source = _checkedListBoxItems };
_collectionViewSource.SortDescriptions.Add(new SortDescription("Item.Name", ListSortDirection.Ascending));
_collectionViewSource.Filter += HandleCollectionViewSourceFilter;
FilteredFeedsList.ItemsSource = _collectionViewSource.View;
Owner = window;
ShowDialog();
}
private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{
var checkedListBoxItem = (CheckedListItem<Feed>) e.Item;
var feed = checkedListBoxItem.Item;
e.Accepted = feed.Link.Contains(FeedLinkFilterText.Text);
}
private void HandleFilterTextChanged(object sender, TextChangedEventArgs e)
{
_collectionViewSource.View.Refresh();
}
private void HandleOkButtonClick(object sender, RoutedEventArgs e)
{
foreach (var item in _checkedListBoxItems.Where(i => i.IsChecked))
{ {
InitializeComponent(); if (OpenComboBox.IsEnabled)
item.Item.MultipleOpenAction = (MultipleOpenAction) ((ComboBoxItem) OpenComboBox.SelectedItem).Tag;
} }
public void Display(Window window) DialogResult = true;
Close();
}
private void HandleSelectAll(object sender, RoutedEventArgs e)
{
foreach (var viewItem in _collectionViewSource.View)
{ {
_checkedListBoxItems = new List<CheckedListItem<Feed>>(); var checkedListItem = (CheckedListItem<Feed>) viewItem;
foreach (var feed in Database.Entities.Feeds) checkedListItem.IsChecked = true;
_checkedListBoxItems.Add(new CheckedListItem<Feed> { Item = feed });
_collectionViewSource = new CollectionViewSource { Source = _checkedListBoxItems };
_collectionViewSource.SortDescriptions.Add(new SortDescription("Item.Name", ListSortDirection.Ascending));
_collectionViewSource.Filter += HandleCollectionViewSourceFilter;
FilteredFeedsList.ItemsSource = _collectionViewSource.View;
Owner = window;
ShowDialog();
}
private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{
var checkedListBoxItem = (CheckedListItem<Feed>) e.Item;
var feed = checkedListBoxItem.Item;
e.Accepted = feed.Link.Contains(FeedLinkFilterText.Text);
}
private void HandleFilterTextChanged(object sender, TextChangedEventArgs e)
{
_collectionViewSource.View.Refresh();
}
private void HandleOkButtonClick(object sender, RoutedEventArgs e)
{
foreach (var item in _checkedListBoxItems.Where(i => i.IsChecked))
{
if (OpenComboBox.IsEnabled)
item.Item.MultipleOpenAction = (MultipleOpenAction) ((ComboBoxItem) OpenComboBox.SelectedItem).Tag;
}
DialogResult = true;
Close();
}
private void HandleSelectAll(object sender, RoutedEventArgs e)
{
foreach (var viewItem in _collectionViewSource.View)
{
var checkedListItem = (CheckedListItem<Feed>) viewItem;
checkedListItem.IsChecked = true;
}
}
private void HandleSelectNone(object sender, RoutedEventArgs e)
{
foreach (var viewItem in _collectionViewSource.View)
{
var checkedListItem = (CheckedListItem<Feed>) viewItem;
checkedListItem.IsChecked = false;
}
}
private void HandleSelectInvert(object sender, RoutedEventArgs e)
{
foreach (var viewItem in _collectionViewSource.View)
{
var checkedListItem = (CheckedListItem<Feed>) viewItem;
checkedListItem.IsChecked = !checkedListItem.IsChecked;
}
}
private void HandleGridMouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
OpenLabel.IsEnabled = !OpenLabel.IsEnabled;
OpenComboBox.IsEnabled = !OpenComboBox.IsEnabled;
} }
} }
}
private void HandleSelectNone(object sender, RoutedEventArgs e)
{
foreach (var viewItem in _collectionViewSource.View)
{
var checkedListItem = (CheckedListItem<Feed>) viewItem;
checkedListItem.IsChecked = false;
}
}
private void HandleSelectInvert(object sender, RoutedEventArgs e)
{
foreach (var viewItem in _collectionViewSource.View)
{
var checkedListItem = (CheckedListItem<Feed>) viewItem;
checkedListItem.IsChecked = !checkedListItem.IsChecked;
}
}
private void HandleGridMouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
OpenLabel.IsEnabled = !OpenLabel.IsEnabled;
OpenComboBox.IsEnabled = !OpenComboBox.IsEnabled;
}
}

View File

@@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.ControlBox" xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.ControlBox"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:options="clr-namespace:FeedCenter.Options"
d:DataContext="{d:DesignInstance Type=feedCenter:Category}" d:DataContext="{d:DesignInstance Type=feedCenter:Category}"
Title="CategoryWindow" Title="CategoryWindow"
Width="300" Width="300"
@@ -28,42 +29,23 @@
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
<Grid Margin="6"> <StackPanel Margin="6"
<Grid.RowDefinitions> options:Spacing.Vertical="5">
<RowDefinition Height="Auto" /> <TextBox mah:TextBoxHelper.UseFloatingWatermark="True"
<RowDefinition Height="*" /> mah:TextBoxHelper.Watermark="{x:Static properties:Resources.categoryNameLabel}"
</Grid.RowDefinitions> mah:TextBoxHelper.SelectAllOnFocus="True"
<Grid.ColumnDefinitions> Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel <StackPanel
Grid.Row="0" options:Spacing.Horizontal="5"
Grid.Column="0">
<TextBox Name="NameTextBox"
VerticalAlignment="Center"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.categoryNameLabel}"
Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}">
</TextBox>
</StackPanel>
<StackPanel
Grid.Column="0"
Grid.Row="1"
Orientation="Horizontal" Orientation="Horizontal"
Margin="0,5,0,0"
HorizontalAlignment="Right"> HorizontalAlignment="Right">
<Button Content="{x:Static properties:Resources.OkayButton}" <Button Content="{x:Static properties:Resources.OkayButton}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75" Width="75"
Margin="0,0,5,0"
IsDefault="True" IsDefault="True"
Click="HandleOkayButtonClick" /> Click="HandleOkayButtonClick" />
<Button Content="{x:Static properties:Resources.CancelButton}" <Button Content="{x:Static properties:Resources.CancelButton}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75" Width="75"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>
</Grid> </StackPanel>
</Window> </Window>

View File

@@ -2,50 +2,49 @@
using ChrisKaczor.Wpf.Validation; using ChrisKaczor.Wpf.Validation;
using FeedCenter.Data; using FeedCenter.Data;
namespace FeedCenter.Options namespace FeedCenter.Options;
public partial class CategoryWindow
{ {
public partial class CategoryWindow public CategoryWindow()
{ {
public CategoryWindow() InitializeComponent();
}
public bool? Display(Category category, Window owner)
{
// Set the data context
DataContext = category;
// Set the title based on the state of the category
Title = string.IsNullOrWhiteSpace(category.Name)
? Properties.Resources.CategoryWindowAdd
: Properties.Resources.CategoryWindowEdit;
// Set the window owner
Owner = owner;
// Show the dialog and result the result
return ShowDialog();
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
var transaction = Database.Entities.BeginTransaction();
if (!this.IsValid())
{ {
InitializeComponent(); transaction.Rollback();
return;
} }
public bool? Display(Category category, Window owner) transaction.Commit();
{ Database.Entities.Refresh();
// Set the data context
DataContext = category;
// Set the title based on the state of the category // Dialog is good
Title = string.IsNullOrWhiteSpace(category.Name) DialogResult = true;
? Properties.Resources.CategoryWindowAdd
: Properties.Resources.CategoryWindowEdit;
// Set the window owner // Close the dialog
Owner = owner; Close();
// Show the dialog and result the result
return ShowDialog();
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
var transaction = Database.Entities.BeginTransaction();
if (!this.IsValid())
{
transaction.Rollback();
return;
}
transaction.Commit();
Database.Entities.Refresh();
// Dialog is good
DialogResult = true;
// Close the dialog
Close();
}
} }
} }

View File

@@ -6,9 +6,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignInstance Type=feedCenter:Feed}" d:DataContext="{d:DesignInstance Type=feedCenter:Feed}"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:options="clr-namespace:FeedCenter.Options"
mc:Ignorable="d" mc:Ignorable="d"
Title="FeedWindow" Title="FeedWindow"
Height="300" Height="350"
Width="450" Width="450"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Icon="/FeedCenter;component/Resources/Application.ico" Icon="/FeedCenter;component/Resources/Application.ico"
@@ -23,175 +25,117 @@
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
<Grid> <Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TabControl Name="OptionsTabControl" <TabControl Name="OptionsTabControl"
Margin="12,12,12,41"> Grid.Row="0"
Grid.Column="0"
mah:HeaderedControlHelper.HeaderFontSize="16"
mah:TabControlHelper.Underlined="SelectedTabItem">
<TabItem Header="{x:Static properties:Resources.generalTab}"> <TabItem Header="{x:Static properties:Resources.generalTab}">
<Grid> <StackPanel Margin="0,4"
<Grid.RowDefinitions> options:Spacing.Vertical="8">
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{x:Static properties:Resources.feedUrlLabel}"
VerticalContentAlignment="Center"
Target="{Binding ElementName=UrlTextBox}"
Margin="6"
Padding="0" />
<TextBox Name="UrlTextBox" <TextBox Name="UrlTextBox"
Grid.Row="0" mah:TextBoxHelper.UseFloatingWatermark="True"
Grid.Column="1" mah:TextBoxHelper.Watermark="{x:Static properties:Resources.feedUrlLabel}"
Margin="6" mah:TextBoxHelper.SelectAllOnFocus="True"
Text="{Binding Path=Source, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" /> Text="{Binding Path=Source, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<Label Content="{x:Static properties:Resources.feedNameLabel}"
VerticalContentAlignment="Center"
Target="{Binding ElementName=NameTextBox}"
Grid.Row="1"
Grid.Column="0"
Margin="6"
Padding="0" />
<TextBox Name="NameTextBox" <TextBox Name="NameTextBox"
Grid.Column="1" mah:TextBoxHelper.UseFloatingWatermark="True"
Grid.Row="1" mah:TextBoxHelper.Watermark="{x:Static properties:Resources.feedNameLabel}"
Margin="6" mah:TextBoxHelper.SelectAllOnFocus="True"
Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" /> Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<Label Content="{x:Static properties:Resources.feedCategoryLabel}" <ComboBox Name="CategoryComboBox"
Target="{Binding ElementName=CategoryComboBox}"
VerticalContentAlignment="Center"
Grid.Row="2"
Grid.Column="0"
Margin="6"
Padding="0" />
<ComboBox Grid.Column="1"
Name="CategoryComboBox"
DisplayMemberPath="Name" DisplayMemberPath="Name"
SelectedValuePath="ID" SelectedValuePath="Id"
SelectedValue="{Binding Path=Category.Id}" SelectedValue="{Binding Path=CategoryId}"
Grid.Row="2" mah:TextBoxHelper.UseFloatingWatermark="True"
Margin="6" /> mah:TextBoxHelper.Watermark="{x:Static properties:Resources.feedCategoryLabel}" />
<CheckBox Grid.ColumnSpan="2"
Grid.Column="0" <CheckBox Name="ReadIntervalCheckBox"
Name="ReadIntervalCheckBox"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
IsChecked="{Binding Path=Enabled, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" IsChecked="{Binding Path=Enabled, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}">
Grid.Row="3" <StackPanel Orientation="Horizontal">
Margin="6">
<DockPanel>
<Label Content="{x:Static properties:Resources.feedReadIntervalPrefix}" <Label Content="{x:Static properties:Resources.feedReadIntervalPrefix}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Margin="0,0,5,0" Margin="0,0,5,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Padding="0" /> Padding="0" />
<TextBox Width="50" <mah:NumericUpDown Width="100"
Text="{Binding Path=CheckInterval, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" Maximum="10080"
IsEnabled="{Binding ElementName=ReadIntervalCheckBox, Path=IsChecked}" /> Minimum="1"
IsEnabled="{Binding ElementName=ReadIntervalCheckBox, Path=IsChecked}"
Value="{Binding CheckInterval, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<Label Content="{x:Static properties:Resources.feedReadIntervalSuffix}" <Label Content="{x:Static properties:Resources.feedReadIntervalSuffix}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Margin="5,0,0,0" Margin="5,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Padding="0" /> Padding="0" />
</DockPanel> </StackPanel>
</CheckBox> </CheckBox>
</Grid> </StackPanel>
</TabItem> </TabItem>
<TabItem Header="{x:Static properties:Resources.readingTab}"> <TabItem Header="{x:Static properties:Resources.readingTab}">
<Grid> <StackPanel Margin="0,4"
<Grid.ColumnDefinitions> options:Spacing.Vertical="8">
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Content="{x:Static properties:Resources.openLabel}"
Target="{Binding ElementName=OpenComboBox}"
Padding="0"
VerticalContentAlignment="Center"
Margin="6" />
<ComboBox Name="OpenComboBox" <ComboBox Name="OpenComboBox"
VerticalContentAlignment="Center"
SelectedValue="{Binding Path=MultipleOpenAction, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}" SelectedValue="{Binding Path=MultipleOpenAction, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}"
SelectedValuePath="Tag" SelectedValuePath="Tag"
Grid.Row="0" mah:TextBoxHelper.UseFloatingWatermark="True"
Grid.Column="1" mah:TextBoxHelper.Watermark="{x:Static properties:Resources.openLabel}">
Margin="6">
<ComboBoxItem Content="{x:Static properties:Resources.openAllSingleToolbarButton}" <ComboBoxItem Content="{x:Static properties:Resources.openAllSingleToolbarButton}"
Tag="{x:Static feedCenter:MultipleOpenAction.SinglePage}" /> Tag="{x:Static feedCenter:MultipleOpenAction.SinglePage}" />
<ComboBoxItem Content="{x:Static properties:Resources.openAllMultipleToolbarButton}" <ComboBoxItem Content="{x:Static properties:Resources.openAllMultipleToolbarButton}"
Tag="{x:Static feedCenter:MultipleOpenAction.IndividualPages}" /> Tag="{x:Static feedCenter:MultipleOpenAction.IndividualPages}" />
</ComboBox> </ComboBox>
</Grid> </StackPanel>
</TabItem> </TabItem>
<TabItem Header="{x:Static properties:Resources.authenticationTab}"> <TabItem Header="{x:Static properties:Resources.authenticationTab}">
<Grid> <StackPanel Margin="0,4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Content="{x:Static properties:Resources.requiresAuthenticationCheckBox}" <CheckBox Content="{x:Static properties:Resources.requiresAuthenticationCheckBox}"
Margin="0,0,0,4"
Name="RequiresAuthenticationCheckBox" Name="RequiresAuthenticationCheckBox"
Grid.ColumnSpan="2" IsChecked="{Binding Path=Authenticate, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
Grid.Row="0"
Grid.Column="0"
IsChecked="{Binding Path=Authenticate, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}"
Margin="6" />
<Label Content="{x:Static properties:Resources.authenticationUserNameLabel}"
Target="{Binding ElementName=AuthenticationUserNameTextBox}"
VerticalContentAlignment="Center"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}"
Grid.Row="1"
Grid.Column="0"
Margin="6"
Padding="20,0,0,0" />
<TextBox Name="AuthenticationUserNameTextBox" <TextBox Name="AuthenticationUserNameTextBox"
Grid.Column="1" Margin="25,0,0,4"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}" IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}"
Grid.Row="1" mah:TextBoxHelper.UseFloatingWatermark="True"
Margin="6" mah:TextBoxHelper.Watermark="{x:Static properties:Resources.authenticationUserNameLabel}"
Text="{Binding Path=Username, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" /> Text="{Binding Path=Username, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<Label Content="{x:Static properties:Resources.authenticationPasswordLabel}"
Target="{Binding ElementName=AuthenticationPasswordTextBox}"
VerticalContentAlignment="Center"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}"
Grid.Row="2"
Grid.Column="0"
Margin="6"
Padding="20,0,0,0" />
<PasswordBox Name="AuthenticationPasswordTextBox" <PasswordBox Name="AuthenticationPasswordTextBox"
Grid.Column="1" Margin="25,0,0,8"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}" Style="{StaticResource MahApps.Styles.PasswordBox.Button.Revealed}"
Grid.Row="2" mah:PasswordBoxBindingBehavior.Password="{Binding Password, UpdateSourceTrigger=Explicit, ValidatesOnDataErrors=True}"
Margin="6" /> mah:TextBoxHelper.UseFloatingWatermark="True"
</Grid> mah:TextBoxHelper.Watermark="{x:Static properties:Resources.authenticationPasswordLabel}"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}" />
</StackPanel>
</TabItem> </TabItem>
</TabControl> </TabControl>
<Button Content="{x:Static properties:Resources.OkayButton}" <StackPanel
Height="23" Grid.Column="0"
HorizontalAlignment="Right" Grid.Row="1"
VerticalAlignment="Bottom" Orientation="Horizontal"
Width="75" Margin="0,5,0,0"
IsDefault="True" HorizontalAlignment="Right">
Margin="0,0,93,12" <Button Content="{x:Static properties:Resources.OkayButton}"
Click="HandleOkayButtonClick" /> HorizontalAlignment="Right"
<Button Content="{x:Static properties:Resources.CancelButton}" VerticalAlignment="Bottom"
Height="23" Width="75"
HorizontalAlignment="Right" Margin="0,0,5,0"
VerticalAlignment="Bottom" IsDefault="True"
Width="75" Click="HandleOkayButtonClick" />
IsCancel="True" <Button Content="{x:Static properties:Resources.CancelButton}"
Margin="0,0,12,12" /> HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
IsCancel="True" />
</StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,96 +1,44 @@
using System.Linq; using ChrisKaczor.Wpf.Validation;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using ChrisKaczor.Wpf.Validation;
using FeedCenter.Data; using FeedCenter.Data;
using System.Windows;
namespace FeedCenter.Options namespace FeedCenter.Options;
public partial class FeedWindow
{ {
public partial class FeedWindow public FeedWindow()
{ {
public FeedWindow() InitializeComponent();
}
public bool? Display(Feed feed, Window owner)
{
CategoryComboBox.ItemsSource = Database.Entities.Categories;
DataContext = feed;
Title = string.IsNullOrWhiteSpace(feed.Link) ? Properties.Resources.FeedWindowAdd : Properties.Resources.FeedWindowEdit;
Owner = owner;
return ShowDialog();
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
var transaction = Database.Entities.BeginTransaction();
if (!this.IsValid(OptionsTabControl))
{ {
InitializeComponent(); transaction.Rollback();
return;
} }
public bool? Display(Feed feed, Window owner) transaction.Commit();
{ Database.Entities.Refresh();
// Bind the category combo box
CategoryComboBox.ItemsSource = Database.Entities.Categories;
// Set the data context DialogResult = true;
DataContext = feed;
// Set the title based on the state of the feed Close();
Title = string.IsNullOrWhiteSpace(feed.Link) ? Properties.Resources.FeedWindowAdd : Properties.Resources.FeedWindowEdit;
// Set the window owner
Owner = owner;
// Show the dialog and result the result
return ShowDialog();
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
var transaction = Database.Entities.BeginTransaction();
var feed = (Feed) DataContext;
// Get a list of all framework elements and explicit binding expressions
var bindingExpressions = this.GetBindingExpressions(new[] { UpdateSourceTrigger.Explicit });
// Loop over each binding expression and clear any existing error
this.ClearAllValidationErrors(bindingExpressions);
// Force all explicit bindings to update the source
this.UpdateAllSources(bindingExpressions);
// See if there are any errors
var hasError = bindingExpressions.Any(b => b.BindingExpression.HasError);
// If there was an error then set focus to the bad controls
if (hasError)
{
// Get the first framework element with an error
var firstErrorElement = bindingExpressions.First(b => b.BindingExpression.HasError).FrameworkElement;
// Loop over each tab item
foreach (TabItem tabItem in OptionsTabControl.Items)
{
// Cast the content as visual
var content = (Visual) tabItem.Content;
// See if the control with the error is a descendant
if (firstErrorElement.IsDescendantOf(content))
{
// Select the tab
tabItem.IsSelected = true;
break;
}
}
// Set focus
firstErrorElement.Focus();
transaction.Rollback();
return;
}
if (RequiresAuthenticationCheckBox.IsChecked.GetValueOrDefault(false))
feed.Password = AuthenticationPasswordTextBox.Password;
transaction.Commit();
Database.Entities.Refresh();
// Dialog is good
DialogResult = true;
// Close the dialog
Close();
}
} }
} }

View File

@@ -9,417 +9,416 @@ using System.Xml;
using FeedCenter.Data; using FeedCenter.Data;
using Microsoft.Win32; using Microsoft.Win32;
namespace FeedCenter.Options namespace FeedCenter.Options;
public partial class FeedsOptionsPanel
{ {
public partial class FeedsOptionsPanel private CollectionViewSource _collectionViewSource;
public FeedsOptionsPanel(Window parentWindow) : base(parentWindow)
{ {
private CollectionViewSource _collectionViewSource; InitializeComponent();
}
public FeedsOptionsPanel(Window parentWindow) : base(parentWindow) public override string CategoryName => Properties.Resources.optionCategoryFeeds;
public override void LoadPanel()
{
base.LoadPanel();
var collectionViewSource = new CollectionViewSource { Source = Database.Entities.Categories };
collectionViewSource.SortDescriptions.Add(new SortDescription("SortKey", ListSortDirection.Ascending));
collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
collectionViewSource.IsLiveSortingRequested = true;
CategoryListBox.ItemsSource = collectionViewSource.View;
CategoryListBox.SelectedIndex = 0;
}
private void SetFeedButtonStates()
{
AddFeedButton.IsEnabled = true;
EditFeedButton.IsEnabled = FeedListBox.SelectedItems.Count == 1;
DeleteFeedButton.IsEnabled = FeedListBox.SelectedItems.Count > 0;
}
private void AddFeed()
{
var feed = Feed.Create();
var category = (Category) CategoryListBox.SelectedItem;
feed.CategoryId = category.Id;
var feedWindow = new FeedWindow();
var result = feedWindow.Display(feed, Window.GetWindow(this));
if (!result.HasValue || !result.Value)
return;
Database.Entities.SaveChanges(() => Database.Entities.Feeds.Add(feed));
FeedListBox.SelectedItem = feed;
SetFeedButtonStates();
}
private void EditSelectedFeed()
{
if (FeedListBox.SelectedItem == null)
return;
var feed = (Feed) FeedListBox.SelectedItem;
var feedWindow = new FeedWindow();
feedWindow.Display(feed, Window.GetWindow(this));
}
private void DeleteSelectedFeeds()
{
if (MessageBox.Show(ParentWindow, Properties.Resources.ConfirmDeleteFeeds, Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var selectedItems = new Feed[FeedListBox.SelectedItems.Count];
FeedListBox.SelectedItems.CopyTo(selectedItems, 0);
foreach (var feed in selectedItems)
Database.Entities.SaveChanges(() => Database.Entities.Feeds.Remove(feed));
SetFeedButtonStates();
}
private void HandleAddFeedButtonClick(object sender, RoutedEventArgs e)
{
AddFeed();
}
private void HandleEditFeedButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedFeed();
}
private void HandleDeleteFeedButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedFeeds();
}
private void HandleImportButtonClick(object sender, RoutedEventArgs e)
{
ImportFeeds();
}
private void HandleExportButtonClick(object sender, RoutedEventArgs e)
{
ExportFeeds();
}
private static void ExportFeeds()
{
var saveFileDialog = new SaveFileDialog
{ {
InitializeComponent(); FileName = Properties.Resources.ApplicationName,
} Filter = Properties.Resources.ImportExportFilter,
FilterIndex = 0,
OverwritePrompt = true
};
public override string CategoryName => Properties.Resources.optionCategoryFeeds; var result = saveFileDialog.ShowDialog();
public override void LoadPanel() if (!result.GetValueOrDefault(false))
return;
var writerSettings = new XmlWriterSettings
{ {
base.LoadPanel(); Indent = true,
CheckCharacters = true,
ConformanceLevel = ConformanceLevel.Document
};
var collectionViewSource = new CollectionViewSource { Source = Database.Entities.Categories }; var xmlWriter = XmlWriter.Create(saveFileDialog.FileName, writerSettings);
collectionViewSource.SortDescriptions.Add(new SortDescription("SortKey", ListSortDirection.Ascending));
collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
collectionViewSource.IsLiveSortingRequested = true;
CategoryListBox.ItemsSource = collectionViewSource.View; xmlWriter.WriteStartElement("opml");
CategoryListBox.SelectedIndex = 0; xmlWriter.WriteStartElement("body");
}
private void SetFeedButtonStates() foreach (var feed in Database.Entities.Feeds.OrderBy(feed => feed.Name))
{ {
AddFeedButton.IsEnabled = true; xmlWriter.WriteStartElement("outline");
EditFeedButton.IsEnabled = FeedListBox.SelectedItems.Count == 1;
DeleteFeedButton.IsEnabled = FeedListBox.SelectedItems.Count > 0;
}
private void AddFeed() xmlWriter.WriteAttributeString("title", feed.Title);
{ xmlWriter.WriteAttributeString("htmlUrl", feed.Link);
var feed = Feed.Create(); xmlWriter.WriteAttributeString("xmlUrl", feed.Source);
var category = (Category) CategoryListBox.SelectedItem;
feed.CategoryId = category.Id;
var feedWindow = new FeedWindow();
var result = feedWindow.Display(feed, Window.GetWindow(this));
if (!result.HasValue || !result.Value)
return;
Database.Entities.SaveChanges(() => Database.Entities.Feeds.Add(feed));
FeedListBox.SelectedItem = feed;
SetFeedButtonStates();
}
private void EditSelectedFeed()
{
if (FeedListBox.SelectedItem == null)
return;
var feed = (Feed) FeedListBox.SelectedItem;
var feedWindow = new FeedWindow();
feedWindow.Display(feed, Window.GetWindow(this));
}
private void DeleteSelectedFeeds()
{
if (MessageBox.Show(ParentWindow, Properties.Resources.ConfirmDeleteFeeds, Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var selectedItems = new Feed[FeedListBox.SelectedItems.Count];
FeedListBox.SelectedItems.CopyTo(selectedItems, 0);
foreach (var feed in selectedItems)
Database.Entities.SaveChanges(() => Database.Entities.Feeds.Remove(feed));
SetFeedButtonStates();
}
private void HandleAddFeedButtonClick(object sender, RoutedEventArgs e)
{
AddFeed();
}
private void HandleEditFeedButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedFeed();
}
private void HandleDeleteFeedButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedFeeds();
}
private void HandleImportButtonClick(object sender, RoutedEventArgs e)
{
ImportFeeds();
}
private void HandleExportButtonClick(object sender, RoutedEventArgs e)
{
ExportFeeds();
}
private static void ExportFeeds()
{
var saveFileDialog = new SaveFileDialog
{
FileName = Properties.Resources.ApplicationName,
Filter = Properties.Resources.ImportExportFilter,
FilterIndex = 0,
OverwritePrompt = true
};
var result = saveFileDialog.ShowDialog();
if (!result.GetValueOrDefault(false))
return;
var writerSettings = new XmlWriterSettings
{
Indent = true,
CheckCharacters = true,
ConformanceLevel = ConformanceLevel.Document
};
var xmlWriter = XmlWriter.Create(saveFileDialog.FileName, writerSettings);
xmlWriter.WriteStartElement("opml");
xmlWriter.WriteStartElement("body");
foreach (var feed in Database.Entities.Feeds.OrderBy(feed => feed.Name))
{
xmlWriter.WriteStartElement("outline");
xmlWriter.WriteAttributeString("title", feed.Title);
xmlWriter.WriteAttributeString("htmlUrl", feed.Link);
xmlWriter.WriteAttributeString("xmlUrl", feed.Source);
xmlWriter.WriteEndElement();
}
xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
xmlWriter.Flush();
xmlWriter.Close();
} }
private static void ImportFeeds() xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
xmlWriter.Flush();
xmlWriter.Close();
}
private static void ImportFeeds()
{
var openFileDialog = new OpenFileDialog
{ {
var openFileDialog = new OpenFileDialog Filter = Properties.Resources.ImportExportFilter,
FilterIndex = 0
};
var result = openFileDialog.ShowDialog();
if (!result.GetValueOrDefault(false))
return;
var xmlReaderSettings = new XmlReaderSettings { IgnoreWhitespace = true };
var xmlReader = XmlReader.Create(openFileDialog.FileName, xmlReaderSettings);
try
{
xmlReader.Read();
xmlReader.ReadStartElement("opml");
xmlReader.ReadStartElement("body");
while (xmlReader.NodeType != XmlNodeType.EndElement)
{ {
Filter = Properties.Resources.ImportExportFilter, var feed = Feed.Create();
FilterIndex = 0 feed.CategoryId = Database.Entities.Categories.First(c => c.IsDefault).Id;
};
var result = openFileDialog.ShowDialog(); while (xmlReader.MoveToNextAttribute())
if (!result.GetValueOrDefault(false))
return;
var xmlReaderSettings = new XmlReaderSettings { IgnoreWhitespace = true };
var xmlReader = XmlReader.Create(openFileDialog.FileName, xmlReaderSettings);
try
{
xmlReader.Read();
xmlReader.ReadStartElement("opml");
xmlReader.ReadStartElement("body");
while (xmlReader.NodeType != XmlNodeType.EndElement)
{ {
var feed = Feed.Create(); switch (xmlReader.Name.ToLower())
feed.CategoryId = Database.Entities.Categories.First(c => c.IsDefault).Id;
while (xmlReader.MoveToNextAttribute())
{ {
switch (xmlReader.Name.ToLower()) case "title":
{ feed.Title = xmlReader.Value;
case "title": break;
feed.Title = xmlReader.Value;
break;
// ReSharper disable once StringLiteralTypo // ReSharper disable once StringLiteralTypo
case "htmlurl": case "htmlurl":
feed.Link = xmlReader.Value; feed.Link = xmlReader.Value;
break; break;
// ReSharper disable once StringLiteralTypo // ReSharper disable once StringLiteralTypo
case "xmlurl": case "xmlurl":
feed.Source = xmlReader.Value; feed.Source = xmlReader.Value;
break; break;
case "text": case "text":
feed.Name = xmlReader.Value; feed.Name = xmlReader.Value;
break; break;
}
} }
if (string.IsNullOrEmpty(feed.Name))
feed.Name = feed.Title;
Database.Entities.Feeds.Add(feed);
xmlReader.MoveToElement();
xmlReader.Skip();
} }
xmlReader.ReadEndElement(); if (string.IsNullOrEmpty(feed.Name))
feed.Name = feed.Title;
xmlReader.ReadEndElement(); Database.Entities.Feeds.Add(feed);
}
finally
{
xmlReader.Close();
}
}
private void SetCategoryButtonStates() xmlReader.MoveToElement();
{
AddCategoryButton.IsEnabled = true;
var selectedId = ((Category) CategoryListBox.SelectedItem).Id; xmlReader.Skip();
EditCategoryButton.IsEnabled = CategoryListBox.SelectedItem != null &&
selectedId != Database.Entities.DefaultCategory.Id;
DeleteCategoryButton.IsEnabled = CategoryListBox.SelectedItem != null &&
selectedId != Database.Entities.DefaultCategory.Id;
}
private void AddCategory()
{
var category = new Category();
var categoryWindow = new CategoryWindow();
var result = categoryWindow.Display(category, Window.GetWindow(this));
if (!result.HasValue || !result.Value)
return;
Database.Entities.SaveChanges(() => Database.Entities.Categories.Add(category));
CategoryListBox.SelectedItem = category;
SetCategoryButtonStates();
}
private void EditSelectedCategory()
{
if (CategoryListBox.SelectedItem == null)
return;
var category = (Category) CategoryListBox.SelectedItem;
var categoryWindow = new CategoryWindow();
categoryWindow.Display(category, Window.GetWindow(this));
}
private void DeleteSelectedCategory()
{
var category = (Category) CategoryListBox.SelectedItem;
if (MessageBox.Show(ParentWindow, string.Format(Properties.Resources.ConfirmDeleteCategory, category.Name), Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var defaultCategory = Database.Entities.DefaultCategory;
foreach (var feed in Database.Entities.Feeds.Where(f => f.CategoryId == category.Id))
Database.Entities.SaveChanges(() => feed.CategoryId = defaultCategory.Id);
var index = CategoryListBox.SelectedIndex;
if (index == CategoryListBox.Items.Count - 1)
CategoryListBox.SelectedIndex = index - 1;
else
CategoryListBox.SelectedIndex = index + 1;
Database.Entities.SaveChanges(() => Database.Entities.Categories.Remove(category));
SetCategoryButtonStates();
}
private void HandleAddCategoryButtonClick(object sender, RoutedEventArgs e)
{
AddCategory();
}
private void HandleEditCategoryButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedCategory();
}
private void HandleDeleteCategoryButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedCategory();
}
private void HandleCategoryListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_collectionViewSource == null)
{
_collectionViewSource = new CollectionViewSource { Source = Database.Entities.Feeds };
_collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
_collectionViewSource.Filter += HandleCollectionViewSourceFilter;
FeedListBox.ItemsSource = _collectionViewSource.View;
} }
_collectionViewSource.View.Refresh(); xmlReader.ReadEndElement();
if (FeedListBox.Items.Count > 0) xmlReader.ReadEndElement();
FeedListBox.SelectedIndex = 0;
SetFeedButtonStates();
SetCategoryButtonStates();
} }
finally
private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{ {
var selectedCategory = (Category) CategoryListBox.SelectedItem; xmlReader.Close();
var feed = (Feed) e.Item;
e.Accepted = feed.CategoryId == selectedCategory.Id;
}
private void CategoryListBox_Drop(object sender, DragEventArgs e)
{
var feedList = (List<Feed>) e.Data.GetData(typeof(List<Feed>));
var category = (Category) ((DataGridRow) sender).Item;
foreach (var feed in feedList!)
Database.Entities.SaveChanges(() => feed.CategoryId = category.Id);
_collectionViewSource.View.Refresh();
var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Normal;
}
private void HandleListBoxItemPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
var selectedItems = FeedListBox.SelectedItems.Cast<Feed>().ToList();
DragDrop.DoDragDrop(FeedListBox, selectedItems, DragDropEffects.Move);
}
private void CategoryListBox_DragEnter(object sender, DragEventArgs e)
{
var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Bold;
}
private void CategoryListBox_DragLeave(object sender, DragEventArgs e)
{
var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Normal;
}
private void HandleListBoxItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
EditSelectedFeed();
}
private void HandleMultipleEditClick(object sender, RoutedEventArgs e)
{
var bulkFeedWindow = new BulkFeedWindow();
bulkFeedWindow.Display(Window.GetWindow(this));
}
private void HandleFeedListPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Get the object that was clicked on
var originalSource = (DependencyObject) e.OriginalSource;
// Look for a row that contains the object
var dataGridRow = (DataGridRow) FeedListBox.ContainerFromElement(originalSource);
// If the selection already contains this row then ignore it
if (dataGridRow != null && FeedListBox.SelectedItems.Contains(dataGridRow.Item))
e.Handled = true;
}
private void CategoryListBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (!EditCategoryButton.IsEnabled)
return;
EditSelectedCategory();
}
private void FeedListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetFeedButtonStates();
} }
} }
private void SetCategoryButtonStates()
{
AddCategoryButton.IsEnabled = true;
var selectedId = ((Category) CategoryListBox.SelectedItem).Id;
EditCategoryButton.IsEnabled = CategoryListBox.SelectedItem != null &&
selectedId != Database.Entities.DefaultCategory.Id;
DeleteCategoryButton.IsEnabled = CategoryListBox.SelectedItem != null &&
selectedId != Database.Entities.DefaultCategory.Id;
}
private void AddCategory()
{
var category = new Category();
var categoryWindow = new CategoryWindow();
var result = categoryWindow.Display(category, Window.GetWindow(this));
if (!result.HasValue || !result.Value)
return;
Database.Entities.SaveChanges(() => Database.Entities.Categories.Add(category));
CategoryListBox.SelectedItem = category;
SetCategoryButtonStates();
}
private void EditSelectedCategory()
{
if (CategoryListBox.SelectedItem == null)
return;
var category = (Category) CategoryListBox.SelectedItem;
var categoryWindow = new CategoryWindow();
categoryWindow.Display(category, Window.GetWindow(this));
}
private void DeleteSelectedCategory()
{
var category = (Category) CategoryListBox.SelectedItem;
if (MessageBox.Show(ParentWindow, string.Format(Properties.Resources.ConfirmDeleteCategory, category.Name), Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var defaultCategory = Database.Entities.DefaultCategory;
foreach (var feed in Database.Entities.Feeds.Where(f => f.CategoryId == category.Id))
Database.Entities.SaveChanges(() => feed.CategoryId = defaultCategory.Id);
var index = CategoryListBox.SelectedIndex;
if (index == CategoryListBox.Items.Count - 1)
CategoryListBox.SelectedIndex = index - 1;
else
CategoryListBox.SelectedIndex = index + 1;
Database.Entities.SaveChanges(() => Database.Entities.Categories.Remove(category));
SetCategoryButtonStates();
}
private void HandleAddCategoryButtonClick(object sender, RoutedEventArgs e)
{
AddCategory();
}
private void HandleEditCategoryButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedCategory();
}
private void HandleDeleteCategoryButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedCategory();
}
private void HandleCategoryListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_collectionViewSource == null)
{
_collectionViewSource = new CollectionViewSource { Source = Database.Entities.Feeds };
_collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
_collectionViewSource.Filter += HandleCollectionViewSourceFilter;
FeedListBox.ItemsSource = _collectionViewSource.View;
}
_collectionViewSource.View.Refresh();
if (FeedListBox.Items.Count > 0)
FeedListBox.SelectedIndex = 0;
SetFeedButtonStates();
SetCategoryButtonStates();
}
private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{
var selectedCategory = (Category) CategoryListBox.SelectedItem;
var feed = (Feed) e.Item;
e.Accepted = feed.CategoryId == selectedCategory.Id;
}
private void CategoryListBox_Drop(object sender, DragEventArgs e)
{
var feedList = (List<Feed>) e.Data.GetData(typeof(List<Feed>));
var category = (Category) ((DataGridRow) sender).Item;
foreach (var feed in feedList!)
Database.Entities.SaveChanges(() => feed.CategoryId = category.Id);
_collectionViewSource.View.Refresh();
var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Normal;
}
private void HandleListBoxItemPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
return;
var selectedItems = FeedListBox.SelectedItems.Cast<Feed>().ToList();
DragDrop.DoDragDrop(FeedListBox, selectedItems, DragDropEffects.Move);
}
private void CategoryListBox_DragEnter(object sender, DragEventArgs e)
{
var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Bold;
}
private void CategoryListBox_DragLeave(object sender, DragEventArgs e)
{
var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Normal;
}
private void HandleListBoxItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
EditSelectedFeed();
}
private void HandleMultipleEditClick(object sender, RoutedEventArgs e)
{
var bulkFeedWindow = new BulkFeedWindow();
bulkFeedWindow.Display(Window.GetWindow(this));
}
private void HandleFeedListPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Get the object that was clicked on
var originalSource = (DependencyObject) e.OriginalSource;
// Look for a row that contains the object
var dataGridRow = (DataGridRow) FeedListBox.ContainerFromElement(originalSource);
// If the selection already contains this row then ignore it
if (dataGridRow != null && FeedListBox.SelectedItems.Contains(dataGridRow.Item))
e.Handled = true;
}
private void CategoryListBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (!EditCategoryButton.IsEnabled)
return;
EditSelectedCategory();
}
private void FeedListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetFeedButtonStates();
}
} }

View File

@@ -1,9 +1,8 @@
namespace FeedCenter.Options namespace FeedCenter.Options;
public enum MultipleLineDisplay
{ {
public enum MultipleLineDisplay Normal,
{ SingleLine,
Normal, FirstLine
SingleLine, }
FirstLine
}
}

View File

@@ -1,76 +1,75 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows.Controls; using System.Windows.Controls;
namespace FeedCenter.Options namespace FeedCenter.Options;
public partial class OptionsWindow
{ {
public partial class OptionsWindow private readonly List<OptionsPanelBase> _optionPanels = new();
public OptionsWindow()
{ {
private readonly List<OptionsPanelBase> _optionPanels = new(); InitializeComponent();
public OptionsWindow() // Add all the option categories
AddCategories();
// Load the category list
LoadCategories();
}
private void AddCategories()
{
_optionPanels.Add(new GeneralOptionsPanel(this));
_optionPanels.Add(new DisplayOptionsPanel(this));
_optionPanels.Add(new FeedsOptionsPanel(this));
_optionPanels.Add(new UpdateOptionsPanel(this));
_optionPanels.Add(new AboutOptionsPanel(this));
}
private void LoadCategories()
{
// Loop over each panel
foreach (var optionsPanel in _optionPanels)
{ {
InitializeComponent(); // Tell the panel to load itself
optionsPanel.LoadPanel();
// Add all the option categories // Add the panel to the category ist
AddCategories(); CategoryListBox.Items.Add(new CategoryListItem(optionsPanel));
// Load the category list // Set the panel into the right side
LoadCategories(); ContentControl.Content = optionsPanel;
} }
private void AddCategories() // Select the first item
CategoryListBox.SelectedItem = CategoryListBox.Items[0];
}
private void SelectCategory(OptionsPanelBase panel)
{
// Set the content
ContentControl.Content = panel;
}
private void HandleSelectedCategoryChanged(object sender, SelectionChangedEventArgs e)
{
// Select the right category
SelectCategory(((CategoryListItem) CategoryListBox.SelectedItem).Panel);
}
private class CategoryListItem
{
public CategoryListItem(OptionsPanelBase panel)
{ {
_optionPanels.Add(new GeneralOptionsPanel(this)); Panel = panel;
_optionPanels.Add(new DisplayOptionsPanel(this));
_optionPanels.Add(new FeedsOptionsPanel(this));
_optionPanels.Add(new UpdateOptionsPanel(this));
_optionPanels.Add(new AboutOptionsPanel(this));
} }
private void LoadCategories() public OptionsPanelBase Panel { get; }
public override string ToString()
{ {
// Loop over each panel return Panel.CategoryName;
foreach (var optionsPanel in _optionPanels)
{
// Tell the panel to load itself
optionsPanel.LoadPanel();
// Add the panel to the category ist
CategoryListBox.Items.Add(new CategoryListItem(optionsPanel));
// Set the panel into the right side
ContentControl.Content = optionsPanel;
}
// Select the first item
CategoryListBox.SelectedItem = CategoryListBox.Items[0];
}
private void SelectCategory(OptionsPanelBase panel)
{
// Set the content
ContentControl.Content = panel;
}
private void HandleSelectedCategoryChanged(object sender, SelectionChangedEventArgs e)
{
// Select the right category
SelectCategory(((CategoryListItem) CategoryListBox.SelectedItem).Panel);
}
private class CategoryListItem
{
public CategoryListItem(OptionsPanelBase panel)
{
Panel = panel;
}
public OptionsPanelBase Panel { get; }
public override string ToString()
{
return Panel.CategoryName;
}
} }
} }
} }

View File

@@ -1,14 +1,13 @@
using Realms; using Realms;
namespace FeedCenter.Options namespace FeedCenter.Options;
{
public class Setting : RealmObject
{
[PrimaryKey]
public string Name { get; set; }
public string Value { get; set; }
[Ignored] public class Setting : RealmObject
public string Version { get; set; } {
} [PrimaryKey]
} public string Name { get; set; }
public string Value { get; set; }
[Ignored]
public string Version { get; set; }
}

View File

@@ -1,55 +1,54 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using System.Windows; using System.Windows;
namespace FeedCenter.Options namespace FeedCenter.Options;
public class Spacing
{ {
public class Spacing public static double GetHorizontal(DependencyObject obj)
{ {
public static double GetHorizontal(DependencyObject obj) return (double) obj.GetValue(HorizontalProperty);
{
return (double) obj.GetValue(HorizontalProperty);
}
public static double GetVertical(DependencyObject obj)
{
return (double) obj.GetValue(VerticalProperty);
}
private static void HorizontalChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
var space = (double) e.NewValue;
var obj = (DependencyObject) sender;
MarginSetter.SetMargin(obj, new Thickness(0, 0, space, 0));
MarginSetter.SetLastItemMargin(obj, new Thickness(0));
}
[UsedImplicitly]
public static void SetHorizontal(DependencyObject obj, double space)
{
obj.SetValue(HorizontalProperty, space);
}
[UsedImplicitly]
public static void SetVertical(DependencyObject obj, double value)
{
obj.SetValue(VerticalProperty, value);
}
private static void VerticalChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
var space = (double) e.NewValue;
var obj = (DependencyObject) sender;
MarginSetter.SetMargin(obj, new Thickness(0, 0, 0, space));
MarginSetter.SetLastItemMargin(obj, new Thickness(0));
}
public static readonly DependencyProperty VerticalProperty =
DependencyProperty.RegisterAttached("Vertical", typeof(double), typeof(Spacing),
new UIPropertyMetadata(0d, VerticalChangedCallback));
public static readonly DependencyProperty HorizontalProperty =
DependencyProperty.RegisterAttached("Horizontal", typeof(double), typeof(Spacing),
new UIPropertyMetadata(0d, HorizontalChangedCallback));
} }
public static double GetVertical(DependencyObject obj)
{
return (double) obj.GetValue(VerticalProperty);
}
private static void HorizontalChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
var space = (double) e.NewValue;
var obj = (DependencyObject) sender;
MarginSetter.SetMargin(obj, new Thickness(0, 0, space, 0));
MarginSetter.SetLastItemMargin(obj, new Thickness(0));
}
[UsedImplicitly]
public static void SetHorizontal(DependencyObject obj, double space)
{
obj.SetValue(HorizontalProperty, space);
}
[UsedImplicitly]
public static void SetVertical(DependencyObject obj, double value)
{
obj.SetValue(VerticalProperty, value);
}
private static void VerticalChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
var space = (double) e.NewValue;
var obj = (DependencyObject) sender;
MarginSetter.SetMargin(obj, new Thickness(0, 0, 0, space));
MarginSetter.SetLastItemMargin(obj, new Thickness(0));
}
public static readonly DependencyProperty VerticalProperty =
DependencyProperty.RegisterAttached("Vertical", typeof(double), typeof(Spacing),
new UIPropertyMetadata(0d, VerticalChangedCallback));
public static readonly DependencyProperty HorizontalProperty =
DependencyProperty.RegisterAttached("Horizontal", typeof(double), typeof(Spacing),
new UIPropertyMetadata(0d, HorizontalChangedCallback));
} }

View File

@@ -1,34 +1,33 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace FeedCenter.Options namespace FeedCenter.Options;
{
public class UserAgentItem
{
public string Caption { get; set; }
public string UserAgent { get; set; }
public static List<UserAgentItem> UserAgents => new() public class UserAgentItem
{
public string Caption { get; set; }
public string UserAgent { get; set; }
public static List<UserAgentItem> UserAgents => new()
{
new UserAgentItem
{ {
new UserAgentItem Caption = Properties.Resources.DefaultUserAgentCaption,
{ UserAgent = string.Empty
Caption = Properties.Resources.DefaultUserAgentCaption, },
UserAgent = string.Empty new UserAgentItem
}, {
new UserAgentItem Caption = "Windows RSS Platform 2.0",
{ UserAgent = "Windows-RSS-Platform/2.0 (MSIE 9.0; Windows NT 6.1)"
Caption = "Windows RSS Platform 2.0", },
UserAgent = "Windows-RSS-Platform/2.0 (MSIE 9.0; Windows NT 6.1)" new UserAgentItem
}, {
new UserAgentItem Caption = "Feedly 1.0",
{ UserAgent = "Feedly/1.0"
Caption = "Feedly 1.0", },
UserAgent = "Feedly/1.0" new UserAgentItem
}, {
new UserAgentItem Caption = "curl",
{ UserAgent = "curl/7.47.0"
Caption = "curl", }
UserAgent = "curl/7.47.0" };
}
};
}
} }

View File

@@ -134,7 +134,7 @@ namespace FeedCenter.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to _Password:. /// Looks up a localized string similar to Password.
/// </summary> /// </summary>
public static string authenticationPasswordLabel { public static string authenticationPasswordLabel {
get { get {
@@ -152,7 +152,7 @@ namespace FeedCenter.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to _User name:. /// Looks up a localized string similar to User name.
/// </summary> /// </summary>
public static string authenticationUserNameLabel { public static string authenticationUserNameLabel {
get { get {
@@ -679,7 +679,7 @@ namespace FeedCenter.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to _Category:. /// Looks up a localized string similar to Category.
/// </summary> /// </summary>
public static string feedCategoryLabel { public static string feedCategoryLabel {
get { get {
@@ -760,7 +760,7 @@ namespace FeedCenter.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Feed _name:. /// Looks up a localized string similar to Name.
/// </summary> /// </summary>
public static string feedNameLabel { public static string feedNameLabel {
get { get {
@@ -868,7 +868,7 @@ namespace FeedCenter.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Feed _URL:. /// Looks up a localized string similar to URL.
/// </summary> /// </summary>
public static string feedUrlLabel { public static string feedUrlLabel {
get { get {
@@ -1102,7 +1102,7 @@ namespace FeedCenter.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to &quot;Open all&quot; _action:. /// Looks up a localized string similar to &quot;Open all&quot; action.
/// </summary> /// </summary>
public static string openLabel { public static string openLabel {
get { get {

View File

@@ -266,7 +266,7 @@
<value>Authentication</value> <value>Authentication</value>
</data> </data>
<data name="feedNameLabel" xml:space="preserve"> <data name="feedNameLabel" xml:space="preserve">
<value>Feed _name:</value> <value>Name</value>
</data> </data>
<data name="feedReadIntervalPrefix" xml:space="preserve"> <data name="feedReadIntervalPrefix" xml:space="preserve">
<value>_Refresh feed every</value> <value>_Refresh feed every</value>
@@ -275,7 +275,7 @@
<value>minutes</value> <value>minutes</value>
</data> </data>
<data name="feedUrlLabel" xml:space="preserve"> <data name="feedUrlLabel" xml:space="preserve">
<value>Feed _URL:</value> <value>URL</value>
</data> </data>
<data name="FeedWindowAdd" xml:space="preserve"> <data name="FeedWindowAdd" xml:space="preserve">
<value>Add Feed</value> <value>Add Feed</value>
@@ -293,10 +293,10 @@
<value>_Check for a new version on startup</value> <value>_Check for a new version on startup</value>
</data> </data>
<data name="authenticationPasswordLabel" xml:space="preserve"> <data name="authenticationPasswordLabel" xml:space="preserve">
<value>_Password:</value> <value>Password</value>
</data> </data>
<data name="authenticationUserNameLabel" xml:space="preserve"> <data name="authenticationUserNameLabel" xml:space="preserve">
<value>_User name:</value> <value>User name</value>
</data> </data>
<data name="requiresAuthenticationCheckBox" xml:space="preserve"> <data name="requiresAuthenticationCheckBox" xml:space="preserve">
<value>_Feed requires authentication</value> <value>_Feed requires authentication</value>
@@ -353,7 +353,7 @@
<value>&lt; Windows Default &gt;</value> <value>&lt; Windows Default &gt;</value>
</data> </data>
<data name="feedCategoryLabel" xml:space="preserve"> <data name="feedCategoryLabel" xml:space="preserve">
<value>_Category:</value> <value>Category</value>
</data> </data>
<data name="optionCategoryCategories" xml:space="preserve"> <data name="optionCategoryCategories" xml:space="preserve">
<value>Categories</value> <value>Categories</value>
@@ -407,7 +407,7 @@
<value>Reading</value> <value>Reading</value>
</data> </data>
<data name="openLabel" xml:space="preserve"> <data name="openLabel" xml:space="preserve">
<value>"Open all" _action:</value> <value>"Open all" action</value>
</data> </data>
<data name="DatabaseUpdate_5" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="DatabaseUpdate_5" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Scripts\DatabaseUpdate_5.sqlce;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value> <value>..\Scripts\DatabaseUpdate_5.sqlce;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>

View File

@@ -3,54 +3,53 @@ using FeedCenter.Options;
using System; using System;
using System.Linq; using System.Linq;
namespace FeedCenter namespace FeedCenter;
public static class SettingsStore
{ {
public static class SettingsStore public static object OpenDataStore()
{ {
public static object OpenDataStore() if (!Database.Exists)
return null;
Database.Load();
return Database.Entities;
}
public static string GetSettingValue(object dataStore, string name, Version _)
{
var entities = (FeedCenterEntities) dataStore;
var setting = entities?.Settings.FirstOrDefault(s => s.Name == name);
return setting?.Value;
}
public static void SetSettingValue(object dataStore, string name, Version _, string value)
{
var entities = (FeedCenterEntities) dataStore;
if (entities == null)
return;
// Try to get the setting from the database that matches the name and version
var setting = entities.Settings.FirstOrDefault(s => s.Name == name);
entities.SaveChanges(() =>
{ {
if (!Database.Exists) // If there was no setting we need to create it
return null; if (setting == null)
Database.Load();
return Database.Entities;
}
public static string GetSettingValue(object dataStore, string name, Version _)
{
var entities = (FeedCenterEntities) dataStore;
var setting = entities?.Settings.FirstOrDefault(s => s.Name == name);
return setting?.Value;
}
public static void SetSettingValue(object dataStore, string name, Version _, string value)
{
var entities = (FeedCenterEntities) dataStore;
if (entities == null)
return;
// Try to get the setting from the database that matches the name and version
var setting = entities.Settings.FirstOrDefault(s => s.Name == name);
entities.SaveChanges(() =>
{ {
// If there was no setting we need to create it // Create the new setting
if (setting == null) setting = new Setting { Name = name };
{
// Create the new setting
setting = new Setting { Name = name };
// Add the setting to the database // Add the setting to the database
entities.Settings.Add(setting); entities.Settings.Add(setting);
} }
// Set the value into the setting // Set the value into the setting
setting.Value = value; setting.Value = value;
}); });
}
} }
} }

View File

@@ -3,33 +3,32 @@ using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
namespace FeedCenter namespace FeedCenter;
public static class SystemConfiguration
{ {
public static class SystemConfiguration private static bool UseDebugPath => Environment.CommandLine.IndexOf("/debugPath", StringComparison.InvariantCultureIgnoreCase) != -1;
public static string DataDirectory => UseDebugPath ? Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) : UserSettingsPath;
public static string UserSettingsPath
{ {
private static bool UseDebugPath => Environment.CommandLine.IndexOf("/debugPath", StringComparison.InvariantCultureIgnoreCase) != -1; get
public static string DataDirectory => UseDebugPath ? Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) : UserSettingsPath;
public static string UserSettingsPath
{ {
get // If we're running in debug mode then use a local path for the database and logs
{ if (UseDebugPath)
// If we're running in debug mode then use a local path for the database and logs return Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
if (UseDebugPath)
return Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// Get the path to the local application data directory // Get the path to the local application data directory
var path = Path.Combine( var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
Resources.ApplicationName); Resources.ApplicationName);
// Make sure it exists - create it if needed // Make sure it exists - create it if needed
if (!Directory.Exists(path)) if (!Directory.Exists(path))
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
return path; return path;
}
} }
} }
} }