mirror of
https://github.com/ckaczor/FeedCenter.git
synced 2026-01-13 17:22:48 -05:00
More UI updates
This commit is contained in:
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
namespace FeedCenter.FeedParsers
|
namespace FeedCenter.FeedParsers;
|
||||||
|
|
||||||
|
internal enum FeedParseError
|
||||||
{
|
{
|
||||||
internal enum FeedParseError
|
Unknown = 0,
|
||||||
{
|
InvalidXml = 1
|
||||||
Unknown = 0,
|
|
||||||
InvalidXml = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(" ", " ");
|
||||||
|
|
||||||
|
// Find ampersands that aren't properly escaped and replace them with escaped versions
|
||||||
|
var r = UnescapedAmpersandRegex();
|
||||||
|
feedText = r.Replace(feedText, "&");
|
||||||
|
|
||||||
|
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(" ", " ");
|
|
||||||
|
|
||||||
// Find ampersands that aren't properly escaped and replace them with escaped versions
|
|
||||||
var r = UnescapedAmpersandRegex();
|
|
||||||
feedText = r.Replace(feedText, "&");
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get the data as a string
|
||||||
|
var data = (string) e.Data.GetData(DataFormats.Text);
|
||||||
|
|
||||||
|
// If the data doesn't look like a URI then it is not allowed
|
||||||
|
if (!Uri.IsWellFormedUriString(data, UriKind.Absolute))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Allowed
|
||||||
|
e.Effects = DragDropEffects.Copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDragDrop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
// Get the data as a string
|
||||||
|
var data = (string) e.Data.GetData(DataFormats.Text);
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
// Default to not allowed
|
data = data[chromeExtension.Length..];
|
||||||
e.Effects = DragDropEffects.None;
|
data = WebUtility.UrlDecode(data);
|
||||||
e.Handled = true;
|
|
||||||
|
|
||||||
// If there isn't any text in the data then it is not allowed
|
|
||||||
if (!e.Data.GetDataPresent(DataFormats.Text))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Get the data as a string
|
|
||||||
var data = (string) e.Data.GetData(DataFormats.Text);
|
|
||||||
|
|
||||||
// If the data doesn't look like a URI then it is not allowed
|
|
||||||
if (!Uri.IsWellFormedUriString(data, UriKind.Absolute))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Allowed
|
|
||||||
e.Effects = DragDropEffects.Copy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleDragDrop(object sender, DragEventArgs e)
|
// Handle the new feed but allow the drag/drop to complete
|
||||||
{
|
Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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()
|
private void TerminateTimer()
|
||||||
{
|
{
|
||||||
_mainTimer = new Timer { Interval = 1000 };
|
StopTimer();
|
||||||
_mainTimer.Tick += HandleMainTimerTick;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TerminateTimer()
|
_mainTimer.Dispose();
|
||||||
{
|
}
|
||||||
StopTimer();
|
|
||||||
|
|
||||||
_mainTimer.Dispose();
|
private void StartTimer()
|
||||||
}
|
{
|
||||||
|
_mainTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
private void StartTimer()
|
private void StopTimer()
|
||||||
{
|
{
|
||||||
_mainTimer.Start();
|
_mainTimer.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopTimer()
|
private void HandleMainTimerTick(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_mainTimer.Stop();
|
// If the background worker is busy then don't do anything
|
||||||
}
|
if (_feedReadWorker.IsBusy)
|
||||||
|
return;
|
||||||
|
|
||||||
private void HandleMainTimerTick(object sender, EventArgs e)
|
// Stop the timer for now
|
||||||
{
|
StopTimer();
|
||||||
// If the background worker is busy then don't do anything
|
|
||||||
if (_feedReadWorker.IsBusy)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Stop the timer for now
|
// Move to the next feed if the scroll interval has expired and the mouse isn't hovering
|
||||||
StopTimer();
|
if (LinkTextList.IsMouseOver)
|
||||||
|
_lastFeedDisplay = DateTime.Now;
|
||||||
|
else if (DateTime.Now - _lastFeedDisplay >= Settings.Default.FeedScrollInterval)
|
||||||
|
NextFeed();
|
||||||
|
|
||||||
// Move to the next feed if the scroll interval has expired and the mouse isn't hovering
|
// Check to see if we should try to read the feeds
|
||||||
if (LinkTextList.IsMouseOver)
|
if (DateTime.Now - _lastFeedRead >= Settings.Default.FeedCheckInterval)
|
||||||
_lastFeedDisplay = DateTime.Now;
|
ReadFeeds();
|
||||||
else if (DateTime.Now - _lastFeedDisplay >= Settings.Default.FeedScrollInterval)
|
|
||||||
NextFeed();
|
|
||||||
|
|
||||||
// Check to see if we should try to read the feeds
|
// Get the timer going again
|
||||||
if (DateTime.Now - _lastFeedRead >= Settings.Default.FeedCheckInterval)
|
StartTimer();
|
||||||
ReadFeeds();
|
|
||||||
|
|
||||||
// Get the timer going again
|
|
||||||
StartTimer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
private static void HandleNotificationIconDoubleClick(object sender, System.EventArgs e)
|
||||||
_notificationIcon.Visible = true;
|
{
|
||||||
}
|
// Bring the main form to the front
|
||||||
|
_mainForm.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
private static void HandleNotificationIconDoubleClick(object sender, System.EventArgs e)
|
private static void HandleContextMenuExitClick(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
// Bring the main form to the front
|
// Close the main form
|
||||||
_mainForm.Activate();
|
_mainForm.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void HandleContextMenuExitClick(object sender, System.EventArgs e)
|
private static void HandleLockWindowClicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
// Close the main form
|
// Toggle the lock setting
|
||||||
_mainForm.Close();
|
Settings.Default.WindowLocked = !Settings.Default.WindowLocked;
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleLockWindowClicked(object sender, System.EventArgs e)
|
// Refresh the menu choice
|
||||||
{
|
((ToolStripMenuItem) sender).Checked = Settings.Default.WindowLocked;
|
||||||
// Toggle the lock setting
|
}
|
||||||
Settings.Default.WindowLocked = !Settings.Default.WindowLocked;
|
|
||||||
|
|
||||||
// Refresh the menu choice
|
public static void Dispose()
|
||||||
((ToolStripMenuItem) sender).Checked = Settings.Default.WindowLocked;
|
{
|
||||||
}
|
// Get rid of the icon
|
||||||
|
_notificationIcon.Visible = false;
|
||||||
|
_notificationIcon.Dispose();
|
||||||
|
_notificationIcon = null;
|
||||||
|
|
||||||
public static void Dispose()
|
_mainForm = null;
|
||||||
{
|
}
|
||||||
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
public static void ShowBalloonTip(string text, ToolTipIcon icon)
|
private static void ShowBalloonTip(string text, ToolTipIcon icon, int timeout)
|
||||||
{
|
{
|
||||||
ShowBalloonTip(text, icon, Settings.Default.BalloonTipTimeout);
|
_notificationIcon.ShowBalloonTip(timeout, Resources.ApplicationDisplayName, text, icon);
|
||||||
}
|
|
||||||
|
|
||||||
private static void ShowBalloonTip(string text, ToolTipIcon icon, int timeout)
|
|
||||||
{
|
|
||||||
_notificationIcon.ShowBalloonTip(timeout, Resources.ApplicationDisplayName, text, icon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace FeedCenter.Options
|
namespace FeedCenter.Options;
|
||||||
|
|
||||||
|
public enum MultipleLineDisplay
|
||||||
{
|
{
|
||||||
public enum MultipleLineDisplay
|
Normal,
|
||||||
{
|
SingleLine,
|
||||||
Normal,
|
FirstLine
|
||||||
SingleLine,
|
|
||||||
FirstLine
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -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"
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
12
Application/Properties/Resources.Designer.cs
generated
12
Application/Properties/Resources.Designer.cs
generated
@@ -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 "Open all" _action:.
|
/// Looks up a localized string similar to "Open all" action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string openLabel {
|
public static string openLabel {
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -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>< Windows Default ></value>
|
<value>< Windows Default ></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>
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user