12 Commits

21 changed files with 235 additions and 64 deletions

View File

@@ -1,22 +1,31 @@
using FeedCenter.Data;
using System;
using System.Linq;
using FeedCenter.Data;
using FeedCenter.Options;
using Realms;
using System;
using System.Linq;
namespace FeedCenter;
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()
{
var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}");
var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}")
{
SchemaVersion = 1,
MigrationCallback = (migration, oldSchemaVersion) =>
{
var newVersionFeeds = migration.NewRealm.All<Feed>();
foreach (var newVersionFeed in newVersionFeeds)
{
if (oldSchemaVersion == 0)
{
newVersionFeed.UserAgent = null;
}
}
}
};
RealmInstance = Realm.GetInstance(realmConfiguration);
@@ -30,6 +39,17 @@ public class FeedCenterEntities
}
}
public RealmObservableCollection<Category> Categories { get; }
public Category DefaultCategory
{
get { return Categories.First(c => c.IsDefault); }
}
public RealmObservableCollection<Feed> Feeds { get; private set; }
private Realm RealmInstance { get; }
public RealmObservableCollection<Setting> Settings { get; private set; }
public void Refresh()
{
RealmInstance.Refresh();
@@ -44,9 +64,4 @@ public class FeedCenterEntities
{
return RealmInstance.BeginWrite();
}
public Category DefaultCategory
{
get { return Categories.First(c => c.IsDefault); }
}
}

View File

@@ -24,7 +24,7 @@
<None Remove="Resources\Warning.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ChrisKaczor.ApplicationUpdate" Version="1.0.5" />
<PackageReference Include="ChrisKaczor.ApplicationUpdate" Version="1.0.7" />
<PackageReference Include="ChrisKaczor.GenericSettingsProvider" Version="1.0.4" />
<PackageReference Include="ChrisKaczor.InstalledBrowsers" Version="1.0.4" />
<PackageReference Include="ChrisKaczor.Wpf.Application.SingleInstance" Version="1.0.5" />
@@ -34,7 +34,7 @@
<PackageReference Include="ChrisKaczor.Wpf.Controls.Toolbar" Version="1.0.3" />
<PackageReference Include="ChrisKaczor.Wpf.Validation" Version="1.0.4" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.ControlBox" Version="1.0.3" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.SnappingWindow" Version="1.0.3" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.SnappingWindow" Version="1.0.4" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="DebounceThrottle" Version="2.0.0" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.0.108" />
@@ -44,7 +44,7 @@
<PackageReference Include="Microsoft.SqlServer.Compact" Version="4.0.8876.1" GeneratePathProperty="true">
<NoWarn>NU1701</NoWarn>
</PackageReference>
<PackageReference Include="Microsoft.Windows.Compatibility" Version="7.0.1" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="7.0.6" />
<PackageReference Include="NameBasedGrid" Version="0.10.1">
<NoWarn>NU1701</NoWarn>
</PackageReference>

View File

@@ -1,12 +1,4 @@
using ChrisKaczor.ApplicationUpdate;
using FeedCenter.Data;
using FeedCenter.FeedParsers;
using FeedCenter.Properties;
using FeedCenter.Xml;
using JetBrains.Annotations;
using Realms;
using Serilog;
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
@@ -19,6 +11,14 @@ using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ChrisKaczor.ApplicationUpdate;
using FeedCenter.Data;
using FeedCenter.FeedParsers;
using FeedCenter.Properties;
using FeedCenter.Xml;
using JetBrains.Annotations;
using Realms;
using Serilog;
namespace FeedCenter;
@@ -98,9 +98,6 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
}
}
[MapTo("Password")]
public string RawPassword { get; set; }
public string Password
{
get => RawPassword;
@@ -122,9 +119,15 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
[MapTo("Name")]
private string RawName { get; set; } = string.Empty;
[MapTo("Password")]
public string RawPassword { get; set; }
[MapTo("Source")]
private string RawSource { get; set; } = string.Empty;
[MapTo("Username")]
public string RawUsername { get; set; }
public string Source
{
get => RawSource;
@@ -139,8 +142,7 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
public string Title { get; set; }
[MapTo("Username")]
public string RawUsername { get; set; }
public string UserAgent { get; set; }
public string Username
{
@@ -200,6 +202,9 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
case FeedReadResult.NotEnabled:
case FeedReadResult.NotModified:
// Reset status to success
LastReadResult = FeedReadResult.Success;
// Ignore
break;
@@ -242,6 +247,17 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
return new Tuple<FeedType, string>(feedType, retrieveResult.Item2);
}
private string GetUserAgent()
{
if (!string.IsNullOrWhiteSpace(UserAgent))
return UserAgent;
if (!string.IsNullOrWhiteSpace(Settings.Default.DefaultUserAgent))
return Settings.Default.DefaultUserAgent;
return "FeedCenter/" + UpdateCheck.LocalVersion;
}
private Tuple<FeedReadResult, string> RetrieveFeed()
{
try
@@ -252,23 +268,32 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
var clientHandler = new HttpClientHandler
{
// Set that we'll accept compressed data
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
AllowAutoRedirect = true
};
_httpClient = new HttpClient(clientHandler);
_httpClient = new HttpClient(clientHandler)
{
// Set a timeout
Timeout = TimeSpan.FromSeconds(10)
};
// 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);
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
_httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
_httpClient.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.9");
_httpClient.DefaultRequestHeaders.CacheControl = CacheControlHeaderValue.Parse("max-age=0");
}
// Set a user agent string
var userAgent = GetUserAgent();
_httpClient.DefaultRequestHeaders.UserAgent.Clear();
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
// If we need to authenticate then set the credentials
_httpClient.DefaultRequestHeaders.Authorization = Authenticate ? new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))) : null;
_httpClient.DefaultRequestHeaders.IfModifiedSince = LastChecked;
// Attempt to get the response
var response = _httpClient.GetAsync(Source).Result;
@@ -299,6 +324,11 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
}
catch (HttpRequestException httpRequestException)
{
if (httpRequestException.StatusCode == HttpStatusCode.NotModified)
{
return Tuple.Create(FeedReadResult.NotModified, string.Empty);
}
Log.Logger.Error(httpRequestException, "Exception");
return HandleHttpRequestException(httpRequestException);
@@ -326,6 +356,9 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
{
switch (httpRequestException.StatusCode)
{
case HttpStatusCode.TooManyRequests:
return Tuple.Create(FeedReadResult.TooManyRequests, string.Empty);
case HttpStatusCode.ServiceUnavailable:
return Tuple.Create(FeedReadResult.TemporarilyUnavailable, string.Empty);
@@ -377,12 +410,12 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo
return FeedReadResult.NotDue;
}
// We're checking it now so update the time
LastChecked = DateTimeOffset.Now;
// Read the feed text
var retrieveResult = RetrieveFeed();
// We're checking it now so update the time
LastChecked = DateTimeOffset.Now;
// Get the information out of the async result
var result = retrieveResult.Item1;
var feedText = retrieveResult.Item2;

View File

@@ -15,5 +15,6 @@ public enum FeedReadResult
ConnectionFailed,
ServerError,
Moved,
TemporarilyUnavailable
TemporarilyUnavailable,
TooManyRequests
}

View File

@@ -178,7 +178,7 @@ public partial class MainWindow
if (DateTime.Now - Settings.Default.LastVersionCheck >= Settings.Default.VersionCheckInterval)
{
// Get the update information
UpdateCheck.CheckForUpdate().Wait();
UpdateCheck.CheckForUpdate(Settings.Default.IncludePrerelease).Wait();
// Update the last check time
Settings.Default.LastVersionCheck = DateTime.Now;

View File

@@ -90,7 +90,7 @@ public partial class MainWindow : IDisposable
// Check for update
if (Settings.Default.CheckVersionAtStartup)
await UpdateCheck.CheckForUpdate();
await UpdateCheck.CheckForUpdate(Settings.Default.IncludePrerelease);
// Show the link if updates are available
if (UpdateCheck.UpdateAvailable)

View File

@@ -24,17 +24,18 @@ public partial class MainWindow
{
StopTimer();
_mainTimer.Dispose();
_mainTimer?.Dispose();
_mainTimer = null;
}
private void StartTimer()
{
_mainTimer.Start();
_mainTimer?.Start();
}
private void StopTimer()
{
_mainTimer.Stop();
_mainTimer?.Stop();
}
private void HandleMainTimerElapsed(object sender, EventArgs e)

View File

@@ -166,6 +166,12 @@ public partial class MainWindow
// Delete the feed
_database.SaveChanges(() => _database.Feeds.Remove(feedToDelete));
// Refresh the database to current settings
ResetDatabase();
// Re-initialize the feed display
DisplayFeed();
}
private void OpenAllFeedItemsOnSinglePage()

View File

@@ -34,6 +34,6 @@ public partial class MainWindow
private void HandleNewVersionLinkClick(object sender, RoutedEventArgs e)
{
UpdateCheck.DisplayUpdateInformation(true);
UpdateCheck.DisplayUpdateInformation(true, Settings.Default.IncludePrerelease);
}
}

View File

@@ -49,14 +49,14 @@ public partial class MainWindow
{
// Set the last window location
Settings.Default.WindowLocation = new Point(Left, Top);
Settings.Default.Save();
// Set the last window size
Settings.Default.WindowSize = new Size(Width, Height);
Settings.Default.Save();
// Save the dock on the navigation tray
Settings.Default.ToolbarLocation = NameBasedGrid.NameBasedGrid.GetRow(NavigationToolbarTray) == "TopToolbarRow" ? Dock.Top : Dock.Bottom;
// Save settings
Settings.Default.Save();
}

View File

@@ -94,6 +94,13 @@
<ComboBoxItem Content="{x:Static properties:Resources.openAllMultipleToolbarButton}"
Tag="{x:Static feedCenter:MultipleOpenAction.IndividualPages}" />
</ComboBox>
<ComboBox Name="UserAgentComboBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.userAgentLabel}"
DisplayMemberPath="Caption"
ItemsSource="{Binding Source={x:Static options:UserAgentItem.UserAgents}}"
SelectedValuePath="UserAgent"
SelectedValue="{Binding Path=UserAgent, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}" />
</StackPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.authenticationTab}">

View File

@@ -12,11 +12,9 @@
d:DesignWidth="300">
<StackPanel options:Spacing.Vertical="10">
<CheckBox Content="{x:Static properties:Resources.startWithWindowsCheckBox}"
Name="StartWithWindowsCheckBox"
IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=StartWithWindows}"
Click="OnSaveSettings" />
<ComboBox Name="BrowserComboBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
<ComboBox mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.defaultBrowserLabel}"
d:DataContext="{d:DesignInstance Type=installedBrowsers:InstalledBrowser}"
DisplayMemberPath="Name"
@@ -24,12 +22,11 @@
SelectedValuePath="Key"
SelectedValue="{Binding Source={x:Static properties:Settings.Default}, Path=Browser}"
SelectionChanged="OnSaveSettings" />
<ComboBox Name="UserAgentComboBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
<ComboBox mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.defaultUserAgentLabel}"
d:DataContext="{d:DesignInstance Type=options:UserAgentItem}"
DisplayMemberPath="Caption"
ItemsSource="{Binding Source={x:Static options:UserAgentItem.UserAgents}}"
ItemsSource="{Binding Source={x:Static options:UserAgentItem.DefaultUserAgents}}"
SelectedValuePath="UserAgent"
SelectedValue="{Binding Source={x:Static properties:Settings.Default}, Path=DefaultUserAgent}"
SelectionChanged="OnSaveSettings" />

View File

@@ -13,6 +13,10 @@
Name="CheckVersionOnStartupCheckBox"
IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=CheckVersionAtStartup}"
Click="OnSaveSettings" />
<CheckBox Content="{x:Static properties:Resources.includePrereleaseCheckBox}"
Name="IncludePrereleaseCheckBox"
IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=IncludePrerelease}"
Click="OnSaveSettings" />
<Button Content="{x:Static properties:Resources.checkVersionNowButton}"
HorizontalAlignment="Left"
Click="HandleCheckVersionNowButtonClick" />

View File

@@ -15,7 +15,7 @@ public partial class UpdateOptionsPanel
private void HandleCheckVersionNowButtonClick(object sender, RoutedEventArgs e)
{
UpdateCheck.DisplayUpdateInformation(true);
UpdateCheck.DisplayUpdateInformation(true, Settings.Default.IncludePrerelease);
}
private void OnSaveSettings(object sender, RoutedEventArgs e)

View File

@@ -1,17 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using FeedCenter.Properties;
namespace FeedCenter.Options;
public class UserAgentItem
{
public string Caption { get; set; }
public string UserAgent { get; set; }
public static List<UserAgentItem> UserAgents => new()
public static List<UserAgentItem> DefaultUserAgents => new()
{
new UserAgentItem
{
Caption = Properties.Resources.DefaultUserAgentCaption,
Caption = Properties.Resources.ApplicationUserAgentCaption,
UserAgent = string.Empty
},
new UserAgentItem
@@ -30,4 +31,29 @@ public class UserAgentItem
UserAgent = "curl/7.47.0"
}
};
public string UserAgent { get; set; }
public static List<UserAgentItem> UserAgents
{
get
{
var defaultUserAgents = DefaultUserAgents;
var applicationDefaultUserAgent = defaultUserAgents.First(dua => dua.UserAgent == Settings.Default.DefaultUserAgent);
var userAgents = new List<UserAgentItem>
{
new()
{
Caption = string.Format(Resources.DefaultUserAgentCaption, applicationDefaultUserAgent.Caption),
UserAgent = null
}
};
userAgents.AddRange(defaultUserAgents);
return userAgents;
}
}
}

