From 4e721efa5510920f23689c54470280e719f6e321 Mon Sep 17 00:00:00 2001 From: Chris Kaczor Date: Wed, 24 Sep 2025 21:08:59 -0400 Subject: [PATCH] Start adding server support --- Application/Data/Database.cs | 13 -- Application/Data/RealmObservableCollection.cs | 13 +- Application/Entities.cs | 70 ++++-- Application/FeedCenter.csproj | 5 +- Application/FeedErrorWindow.xaml.cs | 10 +- Application/Feeds/Account.cs | 203 ++++++++++++++++++ Application/Feeds/AccountReadInput.cs | 11 + Application/Feeds/AccountReadResult.cs | 8 + Application/Feeds/AccountType.cs | 8 + Application/Feeds/Category.cs | 8 +- Application/Feeds/Feed.cs | 15 +- Application/Feeds/FeedItem.cs | 47 +++- Application/Feeds/FeverReader.cs | 145 +++++++++++++ Application/Feeds/GoogleReaderReader.cs | 144 +++++++++++++ Application/Feeds/IAccountReader.cs | 7 + Application/Feeds/LocalReader.cs | 42 ++++ Application/FodyWeavers.xsd | 8 +- Application/MainWindow/FeedCreation.cs | 31 +-- Application/MainWindow/FeedList.cs | 25 +-- Application/MainWindow/FeedReading.cs | 71 ++++-- Application/MainWindow/MainWindow.xaml | 10 +- Application/MainWindow/MainWindow.xaml.cs | 37 ++-- Application/MainWindow/Toolbar.cs | 94 +++++--- Application/Options/AboutOptionsPanel.xaml.cs | 2 +- Application/Options/AccountTypeItem.cs | 24 +++ .../Options/AccountTypeToNameConverter.cs | 22 ++ Application/Options/AccountWindow.xaml | 139 ++++++++++++ Application/Options/AccountWindow.xaml.cs | 78 +++++++ Application/Options/AccountsOptionsPanel.xaml | 98 +++++++++ .../Options/AccountsOptionsPanel.xaml.cs | 129 +++++++++++ Application/Options/BulkFeedWindow.xaml.cs | 12 +- Application/Options/CategoryWindow.xaml | 9 +- Application/Options/CategoryWindow.xaml.cs | 14 +- .../Options/DisplayOptionsPanel.xaml.cs | 2 +- Application/Options/FeedWindow.xaml.cs | 12 +- Application/Options/FeedsOptionsPanel.xaml.cs | 57 ++--- .../Options/GeneralOptionsPanel.xaml.cs | 2 +- Application/Options/OptionsPanelBase.cs | 4 +- Application/Options/OptionsWindow.xaml.cs | 12 +- .../Options/UpdateOptionsPanel.xaml.cs | 2 +- Application/Properties/Resources.Designer.cs | 155 ++++++++++++- Application/Properties/Resources.resx | 53 ++++- Application/SettingsStore.cs | 7 +- Application/SplashWindow.xaml.cs | 22 +- FeedCenter.sln | 4 +- FeedCenter.sln.DotSettings | 3 +- appveyor.yml | 31 ++- 47 files changed, 1652 insertions(+), 266 deletions(-) create mode 100644 Application/Feeds/Account.cs create mode 100644 Application/Feeds/AccountReadInput.cs create mode 100644 Application/Feeds/AccountReadResult.cs create mode 100644 Application/Feeds/AccountType.cs create mode 100644 Application/Feeds/FeverReader.cs create mode 100644 Application/Feeds/GoogleReaderReader.cs create mode 100644 Application/Feeds/IAccountReader.cs create mode 100644 Application/Feeds/LocalReader.cs create mode 100644 Application/Options/AccountTypeItem.cs create mode 100644 Application/Options/AccountTypeToNameConverter.cs create mode 100644 Application/Options/AccountWindow.xaml create mode 100644 Application/Options/AccountWindow.xaml.cs create mode 100644 Application/Options/AccountsOptionsPanel.xaml create mode 100644 Application/Options/AccountsOptionsPanel.xaml.cs diff --git a/Application/Data/Database.cs b/Application/Data/Database.cs index 0503592..31e669c 100644 --- a/Application/Data/Database.cs +++ b/Application/Data/Database.cs @@ -7,18 +7,5 @@ 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() - { - if (Loaded) return; - - Entities = new FeedCenterEntities(); - - Loaded = true; - } } \ No newline at end of file diff --git a/Application/Data/RealmObservableCollection.cs b/Application/Data/RealmObservableCollection.cs index 27eafcc..f8c2f13 100644 --- a/Application/Data/RealmObservableCollection.cs +++ b/Application/Data/RealmObservableCollection.cs @@ -4,24 +4,17 @@ using System.Collections.Specialized; namespace FeedCenter.Data; -public class RealmObservableCollection : ObservableCollection where T : IRealmObject +public class RealmObservableCollection(Realm realm) : ObservableCollection(realm.All()) where T : IRealmObject { - private readonly Realm _realm; - - public RealmObservableCollection(Realm realm) : base(realm.All()) - { - _realm = realm; - } - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) foreach (T item in e.OldItems) - _realm.Remove(item); + realm.Remove(item); if (e.NewItems != null) foreach (T item in e.NewItems) - _realm.Add(item); + realm.Add(item); base.OnCollectionChanged(e); } diff --git a/Application/Entities.cs b/Application/Entities.cs index fc33045..0fb3870 100644 --- a/Application/Entities.cs +++ b/Application/Entities.cs @@ -1,8 +1,8 @@ -using System; -using System.Linq; -using FeedCenter.Data; +using FeedCenter.Data; using FeedCenter.Options; using Realms; +using System; +using System.Linq; namespace FeedCenter; @@ -12,16 +12,52 @@ public class FeedCenterEntities { var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}") { - SchemaVersion = 1, + SchemaVersion = 2, MigrationCallback = (migration, oldSchemaVersion) => { + if (oldSchemaVersion == 1) + migration.NewRealm.Add(Account.CreateDefault()); + + var localAccount = migration.NewRealm.All().First(a => a.Type == AccountType.Local); + + var newVersionCategories = migration.NewRealm.All(); + + foreach (var newVersionCategory in newVersionCategories) + { + switch (oldSchemaVersion) + { + case 1: + newVersionCategory.Account = localAccount; + newVersionCategory.RemoteId = null; + break; + } + } + var newVersionFeeds = migration.NewRealm.All(); foreach (var newVersionFeed in newVersionFeeds) { - if (oldSchemaVersion == 0) + switch (oldSchemaVersion) { - newVersionFeed.UserAgent = null; + case 0: + newVersionFeed.UserAgent = null; + break; + case 1: + newVersionFeed.Account = localAccount; + newVersionFeed.RemoteId = null; + break; + } + } + + var newVersionFeedItems = migration.NewRealm.All(); + + foreach (var newVersionFeedItem in newVersionFeedItems) + { + switch (oldSchemaVersion) + { + case 1: + newVersionFeedItem.RemoteId = null; + break; } } } @@ -29,13 +65,20 @@ public class FeedCenterEntities RealmInstance = Realm.GetInstance(realmConfiguration); + Accounts = new RealmObservableCollection(RealmInstance); Settings = new RealmObservableCollection(RealmInstance); Feeds = new RealmObservableCollection(RealmInstance); Categories = new RealmObservableCollection(RealmInstance); + if (!Accounts.Any()) + { + RealmInstance.Write(() => Accounts.Add(Account.CreateDefault())); + } + if (!Categories.Any()) { - RealmInstance.Write(() => Categories.Add(Category.CreateDefault())); + var localAccount = Accounts.First(a => a.Type == AccountType.Local); + RealmInstance.Write(() => Categories.Add(Category.CreateDefault(localAccount))); } } @@ -46,15 +89,16 @@ public class FeedCenterEntities get { return Categories.First(c => c.IsDefault); } } - public RealmObservableCollection Feeds { get; private set; } - private Realm RealmInstance { get; } - public RealmObservableCollection Settings { get; private set; } - - public void Refresh() + public Account LocalAccount { - RealmInstance.Refresh(); + get { return Accounts.First(a => a.Type == AccountType.Local); } } + public RealmObservableCollection Feeds { get; } + public RealmObservableCollection Accounts { get; } + private Realm RealmInstance { get; } + public RealmObservableCollection Settings { get; } + public void SaveChanges(Action action) { RealmInstance.Write(action); diff --git a/Application/FeedCenter.csproj b/Application/FeedCenter.csproj index 2da3288..af2a19e 100644 --- a/Application/FeedCenter.csproj +++ b/Application/FeedCenter.csproj @@ -1,6 +1,6 @@  - net70-windows + net8.0-windows7.0 WinExe false false @@ -25,6 +25,7 @@ + @@ -48,7 +49,7 @@ NU1701 - + diff --git a/Application/FeedErrorWindow.xaml.cs b/Application/FeedErrorWindow.xaml.cs index 7b4cc65..de70573 100644 --- a/Application/FeedErrorWindow.xaml.cs +++ b/Application/FeedErrorWindow.xaml.cs @@ -5,7 +5,6 @@ using System.Windows; using System.Windows.Data; using System.Windows.Input; using ChrisKaczor.InstalledBrowsers; -using FeedCenter.Data; using FeedCenter.Options; using FeedCenter.Properties; @@ -14,6 +13,7 @@ namespace FeedCenter; public partial class FeedErrorWindow { private CollectionViewSource _collectionViewSource; + private readonly FeedCenterEntities _entities = new(); public FeedErrorWindow() { @@ -23,7 +23,7 @@ public partial class FeedErrorWindow public void Display(Window owner) { // Create a view and sort it by name - _collectionViewSource = new CollectionViewSource { Source = Database.Entities.Feeds }; + _collectionViewSource = new CollectionViewSource { Source = _entities.Feeds }; _collectionViewSource.Filter += HandleCollectionViewSourceFilter; _collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending)); @@ -62,7 +62,7 @@ public partial class FeedErrorWindow var feed = (Feed) FeedDataGrid.SelectedItem; - var feedWindow = new FeedWindow(); + var feedWindow = new FeedWindow(_entities); feedWindow.Display(feed, GetWindow(this)); } @@ -74,7 +74,7 @@ public partial class FeedErrorWindow var feed = (Feed) FeedDataGrid.SelectedItem; - Database.Entities.SaveChanges(() => Database.Entities.Feeds.Remove(feed)); + _entities.SaveChanges(() => _entities.Feeds.Remove(feed)); SetFeedButtonStates(); } @@ -118,8 +118,6 @@ public partial class FeedErrorWindow entities.SaveChanges(() => feed.Read(true)); }); - Database.Entities.Refresh(); - var selectedIndex = FeedDataGrid.SelectedIndex; _collectionViewSource.View.Refresh(); diff --git a/Application/Feeds/Account.cs b/Application/Feeds/Account.cs new file mode 100644 index 0000000..64c1145 --- /dev/null +++ b/Application/Feeds/Account.cs @@ -0,0 +1,203 @@ +using Realms; +using System; +using System.Collections; +using System.ComponentModel; +using System.Linq; + +namespace FeedCenter; + +public class Account : RealmObject, INotifyDataErrorInfo +{ + public const string DefaultName = "< Local >"; + + private readonly DataErrorDictionary _dataErrorDictionary; + + public Account() : this(AccountType.Local) + { + } + + public Account(AccountType type) + { + Type = type; + + _dataErrorDictionary = new DataErrorDictionary(); + _dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged; + } + + [PrimaryKey] + public Guid Id { get; set; } = Guid.NewGuid(); + + public AccountType Type + { + get => Enum.TryParse(TypeRaw, out AccountType result) ? result : AccountType.Local; + set => TypeRaw = value.ToString(); + } + + public bool SupportsFeedEdit => Type switch + { + AccountType.Fever => false, + AccountType.GoogleReader => false, + AccountType.Local => true, + _ => throw new NotSupportedException() + }; + + public bool SupportsFeedDelete => Type switch + { + AccountType.Fever => false, + AccountType.GoogleReader => false, + AccountType.Local => true, + _ => throw new NotSupportedException() + }; + + private string TypeRaw { get; set; } + + public string Name + { + get => RawName; + set + { + RawName = value; + + ValidateString(nameof(Name), RawName); + RaisePropertyChanged(); + } + } + + [MapTo("Name")] + private string RawName { get; set; } = string.Empty; + + public string Url + { + get => RawUrl; + set + { + RawUrl = value; + + ValidateString(nameof(Url), RawUrl); + RaisePropertyChanged(); + } + } + + [MapTo("Url")] + public string RawUrl { get; set; } + + public string Username + { + get => RawUsername; + set + { + RawUsername = value; + + if (!Authenticate) + { + _dataErrorDictionary.ClearErrors(nameof(Username)); + return; + } + + ValidateString(nameof(Username), RawUsername); + RaisePropertyChanged(); + } + } + + [MapTo("Username")] + public string RawUsername { get; set; } + + public string Password + { + get => RawPassword; + set + { + RawPassword = value; + + if (!Authenticate) + { + _dataErrorDictionary.ClearErrors(nameof(Password)); + return; + } + + ValidateString(nameof(Password), RawPassword); + RaisePropertyChanged(); + } + } + + [MapTo("Password")] + public string RawPassword { get; set; } + + public bool Authenticate { get; set; } + + public bool Enabled { get; set; } = true; + + public int CheckInterval { get; set; } = 60; + + public DateTimeOffset LastChecked { get; set; } + + public bool HasErrors => _dataErrorDictionary.Any(); + + public IEnumerable GetErrors(string propertyName) + { + return _dataErrorDictionary.GetErrors(propertyName); + } + + public event EventHandler ErrorsChanged; + + 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"); + } + + public static Account CreateDefault() + { + return new Account { Name = DefaultName, Type = AccountType.Local }; + } + + public int GetProgressSteps(FeedCenterEntities entities) + { + var progressSteps = Type switch + { + // Delegate to the right reader based on the account type + AccountType.Fever => new FeverReader().GetProgressSteps(entities), + AccountType.GoogleReader => new GoogleReaderReader().GetProgressSteps(entities), + AccountType.Local => new LocalReader().GetProgressSteps(entities), + _ => throw new NotSupportedException() + }; + + return progressSteps; + } + + public AccountReadResult Read(AccountReadInput accountReadInput) + { + // If not enabled then do nothing + if (!Enabled) + return AccountReadResult.NotEnabled; + + // Check if we're forcing a read + if (!accountReadInput.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 AccountReadResult.NotDue; + } + + var accountReadResult = Type switch + { + // Delegate to the right reader based on the account type + AccountType.Fever => new FeverReader().Read(this, accountReadInput), + AccountType.GoogleReader => new GoogleReaderReader().Read(this, accountReadInput), + AccountType.Local => new LocalReader().Read(this, accountReadInput), + _ => throw new NotSupportedException() + }; + + return accountReadResult; + } +} \ No newline at end of file diff --git a/Application/Feeds/AccountReadInput.cs b/Application/Feeds/AccountReadInput.cs new file mode 100644 index 0000000..8f3f627 --- /dev/null +++ b/Application/Feeds/AccountReadInput.cs @@ -0,0 +1,11 @@ +using System; + +namespace FeedCenter; + +public class AccountReadInput(FeedCenterEntities entities, Guid? feedId, bool forceRead, Action incrementProgress) +{ + public FeedCenterEntities Entities { get; set; } = entities; + public Guid? FeedId { get; set; } = feedId; + public bool ForceRead { get; set; } = forceRead; + public Action IncrementProgress { get; private set; } = incrementProgress ?? throw new ArgumentNullException(nameof(incrementProgress)); +} \ No newline at end of file diff --git a/Application/Feeds/AccountReadResult.cs b/Application/Feeds/AccountReadResult.cs new file mode 100644 index 0000000..5ba9377 --- /dev/null +++ b/Application/Feeds/AccountReadResult.cs @@ -0,0 +1,8 @@ +namespace FeedCenter; + +public enum AccountReadResult +{ + Success, + NotDue, + NotEnabled +} \ No newline at end of file diff --git a/Application/Feeds/AccountType.cs b/Application/Feeds/AccountType.cs new file mode 100644 index 0000000..8589ab3 --- /dev/null +++ b/Application/Feeds/AccountType.cs @@ -0,0 +1,8 @@ +namespace FeedCenter; + +public enum AccountType +{ + Local, + Fever, + GoogleReader +} \ No newline at end of file diff --git a/Application/Feeds/Category.cs b/Application/Feeds/Category.cs index 0f756ad..c5e2f0e 100644 --- a/Application/Feeds/Category.cs +++ b/Application/Feeds/Category.cs @@ -22,6 +22,10 @@ public class Category : RealmObject, INotifyDataErrorInfo [PrimaryKey] public Guid Id { get; set; } = Guid.NewGuid(); + public string RemoteId { get; set; } + + public Account Account { get; set; } + public bool IsDefault { get; internal set; } public string Name @@ -56,9 +60,9 @@ public class Category : RealmObject, INotifyDataErrorInfo ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName)); } - public static Category CreateDefault() + public static Category CreateDefault(Account account) { - return new Category { Name = DefaultName, IsDefault = true }; + return new Category { Name = DefaultName, IsDefault = true, Account = account }; } private void ValidateName() diff --git a/Application/Feeds/Feed.cs b/Application/Feeds/Feed.cs index efe5f07..dbb2e36 100644 --- a/Application/Feeds/Feed.cs +++ b/Application/Feeds/Feed.cs @@ -12,7 +12,6 @@ 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; @@ -34,14 +33,16 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo _dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged; } + [PrimaryKey] + public Guid Id { get; set; } + + public string RemoteId { get; set; } 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; } + public Account Account { get; set; } [UsedImplicitly] public IList Items { get; } @@ -171,9 +172,9 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo public event EventHandler ErrorsChanged; - public static Feed Create() + public static Feed Create(FeedCenterEntities entities) { - return new Feed { Id = Guid.NewGuid(), CategoryId = Database.Entities.DefaultCategory.Id }; + return new Feed { Id = Guid.NewGuid(), CategoryId = entities.DefaultCategory.Id, Account = entities.LocalAccount }; } private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e) @@ -215,7 +216,7 @@ public partial class Feed : RealmObject, INotifyDataErrorInfo break; } - // If the feed was successfully read and we have no last update timestamp - set the last update timestamp to now + // 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; diff --git a/Application/Feeds/FeedItem.cs b/Application/Feeds/FeedItem.cs index 6203d98..5b09604 100644 --- a/Application/Feeds/FeedItem.cs +++ b/Application/Feeds/FeedItem.cs @@ -1,25 +1,28 @@ -using System; -using System.Text.RegularExpressions; -using FeedCenter.Options; +using FeedCenter.Options; using Realms; +using System; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace FeedCenter; public partial class FeedItem : RealmObject { + [PrimaryKey] + public Guid Id { get; set; } + public bool BeenRead { get; set; } public string Description { 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 string RemoteId { get; set; } public static FeedItem Create() { @@ -42,7 +45,7 @@ public partial class FeedItem : RealmObject case MultipleLineDisplay.FirstLine: // Find the first newline - var newlineIndex = title.IndexOf("\n", StringComparison.Ordinal); + var newlineIndex = title.IndexOf('\n', StringComparison.Ordinal); // If a newline was found return everything before it if (newlineIndex > -1) @@ -70,6 +73,34 @@ public partial class FeedItem : RealmObject return title; } + public async Task MarkAsRead(FeedCenterEntities entities) + { + var feed = entities.Feeds.FirstOrDefault(f => f.Id == FeedId); + + entities.SaveChanges(() => + { + BeenRead = true; + New = false; + }); + + if (feed == null || feed.Account.Type == AccountType.Local) + return; + + switch (feed.Account.Type) + { + case AccountType.Fever: + // Delegate to the right reader based on the account type + await FeverReader.MarkFeedItemRead(feed.Account, RemoteId); + + break; + case AccountType.Local: + break; + default: + Debug.Assert(false); + break; + } + } + [GeneratedRegex("\\n")] private static partial Regex NewlineRegex(); diff --git a/Application/Feeds/FeverReader.cs b/Application/Feeds/FeverReader.cs new file mode 100644 index 0000000..891e74f --- /dev/null +++ b/Application/Feeds/FeverReader.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using ChrisKaczor.FeverClient; + +namespace FeedCenter; + +internal class FeverReader : IAccountReader +{ + public int GetProgressSteps(FeedCenterEntities entities) + { + return 7; + } + + public AccountReadResult Read(Account account, AccountReadInput accountReadInput) + { + var checkTime = DateTimeOffset.UtcNow; + + var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty; + + var feverClient = new FeverClient(account.Url, apiKey); + + accountReadInput.IncrementProgress(); + + var feverFeeds = feverClient.GetFeeds().Result.ToList(); + + accountReadInput.IncrementProgress(); + + var allFeverFeedItems = feverClient.GetAllFeedItems().Result.ToList(); + + accountReadInput.IncrementProgress(); + + var transaction = accountReadInput.Entities.BeginTransaction(); + + foreach (var feverFeed in feverFeeds) + { + var feed = accountReadInput.Entities.Feeds.FirstOrDefault(f => f.RemoteId == feverFeed.Id.ToString() && f.Account.Id == account.Id); + + if (feed == null) + { + feed = new Feed + { + Id = Guid.NewGuid(), + RemoteId = feverFeed.Id.ToString(), + Title = feverFeed.Title, + Source = feverFeed.Url, + Link = feverFeed.SiteUrl, + Account = account, + Name = feverFeed.Title, + CategoryId = accountReadInput.Entities.DefaultCategory.Id, + Enabled = true, + CheckInterval = 0, + }; + + accountReadInput.Entities.Feeds.Add(feed); + } + + feed.Name = feverFeed.Title; + feed.Title = feverFeed.Title; + feed.Link = feverFeed.SiteUrl; + feed.Source = feverFeed.Url; + feed.LastReadResult = FeedReadResult.Success; + feed.LastChecked = checkTime; + + accountReadInput.IncrementProgress(); + + var feverFeedItems = allFeverFeedItems + .Where(f => f.FeedId == feverFeed.Id) + .OrderByDescending(fi => fi.CreatedOnTime).ToList(); + + var sequence = 1; + + foreach (var feverFeedItem in feverFeedItems) + { + var feedItem = feed.Items.FirstOrDefault(f => f.RemoteId == feverFeedItem.Id.ToString()); + + if (feedItem == null) + { + feedItem = new FeedItem + { + Id = Guid.NewGuid(), + RemoteId = feverFeedItem.Id.ToString(), + Title = feverFeedItem.Title, + Link = feverFeedItem.Url, + Description = feverFeedItem.Html, + BeenRead = feverFeedItem.IsRead, + FeedId = feed.Id, + Guid = Guid.NewGuid().ToString(), + Sequence = sequence++, + }; + + feed.Items.Add(feedItem); + } + + feedItem.LastFound = checkTime; + feedItem.BeenRead = feverFeedItem.IsRead; + feedItem.Sequence = sequence++; + } + + accountReadInput.IncrementProgress(); + + var feedItemsNotSeen = feed.Items.Where(fi => fi.LastFound != checkTime).ToList(); + + foreach (var feedItemNotSeen in feedItemsNotSeen) + { + feed.Items.Remove(feedItemNotSeen); + } + } + + accountReadInput.IncrementProgress(); + + var feedsNotSeen = accountReadInput.Entities.Feeds.Where(f => f.Account.Id == account.Id && f.LastChecked != checkTime).ToList(); + + foreach (var feedNotSeen in feedsNotSeen) + { + accountReadInput.Entities.Feeds.Remove(feedNotSeen); + } + + account.LastChecked = checkTime; + + transaction.Commit(); + + accountReadInput.IncrementProgress(); + + return AccountReadResult.Success; + } + + public static async Task MarkFeedItemRead(Account account, string feedItemId) + { + var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty; + + var feverClient = new FeverClient(account.Url, apiKey); + + await feverClient.MarkFeedItemAsRead(int.Parse(feedItemId)); + } + + private static string GetApiKey(Account account) + { + var input = $"{account.Username}:{account.Password}"; + var hash = MD5.HashData(Encoding.UTF8.GetBytes(input)); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/Application/Feeds/GoogleReaderReader.cs b/Application/Feeds/GoogleReaderReader.cs new file mode 100644 index 0000000..86e739c --- /dev/null +++ b/Application/Feeds/GoogleReaderReader.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace FeedCenter; + +internal class GoogleReaderReader : IAccountReader +{ + public int GetProgressSteps(FeedCenterEntities entities) + { + return 7; + } + + public AccountReadResult Read(Account account, AccountReadInput accountReadInput) + { + var checkTime = DateTimeOffset.UtcNow; + + //var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty; + + //var feverClient = new FeverClient.FeverClient(account.Url, apiKey); + + //accountReadInput.IncrementProgress(); + + //var feverFeeds = feverClient.GetFeeds().Result.ToList(); + + //accountReadInput.IncrementProgress(); + + //var allFeverFeedItems = feverClient.GetAllFeedItems().Result.ToList(); + + //accountReadInput.IncrementProgress(); + + var transaction = accountReadInput.Entities.BeginTransaction(); + + //foreach (var feverFeed in feverFeeds) + //{ + // var feed = accountReadInput.Entities.Feeds.FirstOrDefault(f => f.RemoteId == feverFeed.Id.ToString() && f.Account.Id == account.Id); + + // if (feed == null) + // { + // feed = new Feed + // { + // Id = Guid.NewGuid(), + // RemoteId = feverFeed.Id.ToString(), + // Title = feverFeed.Title, + // Source = feverFeed.Url, + // Link = feverFeed.SiteUrl, + // Account = account, + // Name = feverFeed.Title, + // CategoryId = accountReadInput.Entities.DefaultCategory.Id, + // Enabled = true, + // CheckInterval = 0, + // }; + + // accountReadInput.Entities.Feeds.Add(feed); + // } + + // feed.Name = feverFeed.Title; + // feed.Title = feverFeed.Title; + // feed.Link = feverFeed.SiteUrl; + // feed.Source = feverFeed.Url; + // feed.LastReadResult = FeedReadResult.Success; + // feed.LastChecked = checkTime; + + // accountReadInput.IncrementProgress(); + + // var feverFeedItems = allFeverFeedItems + // .Where(f => f.FeedId == feverFeed.Id) + // .OrderByDescending(fi => fi.CreatedOnTime).ToList(); + + // var sequence = 1; + + // foreach (var feverFeedItem in feverFeedItems) + // { + // var feedItem = feed.Items.FirstOrDefault(f => f.RemoteId == feverFeedItem.Id.ToString()); + + // if (feedItem == null) + // { + // feedItem = new FeedItem + // { + // Id = Guid.NewGuid(), + // RemoteId = feverFeedItem.Id.ToString(), + // Title = feverFeedItem.Title, + // Link = feverFeedItem.Url, + // Description = feverFeedItem.Html, + // BeenRead = feverFeedItem.IsRead, + // FeedId = feed.Id, + // Guid = Guid.NewGuid().ToString(), + // Sequence = sequence++, + // }; + + // feed.Items.Add(feedItem); + // } + + // feedItem.LastFound = checkTime; + // feedItem.BeenRead = feverFeedItem.IsRead; + // feedItem.Sequence = sequence++; + // } + + // accountReadInput.IncrementProgress(); + + // var feedItemsNotSeen = feed.Items.Where(fi => fi.LastFound != checkTime).ToList(); + + // foreach (var feedItemNotSeen in feedItemsNotSeen) + // { + // feed.Items.Remove(feedItemNotSeen); + // } + //} + + accountReadInput.IncrementProgress(); + + //var feedsNotSeen = accountReadInput.Entities.Feeds.Where(f => f.Account.Id == account.Id && f.LastChecked != checkTime).ToList(); + + //foreach (var feedNotSeen in feedsNotSeen) + //{ + // accountReadInput.Entities.Feeds.Remove(feedNotSeen); + //} + + account.LastChecked = checkTime; + + transaction.Commit(); + + accountReadInput.IncrementProgress(); + + return AccountReadResult.Success; + } + + public static async Task MarkFeedItemRead(Account account, string feedItemId) + { + //var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty; + + //var feverClient = new FeverClient.FeverClient(account.Url, apiKey); + + //await feverClient.MarkFeedItemAsRead(int.Parse(feedItemId)); + } + + //private static string GetApiKey(Account account) + //{ + // var input = $"{account.Username}:{account.Password}"; + // var hash = MD5.HashData(Encoding.UTF8.GetBytes(input)); + // return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + //} +} \ No newline at end of file diff --git a/Application/Feeds/IAccountReader.cs b/Application/Feeds/IAccountReader.cs new file mode 100644 index 0000000..090a0b8 --- /dev/null +++ b/Application/Feeds/IAccountReader.cs @@ -0,0 +1,7 @@ +namespace FeedCenter; + +public interface IAccountReader +{ + public int GetProgressSteps(FeedCenterEntities entities); + public AccountReadResult Read(Account account, AccountReadInput accountReadInput); +} \ No newline at end of file diff --git a/Application/Feeds/LocalReader.cs b/Application/Feeds/LocalReader.cs new file mode 100644 index 0000000..acd3ed6 --- /dev/null +++ b/Application/Feeds/LocalReader.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FeedCenter; + +public class LocalReader : IAccountReader +{ + public int GetProgressSteps(FeedCenterEntities entities) + { + var enabledFeedCount = entities.Feeds.Count(f => f.Account.Type == AccountType.Local && f.Enabled); + + return enabledFeedCount; + } + + public AccountReadResult Read(Account account, AccountReadInput accountReadInput) + { + var checkTime = DateTimeOffset.UtcNow; + + // Create the list of feeds to read + var feedsToRead = new List(); + + // If we have a single feed then add it to the list - otherwise add them all + if (accountReadInput.FeedId != null) + feedsToRead.Add(accountReadInput.Entities.Feeds.First(feed => feed.Id == accountReadInput.FeedId)); + else + feedsToRead.AddRange(accountReadInput.Entities.Feeds.Where(f => f.Account.Type == AccountType.Local)); + + // Loop over each feed and read it + foreach (var feed in feedsToRead) + { + // Read the feed + accountReadInput.Entities.SaveChanges(() => feed.Read(accountReadInput.ForceRead)); + + accountReadInput.IncrementProgress(); + } + + accountReadInput.Entities.SaveChanges(() => account.LastChecked = checkTime); + + return AccountReadResult.Success; + } +} \ No newline at end of file diff --git a/Application/FodyWeavers.xsd b/Application/FodyWeavers.xsd index fa5cb18..f526bdd 100644 --- a/Application/FodyWeavers.xsd +++ b/Application/FodyWeavers.xsd @@ -5,13 +5,7 @@ - - - - Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/main/Realm/Realm.Weaver/Analytics.cs - - - + diff --git a/Application/MainWindow/FeedCreation.cs b/Application/MainWindow/FeedCreation.cs index 18d3407..4042520 100644 --- a/Application/MainWindow/FeedCreation.cs +++ b/Application/MainWindow/FeedCreation.cs @@ -21,9 +21,8 @@ public partial class MainWindow private void HandleNewFeed(string feedUrl) { // Create and configure the new feed - var feed = Feed.Create(); + var feed = Feed.Create(_database); feed.Source = feedUrl; - feed.CategoryId = _database.DefaultCategory.Id; // Try to detect the feed type var feedTypeResult = feed.DetectFeedType(); @@ -31,7 +30,7 @@ public partial class MainWindow // If we can't figure it out it could be an HTML page if (feedTypeResult.Item1 == FeedType.Unknown) { - // Only check if the feed was able to be read - otherwise fall through and show the dialog + // Only check if the feed was read - otherwise fall through and show the dialog if (feedTypeResult.Item2.Length > 0) { // Create and load an HTML document with the text @@ -87,19 +86,11 @@ public partial class MainWindow // Show a tip NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), H.NotifyIcon.Core.NotificationIcon.Info); - - _currentFeed = feed; - - // Refresh the database to current settings - ResetDatabase(); - - // Re-initialize the feed display - DisplayFeed(); } else { // Feed read failed - create a new feed window - var feedForm = new FeedWindow(); + var feedForm = new FeedWindow(_database); var dialogResult = feedForm.Display(feed, this); @@ -109,14 +100,14 @@ public partial class MainWindow // Add the feed to the feed table _database.SaveChanges(() => _database.Feeds.Add(feed)); - - _currentFeed = feed; - - // Refresh the database to current settings - ResetDatabase(); - - // Re-initialize the feed display - DisplayFeed(); } + + _currentFeed = feed; + + // Refresh the database to current settings + ResetDatabase(); + + // Re-initialize the feed display + DisplayFeed(); } } \ No newline at end of file diff --git a/Application/MainWindow/FeedList.cs b/Application/MainWindow/FeedList.cs index 3fc9551..71a6d16 100644 --- a/Application/MainWindow/FeedList.cs +++ b/Application/MainWindow/FeedList.cs @@ -2,6 +2,7 @@ using FeedCenter.Properties; using System; using System.Linq; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -30,7 +31,7 @@ public partial class MainWindow } } - private void HandleItemMouseUp(object sender, MouseButtonEventArgs e) + private async void HandleItemMouseUp(object sender, MouseButtonEventArgs e) { // Only handle the middle button if (e.ChangedButton != MouseButton.Middle) @@ -39,18 +40,14 @@ public partial class MainWindow // 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); + + // The feed item has been read and is no longer new + await feedItem.MarkAsRead(_database); } - private void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e) + private async void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e) { // Get the feed item var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; @@ -59,15 +56,11 @@ public partial class MainWindow 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); + + // The feed item has been read and is no longer new + await feedItem.MarkAsRead(_database); } private void HandleFeedButtonClick(object sender, RoutedEventArgs e) diff --git a/Application/MainWindow/FeedReading.cs b/Application/MainWindow/FeedReading.cs index f283c5e..3c29c5b 100644 --- a/Application/MainWindow/FeedReading.cs +++ b/Application/MainWindow/FeedReading.cs @@ -34,13 +34,13 @@ public partial class MainWindow } } - private void SetProgressMode(bool value, int feedCount) + private void SetProgressMode(bool showProgress, int maximum) { // Refresh the progress bar if we need it - if (value) + if (showProgress) { FeedReadProgress.Value = 0; - FeedReadProgress.Maximum = feedCount + 2; + FeedReadProgress.Maximum = maximum + 2; FeedReadProgress.Visibility = Visibility.Visible; } else @@ -76,11 +76,13 @@ public partial class MainWindow return; // Don't read if there is nothing to read - if (!_database.Feeds.Any()) + if (!_database.Accounts.Any()) return; + var progressSteps = _database.Accounts.Sum(a => a.GetProgressSteps(_database)); + // Switch to progress mode - SetProgressMode(true, _database.Feeds.Count); + SetProgressMode(true, progressSteps); // Create the input class var workerInput = new FeedReadWorkerInput(forceRead); @@ -138,36 +140,69 @@ public partial class MainWindow var database = new FeedCenterEntities(); // Get the worker - var worker = (BackgroundWorker) sender; + var worker = (BackgroundWorker)sender; // Get the input information - var workerInput = (FeedReadWorkerInput) e.Argument ?? new FeedReadWorkerInput(); + var workerInput = (FeedReadWorkerInput)e.Argument ?? new FeedReadWorkerInput(); // Setup for progress var currentProgress = 0; - // Create the list of feeds to read - var feedsToRead = new List(); + var accountsToRead = new List(); - // If we have a single feed then add it to the list - otherwise add them all + // If we have a single feed then get the account for that feed 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)); + var feed = database.Feeds.FirstOrDefault(f => f.Id == workerInput.FeedId); + if (feed != null) + accountsToRead.Add(feed.Account); + } + else + { + // Otherwise get all accounts + accountsToRead.AddRange(database.Accounts); + } + + var incrementProgress = () => + { // Increment progress currentProgress += 1; // Report progress worker.ReportProgress(currentProgress); + }; + + // Loop over each account and read it + foreach (var account in accountsToRead) + { + var accountReadInput = new AccountReadInput(database, workerInput.FeedId, workerInput.ForceRead, incrementProgress); + + account.Read(accountReadInput); } + //// Create the list of feeds to read + //var feedsToRead = new List(); + + //// If we have a single feed then add it to the list - otherwise add them all + //if (workerInput.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; diff --git a/Application/MainWindow/MainWindow.xaml b/Application/MainWindow/MainWindow.xaml index 5f79d4c..a425f67 100644 --- a/Application/MainWindow/MainWindow.xaml +++ b/Application/MainWindow/MainWindow.xaml @@ -285,11 +285,13 @@ - - - + + - diff --git a/Application/MainWindow/MainWindow.xaml.cs b/Application/MainWindow/MainWindow.xaml.cs index 250f4a6..1ffffbd 100644 --- a/Application/MainWindow/MainWindow.xaml.cs +++ b/Application/MainWindow/MainWindow.xaml.cs @@ -1,12 +1,12 @@ using ChrisKaczor.ApplicationUpdate; using ChrisKaczor.Wpf.Application; -using FeedCenter.Data; using FeedCenter.Properties; using Serilog; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; @@ -39,19 +39,19 @@ public partial class MainWindow : IDisposable base.OnSourceInitialized(e); // Initialize the window - Initialize(); + Initialize().ContinueWith(_ => { }, TaskScheduler.FromCurrentSynchronizationContext()); } - protected override async void OnClosed(EventArgs e) + protected override void OnClosed(EventArgs e) { base.OnClosed(e); - await SingleInstance.Stop(); + SingleInstance.Stop().ContinueWith(_ => { }, TaskScheduler.FromCurrentSynchronizationContext()); } - public async void Initialize() + public async Task Initialize() { - // Setup the update handler + // Set up the update handler InitializeUpdate(); // Show the notification icon @@ -72,8 +72,8 @@ public partial class MainWindow : IDisposable _feedReadWorker.ProgressChanged += HandleFeedReadWorkerProgressChanged; _feedReadWorker.RunWorkerCompleted += HandleFeedReadWorkerCompleted; - // Setup the database - _database = Database.Entities; + // Set up the database + _database = new FeedCenterEntities(); // Initialize the single instance listener SingleInstance.MessageReceived += SingleInstance_MessageReceived; @@ -151,7 +151,7 @@ public partial class MainWindow : IDisposable var currentId = _currentFeed?.IsValid ?? false ? _currentFeed.Id : Guid.Empty; // Create a new database object - _database.Refresh(); + _database = new FeedCenterEntities(); _feedList = _currentCategory == null ? _database.Feeds.ToList() @@ -188,7 +188,9 @@ public partial class MainWindow : IDisposable private void UpdateToolbarButtonState() { // Cache the feed count to save (a little) time - var feedCount = Settings.Default.DisplayEmptyFeeds ? _feedList.Count() : _feedList.Count(x => x.Items.Any(y => !y.BeenRead)); + var feedCount = Settings.Default.DisplayEmptyFeeds + ? _feedList.Count() + : _feedList.Count(x => x.Items.Any(y => !y.BeenRead)); // Set button states PreviousToolbarButton.IsEnabled = feedCount > 1; @@ -200,6 +202,11 @@ public partial class MainWindow : IDisposable FeedLabel.Visibility = feedCount == 0 ? Visibility.Hidden : Visibility.Visible; FeedButton.Visibility = feedCount == 0 ? Visibility.Hidden : Visibility.Visible; CategoryGrid.Visibility = _database.Categories.Count > 1 ? Visibility.Visible : Visibility.Collapsed; + + EditCurrentFeedMenuItem.Visibility = _currentFeed?.Account.SupportsFeedEdit ?? false ? Visibility.Visible : Visibility.Collapsed; + DeleteCurrentFeedMenuItem.Visibility = _currentFeed?.Account.SupportsFeedDelete ?? false ? Visibility.Visible : Visibility.Collapsed; + CurrentFeedMenu.Visibility = EditCurrentFeedMenuItem.IsVisible || DeleteCurrentFeedMenuItem.IsVisible ? Visibility.Visible : Visibility.Collapsed; + SettingsMenuSeparator.Visibility = CurrentFeedMenu.Visibility; } private void InitializeDisplay() @@ -405,16 +412,14 @@ public partial class MainWindow : IDisposable } UpdateOpenAllButton(); + UpdateToolbarButtonState(); } - private void MarkAllItemsAsRead() + private async Task MarkAllItemsAsRead() { // Loop over all items and mark them as read - _database.SaveChanges(() => - { - foreach (FeedItem feedItem in LinkTextList.Items) - feedItem.BeenRead = true; - }); + foreach (FeedItem feedItem in LinkTextList.Items) + await feedItem.MarkAsRead(_database); // Clear the list LinkTextList.Items.Clear(); diff --git a/Application/MainWindow/Toolbar.cs b/Application/MainWindow/Toolbar.cs index b95fcbe..d82654f 100644 --- a/Application/MainWindow/Toolbar.cs +++ b/Application/MainWindow/Toolbar.cs @@ -1,12 +1,16 @@ -using System.IO; +using ChrisKaczor.InstalledBrowsers; +using FeedCenter.Options; +using FeedCenter.Properties; +using System; +using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using System.Web.UI; using System.Windows; using System.Windows.Controls; -using ChrisKaczor.InstalledBrowsers; -using FeedCenter.Options; -using FeedCenter.Properties; +using Serilog; +using Serilog.Events; namespace FeedCenter; @@ -22,7 +26,7 @@ public partial class MainWindow NextFeed(); } - private void OpenAllFeedItemsIndividually() + private async Task OpenAllFeedItemsIndividually() { // Create a new list of feed items var feedItems = (from FeedItem feedItem in LinkTextList.Items select feedItem).ToList(); @@ -40,7 +44,7 @@ public partial class MainWindow if (InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link)) { // Mark the feed as read - _database.SaveChanges(() => feedItem.BeenRead = true); + await feedItem.MarkAsRead(_database); // Remove the item LinkTextList.Items.Remove(feedItem); @@ -71,9 +75,21 @@ public partial class MainWindow UpdateErrorLink(); } - private void HandleMarkReadToolbarButtonClick(object sender, RoutedEventArgs e) + private static void HandleException(Exception exception) { - MarkAllItemsAsRead(); + Log.Logger.Write(LogEventLevel.Debug, exception, ""); + } + + private async void HandleMarkReadToolbarButtonClick(object sender, RoutedEventArgs e) + { + try + { + await MarkAllItemsAsRead(); + } + catch (Exception exception) + { + HandleException(exception); + } } private void HandleShowErrorsButtonClick(object sender, RoutedEventArgs e) @@ -108,35 +124,49 @@ public partial class MainWindow ReadFeeds(true); } - private void HandleOpenAllMenuItemClick(object sender, RoutedEventArgs e) + private async void HandleOpenAllMenuItemClick(object sender, RoutedEventArgs e) { - var menuItem = (MenuItem) e.Source; + try + { + var menuItem = (MenuItem) e.Source; - if (Equals(menuItem, MenuOpenAllSinglePage)) - OpenAllFeedItemsOnSinglePage(); - else if (Equals(menuItem, MenuOpenAllMultiplePages)) - OpenAllFeedItemsIndividually(); + if (Equals(menuItem, MenuOpenAllSinglePage)) + await OpenAllFeedItemsOnSinglePage(); + else if (Equals(menuItem, MenuOpenAllMultiplePages)) + await OpenAllFeedItemsIndividually(); + } + catch (Exception exception) + { + HandleException(exception); + } } - private void HandleOpenAllToolbarButtonClick(object sender, RoutedEventArgs e) + private async void HandleOpenAllToolbarButtonClick(object sender, RoutedEventArgs e) { - var multipleOpenAction = _currentFeed.MultipleOpenAction; - - switch (multipleOpenAction) + try { - case MultipleOpenAction.IndividualPages: - OpenAllFeedItemsIndividually(); - break; - case MultipleOpenAction.SinglePage: - OpenAllFeedItemsOnSinglePage(); - break; + var multipleOpenAction = _currentFeed.MultipleOpenAction; + + switch (multipleOpenAction) + { + case MultipleOpenAction.IndividualPages: + await OpenAllFeedItemsIndividually(); + break; + case MultipleOpenAction.SinglePage: + await OpenAllFeedItemsOnSinglePage(); + break; + } + } + catch (Exception exception) + { + HandleException(exception); } } private void HandleEditCurrentFeedMenuItemClick(object sender, RoutedEventArgs e) { // Create a new feed window - var feedWindow = new FeedWindow(); + var feedWindow = new FeedWindow(_database); // Display the feed window and get the result var result = feedWindow.Display(_currentFeed, this); @@ -174,19 +204,19 @@ public partial class MainWindow DisplayFeed(); } - private void OpenAllFeedItemsOnSinglePage() + private async Task OpenAllFeedItemsOnSinglePage() { var fileName = Path.GetTempFileName() + ".html"; TextWriter textWriter = new StreamWriter(fileName); - using (var htmlTextWriter = new HtmlTextWriter(textWriter)) + await using (var htmlTextWriter = new HtmlTextWriter(textWriter)) { htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Html); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Head); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Title); - htmlTextWriter.Write(_currentFeed.Title); + await htmlTextWriter.WriteAsync(_currentFeed.Title); htmlTextWriter.RenderEndTag(); htmlTextWriter.AddAttribute("http-equiv", "Content-Type"); @@ -214,13 +244,13 @@ public partial class MainWindow htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, item.Link); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.A); - htmlTextWriter.Write(item.Title.Length == 0 ? item.Link : item.Title); + await htmlTextWriter.WriteAsync(item.Title.Length == 0 ? item.Link : item.Title); htmlTextWriter.RenderEndTag(); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Br); htmlTextWriter.RenderEndTag(); - htmlTextWriter.Write(item.Description); + await htmlTextWriter.WriteAsync(item.Description); htmlTextWriter.RenderEndTag(); @@ -231,11 +261,11 @@ public partial class MainWindow htmlTextWriter.RenderEndTag(); } - textWriter.Flush(); + await textWriter.FlushAsync(); textWriter.Close(); InstalledBrowser.OpenLink(Settings.Default.Browser, fileName); - MarkAllItemsAsRead(); + await MarkAllItemsAsRead(); } } \ No newline at end of file diff --git a/Application/Options/AboutOptionsPanel.xaml.cs b/Application/Options/AboutOptionsPanel.xaml.cs index 3f46b03..fcab914 100644 --- a/Application/Options/AboutOptionsPanel.xaml.cs +++ b/Application/Options/AboutOptionsPanel.xaml.cs @@ -4,7 +4,7 @@ namespace FeedCenter.Options; public partial class AboutOptionsPanel { - public AboutOptionsPanel(Window parentWindow) : base(parentWindow) + public AboutOptionsPanel(Window parentWindow, FeedCenterEntities entities) : base(parentWindow, entities) { InitializeComponent(); } diff --git a/Application/Options/AccountTypeItem.cs b/Application/Options/AccountTypeItem.cs new file mode 100644 index 0000000..e07e5ac --- /dev/null +++ b/Application/Options/AccountTypeItem.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace FeedCenter.Options +{ + public class AccountTypeItem + { + public AccountType AccountType { get; set; } + public string Name { get; set; } + + public static List AccountTypes => + [ + new() + { + Name = Properties.Resources.AccountTypeFever, + AccountType = AccountType.Fever + }, + //new() + //{ + // Name = Properties.Resources.AccountTypeGoogleReader, + // AccountType = AccountType.GoogleReader + //} + ]; + } +} \ No newline at end of file diff --git a/Application/Options/AccountTypeToNameConverter.cs b/Application/Options/AccountTypeToNameConverter.cs new file mode 100644 index 0000000..ce2e5f5 --- /dev/null +++ b/Application/Options/AccountTypeToNameConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Windows.Data; + +namespace FeedCenter.Options; + +public class AccountTypeToNameConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is AccountType accountType) + return AccountTypeItem.AccountTypes.First(at => at.AccountType == accountType).Name; + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Application/Options/AccountWindow.xaml b/Application/Options/AccountWindow.xaml new file mode 100644 index 0000000..9631acb --- /dev/null +++ b/Application/Options/AccountWindow.xaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +