More UI updates

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

View File

@@ -6,71 +6,70 @@ using System.Linq;
using JetBrains.Annotations;
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();
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
}
RawName = value;
[Ignored]
public ICollection<Feed> Feeds { get; set; }
[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");
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");
}
}

View File

@@ -3,41 +3,40 @@ using System.Collections;
using System.Collections.Generic;
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)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
return TryGetValue(propertyName, out var value) ? value : null;
}
public IEnumerable GetErrors(string propertyName)
{
return TryGetValue(propertyName, out var value) ? value : null;
}
public void AddError(string propertyName, string error)
{
if (!ContainsKey(propertyName))
this[propertyName] = new List<string>();
public void AddError(string propertyName, string error)
{
if (!ContainsKey(propertyName))
this[propertyName] = new List<string>();
if (this[propertyName].Contains(error))
return;
if (this[propertyName].Contains(error))
return;
this[propertyName].Add(error);
OnErrorsChanged(propertyName);
}
this[propertyName].Add(error);
OnErrorsChanged(propertyName);
}
public void ClearErrors(string propertyName)
{
if (!ContainsKey(propertyName))
return;
public void ClearErrors(string propertyName)
{
if (!ContainsKey(propertyName))
return;
Remove(propertyName);
OnErrorsChanged(propertyName);
}
Remove(propertyName);
OnErrorsChanged(propertyName);
}
}

View File

@@ -20,431 +20,480 @@ using System.Net.Sockets;
using System.Text;
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,
SinglePage
_dataErrorDictionary = new DataErrorDictionary();
_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,
Rss,
Rdf,
Atom
get => Enum.TryParse(LastReadResultRaw, out FeedReadResult result) ? result : FeedReadResult.Success;
set => LastReadResultRaw = value.ToString();
}
public enum FeedReadResult
// ReSharper disable once UnusedMember.Global
public string LastReadResultDescription
{
Success,
NotModified,
NotDue,
UnknownError,
InvalidXml,
NotEnabled,
Unauthorized,
NoResponse,
NotFound,
Timeout,
ConnectionFailed,
ServerError,
Moved
get
{
// Cast the last read result to the proper enum
var lastReadResult = LastReadResult;
// 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();
}
}
#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();
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
RawName = value;
ValidateString(nameof(Name), RawName);
RaisePropertyChanged();
}
}
public bool Authenticate { get; set; }
[MapTo("Password")]
public string RawPassword { 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
public string Password
{
get => RawPassword;
set
{
get => Enum.TryParse(LastReadResultRaw, out FeedReadResult result) ? result : FeedReadResult.Success;
set => LastReadResultRaw = value.ToString();
}
RawPassword = value;
// ReSharper disable once UnusedMember.Global
public string LastReadResultDescription
{
get
if (!Authenticate)
{
// Cast the last read result to the proper enum
var lastReadResult = LastReadResult;
// 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();
_dataErrorDictionary.ClearErrors(nameof(Password));
return;
}
ValidateString(nameof(Password), RawPassword);
RaisePropertyChanged();
}
}
private string LastReadResultRaw { get; set; }
[MapTo("Name")]
private string RawName { get; set; } = string.Empty;
public DateTimeOffset LastUpdated { get; set; }
public string Link { get; set; }
[MapTo("Source")]
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;
set => MultipleOpenActionRaw = value.ToString();
RawSource = value;
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;
set
RawUsername = value;
if (!Authenticate)
{
RawName = value;
ValidateString(nameof(Name), RawName);
RaisePropertyChanged();
_dataErrorDictionary.ClearErrors(nameof(Username));
return;
}
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")]
private string RawName { get; set; } = string.Empty;
Log.Logger.Information("Done reading feed: {0}", result);
[MapTo("Source")]
private string RawSource { get; set; } = string.Empty;
return result;
}
public string Source
public Tuple<FeedType, string> DetectFeedType()
{
var retrieveResult = RetrieveFeed();
if (retrieveResult.Item1 != FeedReadResult.Success)
{
get => RawSource;
set
{
RawSource = value;
return new Tuple<FeedType, string>(FeedType.Unknown, string.Empty);
}
ValidateString(nameof(Source), RawSource);
RaisePropertyChanged();
var feedType = FeedType.Unknown;
try
{
feedType = FeedParserBase.DetectFeedType(retrieveResult.Item2);
}
catch
{
// Ignore
}
return new Tuple<FeedType, string>(feedType, retrieveResult.Item2);
}
private Tuple<FeedReadResult, string> RetrieveFeed()
{
try
{
// Create and configure the HTTP client if needed
if (_httpClient == null)
{
var clientHandler = new HttpClientHandler
{
// Set that we'll accept compressed data
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
AllowAutoRedirect = true
};
_httpClient = new HttpClient(clientHandler);
// Set a user agent string
var userAgent = string.IsNullOrWhiteSpace(Settings.Default.DefaultUserAgent) ? "FeedCenter/" + UpdateCheck.LocalVersion : Settings.Default.DefaultUserAgent;
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
// Set a timeout
_httpClient.Timeout = TimeSpan.FromSeconds(10);
}
// If we need to authenticate then set the credentials
_httpClient.DefaultRequestHeaders.Authorization = Authenticate ? new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))) : null;
// Attempt to get the response
var feedStream = _httpClient.GetStreamAsync(Source).Result;
// Create the text reader
using StreamReader textReader = new XmlSanitizingStream(feedStream, Encoding.UTF8);
// Get the feed text
var feedText = textReader.ReadToEnd();
if (string.IsNullOrEmpty(feedText))
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
// Get rid of any leading and trailing whitespace
feedText = feedText.Trim();
// Clean up common invalid XML characters
feedText = feedText.Replace("&nbsp;", "&#160;");
// Find ampersands that aren't properly escaped and replace them with escaped versions
var r = UnescapedAmpersandRegex();
feedText = r.Replace(feedText, "&amp;");
return Tuple.Create(FeedReadResult.Success, feedText);
}
public string Title { get; set; }
public string Username { get; set; }
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
catch (IOException ioException)
{
return _dataErrorDictionary.GetErrors(propertyName);
Log.Logger.Error(ioException, "Exception");
return Tuple.Create(FeedReadResult.ConnectionFailed, string.Empty);
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public static Feed Create()
catch (AggregateException aggregateException)
{
return new Feed { Id = Guid.NewGuid(), CategoryId = Database.Entities.DefaultCategory.Id };
}
Log.Logger.Error(aggregateException, "Exception");
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
var innerException = aggregateException.InnerException;
private void ValidateString(string propertyName, string value)
{
_dataErrorDictionary.ClearErrors(propertyName);
if (innerException is not HttpRequestException httpRequestException)
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
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)
switch (httpRequestException.StatusCode)
{
case FeedReadResult.NotDue:
case FeedReadResult.NotEnabled:
case FeedReadResult.NotModified:
case HttpStatusCode.ServiceUnavailable:
return Tuple.Create(FeedReadResult.TemporarilyUnavailable, string.Empty);
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;
default:
// Save as last result
LastReadResult = result;
case WebExceptionStatus.Timeout:
result = FeedReadResult.Timeout;
break;
}
// 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;
Log.Logger.Error(webException, "Exception");
Log.Logger.Information("Done reading feed: {0}", result);
if (result == FeedReadResult.UnknownError)
Debug.Print("Unknown error");
return result;
return Tuple.Create(result, string.Empty);
}
public Tuple<FeedType, string> DetectFeedType()
catch (Exception exception)
{
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();
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;
}
private Tuple<FeedReadResult, string> RetrieveFeed()
catch (FeedParseException feedParseException)
{
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
};
Log.Logger.Error(feedParseException, "Exception");
_httpClient = new HttpClient(clientHandler);
// Set a user agent string
var userAgent = string.IsNullOrWhiteSpace(Settings.Default.DefaultUserAgent) ? "FeedCenter/" + UpdateCheck.LocalVersion : Settings.Default.DefaultUserAgent;
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
// Set a timeout
_httpClient.Timeout = TimeSpan.FromSeconds(10);
}
// If we need to authenticate then set the credentials
_httpClient.DefaultRequestHeaders.Authorization = Authenticate ? new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))) : null;
// Attempt to get the response
var feedStream = _httpClient.GetStreamAsync(Source).Result;
// Create the text reader
using StreamReader textReader = new XmlSanitizingStream(feedStream, Encoding.UTF8);
// Get the feed text
var feedText = textReader.ReadToEnd();
if (string.IsNullOrEmpty(feedText))
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
// Get rid of any leading and trailing whitespace
feedText = feedText.Trim();
// Clean up common invalid XML characters
feedText = feedText.Replace("&nbsp;", "&#160;");
// Find ampersands that aren't properly escaped and replace them with escaped versions
var r = UnescapedAmpersandRegex();
feedText = r.Replace(feedText, "&amp;");
return Tuple.Create(FeedReadResult.Success, feedText);
}
catch (IOException ioException)
{
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);
}
return FeedReadResult.InvalidXml;
}
private FeedReadResult ReadFeed(bool forceRead)
catch (InvalidFeedFormatException exception)
{
try
{
// If not enabled then do nothing
if (!Enabled)
return FeedReadResult.NotEnabled;
Log.Logger.Error(exception, "Exception");
// 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();
// 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;
}
return FeedReadResult.InvalidXml;
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception");
[GeneratedRegex("&(?!(?:[a-z]+|#[0-9]+|#x[0-9a-f]+);)")]
private static partial Regex UnescapedAmpersandRegex();
#endregion
return FeedReadResult.UnknownError;
}
}
[GeneratedRegex("&(?!(?:[a-z]+|#[0-9]+|#x[0-9a-f]+);)")]
private static partial Regex UnescapedAmpersandRegex();
#endregion
}

View File

@@ -3,84 +3,83 @@ using System.Text.RegularExpressions;
using FeedCenter.Options;
using 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; }
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();
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();
}