View File

@@ -133,6 +133,15 @@ namespace FeedCenter.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Feed Center.
/// </summary>
public static string ApplicationUserAgentCaption {
get {
return ResourceManager.GetString("ApplicationUserAgentCaption", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Password.
/// </summary>
@@ -517,7 +526,7 @@ namespace FeedCenter.Properties {
}
/// <summary>
/// Looks up a localized string similar to Feed Center.
/// Looks up a localized string similar to Default ({0}).
/// </summary>
public static string DefaultUserAgentCaption {
get {
@@ -831,6 +840,15 @@ namespace FeedCenter.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Temporarily unavailable.
/// </summary>
public static string FeedReadResult_TemporarilyUnavailable {
get {
return ResourceManager.GetString("FeedReadResult_TemporarilyUnavailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Timeout.
/// </summary>
@@ -840,6 +858,15 @@ namespace FeedCenter.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Too many requests.
/// </summary>
public static string FeedReadResult_TooManyRequests {
get {
return ResourceManager.GetString("FeedReadResult_TooManyRequests", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not authorized.
/// </summary>
@@ -930,6 +957,15 @@ namespace FeedCenter.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Include _prerelease.
/// </summary>
public static string includePrereleaseCheckBox {
get {
return ResourceManager.GetString("includePrereleaseCheckBox", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Last Updated.
/// </summary>
@@ -1461,6 +1497,15 @@ namespace FeedCenter.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to User agent.
/// </summary>
public static string userAgentLabel {
get {
return ResourceManager.GetString("userAgentLabel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version {0}.
/// </summary>

View File

@@ -524,6 +524,9 @@
<value>Category: {0}</value>
</data>
<data name="DefaultUserAgentCaption" xml:space="preserve">
<value>Default ({0})</value>
</data>
<data name="ApplicationUserAgentCaption" xml:space="preserve">
<value>Feed Center</value>
</data>
<data name="defaultUserAgentLabel" xml:space="preserve">
@@ -549,4 +552,16 @@ All feeds currently in category "{0}" will be moved to the default category.</va
<data name="ConfirmDeleteFeeds" xml:space="preserve">
<value>Are you sure you want to delete the selected feeds?</value>
</data>
<data name="userAgentLabel" xml:space="preserve">
<value>User agent</value>
</data>
<data name="FeedReadResult_TemporarilyUnavailable" xml:space="preserve">
<value>Temporarily unavailable</value>
</data>
<data name="FeedReadResult_TooManyRequests" xml:space="preserve">
<value>Too many requests</value>
</data>
<data name="includePrereleaseCheckBox" xml:space="preserve">
<value>Include _prerelease</value>
</data>
</root>

View File

@@ -12,7 +12,7 @@ namespace FeedCenter.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.5.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@@ -302,5 +302,17 @@ namespace FeedCenter.Properties {
return ((string)(this["DatabaseFile"]));
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool IncludePrerelease {
get {
return ((bool)(this["IncludePrerelease"]));
}
set {
this["IncludePrerelease"] = value;
}
}
}
}

View File

@@ -74,5 +74,8 @@
<Setting Name="DatabaseFile" Type="System.String" Scope="Application">
<Value Profile="(Default)">FeedCenter.realm</Value>
</Setting>
<Setting Name="IncludePrerelease" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@@ -36,6 +36,9 @@ public static class SettingsStore
// Try to get the setting from the database that matches the name and version
var setting = entities.Settings.FirstOrDefault(s => s.Name == name);
if (setting?.Value == value)
return;
entities.SaveChanges(() =>
{
// If there was no setting we need to create it

View File

@@ -49,6 +49,9 @@
<setting name="DefaultUserAgent" serializeAs="String">
<value />
</setting>
<setting name="IncludePrerelease" serializeAs="String">
<value>False</value>
</setting>
</FeedCenter.Properties.Settings>
</userSettings>
<applicationSettings>