73 Commits

Author SHA1 Message Date
9717e2d6af Fix System.Drawing.Common dependency
- Add System.Drawing.Common explicitly and prevent pruning
- Update other packages
2025-11-24 13:38:18 -05:00
7ace91684c Handle notification icon error until dependency issue gets worked out 2025-11-24 09:54:16 -05:00
c186de0394 Set environment variables 2025-11-22 13:45:45 -05:00
41d0b055dd Add .NET 10 to build image 2025-11-22 13:01:56 -05:00
2d32d740a4 Improve Fever performance 2025-11-22 12:07:05 -05:00
5e7fa4a2e0 Update project 2025-11-15 22:37:45 -05:00
66ea567eaa Start cleaning up account types 2025-11-15 15:30:23 -05:00
6bae35a255 Start adding Miniflux support plus other cleanup
- Modernize old async code
- Update to .NET 10
- Adjust namespace
- Bypass update check when debugging
2025-11-13 10:33:56 -05:00
cdd22c6632 Fix migration 2025-09-25 17:37:50 -04:00
b68f794e7d Try prerelease with a different environment 2025-09-25 16:11:29 -04:00
4291d301cc Try to fix prerelease tagging 2025-09-25 15:54:45 -04:00
af88692aab Fix artifact 2025-09-25 15:37:58 -04:00
3b1303c7d4 Add installer directory reference 2025-09-25 15:22:01 -04:00
ce1fed6636 Fix publish warning and build restore 2025-09-25 15:18:47 -04:00
40f7dd0fa4 Fix version number 2025-09-25 15:09:54 -04:00
00c2cb7227 Update build steps 2025-09-25 15:06:09 -04:00
4d7d7c041a Update to .NET 9 2025-09-25 14:37:42 -04:00
636c7f9b2a Merge branch 'main' into prerelease
# Conflicts:
#	Application/FeedCenter.csproj
#	Application/Properties/Resources.resx
2025-09-25 12:53:39 -04:00
41029671dc Update installer to self-contained 2025-09-25 12:34:36 -04:00
4e721efa55 Start adding server support 2025-09-24 21:08:59 -04:00
f3145d8811 Add prerelease option 2025-09-24 18:29:32 -04:00
9e2e7aabe8 Fix hang when moving window during refresh 2024-12-08 21:51:40 -05:00
7ee84f079b Revert attempts 2024-06-25 21:36:13 -04:00
8f70003bef Try previous image? 2024-06-25 21:29:33 -04:00
38f093dc5c Increase logging 2024-06-25 21:26:44 -04:00
e5bdc80d10 Try fixing build 2024-06-25 20:46:00 -04:00
260268194a Handle a few more errors and update a few packages 2024-06-18 20:34:22 -04:00
8ecde89be0 Crash fixes 2023-07-14 17:01:52 -04:00
845e80577c Undo previous change and set status to "success" if feed read but wasn't modified 2023-07-11 17:42:29 -04:00
31a04b13e6 Exclude "not modified" from errors 2023-07-11 17:35:03 -04:00
64d893ae0f Update HTTP request headers 2023-06-22 17:08:20 -04:00
0ddd38e3f2 Add support for per-feed user agent 2023-06-15 17:28:38 -04:00
f5f78c8825 Remove extra restore now that config is fixed 2023-04-29 19:06:58 -04:00
477185341e Branch and config updates 2023-04-29 19:03:25 -04:00
1db13fbe2e Update installer to detect .NET runtime 2023-04-29 18:42:53 -04:00
c64ec21bd9 Adjust feed read exception handling 2023-04-28 14:44:01 -04:00
45009a31b9 ID doesn't work in a release build for some reason 2023-04-28 13:02:18 -04:00
2a7ef356bd Set tray icon ID 2023-04-28 12:50:49 -04:00
105b6e7d30 Update package versions 2023-04-28 12:47:40 -04:00
965f753489 Update packages 2023-04-28 08:00:17 -04:00
019ebd46be Try restoring the application as well 2023-04-27 21:35:49 -04:00
33a3aac48c Try workaround for AppVeyor 2023-04-27 21:23:33 -04:00
93a839f7f3 Add back restore step 2023-04-27 20:56:36 -04:00
4fc28ed0c1 Update build/deploy configuration 2023-04-27 20:51:18 -04:00
9e10ac0ab9 Update minor version 2023-04-26 22:19:25 -04:00
ac7c20e6bf Fix toolbar button handling 2023-04-26 22:14:49 -04:00
7638d9c2c7 More UI updates and cleanup 2023-04-26 21:57:19 -04:00
edd01cc052 Keep tray icon lock menu option in sync 2023-04-24 21:30:56 -04:00
fb777f4837 Add localization file to tweak text 2023-04-24 21:04:11 -04:00
fcc6cc05e9 Fix icon resource 2023-04-24 20:20:43 -04:00
504cc80470 Back to WiX 3.11 for now 2023-04-24 19:04:26 -04:00
586a0497d6 Remove WinForms references and start on installer 2023-04-22 09:05:41 -04:00
d6a2fd5a46 More UI updates 2023-04-16 12:57:17 -04:00
5c0c84a068 Rework much of options UI 2023-04-13 20:05:55 -04:00
ace251fd4f Validation WIP 2023-04-12 17:14:12 -04:00
68aec56824 UI rework and fixes 2023-04-12 17:13:43 -04:00
242663c3e5 More cleanup and UI work 2023-04-12 13:44:15 -04:00
a81cf5e69f More cleanup and fixes 2023-04-12 11:35:19 -04:00
64d0f770ca Code cleanup 2023-04-07 22:24:52 -04:00
96d327270f Some cleanup 2023-04-07 22:10:03 -04:00
49842a1663 Fix resource reference and switch to package 2023-04-07 20:23:24 -04:00
e130be35a4 Update packages 2023-04-07 20:00:13 -04:00
eac0313f23 Switch to NuGet packages 2023-04-07 18:41:18 -04:00
6514f23329 Rework database loading/migration 2023-04-06 17:20:38 -04:00
b5f570688d More modernization
- Split generic "common" libraries into specific libraries
- Use other packages in lieu of custom code
- General cleanup
2023-04-05 16:06:38 -04:00
f480a6c373 Start modernization 2023-03-10 12:18:03 -05:00
a0214b98f1 Fix parsing of Atom feeds with attribute namespaces 2018-07-18 08:16:50 -04:00
59bb8ae3cd Make sure to dispose resources created during feed reading 2018-07-17 12:02:53 -04:00
840bd1acd0 Remove post-build steps 2018-04-06 13:10:58 -04:00
ed1d5566f2 Update Common version 2018-04-06 10:29:31 -04:00
be1bd39974 Add JSON binary to installer 2018-04-06 10:13:54 -04:00
92469c15aa Switch to getting updates from GitHub 2018-04-06 09:57:00 -04:00
0826ecfce3 Try to fix line endings on SQL scripts 2018-04-05 16:39:36 -04:00
138 changed files with 7600 additions and 8082 deletions

1
.gitattributes vendored
View File

@@ -2,6 +2,7 @@
# Set default behavior to automatically normalize line endings. # Set default behavior to automatically normalize line endings.
############################################################################### ###############################################################################
* text=auto * text=auto
*.sqlce text eol=crlf
############################################################################### ###############################################################################
# Set default behavior for command prompt diff. # Set default behavior for command prompt diff.

6
.gitmodules vendored
View File

@@ -1,6 +0,0 @@
[submodule "Common.Wpf"]
path = Common.Wpf
url = https://github.com/ckaczor/Common.Wpf.git
[submodule "Common"]
path = Common
url = https://github.com/ckaczor/Common.git

View File

@@ -0,0 +1,188 @@
using FeedCenter.Feeds;
using Realms;
using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
namespace FeedCenter.Accounts;
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;
}
private IAccountReader _accountReader;
private IAccountReader GetAccountReader()
{
_accountReader ??= AccountReaderFactory.CreateAccountReader(this);
return _accountReader;
}
[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 => GetAccountReader().SupportsFeedEdit;
public bool SupportsFeedDelete => GetAccountReader().SupportsFeedDelete;
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<DataErrorsChangedEventArgs> 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 async Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var progressSteps = await GetAccountReader().GetProgressSteps(accountReadInput);
return progressSteps;
}
public async Task<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 = await GetAccountReader().Read(accountReadInput);
return accountReadResult;
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace FeedCenter.Accounts;
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));
}

View File

@@ -0,0 +1,8 @@
namespace FeedCenter.Accounts;
public enum AccountReadResult
{
Success,
NotDue,
NotEnabled
}

View File

@@ -0,0 +1,16 @@
using System;
namespace FeedCenter.Accounts;
internal static class AccountReaderFactory
{
internal static IAccountReader CreateAccountReader(Account account) =>
account.Type switch
{
AccountType.Miniflux => new MinifluxReader(account),
AccountType.Local => new LocalReader(account),
AccountType.Fever => new FeverReader(account),
AccountType.GoogleReader => new GoogleReaderReader(account),
_ => throw new NotSupportedException($"Account type '{account.Type}' is not supported."),
};
}

View File

@@ -0,0 +1,9 @@
namespace FeedCenter.Accounts;
public enum AccountType
{
Local,
Fever,
GoogleReader,
Miniflux
}

View File

@@ -0,0 +1,175 @@
using ChrisKaczor.FeverClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using FeedCenter.Feeds;
namespace FeedCenter.Accounts;
using FeverFeedItem = ChrisKaczor.FeverClient.Models.FeedItem;
internal class FeverReader(Account account) : IAccountReader
{
public async Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
var feverClient = new FeverClient(account.Url, apiKey);
var feeds = await feverClient.GetFeeds();
return feeds.Count() * 2 + 5;
}
public async Task<AccountReadResult> Read(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 = (await feverClient.GetFeeds()).ToList();
accountReadInput.IncrementProgress();
var allFeverFeedItems = await GetAllFeverFeedItems(feverClient);
accountReadInput.IncrementProgress();
var existingFeedsByRemoteId = accountReadInput.Entities.Feeds.Where(f => f.Account.Id == account.Id) .ToDictionary(f => f.RemoteId);
var transaction = accountReadInput.Entities.BeginTransaction();
foreach (var feverFeed in feverFeeds)
{
var feed = existingFeedsByRemoteId.GetValueOrDefault(feverFeed.Id.ToString(), null);
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.GetValueOrDefault(feverFeed.Id, []);
var existingFeedItemsByRemoteId = feed.Items.ToDictionary(fi => fi.RemoteId);
var sequence = 1;
foreach (var feverFeedItem in feverFeedItems)
{
var feedItem = existingFeedItemsByRemoteId.GetValueOrDefault(feverFeedItem.Id.ToString(), null);
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;
await transaction.CommitAsync();
accountReadInput.IncrementProgress();
return AccountReadResult.Success;
}
private static async Task<Dictionary<int, List<FeverFeedItem>>> GetAllFeverFeedItems(FeverClient feverClient)
{
var allFeverFeedItems = new List<FeverFeedItem>();
await foreach (var page in feverClient.GetAllFeedItems())
{
allFeverFeedItems.AddRange(page);
}
var grouped = allFeverFeedItems.OrderByDescending(fi => fi.CreatedOnTime).GroupBy(fi => fi.FeedId);
return grouped.ToDictionary(g => g.Key, g => g.ToList());
}
public async Task MarkFeedItemRead(string feedItemId)
{
var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
var feverClient = new FeverClient(account.Url, apiKey);
await feverClient.MarkFeedItemAsRead(int.Parse(feedItemId));
}
public bool SupportsFeedDelete => false;
public bool SupportsFeedEdit => false;
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();
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Threading.Tasks;
namespace FeedCenter.Accounts;
internal class GoogleReaderReader(Account account) : IAccountReader
{
public Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
return Task.FromResult(7);
}
public async Task<AccountReadResult> Read(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;
await transaction.CommitAsync();
accountReadInput.IncrementProgress();
return AccountReadResult.Success;
}
public Task MarkFeedItemRead(string feedItemId)
{
//var apiKey = account.Authenticate ? GetApiKey(account) : string.Empty;
//var feverClient = new FeverClient.FeverClient(account.Url, apiKey);
//await feverClient.MarkFeedItemAsRead(int.Parse(feedItemId));
return Task.CompletedTask;
}
public bool SupportsFeedDelete => false;
public bool SupportsFeedEdit => false;
//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();
//}
}

View File

@@ -0,0 +1,12 @@
using System.Threading.Tasks;
namespace FeedCenter.Accounts;
public interface IAccountReader
{
public Task<int> GetProgressSteps(AccountReadInput accountReadInput);
public Task<AccountReadResult> Read(AccountReadInput accountReadInput);
public Task MarkFeedItemRead(string feedItemId);
public bool SupportsFeedDelete { get; }
public bool SupportsFeedEdit { get; }
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FeedCenter.Feeds;
namespace FeedCenter.Accounts;
public class LocalReader(Account account) : IAccountReader
{
public Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var enabledFeedCount = accountReadInput.Entities.Feeds.Count(f => f.Account.Type == AccountType.Local && f.Enabled);
return Task.FromResult(enabledFeedCount);
}
public Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
var checkTime = DateTimeOffset.UtcNow;
// Create the list of feeds to read
var feedsToRead = new List<Feed>();
// If we have a single feed then add it to the list - otherwise add them all
if (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 Task.FromResult(AccountReadResult.Success);
}
public Task MarkFeedItemRead(string feedItemId)
{
throw new NotImplementedException();
}
public bool SupportsFeedDelete => true;
public bool SupportsFeedEdit => true;
}

View File

@@ -0,0 +1,151 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using ChrisKaczor.MinifluxClient;
using FeedCenter.Feeds;
namespace FeedCenter.Accounts;
internal class MinifluxReader(Account account) : IAccountReader
{
public async Task<int> GetProgressSteps(AccountReadInput accountReadInput)
{
var minifluxClient = new MinifluxClient(account.Url, account.Password);
int feedCount;
if (accountReadInput.FeedId.HasValue)
{
feedCount = 1;
}
else
{
var feeds = await minifluxClient.GetFeeds();
feedCount = feeds.Count();
}
return feedCount * 2 + 4;
}
public async Task<AccountReadResult> Read(AccountReadInput accountReadInput)
{
var checkTime = DateTimeOffset.UtcNow;
var minifluxClient = new MinifluxClient(account.Url, account.Password);
accountReadInput.IncrementProgress();
var localFeeds = accountReadInput.Entities.Feeds.ToList();
var remoteFeeds = (await minifluxClient.GetFeeds()).ToList();
if (accountReadInput.FeedId.HasValue)
{
localFeeds = localFeeds.Where(f => f.Id == accountReadInput.FeedId.Value).ToList();
remoteFeeds = remoteFeeds.Where(rf => rf.Id.ToString() == localFeeds.First().RemoteId).ToList();
}
accountReadInput.IncrementProgress();
var transaction = accountReadInput.Entities.BeginTransaction();
foreach (var remoteFeed in remoteFeeds)
{
var feed = accountReadInput.Entities.Feeds.FirstOrDefault(f => f.RemoteId == remoteFeed.Id.ToString() && f.Account.Id == account.Id);
if (feed == null)
{
feed = new Feed
{
Id = Guid.NewGuid(),
RemoteId = remoteFeed.Id.ToString(),
Title = remoteFeed.Title,
Source = remoteFeed.FeedUrl,
Link = remoteFeed.SiteUrl,
Account = account,
Name = remoteFeed.Title,
CategoryId = accountReadInput.Entities.DefaultCategory.Id,
Enabled = true,
CheckInterval = 0,
};
accountReadInput.Entities.Feeds.Add(feed);
}
feed.Name = remoteFeed.Title;
feed.Title = remoteFeed.Title;
feed.Link = remoteFeed.SiteUrl;
feed.Source = remoteFeed.FeedUrl;
feed.LastReadResult = FeedReadResult.Success;
feed.LastChecked = checkTime;
accountReadInput.IncrementProgress();
var sequence = 1;
var remoteFeedItems = (await minifluxClient.GetFeedEntries(remoteFeed.Id, SortField.PublishedAt, SortDirection.Descending)).ToList();
foreach (var remoteFeedItem in remoteFeedItems)
{
var feedItem = feed.Items.FirstOrDefault(f => f.RemoteId == remoteFeedItem.Id.ToString());
if (feedItem == null)
{
feedItem = new FeedItem
{
Id = Guid.NewGuid(),
RemoteId = remoteFeedItem.Id.ToString(),
Title = remoteFeedItem.Title,
Link = remoteFeedItem.Url,
Description = remoteFeedItem.Content,
BeenRead = remoteFeedItem.Status == "read",
FeedId = feed.Id,
Guid = Guid.NewGuid().ToString(),
Sequence = sequence++,
};
feed.Items.Add(feedItem);
}
feedItem.LastFound = checkTime;
feedItem.BeenRead = remoteFeedItem.Status == "read";
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 = localFeeds.Where(f => f.Account.Id == account.Id && f.LastChecked != checkTime).ToList();
foreach (var feedNotSeen in feedsNotSeen)
{
accountReadInput.Entities.Feeds.Remove(feedNotSeen);
}
account.LastChecked = checkTime;
await transaction.CommitAsync();
accountReadInput.IncrementProgress();
return AccountReadResult.Success;
}
public async Task MarkFeedItemRead(string feedItemId)
{
var minifluxClient = new MinifluxClient(account.Url, account.Password);
await minifluxClient.MarkFeedEntries([long.Parse(feedItemId)], FeedEntryStatus.Read);
}
public bool SupportsFeedDelete => true;
public bool SupportsFeedEdit => true;
}

View File

@@ -4,7 +4,7 @@
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Common.Wpf;component/Toolbar/SplitButton/SplitButtonStyle.xaml" /> <ResourceDictionary Source="pack://application:,,,/ChrisKaczor.Wpf.Controls.Toolbar;component/SplitButton/SplitButtonStyle.xaml" />
<ResourceDictionary Source="Style.xaml" /> <ResourceDictionary Source="Style.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -1,15 +1,15 @@
using Common.Debug; using ChrisKaczor.GenericSettingsProvider;
using Common.Helpers; using ChrisKaczor.Wpf.Application;
using Common.IO; using FeedCenter.Data;
using Common.Settings;
using Common.Wpf.Extensions;
using FeedCenter.Properties; using FeedCenter.Properties;
using Serilog;
using System; using System;
using System.Diagnostics; using System.IO;
using System.Globalization; using System.Linq;
using System.Windows.Threading;
namespace FeedCenter;
namespace FeedCenter
{
public partial class App public partial class App
{ {
// ReSharper disable ConvertPropertyToExpressionBody // ReSharper disable ConvertPropertyToExpressionBody
@@ -26,6 +26,8 @@ namespace FeedCenter
} }
// ReSharper restore ConvertPropertyToExpressionBody // ReSharper restore ConvertPropertyToExpressionBody
public static string Name => FeedCenter.Properties.Resources.ApplicationName;
[STAThread] [STAThread]
public static void Main() public static void Main()
{ {
@@ -33,39 +35,54 @@ namespace FeedCenter
var app = new App(); var app = new App();
app.InitializeComponent(); app.InitializeComponent();
// Create an isolation handle to see if we are already running // Create a single instance handle to see if we are already running
var isolationHandle = ApplicationIsolation.GetIsolationHandle(FeedCenter.Properties.Resources.ApplicationName); var isolationHandle = SingleInstance.GetSingleInstanceHandleAsync(Name).GetAwaiter().GetResult();
// If there is another copy then pass it the command line and exit // If there is another copy then pass it the command line and exit
if (isolationHandle == null) if (isolationHandle == null)
{
InterprocessMessageSender.SendMessage(FeedCenter.Properties.Resources.ApplicationName, Environment.CommandLine);
return; return;
}
// Use the handle over the lifetime of the application // Use the handle over the lifetime of the application
using (isolationHandle) using (isolationHandle)
{ {
// Set the data directory based on debug or not // Set the path
AppDomain.CurrentDomain.SetData("DataDirectory", SystemConfiguration.DataDirectory); LegacyDatabase.DatabasePath = SystemConfiguration.DataDirectory;
LegacyDatabase.DatabaseFile = Path.Combine(SystemConfiguration.DataDirectory,
Settings.Default.DatabaseFile_Legacy);
Database.DatabasePath = SystemConfiguration.DataDirectory;
Database.DatabaseFile = Path.Combine(SystemConfiguration.DataDirectory, Settings.Default.DatabaseFile);
// Get the generic provider // Get the generic provider
var genericProvider = (GenericSettingsProvider) Settings.Default.Providers[typeof(GenericSettingsProvider).Name]; var genericProvider =
(GenericSettingsProvider) Settings.Default.Providers[nameof(GenericSettingsProvider)];
if (genericProvider == null) if (genericProvider == null)
return; return;
// Set the callbacks into the provider // Set the callbacks into the provider
genericProvider.OpenDataStore = SettingsStore.OpenDataStore; genericProvider.OpenDataStore = SettingsStore.OpenDataStore;
genericProvider.CloseDataStore = SettingsStore.CloseDataStore;
genericProvider.GetSettingValue = SettingsStore.GetSettingValue; genericProvider.GetSettingValue = SettingsStore.GetSettingValue;
genericProvider.SetSettingValue = SettingsStore.SetSettingValue; genericProvider.SetSettingValue = SettingsStore.SetSettingValue;
genericProvider.DeleteSettingsForVersion = SettingsStore.DeleteSettingsForVersion;
genericProvider.GetVersionList = SettingsStore.GetVersionList;
genericProvider.DeleteOldVersionsOnUpgrade = !IsDebugBuild;
// Initialize the tracer with the current process ID Log.Logger = new LoggerConfiguration()
Tracer.Initialize(SystemConfiguration.UserSettingsPath, FeedCenter.Properties.Resources.ApplicationName, Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture), false); .Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.File(
Path.Join(SystemConfiguration.UserSettingsPath,
$"{FeedCenter.Properties.Resources.ApplicationName}_.txt"),
rollingInterval: RollingInterval.Day, retainedFileCountLimit: 5,
outputTemplate: "[{Timestamp:u} - {ThreadId} - {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
Log.Logger.Information("---");
Log.Logger.Information("Application started");
Log.Logger.Information("Command line arguments:");
foreach (var arg in Environment.GetCommandLineArgs()
.Select((value, index) => (Value: value, Index: index)))
Log.Logger.Information("\tArg {0}: {1}", arg.Index, arg.Value);
Current.DispatcherUnhandledException += HandleCurrentDispatcherUnhandledException; Current.DispatcherUnhandledException += HandleCurrentDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += HandleCurrentDomainUnhandledException; AppDomain.CurrentDomain.UnhandledException += HandleCurrentDomainUnhandledException;
@@ -89,27 +106,19 @@ namespace FeedCenter
if (!IsDebugBuild) if (!IsDebugBuild)
Current.SetStartWithWindows(Settings.Default.StartWithWindows); Current.SetStartWithWindows(Settings.Default.StartWithWindows);
// Initialize the window
mainWindow.Initialize();
// Run the app // Run the app
app.Run(mainWindow); app.Run(mainWindow);
// Terminate the tracer
Tracer.Dispose();
} }
} }
private static void HandleCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) private static void HandleCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
Tracer.WriteException((Exception) e.ExceptionObject); Log.Logger.Error((Exception) e.ExceptionObject, "Exception");
Tracer.Flush();
} }
private static void HandleCurrentDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) private static void HandleCurrentDispatcherUnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{ {
Tracer.WriteException(e.Exception); Log.Logger.Error(e.Exception, "Exception");
Tracer.Flush();
}
} }
} }

View File

@@ -1,65 +0,0 @@
using Common.Debug;
using Common.Internet;
using FeedCenter.Properties;
using System;
using System.Diagnostics;
namespace FeedCenter
{
public static class BrowserCommon
{
public static Browser FindBrowser(string browserKey)
{
Browser browser = null;
// Get the list of installed browsers
var browsers = Browser.DetectInstalledBrowsers();
// Make sure the desired browser exists
if (browsers.ContainsKey(browserKey))
{
// Get the browser
browser = browsers[browserKey];
}
return browser;
}
public static bool OpenLink(string url)
{
// Get the browser
Browser browser = FindBrowser(Settings.Default.Browser);
// Start the browser
return OpenLink(browser, url);
}
public static bool OpenLink(Browser browser, string url)
{
try
{
// Don't bother with empty links
if (String.IsNullOrEmpty(url))
return true;
// Add quotes around the URL for safety
url = $"\"{url}\"";
// Start the browser
if (browser == null)
Process.Start(url);
else
Process.Start(browser.Command, url);
return true;
}
catch (Exception exception)
{
// Just log the exception
Tracer.WriteException(exception);
return false;
}
}
}
}

View File

@@ -1,27 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FeedCenter
{
using System;
using System.Collections.Generic;
public partial class Category
{
public Category()
{
this.Feeds = new HashSet<Feed>();
}
public System.Guid ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Feed> Feeds { get; set; }
}
}

View File

@@ -1,223 +1,11 @@
using Common.Debug; using System.IO;
using FeedCenter.Properties;
using System; namespace FeedCenter.Data;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.IO;
using System.Linq;
namespace FeedCenter.Data
{
public static class Database public static class Database
{ {
#region Static database settings public static string DatabaseFile { get; set; }
public static string DatabasePath { get; set; }
public static string DatabasePath; public static bool Exists => File.Exists(DatabaseFile);
#endregion
#region File version
private enum SqlServerCeFileVersion
{
Unknown,
Version20,
Version30,
Version35,
Version40,
}
private static SqlServerCeFileVersion GetFileVersion(string databasePath)
{
// Create a mapping of version numbers to the version enumeration
var versionMapping = new Dictionary<int, SqlServerCeFileVersion>
{
{ 0x73616261, SqlServerCeFileVersion.Version20 },
{ 0x002dd714, SqlServerCeFileVersion.Version30 },
{ 0x00357b9d, SqlServerCeFileVersion.Version35 },
{ 0x003d0900, SqlServerCeFileVersion.Version40 }
};
int signature;
try
{
// Open the database file
using (var stream = new FileStream(databasePath, FileMode.Open, FileAccess.Read))
{
// Read the file using the binary reader
var reader = new BinaryReader(stream);
// Seek to the version signature
stream.Seek(16, SeekOrigin.Begin);
// Read the version signature
signature = reader.ReadInt32();
}
}
catch (Exception exception)
{
Tracer.WriteException(exception);
throw;
}
// If we know about the version number then return the right enumeration - otherwise unknown
return versionMapping.ContainsKey(signature) ? versionMapping[signature] : SqlServerCeFileVersion.Unknown;
}
#endregion
public static bool DatabaseExists => File.Exists(DatabasePath);
public static void CreateDatabase()
{
Tracer.WriteLine("Creating database engine");
// Create the database engine
using (var engine = new SqlCeEngine($"Data Source={DatabasePath}"))
{
Tracer.WriteLine("Creating database");
// Create the database itself
engine.CreateDatabase();
Tracer.WriteLine("Running database script");
// Run the creation script
ExecuteScript(Resources.CreateDatabase);
}
}
private static int GetVersion(SqlCeConnection connection)
{
string versionString;
try
{
// Check the database version table
using (var command = new SqlCeCommand("SELECT Value FROM DatabaseVersion", connection))
versionString = command.ExecuteScalar().ToString();
}
catch (SqlCeException)
{
// Check the setting table for the version
using (var command = new SqlCeCommand("SELECT Value FROM Setting WHERE Name = 'DatabaseVersion'", connection))
versionString = command.ExecuteScalar().ToString();
}
if (string.IsNullOrEmpty(versionString))
versionString = "0";
Tracer.WriteLine("Database version: {0}", versionString);
return int.Parse(versionString);
}
public static void UpdateDatabase()
{
Tracer.WriteLine("Getting database file version");
// Get the database file version
var fileVersion = GetFileVersion(DatabasePath);
Tracer.WriteLine("Database file version: {0}", fileVersion);
// See if we need to upgrade the database file version
if (fileVersion != SqlServerCeFileVersion.Version40)
{
Tracer.WriteLine("Creating database engine");
// Create the database engine
using (var engine = new SqlCeEngine($"Data Source={DatabasePath}"))
{
Tracer.WriteLine("Upgrading database");
// Upgrade the database (if needed)
engine.Upgrade();
}
}
Tracer.WriteLine("Getting database version");
// Create a database connection
using (var connection = new SqlCeConnection($"Data Source={DatabasePath}"))
{
// Open the connection
connection.Open();
// Get the database version
var databaseVersion = GetVersion(connection);
// Create a dictionary of database upgrade scripts and their version numbers
var scriptList = new Dictionary<int, string>();
// Loop over the properties of the resource object looking for update scripts
foreach (var property in typeof(Resources).GetProperties().Where(property => property.Name.StartsWith("DatabaseUpdate")))
{
// Get the name of the property
var propertyName = property.Name;
// Extract the version from the name
var version = int.Parse(propertyName.Substring(propertyName.IndexOf("_", StringComparison.Ordinal) + 1));
// Add to the script list
scriptList[version] = propertyName;
}
// Loop over the scripts ordered by version
foreach (var pair in scriptList.OrderBy(pair => pair.Key))
{
// If the database version is less than or equal to the script version the script needs to run
if (databaseVersion <= pair.Key)
{
// Get the script text
var scriptText = Resources.ResourceManager.GetString(pair.Value);
// Run the script
ExecuteScript(scriptText);
}
}
}
}
public static void MaintainDatabase()
{
Tracer.WriteLine("Creating database engine");
// Create the database engine
using (var engine = new SqlCeEngine($"Data Source={DatabasePath}"))
{
Tracer.WriteLine("Shrinking database");
// Compact the database
engine.Shrink();
}
}
private static void ExecuteScript(string scriptText)
{
// Create a database connection
using (var connection = new SqlCeConnection($"Data Source={DatabasePath}"))
{
// Open the connection
connection.Open();
// Setup the delimiters
var delimiters = new[] { "\r\nGO\r\n" };
// Split the script at the delimiters
var statements = scriptText.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
// Loop over each statement in the script
foreach (var statement in statements)
{
// Execute the statement
using (var command = new SqlCeCommand(statement, connection))
command.ExecuteNonQuery();
}
}
}
}
} }

View File

@@ -1,13 +0,0 @@
using System.Data.SqlTypes;
namespace FeedCenter.Data
{
public static class Extensions
{
#region SqlDateTime
public static SqlDateTime SqlDateTimeZero = new SqlDateTime(0, 0);
#endregion
}
}

View File

@@ -0,0 +1,235 @@
using Dapper;
using FeedCenter.Options;
using FeedCenter.Properties;
using Realms;
using Serilog;
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.IO;
using System.Linq;
using FeedCenter.Feeds;
namespace FeedCenter.Data;
public static class LegacyDatabase
{
public static string DatabaseFile { get; set; }
public static string DatabasePath { get; set; }
private enum SqlServerCeFileVersion
{
Unknown,
Version20,
Version30,
Version35,
Version40,
}
private static SqlServerCeFileVersion GetFileVersion(string databasePath)
{
// Create a mapping of version numbers to the version enumeration
var versionMapping = new Dictionary<int, SqlServerCeFileVersion>
{
{ 0x73616261, SqlServerCeFileVersion.Version20 },
{ 0x002dd714, SqlServerCeFileVersion.Version30 },
{ 0x00357b9d, SqlServerCeFileVersion.Version35 },
{ 0x003d0900, SqlServerCeFileVersion.Version40 }
};
int signature;
try
{
// Open the database file
using var stream = new FileStream(databasePath, FileMode.Open, FileAccess.Read);
// Read the file using the binary reader
var reader = new BinaryReader(stream);
// Seek to the version signature
stream.Seek(16, SeekOrigin.Begin);
// Read the version signature
signature = reader.ReadInt32();
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception");
throw;
}
// If we know about the version number then return the right enumeration - otherwise unknown
return versionMapping.TryGetValue(signature, out var value) ? value : SqlServerCeFileVersion.Unknown;
}
public static bool Exists => File.Exists(DatabaseFile);
private static int GetVersion(SqlCeConnection connection)
{
string versionString;
try
{
// Check the database version table
using var command = new SqlCeCommand("SELECT Value FROM DatabaseVersion", connection);
versionString = command.ExecuteScalar().ToString();
}
catch (SqlCeException)
{
// Check the setting table for the version
using var command = new SqlCeCommand("SELECT Value FROM Setting WHERE Name = 'DatabaseVersion'", connection);
versionString = command.ExecuteScalar().ToString();
}
if (string.IsNullOrEmpty(versionString))
versionString = "0";
Log.Logger.Information("Database version: {0}", versionString);
return int.Parse(versionString);
}
public static void UpdateDatabase()
{
Log.Logger.Information("Getting database file version");
// Get the database file version
var fileVersion = GetFileVersion(DatabaseFile);
Log.Logger.Information("Database file version: {0}", fileVersion);
// See if we need to upgrade the database file version
if (fileVersion != SqlServerCeFileVersion.Version40)
{
Log.Logger.Information("Creating database engine");
// Create the database engine
using var engine = new SqlCeEngine($"Data Source={DatabaseFile}");
Log.Logger.Information("Upgrading database");
// Upgrade the database (if needed)
engine.Upgrade();
}
Log.Logger.Information("Getting database version");
// Create a database connection
using var connection = new SqlCeConnection($"Data Source={DatabaseFile}");
// Open the connection
connection.Open();
// Get the database version
var databaseVersion = GetVersion(connection);
// Create a dictionary of database upgrade scripts and their version numbers
var scriptList = new Dictionary<int, string>();
// Loop over the properties of the resource object looking for update scripts
foreach (var property in typeof(Resources).GetProperties().Where(property => property.Name.StartsWith("DatabaseUpdate")))
{
// Get the name of the property
var propertyName = property.Name;
// Extract the version from the name
var version = int.Parse(propertyName[(propertyName.IndexOf("_", StringComparison.Ordinal) + 1)..]);
// Add to the script list
scriptList[version] = propertyName;
}
// Loop over the scripts ordered by version
foreach (var pair in scriptList.OrderBy(pair => pair.Key))
{
// If the database version is beyond this script then we can skip it
if (databaseVersion > pair.Key) continue;
// Get the script text
var scriptText = Resources.ResourceManager.GetString(pair.Value);
// Run the script
ExecuteScript(scriptText);
}
}
public static void MaintainDatabase()
{
Log.Logger.Information("Creating database engine");
// Create the database engine
using var engine = new SqlCeEngine($"Data Source={DatabaseFile}");
Log.Logger.Information("Shrinking database");
// Compact the database
engine.Shrink();
}
public static void MigrateDatabase()
{
var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}");
var realm = Realm.GetInstance(realmConfiguration);
if (!File.Exists(DatabaseFile))
return;
using var connection = new SqlCeConnection($"Data Source={DatabaseFile}");
connection.Open();
var settings = connection.Query<Setting>("SELECT * FROM Setting").OrderBy(s => s.Version).ToList();
var categories = connection.Query<Category>("SELECT * FROM Category").ToList();
var feeds = connection.Query<Feed>("SELECT * FROM Feed").ToList();
var feedItems = connection.Query<FeedItem>("SELECT * FROM FeedItem").ToList();
realm.Write(() =>
{
foreach (var category in categories)
{
category.IsDefault = category.Name == Category.DefaultName;
}
foreach (var feed in feeds)
{
feed.CategoryId = categories.First(c => c.Id == feed.CategoryId).Id;
}
foreach (var feedItem in feedItems)
{
var feed = feeds.First(f => f.Id == feedItem.FeedId);
feed.Items.Add(feedItem);
}
realm.Add(feeds);
realm.Add(categories);
realm.Add(settings, true);
});
connection.Close();
File.Move(DatabaseFile, DatabaseFile + "_bak");
}
private static void ExecuteScript(string scriptText)
{
// Create a database connection
using var connection = new SqlCeConnection($"Data Source={DatabaseFile}");
// Open the connection
connection.Open();
// Setup the delimiters
var delimiters = new[] { "\r\nGO\r\n" };
// Split the script at the delimiters
var statements = scriptText.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
// Loop over each statement in the script
foreach (var statement in statements)
{
// Execute the statement
using var command = new SqlCeCommand(statement, connection);
command.ExecuteNonQuery();
}
}
}

View File

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

Binary file not shown.

View File

@@ -1,139 +1,120 @@
using System.Collections.ObjectModel; using FeedCenter.Data;
using System.ComponentModel; using FeedCenter.Options;
using System.Data.Entity.Infrastructure; using Realms;
using System;
using System.Linq; using System.Linq;
using FeedCenter.Accounts;
using FeedCenter.Feeds;
namespace FeedCenter namespace FeedCenter;
{
public partial class FeedCenterEntities
{
#region Dispose
protected override void Dispose(bool disposing) public class FeedCenterEntities
{ {
if (disposing) public FeedCenterEntities()
{ {
var manager = ((IObjectContextAdapter) this).ObjectContext.ObjectStateManager; var realmConfiguration = new RealmConfiguration($"{Database.DatabaseFile}")
manager.ObjectStateManagerChanged -= HandleObjectStateManagerObjectStateManagerChanged; {
_hookedStateManager = false; SchemaVersion = 2,
MigrationCallback = (migration, oldSchemaVersion) =>
{
Account localAccount;
if (oldSchemaVersion == 1)
{
localAccount = Account.CreateDefault();
migration.NewRealm.Add(localAccount);
}
else
{
localAccount = migration.NewRealm.All<Account>().First(a => a.Type == AccountType.Local);
} }
base.Dispose(disposing); var newVersionCategories = migration.NewRealm.All<Category>();
}
#endregion foreach (var newVersionCategory in newVersionCategories)
private bool _hookedStateManager;
#region All categories
private ObservableCollection<Category> _allCategories;
public ObservableCollection<Category> AllCategories
{ {
get switch (oldSchemaVersion)
{ {
if (_allCategories == null) case 1:
{ newVersionCategory.Account = localAccount;
_allCategories = new ObservableCollection<Category>(Categories); newVersionCategory.RemoteId = null;
break;
if (!_hookedStateManager)
{
var manager = ((IObjectContextAdapter) this).ObjectContext.ObjectStateManager;
manager.ObjectStateManagerChanged += HandleObjectStateManagerObjectStateManagerChanged;
_hookedStateManager = true;
} }
} }
return _allCategories; var newVersionFeeds = migration.NewRealm.All<Feed>();
foreach (var newVersionFeed in newVersionFeeds)
{
switch (oldSchemaVersion)
{
case 0:
newVersionFeed.UserAgent = null;
break;
case 1:
newVersionFeed.Account = localAccount;
newVersionFeed.RemoteId = null;
break;
} }
} }
var newVersionFeedItems = migration.NewRealm.All<FeedItem>();
foreach (var newVersionFeedItem in newVersionFeedItems)
{
switch (oldSchemaVersion)
{
case 1:
newVersionFeedItem.RemoteId = null;
break;
}
}
}
};
RealmInstance = Realm.GetInstance(realmConfiguration);
Accounts = new RealmObservableCollection<Account>(RealmInstance);
Settings = new RealmObservableCollection<Setting>(RealmInstance);
Feeds = new RealmObservableCollection<Feed>(RealmInstance);
Categories = new RealmObservableCollection<Category>(RealmInstance);
if (!Accounts.Any())
{
RealmInstance.Write(() => Accounts.Add(Account.CreateDefault()));
}
if (!Categories.Any())
{
var localAccount = Accounts.First(a => a.Type == AccountType.Local);
RealmInstance.Write(() => Categories.Add(Category.CreateDefault(localAccount)));
}
}
public RealmObservableCollection<Category> Categories { get; }
public Category DefaultCategory public Category DefaultCategory
{ {
get { return AllCategories.First(c => c.IsDefault); } get { return Categories.First(c => c.IsDefault); }
} }
#endregion public Account LocalAccount
#region All feeds
private ObservableCollection<Feed> _allFeeds;
public ObservableCollection<Feed> AllFeeds
{ {
get get { return Accounts.First(a => a.Type == AccountType.Local); }
}
public RealmObservableCollection<Feed> Feeds { get; }
public RealmObservableCollection<Account> Accounts { get; }
private Realm RealmInstance { get; }
public RealmObservableCollection<Setting> Settings { get; }
public void SaveChanges(Action action)
{ {
if (_allFeeds == null) RealmInstance.Write(action);
}
public Transaction BeginTransaction()
{ {
_allFeeds = new ObservableCollection<Feed>(Feeds); return RealmInstance.BeginWrite();
if (!_hookedStateManager)
{
var manager = ((IObjectContextAdapter) this).ObjectContext.ObjectStateManager;
manager.ObjectStateManagerChanged += HandleObjectStateManagerObjectStateManagerChanged;
_hookedStateManager = true;
}
}
return _allFeeds;
}
}
#endregion
#region Object state manager
void HandleObjectStateManagerObjectStateManagerChanged(object sender, CollectionChangeEventArgs e)
{
var element = e.Element as Category;
if (element != null)
{
if (_allCategories == null)
return;
var category = element;
switch (e.Action)
{
case CollectionChangeAction.Add:
_allCategories.Add(category);
break;
case CollectionChangeAction.Remove:
_allCategories.Remove(category);
break;
case CollectionChangeAction.Refresh:
_allCategories.Clear();
foreach (var loopCategory in Categories)
_allCategories.Add(loopCategory);
break;
}
}
else if (e.Element is Feed)
{
if (_allFeeds == null)
return;
var feed = (Feed) e.Element;
switch (e.Action)
{
case CollectionChangeAction.Add:
_allFeeds.Add(feed);
break;
case CollectionChangeAction.Remove:
_allFeeds.Remove(feed);
break;
case CollectionChangeAction.Refresh:
_allFeeds.Clear();
foreach (var loopfeed in Feeds)
_allFeeds.Add(loopfeed);
break;
}
}
}
#endregion
} }
} }

View File

@@ -1,59 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FeedCenter
{
using System;
using System.Collections.Generic;
public partial class Feed
{
public Feed()
{
this.Name = "";
this.Title = "";
this.Source = "";
this.Link = "";
this.Description = "";
this.LastChecked = new DateTime(599266080000000000, DateTimeKind.Unspecified);
this.CheckInterval = 60;
this.Enabled = true;
this.Authenticate = false;
this.Username = "";
this.Password = "";
this.Domain = "";
this.LastUpdated = new DateTime(599266080000000000, DateTimeKind.Unspecified);
this.Actions = new HashSet<FeedAction>();
this.Items = new HashSet<FeedItem>();
}
public System.Guid ID { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public string Source { get; set; }
public string Link { get; set; }
public string Description { get; set; }
public System.DateTime LastChecked { get; set; }
public int CheckInterval { get; set; }
public bool Enabled { get; set; }
public bool Authenticate { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Domain { get; set; }
public FeedCenter.FeedReadResult LastReadResult { get; set; }
public System.DateTime LastUpdated { get; set; }
public FeedCenter.FeedItemComparison ItemComparison { get; set; }
private System.Guid CategoryID { get; set; }
public FeedCenter.MultipleOpenAction MultipleOpenAction { get; set; }
public virtual Category Category { get; set; }
public virtual ICollection<FeedAction> Actions { get; set; }
public virtual ICollection<FeedItem> Items { get; set; }
}
}

View File

@@ -1,26 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FeedCenter
{
using System;
using System.Collections.Generic;
public partial class FeedAction
{
public System.Guid ID { get; set; }
public System.Guid FeedID { get; set; }
public int Field { get; set; }
public string Search { get; set; }
public string Replace { get; set; }
public int Sequence { get; set; }
public virtual Feed Feed { get; set; }
}
}

View File

@@ -1,473 +1,110 @@
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk">
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <TargetFramework>net10.0-windows8.0</TargetFramework>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{BD3D12F2-DE23-4466-83B1-1EB617A877A4}</ProjectGuid>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RootNamespace>FeedCenter</RootNamespace> <UseWindowsForms>false</UseWindowsForms>
<AssemblyName>FeedCenter</AssemblyName> <UseWPF>true</UseWPF>
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> <ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
<TargetFrameworkProfile> <RestoreEnablePackagePruning>false</RestoreEnablePackagePruning>
</TargetFrameworkProfile>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<IsWebBootstrapper>false</IsWebBootstrapper>
<SccProjectName>
</SccProjectName>
<SccLocalPath>
</SccLocalPath>
<SccAuxPath>
</SccAuxPath>
<SccProvider>
</SccProvider>
<PublishUrl>\\server\d\FeedCenter\</PublishUrl>
<Install>true</Install>
<InstallFrom>Unc</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ProductName>Feed Center</ProductName>
<PublisherName>Feed Center</PublisherName>
<CreateWebPageOnPublish>true</CreateWebPageOnPublish>
<WebPage>Publish.html</WebPage>
<OpenBrowserOnPublish>false</OpenBrowserOnPublish>
<ApplicationRevision>215</ApplicationRevision>
<ApplicationVersion>0.1.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<StartupObject> <EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
</StartupObject>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>
</AssemblyOriginatorKeyFile>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\Application.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<TargetZone>LocalIntranet</TargetZone>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<GenerateManifests>false</GenerateManifests> <GenerateManifests>false</GenerateManifests>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<ApplicationManifest>Properties\app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<CodeAnalysisLogFile>bin\Debug\FeedCenter.exe.CodeAnalysisLog.xml</CodeAnalysisLogFile>
<CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression>
<CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSetDirectories>;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets</CodeAnalysisRuleSetDirectories>
<CodeAnalysisIgnoreBuiltInRuleSets>false</CodeAnalysisIgnoreBuiltInRuleSets>
<CodeAnalysisRuleDirectories>;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules</CodeAnalysisRuleDirectories>
<CodeAnalysisIgnoreBuiltInRules>false</CodeAnalysisIgnoreBuiltInRules>
<CodeAnalysisFailOnMissingRules>false</CodeAnalysisFailOnMissingRules>
<UseVSHostingProcess>true</UseVSHostingProcess>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<CodeAnalysisLogFile>bin\Release\FeedCenter.exe.CodeAnalysisLog.xml</CodeAnalysisLogFile>
<CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression>
<CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSetDirectories>;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets</CodeAnalysisRuleSetDirectories>
<CodeAnalysisRuleDirectories>;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules</CodeAnalysisRuleDirectories>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisIgnoreBuiltInRuleSets>false</CodeAnalysisIgnoreBuiltInRuleSets>
<CodeAnalysisIgnoreBuiltInRules>false</CodeAnalysisIgnoreBuiltInRules>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL"> <None Remove="Resources\Application.ico" />
<SpecificVersion>False</SpecificVersion> <None Remove="Resources\Comments-edit.ico" />
<HintPath>..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.dll</HintPath> <None Remove="Resources\Compile.ico" />
</Reference> <None Remove="Resources\Left.ico" />
<Reference Include="EntityFramework.SqlServer"> <None Remove="Resources\News.ico" />
<HintPath>..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.SqlServer.dll</HintPath> <None Remove="Resources\Right.ico" />
</Reference> <None Remove="Resources\Rss-Download.ico" />
<Reference Include="EntityFramework.SqlServerCompact"> <None Remove="Resources\Warning.ico" />
<HintPath>..\packages\EntityFramework.SqlServerCompact.6.1.1\lib\net45\EntityFramework.SqlServerCompact.dll</HintPath>
</Reference>
<Reference Include="HtmlAgilityPack, Version=1.4.9.0, Culture=neutral, PublicKeyToken=bd319b19eaf3b43a, processorArchitecture=MSIL">
<HintPath>..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NameBasedGrid, Version=0.10.1.0, Culture=neutral, PublicKeyToken=a434c4ad23d0fd33, processorArchitecture=MSIL">
<HintPath>..\packages\NameBasedGrid.0.10.1\lib\net40\NameBasedGrid.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<Private>True</Private>
<HintPath>..\packages\Microsoft.SqlServer.Compact.4.0.8854.1\lib\net40\System.Data.SqlServerCe.dll</HintPath>
</Reference>
<Reference Include="System.Data.SqlServerCe.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<Private>True</Private>
<HintPath>..\packages\Microsoft.SqlServer.Compact.4.0.8854.1\lib\net40\System.Data.SqlServerCe.Entity.dll</HintPath>
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Core" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FeedChooserWindow.xaml.cs"> <PackageReference Include="ChrisKaczor.ApplicationUpdate" Version="1.0.7" />
<DependentUpon>FeedChooserWindow.xaml</DependentUpon> <PackageReference Include="ChrisKaczor.FeverClient" Version="1.0.3" />
</Compile> <PackageReference Include="ChrisKaczor.GenericSettingsProvider" Version="1.0.4" />
<Compile Include="MainWindow\CategoryList.cs" /> <PackageReference Include="ChrisKaczor.InstalledBrowsers" Version="1.0.4" />
<Compile Include="MainWindow\CommandLine.cs" /> <PackageReference Include="ChrisKaczor.MinifluxClient" Version="1.0.3" />
<Compile Include="MainWindow\DragDrop.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Application.SingleInstance" Version="1.0.5" />
<Compile Include="MainWindow\FeedCreation.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Application.StartWithWindows" Version="1.0.5" />
<Compile Include="MainWindow\FeedList.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Controls.HtmlTextBlock" Version="1.0.6" />
<Compile Include="MainWindow\FeedReading.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Controls.Link" Version="1.0.4" />
<Compile Include="MainWindow\Header.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Controls.Toolbar" Version="1.0.3" />
<Compile Include="MainWindow\Timer.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Validation" Version="1.0.4" />
<Compile Include="MainWindow\Toolbar.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Windows.ControlBox" Version="1.0.3" />
<Compile Include="MainWindow\WindowHandler.cs" /> <PackageReference Include="ChrisKaczor.Wpf.Windows.SnappingWindow" Version="1.0.4" />
<Compile Include="SystemConfiguration.cs" /> <PackageReference Include="Dapper" Version="2.1.66" />
<Compile Include="MainWindow\UpdateHandler.cs" /> <PackageReference Include="DebounceThrottle" Version="3.0.1" />
<Page Include="App.xaml"> <PackageReference Include="H.NotifyIcon.Wpf" Version="2.3.2" />
<Generator>MSBuild:Compile</Generator> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<SubType>Designer</SubType> <PackageReference Include="HtmlTextWriter" Version="3.0.2" />
</Page> <PackageReference Include="MahApps.Metro" Version="2.4.11" />
<Compile Include="BrowserCommon.cs" /> <PackageReference Include="Microsoft.SqlServer.Compact" Version="4.0.8876.1" GeneratePathProperty="true">
<Compile Include="Category.cs"> <NoWarn>NU1701</NoWarn>
<DependentUpon>Model.tt</DependentUpon> </PackageReference>
</Compile> <PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.0" />
<Compile Include="Data\Extensions.cs" /> <PackageReference Include="NameBasedGrid" Version="1.0.0">
<Compile Include="Entities.cs" /> <NoWarn>NU1701</NoWarn>
<Compile Include="Feed.cs"> </PackageReference>
<DependentUpon>Model.tt</DependentUpon> <PackageReference Include="Realm" Version="20.1.0" />
</Compile> <PackageReference Include="Serilog" Version="4.3.0" />
<Compile Include="FeedAction.cs"> <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<DependentUpon>Model.tt</DependentUpon> <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
</Compile> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<Compile Include="FeedErrorWindow.xaml.cs"> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<DependentUpon>FeedErrorWindow.xaml</DependentUpon> <PackageReference Include="System.Drawing.Common" Version="10.0.0" />
</Compile>
<Compile Include="FeedItem.cs">
<DependentUpon>Model.tt</DependentUpon>
</Compile>
<Compile Include="Feeds\Category.cs" />
<Compile Include="Model.Context.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Model.Context.tt</DependentUpon>
</Compile>
<Compile Include="Model.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Model.tt</DependentUpon>
</Compile>
<Compile Include="Model.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Model.edmx</DependentUpon>
</Compile>
<Compile Include="NotificationIcon.cs" />
<Compile Include="Options\AboutOptionsPanel.xaml.cs">
<DependentUpon>AboutOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Options\BulkFeedWindow.xaml.cs">
<DependentUpon>BulkFeedWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Options\CategoryWindow.xaml.cs">
<DependentUpon>CategoryWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Options\DisplayOptionsPanel.xaml.cs">
<DependentUpon>DisplayOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Options\FeedsOptionsPanel.xaml.cs">
<DependentUpon>FeedsOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Options\FeedWindow.xaml.cs">
<DependentUpon>FeedWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Options\GeneralOptionsPanel.xaml.cs">
<DependentUpon>GeneralOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Options\Options.cs" />
<Compile Include="Options\OptionsPanelBase.cs" />
<Compile Include="Options\OptionsWindow.xaml.cs">
<DependentUpon>OptionsWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Options\UpdateOptionsPanel.xaml.cs">
<DependentUpon>UpdateOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Setting.cs">
<DependentUpon>Model.tt</DependentUpon>
</Compile>
<Compile Include="SettingsStore.cs" />
<Compile Include="SplashWindow.xaml.cs">
<DependentUpon>SplashWindow.xaml</DependentUpon>
</Compile>
<Page Include="FeedChooserWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="FeedErrorWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="MainWindow\MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="Data\Database.cs" />
<Compile Include="FeedParsers\AtomParser.cs" />
<Compile Include="FeedParsers\FeedParserBase.cs" />
<Compile Include="FeedParsers\RdfParser.cs" />
<Compile Include="FeedParsers\RssParser.cs" />
<Compile Include="Feeds\Feed.cs" />
<Compile Include="Feeds\FeedAction.cs" />
<Compile Include="Feeds\FeedItem.cs" />
<Compile Include="MainWindow\MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="Options\AboutOptionsPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\BulkFeedWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\CategoryWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\DisplayOptionsPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\FeedsOptionsPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\FeedWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\GeneralOptionsPanel.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Options\OptionsWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Options\UpdateOptionsPanel.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="SplashWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Style.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<None Include="app.config">
<SubType>Designer</SubType>
</None>
<EntityDeploy Include="Model.edmx">
<Generator>EntityModelCodeGenerator</Generator>
<LastGenOutput>Model.Designer.cs</LastGenOutput>
</EntityDeploy>
<None Include="Model.Context.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>Model.Context.cs</LastGenOutput>
<DependentUpon>Model.edmx</DependentUpon>
</None>
<None Include="Model.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>Model.cs</LastGenOutput>
<DependentUpon>Model.edmx</DependentUpon>
</None>
<None Include="packages.config" />
<None Include="Properties\app.manifest" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<AppDesigner Include="Properties\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Resource Include="Resources\Application.ico" /> <Resource Include="Resources\Application.ico" />
</ItemGroup>
<ItemGroup>
<None Include="Scripts\DatabaseUpdate_6.sqlce" />
<None Include="Scripts\DatabaseUpdate_5.sqlce" />
<None Include="Scripts\DatabaseUpdate_4.sqlce" />
<None Include="Scripts\DatabaseUpdate_3.sqlce" />
<None Include="Scripts\DatabaseUpdate_2.sqlce" />
<None Include="Scripts\DatabaseUpdate_1.sqlce" />
<None Include="Scripts\CreateDatabase.sqlce" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Windows.Installer.4.5">
<Visible>False</Visible>
<ProductName>Windows Installer 4.5</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Rss-Download.ico" />
<Resource Include="Resources\Comments-edit.ico" /> <Resource Include="Resources\Comments-edit.ico" />
<Resource Include="Resources\Warning.ico" />
<Resource Include="Resources\News.ico" />
<Resource Include="Resources\Compile.ico" /> <Resource Include="Resources\Compile.ico" />
<Resource Include="Resources\Left.ico" /> <Resource Include="Resources\Left.ico" />
<Resource Include="Resources\News.ico" />
<Resource Include="Resources\Right.ico" /> <Resource Include="Resources\Right.ico" />
<Resource Include="Resources\Rss-Download.ico" />
<Resource Include="Resources\Warning.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" /> <Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Common.Wpf\Common.Native\Common.Native.csproj"> <EmbeddedResource Update="Properties\Resources.resx">
<Project>{ed1c07a1-54f5-4796-8b06-2a0bb1960d84}</Project> <Generator>PublicResXFileCodeGenerator</Generator>
<Name>Common.Native</Name> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
</ProjectReference> </EmbeddedResource>
<ProjectReference Include="..\Common.Wpf\Common.Wpf.csproj">
<Project>{0074c983-550e-4094-9e8c-f566fb669297}</Project>
<Name>Common.Wpf</Name>
</ProjectReference>
<ProjectReference Include="..\Common\Common.csproj">
<Project>{17864d82-457d-4a0a-bc10-1d07f2b3a5d6}</Project>
<Name>Common</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<Target Name="AddSqlServerCompact_x86" AfterTargets="Publish" Condition="'$(RuntimeIdentifier)' == 'win-x86'">
<Exec Command="xcopy /s /y /i &quot;$(PkgMicrosoft_SqlServer_Compact)\NativeBinaries\x86\*.*&quot; &quot;$(PublishDir)x86&quot;" />
</Target>
<Target Name="AddSqlServerCompact_x64" AfterTargets="Publish" Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<Exec Command="xcopy /s /y /i &quot;$(PkgMicrosoft_SqlServer_Compact)\NativeBinaries\amd64\*.*&quot; &quot;$(PublishDir)amd64&quot;" />
</Target>
<PropertyGroup> <PropertyGroup>
<PostBuildEvent> <PostBuildEvent>
if not exist "$(TargetDir)x86" md "$(TargetDir)x86" xcopy /s /y /i "$(PkgMicrosoft_SqlServer_Compact)\NativeBinaries\x86\*.*" "$(TargetDir)x86"
xcopy /s /y "$(SolutionDir)packages\Microsoft.SqlServer.Compact.4.0.8854.1\NativeBinaries\x86\*.*" "$(TargetDir)x86" xcopy /s /y /i "$(PkgMicrosoft_SqlServer_Compact)\NativeBinaries\amd64\*.*" "$(TargetDir)amd64"
if not exist "$(TargetDir)amd64" md "$(TargetDir)amd64" </PostBuildEvent>
xcopy /s /y "$(SolutionDir)packages\Microsoft.SqlServer.Compact.4.0.8854.1\NativeBinaries\amd64\*.*" "$(TargetDir)amd64"</PostBuildEvent> <ApplicationIcon>Resources\Application.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project> </Project>

View File

@@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=Feeds/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=feeds/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mainwindow/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mainwindow/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,49 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11018.127
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeedCenter", "FeedCenter.csproj", "{BD3D12F2-DE23-4466-83B1-1EB617A877A4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{1462AAAD-B01B-4FF6-9B9F-595D239C9D1E}"
ProjectSection(SolutionItems) = preProject
..\.gitignore = ..\.gitignore
..\appveyor.yml = ..\appveyor.yml
..\LICENSE.md = ..\LICENSE.md
..\README.md = ..\README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|Mixed Platforms = Debug|Mixed Platforms
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|Mixed Platforms = Release|Mixed Platforms
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|x64.ActiveCfg = Debug|x64
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|x64.Build.0 = Debug|x64
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|x86.ActiveCfg = Debug|x86
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Debug|x86.Build.0 = Debug|x86
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|Any CPU.Build.0 = Release|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|x64.ActiveCfg = Release|x86
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|x86.ActiveCfg = Release|Any CPU
{BD3D12F2-DE23-4466-83B1-1EB617A877A4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B1D182A0-263B-4AB8-8413-56303DBD4CCC}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kaczor/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Miniflux/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="FeedCenterEntities" Identifier="FeedCenter.FeedCenterEntities" ProviderType="Microsoft.VisualStudio.DataDesign.DataSourceProviders.EntityDataModel.EdmDataSourceProvider" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>FeedCenter.FeedCenterEntities, Model.Designer.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="FeedCenterEntities" Identifier="FeedCenter.FeedCenterEntities" ProviderType="Microsoft.VisualStudio.DataDesign.DataSourceProviders.EntityDataModel.EdmDataSourceProvider" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>FeedCenter.FeedCenterEntities, Model.Designer.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="FeedCenterEntities" Identifier="FeedCenter.FeedCenterEntities" ProviderType="Microsoft.VisualStudio.DataDesign.DataSourceProviders.EntityDataModel.EdmDataSourceProvider" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>FeedCenter.FeedCenterEntities, Model.Designer.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@@ -3,8 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:windows="clr-namespace:Common.Wpf.Windows;assembly=Common.Wpf"
xmlns:my="clr-namespace:FeedCenter.Properties" xmlns:my="clr-namespace:FeedCenter.Properties"
xmlns:controlBox="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.ControlBox"
mc:Ignorable="d" mc:Ignorable="d"
Title="{x:Static my:Resources.FeedChooserWindow}" Title="{x:Static my:Resources.FeedChooserWindow}"
Height="247.297" Height="247.297"
@@ -12,14 +12,26 @@
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Icon="/FeedCenter;component/Resources/Application.ico" Icon="/FeedCenter;component/Resources/Application.ico"
FocusManager.FocusedElement="{Binding ElementName=FeedDataGrid}" FocusManager.FocusedElement="{Binding ElementName=FeedDataGrid}"
windows:ControlBox.HasMaximizeButton="False" controlBox:ControlBox.HasMaximizeButton="False"
windows:ControlBox.HasMinimizeButton="False"> controlBox:ControlBox.HasMinimizeButton="False">
<Grid> <Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="6">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DataGrid AutoGenerateColumns="False" <DataGrid Grid.Row="0"
Grid.Column="0"
AutoGenerateColumns="False"
x:Name="FeedDataGrid" x:Name="FeedDataGrid"
CanUserReorderColumns="False" CanUserReorderColumns="False"
GridLinesVisibility="None" GridLinesVisibility="None"
@@ -27,16 +39,11 @@
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
HeadersVisibility="Column" HeadersVisibility="Column"
Margin="6" BorderThickness="1,1,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Background="{x:Null}" Background="{x:Null}"
CanUserSortColumns="True" CanUserSortColumns="True"
MouseDoubleClick="HandleMouseDoubleClick"> MouseDoubleClick="HandleMouseDoubleClick">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness"
Value="0" />
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="{x:Static my:Resources.FeedNameColumnHeader}" <DataGridTextColumn Header="{x:Static my:Resources.FeedNameColumnHeader}"
Binding="{Binding Item2}" Binding="{Binding Item2}"
@@ -44,22 +51,24 @@
SortDirection="Ascending" /> SortDirection="Ascending" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<StackPanel
Grid.Column="0"
Grid.Row="1"
Orientation="Horizontal"
Margin="0,5,0,0"
HorizontalAlignment="Right">
<Button Content="{x:Static my:Resources.OkayButton}" <Button Content="{x:Static my:Resources.OkayButton}"
Height="23" HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
Margin="0,0,5,0"
IsDefault="True" IsDefault="True"
Width="75" Click="HandleOkayButtonClick" />
Click="HandleOkayButtonClick"
Margin="0,0,90,10"
Grid.Row="1"
VerticalAlignment="Bottom"
HorizontalAlignment="Right" />
<Button Content="{x:Static my:Resources.CancelButton}" <Button Content="{x:Static my:Resources.CancelButton}"
Height="23" HorizontalAlignment="Right"
IsCancel="True"
Width="75"
Margin="0,0,10,10"
Grid.Row="1"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
HorizontalAlignment="Right" /> Width="75"
IsCancel="True" />
</StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
{
public partial class FeedChooserWindow public partial class FeedChooserWindow
{ {
private string _returnLink; private string _returnLink;
@@ -49,4 +49,3 @@ namespace FeedCenter
} }
} }
} }
}

View File

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

View File

@@ -1,27 +1,30 @@
using FeedCenter.Options; using System.ComponentModel;
using System.ComponentModel; using System.Linq;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Input; using System.Windows.Input;
using ChrisKaczor.InstalledBrowsers;
using FeedCenter.Feeds;
using FeedCenter.Options;
using FeedCenter.Properties;
namespace FeedCenter;
namespace FeedCenter
{
public partial class FeedErrorWindow public partial class FeedErrorWindow
{ {
private CollectionViewSource _collectionViewSource;
private readonly FeedCenterEntities _entities = new();
public FeedErrorWindow() public FeedErrorWindow()
{ {
InitializeComponent(); InitializeComponent();
} }
private FeedCenterEntities _database; public void Display(Window owner)
private CollectionViewSource _collectionViewSource;
public bool? Display(Window owner)
{ {
_database = new FeedCenterEntities();
// Create a view and sort it by name // Create a view and sort it by name
_collectionViewSource = new CollectionViewSource { Source = _database.AllFeeds }; _collectionViewSource = new CollectionViewSource { Source = _entities.Feeds };
_collectionViewSource.Filter += HandleCollectionViewSourceFilter; _collectionViewSource.Filter += HandleCollectionViewSourceFilter;
_collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending)); _collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
@@ -33,14 +36,14 @@ namespace FeedCenter
Owner = owner; Owner = owner;
// Show the dialog and result the result // Show the dialog and result the result
return ShowDialog(); ShowDialog();
} }
private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e) private static void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{ {
var feed = (Feed) e.Item; var feed = (Feed) e.Item;
e.Accepted = (feed.LastReadResult != FeedReadResult.Success); e.Accepted = feed.LastReadResult != FeedReadResult.Success;
} }
private void HandleEditFeedButtonClick(object sender, RoutedEventArgs e) private void HandleEditFeedButtonClick(object sender, RoutedEventArgs e)
@@ -60,49 +63,44 @@ namespace FeedCenter
var feed = (Feed) FeedDataGrid.SelectedItem; var feed = (Feed) FeedDataGrid.SelectedItem;
var feedWindow = new FeedWindow(); var feedWindow = new FeedWindow(_entities);
feedWindow.Display(_database, feed, GetWindow(this)); feedWindow.Display(feed, GetWindow(this));
} }
private void DeleteSelectedFeed() private void DeleteSelectedFeed()
{ {
if (MessageBox.Show(this, Properties.Resources.ConfirmDeleteFeed, Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
var feed = (Feed) FeedDataGrid.SelectedItem; var feed = (Feed) FeedDataGrid.SelectedItem;
_database.Feeds.Remove(feed); _entities.SaveChanges(() => _entities.Feeds.Remove(feed));
SetFeedButtonStates(); SetFeedButtonStates();
} }
private void SetFeedButtonStates() private void SetFeedButtonStates()
{ {
EditFeedButton.IsEnabled = (FeedDataGrid.SelectedItem != null); var feed = FeedDataGrid.SelectedItem as Feed;
DeleteFeedButton.IsEnabled = (FeedDataGrid.SelectedItem != null);
RefreshCurrent.IsEnabled = (FeedDataGrid.SelectedItem != null); EditFeedButton.IsEnabled = feed != null;
OpenPage.IsEnabled = (FeedDataGrid.SelectedItem != null); DeleteFeedButton.IsEnabled = feed != null;
OpenFeed.IsEnabled = (FeedDataGrid.SelectedItem != null); RefreshCurrent.IsEnabled = feed != null;
OpenPage.IsEnabled = feed != null && !string.IsNullOrEmpty(feed.Link);
OpenFeed.IsEnabled = FeedDataGrid.SelectedItem != null;
} }
private void HandleOpenPageButtonClick(object sender, RoutedEventArgs e) private void HandleOpenPageButtonClick(object sender, RoutedEventArgs e)
{ {
var feed = (Feed) FeedDataGrid.SelectedItem; var feed = (Feed) FeedDataGrid.SelectedItem;
BrowserCommon.OpenLink(feed.Link); InstalledBrowser.OpenLink(Settings.Default.Browser, feed.Link);
} }
private void HandleOpenFeedButtonClick(object sender, RoutedEventArgs e) private void HandleOpenFeedButtonClick(object sender, RoutedEventArgs e)
{ {
var feed = (Feed) FeedDataGrid.SelectedItem; var feed = (Feed) FeedDataGrid.SelectedItem;
BrowserCommon.OpenLink(feed.Source); InstalledBrowser.OpenLink(Settings.Default.Browser, feed.Source);
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
// Save the actual settings
_database.SaveChanges();
DialogResult = true;
Close();
} }
private async void HandleRefreshCurrentButtonClick(object sender, RoutedEventArgs e) private async void HandleRefreshCurrentButtonClick(object sender, RoutedEventArgs e)
@@ -110,8 +108,16 @@ namespace FeedCenter
IsEnabled = false; IsEnabled = false;
Mouse.OverrideCursor = Cursors.Wait; Mouse.OverrideCursor = Cursors.Wait;
var feed = (Feed) FeedDataGrid.SelectedItem; var feedId = ((Feed) FeedDataGrid.SelectedItem).Id;
await feed.ReadAsync(_database, true);
await Task.Run(() =>
{
var entities = new FeedCenterEntities();
var feed = entities.Feeds.First(f => f.Id == feedId);
entities.SaveChanges(() => feed.Read(true));
});
var selectedIndex = FeedDataGrid.SelectedIndex; var selectedIndex = FeedDataGrid.SelectedIndex;
@@ -127,5 +133,9 @@ namespace FeedCenter
Mouse.OverrideCursor = null; Mouse.OverrideCursor = null;
IsEnabled = true; IsEnabled = true;
} }
private void FeedDataGrid_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
SetFeedButtonStates();
} }
} }

View File

@@ -1,38 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FeedCenter
{
using System;
using System.Collections.Generic;
public partial class FeedItem
{
public FeedItem()
{
this.Title = "";
this.Link = "";
this.Description = "";
this.LastFound = new DateTime(599266080000000000, DateTimeKind.Unspecified);
}
public System.Guid ID { get; set; }
public System.Guid FeedID { get; set; }
public string Title { get; set; }
public string Link { get; set; }
public string Description { get; set; }
public bool BeenRead { get; set; }
public System.DateTime LastFound { get; set; }
public bool New { get; set; }
public string Guid { get; set; }
public int Sequence { get; set; }
public virtual Feed Feed { get; set; }
}
}

View File

@@ -1,9 +1,10 @@
using System; using Serilog;
using Common.Debug; using System;
using System.Xml; using System.Xml;
using FeedCenter.Feeds;
namespace FeedCenter.FeedParsers;
namespace FeedCenter.FeedParsers
{
internal class AtomParser : FeedParserBase internal class AtomParser : FeedParserBase
{ {
public AtomParser(Feed feed) : base(feed) { } public AtomParser(Feed feed) : base(feed) { }
@@ -44,13 +45,13 @@ namespace FeedCenter.FeedParsers
if (node.Attributes == null) if (node.Attributes == null)
break; break;
XmlNode relNode = node.Attributes["rel"]; XmlNode relNode = GetAttribute(node, "rel");
if (relNode != null) if (relNode != null)
rel = relNode.InnerText; rel = relNode.InnerText;
if (string.IsNullOrEmpty(rel) || rel == "alternate") if (string.IsNullOrEmpty(rel) || rel == "alternate")
Feed.Link = node.Attributes["href"].InnerText.Trim(); Feed.Link = GetAttribute(node, "href").InnerText.Trim();
break; break;
@@ -68,7 +69,7 @@ namespace FeedCenter.FeedParsers
} }
catch (XmlException xmlException) catch (XmlException xmlException)
{ {
Tracer.WriteLine("XML error: " + xmlException.Message + "\n" + feedText); Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
} }
@@ -103,14 +104,14 @@ namespace FeedCenter.FeedParsers
if (childNode.Attributes == null) if (childNode.Attributes == null)
break; break;
XmlNode relNode = childNode.Attributes["rel"]; XmlNode relNode = GetAttribute(childNode, "rel");
if (relNode != null) if (relNode != null)
rel = relNode.InnerText.Trim(); rel = relNode.InnerText.Trim();
if (string.IsNullOrEmpty(rel) || rel == "alternate") if (string.IsNullOrEmpty(rel) || rel == "alternate")
{ {
var link = childNode.Attributes["href"].InnerText; var link = GetAttribute(childNode, "href").InnerText;
if (link.StartsWith("/")) if (link.StartsWith("/"))
{ {
@@ -131,5 +132,12 @@ namespace FeedCenter.FeedParsers
return feedItem; return feedItem;
} }
private static XmlAttribute GetAttribute(XmlNode node, string attributeName)
{
if (node?.Attributes == null)
return null;
return node.Attributes[attributeName, node.NamespaceURI] ?? node.Attributes[attributeName];
} }
} }

View File

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

View File

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

View File

@@ -1,38 +1,20 @@
using Common.Debug; using Serilog;
using System; using System;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using FeedCenter.Feeds;
namespace FeedCenter.FeedParsers namespace FeedCenter.FeedParsers;
{
[Serializable]
internal class InvalidFeedFormatException : ApplicationException
{
internal InvalidFeedFormatException(Exception exception)
: base(string.Empty, exception)
{
}
}
internal abstract class FeedParserBase internal abstract class FeedParserBase
{ {
#region Member variables
protected readonly Feed Feed; protected readonly Feed Feed;
#endregion
#region Constructor
protected FeedParserBase(Feed feed) protected FeedParserBase(Feed feed)
{ {
Feed = feed; Feed = feed;
} }
#endregion
#region Methods
public abstract FeedReadResult ParseFeed(string feedText); public abstract FeedReadResult ParseFeed(string feedText);
protected abstract FeedItem ParseFeedItem(XmlNode node); protected abstract FeedItem ParseFeedItem(XmlNode node);
@@ -40,7 +22,7 @@ namespace FeedCenter.FeedParsers
protected void HandleFeedItem(XmlNode node, ref int sequence) protected void HandleFeedItem(XmlNode node, ref int sequence)
{ {
// Build a feed item from the node // Build a feed item from the node
FeedItem newFeedItem = ParseFeedItem(node); var newFeedItem = ParseFeedItem(node);
if (newFeedItem == null) if (newFeedItem == null)
return; return;
@@ -50,15 +32,12 @@ namespace FeedCenter.FeedParsers
return; return;
// Look for an item that has the same guid // Look for an item that has the same guid
FeedItem existingFeedItem = Feed.Items.FirstOrDefault(item => item.Guid == newFeedItem.Guid && item.ID != newFeedItem.ID); var existingFeedItem = Feed.Items.FirstOrDefault(item => item.Guid == newFeedItem.Guid && item.Id != newFeedItem.Id);
// Check to see if we already have this feed item // Check to see if we already have this feed item
if (existingFeedItem == null) if (existingFeedItem == null)
{ {
Tracer.WriteLine("New link: " + newFeedItem.Link); Log.Logger.Information("New link: " + newFeedItem.Link);
// Associate the new item with the right feed
newFeedItem.Feed = Feed;
// Set the item as new // Set the item as new
newFeedItem.New = true; newFeedItem.New = true;
@@ -71,7 +50,7 @@ namespace FeedCenter.FeedParsers
} }
else else
{ {
Tracer.WriteLine("Existing link: " + newFeedItem.Link); Log.Logger.Information("Existing link: " + newFeedItem.Link);
// Update the fields in the existing item // Update the fields in the existing item
existingFeedItem.Link = newFeedItem.Link; existingFeedItem.Link = newFeedItem.Link;
@@ -96,27 +75,17 @@ namespace FeedCenter.FeedParsers
sequence++; sequence++;
} }
#endregion
#region Parser creation and detection
public static FeedParserBase CreateFeedParser(Feed feed, string feedText) public static FeedParserBase CreateFeedParser(Feed feed, string feedText)
{ {
FeedType feedType = DetectFeedType(feedText); var feedType = DetectFeedType(feedText);
switch (feedType) return feedType switch
{ {
case FeedType.Rss: FeedType.Rss => new RssParser(feed),
return new RssParser(feed); FeedType.Rdf => new RdfParser(feed),
FeedType.Atom => new AtomParser(feed),
case FeedType.Rdf: _ => throw new ArgumentException($"Feed type {feedType} is not supported")
return new RdfParser(feed); };
case FeedType.Atom:
return new AtomParser(feed);
}
throw new ArgumentException($"Feed type {feedType} is not supported");
} }
public static FeedType DetectFeedType(string feedText) public static FeedType DetectFeedType(string feedText)
@@ -148,14 +117,17 @@ namespace FeedCenter.FeedParsers
// No clue! // No clue!
return FeedType.Unknown; return FeedType.Unknown;
} }
catch (XmlException xmlException)
{
Log.Logger.Error(xmlException, "Exception: {0}", feedText);
throw new FeedParseException(FeedParseError.InvalidXml);
}
catch (Exception exception) catch (Exception exception)
{ {
Tracer.WriteException(exception); Log.Logger.Error(exception, "Exception: {0}", feedText);
return FeedType.Unknown; throw new FeedParseException(FeedParseError.InvalidXml);
} }
} }
#endregion
}
} }

View File

@@ -0,0 +1,12 @@
using System;
namespace FeedCenter.FeedParsers;
[Serializable]
internal class InvalidFeedFormatException : ApplicationException
{
internal InvalidFeedFormatException(Exception exception)
: base(string.Empty, exception)
{
}
}

View File

@@ -1,9 +1,10 @@
using Common.Debug; using FeedCenter.Xml;
using Common.Xml; using Serilog;
using System.Xml; using System.Xml;
using FeedCenter.Feeds;
namespace FeedCenter.FeedParsers;
namespace FeedCenter.FeedParsers
{
internal class RdfParser : FeedParserBase internal class RdfParser : FeedParserBase
{ {
public RdfParser(Feed feed) : base(feed) { } public RdfParser(Feed feed) : base(feed) { }
@@ -19,7 +20,7 @@ namespace FeedCenter.FeedParsers
document.LoadXml(feedText); document.LoadXml(feedText);
// Create the namespace manager // Create the namespace manager
XmlNamespaceManager namespaceManager = document.GetAllNamespaces(); var namespaceManager = document.GetAllNamespaces();
// Get the root node // Get the root node
XmlNode rootNode = document.DocumentElement; XmlNode rootNode = document.DocumentElement;
@@ -29,7 +30,7 @@ namespace FeedCenter.FeedParsers
return FeedReadResult.UnknownError; return FeedReadResult.UnknownError;
// Get the channel node // Get the channel node
XmlNode channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager); var channelNode = rootNode.SelectSingleNode("default:channel", namespaceManager);
if (channelNode == null) if (channelNode == null)
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
@@ -55,7 +56,7 @@ namespace FeedCenter.FeedParsers
} }
// Initialize the sequence number for items // Initialize the sequence number for items
int sequence = 0; var sequence = 0;
// Loop over all nodes in the channel node // Loop over all nodes in the channel node
foreach (XmlNode node in rootNode.ChildNodes) foreach (XmlNode node in rootNode.ChildNodes)
@@ -73,7 +74,7 @@ namespace FeedCenter.FeedParsers
} }
catch (XmlException xmlException) catch (XmlException xmlException)
{ {
Tracer.WriteLine("XML error: " + xmlException.Message + "\n" + feedText); Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
} }
@@ -82,7 +83,7 @@ namespace FeedCenter.FeedParsers
protected override FeedItem ParseFeedItem(XmlNode node) protected override FeedItem ParseFeedItem(XmlNode node)
{ {
// Create a new feed item // Create a new feed item
FeedItem feedItem = FeedItem.Create(); var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node // Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes) foreach (XmlNode childNode in node.ChildNodes)
@@ -111,4 +112,3 @@ namespace FeedCenter.FeedParsers
return feedItem; return feedItem;
} }
} }
}

View File

@@ -1,10 +1,11 @@
using Common.Debug; using FeedCenter.Xml;
using Common.Xml; using Serilog;
using System; using System;
using System.Xml; using System.Xml;
using FeedCenter.Feeds;
namespace FeedCenter.FeedParsers;
namespace FeedCenter.FeedParsers
{
internal class RssParser : FeedParserBase internal class RssParser : FeedParserBase
{ {
public RssParser(Feed feed) : base(feed) { } public RssParser(Feed feed) : base(feed) { }
@@ -20,7 +21,7 @@ namespace FeedCenter.FeedParsers
document.LoadXml(feedText); document.LoadXml(feedText);
// Create the namespace manager // Create the namespace manager
XmlNamespaceManager namespaceManager = document.GetAllNamespaces(); var namespaceManager = document.GetAllNamespaces();
// Get the root node // Get the root node
XmlNode rootNode = document.DocumentElement; XmlNode rootNode = document.DocumentElement;
@@ -37,7 +38,7 @@ namespace FeedCenter.FeedParsers
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
// Initialize the sequence number for items // Initialize the sequence number for items
int sequence = 0; var sequence = 0;
// Loop over all nodes in the channel node // Loop over all nodes in the channel node
foreach (XmlNode node in channelNode.ChildNodes) foreach (XmlNode node in channelNode.ChildNodes)
@@ -67,7 +68,7 @@ namespace FeedCenter.FeedParsers
} }
catch (XmlException xmlException) catch (XmlException xmlException)
{ {
Tracer.WriteLine("XML error: " + xmlException.Message + "\n" + feedText); Log.Logger.Error(xmlException, "Exception: {0}", feedText);
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
} }
@@ -76,7 +77,7 @@ namespace FeedCenter.FeedParsers
protected override FeedItem ParseFeedItem(XmlNode node) protected override FeedItem ParseFeedItem(XmlNode node)
{ {
// Create a new feed item // Create a new feed item
FeedItem feedItem = FeedItem.Create(); var feedItem = FeedItem.Create();
// Loop over all nodes in the feed node // Loop over all nodes in the feed node
foreach (XmlNode childNode in node.ChildNodes) foreach (XmlNode childNode in node.ChildNodes)
@@ -95,12 +96,12 @@ namespace FeedCenter.FeedParsers
case "guid": case "guid":
feedItem.Guid = childNode.InnerText.Trim(); feedItem.Guid = childNode.InnerText.Trim();
bool permaLink = true; var permaLink = true;
if (childNode.Attributes != null) if (childNode.Attributes != null)
{ {
var permaLinkNode = childNode.Attributes.GetNamedItem("isPermaLink"); var permaLinkNode = childNode.Attributes.GetNamedItem("isPermaLink");
permaLink = (permaLinkNode == null || permaLinkNode.Value == "true"); permaLink = permaLinkNode == null || permaLinkNode.Value == "true";
} }
if (permaLink && Uri.IsWellFormedUriString(feedItem.Guid, UriKind.Absolute)) if (permaLink && Uri.IsWellFormedUriString(feedItem.Guid, UriKind.Absolute))
@@ -120,4 +121,3 @@ namespace FeedCenter.FeedParsers
return feedItem; return feedItem;
} }
} }
}

View File

@@ -1,17 +1,76 @@
using System; using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using FeedCenter.Accounts;
using JetBrains.Annotations;
using Realms;
namespace FeedCenter namespace FeedCenter.Feeds;
public class Category : RealmObject, INotifyDataErrorInfo
{ {
public partial class Category public const string DefaultName = "< default >";
private readonly DataErrorDictionary _dataErrorDictionary;
public Category()
{ {
public static Category Create() _dataErrorDictionary = new DataErrorDictionary();
{ _dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
return new Category { ID = Guid.NewGuid() };
} }
public bool IsDefault => Name == "< default >"; [PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
// ReSharper disable once UnusedMember.Global public string RemoteId { get; set; }
public Account Account { get; set; }
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 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(Account account)
{
return new Category { Name = DefaultName, IsDefault = true, Account = account };
}
private void ValidateName()
{
_dataErrorDictionary.ClearErrors(nameof(Name));
if (string.IsNullOrWhiteSpace(Name))
_dataErrorDictionary.AddError(nameof(Name), "Name cannot be empty");
} }
} }

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
namespace FeedCenter.Feeds;
internal class DataErrorDictionary : Dictionary<string, List<string>>
{
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
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 void AddError(string propertyName, string error)
{
if (!ContainsKey(propertyName))
this[propertyName] = new List<string>();
if (this[propertyName].Contains(error))
return;
this[propertyName].Add(error);
OnErrorsChanged(propertyName);
}
public void ClearErrors(string propertyName)
{
if (!ContainsKey(propertyName))
return;
Remove(propertyName);
OnErrorsChanged(propertyName);
}
}

View File

@@ -1,73 +1,201 @@
using Common.Debug; using System;
using Common.Xml; using System.Collections;
using FeedCenter.FeedParsers; using System.Collections.Generic;
using System; using System.ComponentModel;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Common.Update; using ChrisKaczor.ApplicationUpdate;
using FeedCenter.Accounts;
using FeedCenter.FeedParsers;
using FeedCenter.Properties;
using FeedCenter.Xml;
using JetBrains.Annotations;
using Realms;
using Serilog;
namespace FeedCenter namespace FeedCenter.Feeds;
{
#region Enumerations
public enum MultipleOpenAction public partial class Feed : RealmObject, INotifyDataErrorInfo
{ {
IndividualPages, private static HttpClient _httpClient;
SinglePage
private readonly DataErrorDictionary _dataErrorDictionary;
public Feed()
{
_dataErrorDictionary = new DataErrorDictionary();
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
} }
public enum FeedType [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;
public Account Account { get; set; }
[UsedImplicitly]
public IList<FeedItem> Items { get; }
public DateTimeOffset LastChecked { get; set; }
public FeedReadResult LastReadResult
{ {
Unknown, get => Enum.TryParse(LastReadResultRaw, out FeedReadResult result) ? result : FeedReadResult.Success;
Rss, set => LastReadResultRaw = value.ToString();
Rdf,
Atom
} }
public enum FeedItemComparison : byte // ReSharper disable once UnusedMember.Global
public string LastReadResultDescription
{ {
Default, get
Title {
// 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();
}
} }
public enum FeedReadResult private string LastReadResultRaw { get; set; }
public DateTimeOffset LastUpdated { get; set; }
public string Link { get; set; }
public MultipleOpenAction MultipleOpenAction
{ {
Success, get => Enum.TryParse(MultipleOpenActionRaw, out MultipleOpenAction result) ? result : MultipleOpenAction.IndividualPages;
NotModified, set => MultipleOpenActionRaw = value.ToString();
NotDue,
UnknownError,
InvalidXml,
NotEnabled,
Unauthorized,
NoResponse,
NotFound,
Timeout,
ConnectionFailed,
ServerError
} }
#endregion private string MultipleOpenActionRaw { get; set; }
public partial class Feed public string Name
{ {
public static Feed Create(FeedCenterEntities database) get => RawName;
set
{ {
return new Feed { ID = Guid.NewGuid(), CategoryID = database.DefaultCategory.ID }; RawName = value;
ValidateString(nameof(Name), RawName);
RaisePropertyChanged();
}
} }
#region Reading public string Password
public FeedReadResult Read(FeedCenterEntities database, bool forceRead = false)
{ {
Tracer.WriteLine("Reading feed: {0}", Source); get => RawPassword;
Tracer.IncrementIndentLevel(); set
{
RawPassword = value;
var result = ReadFeed(database, forceRead); if (!Authenticate)
{
_dataErrorDictionary.ClearErrors(nameof(Password));
return;
}
ValidateString(nameof(Password), RawPassword);
RaisePropertyChanged();
}
}
[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;
set
{
RawSource = value;
ValidateString(nameof(Source), RawSource);
RaisePropertyChanged();
}
}
public string Title { get; set; }
public string UserAgent { get; set; }
public string Username
{
get => RawUsername;
set
{
RawUsername = value;
if (!Authenticate)
{
_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(FeedCenterEntities entities)
{
return new Feed { Id = Guid.NewGuid(), CategoryId = entities.DefaultCategory.Id, Account = entities.LocalAccount };
}
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 FeedReadResult Read(bool forceRead = false)
{
Log.Logger.Information("Reading feed: {0}", Source);
var result = ReadFeed(forceRead);
// Handle the result // Handle the result
switch (result) switch (result)
@@ -75,6 +203,10 @@ namespace FeedCenter
case FeedReadResult.NotDue: case FeedReadResult.NotDue:
case FeedReadResult.NotEnabled: case FeedReadResult.NotEnabled:
case FeedReadResult.NotModified: case FeedReadResult.NotModified:
// Reset status to success
LastReadResult = FeedReadResult.Success;
// Ignore // Ignore
break; break;
@@ -85,21 +217,15 @@ namespace FeedCenter
break; 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 == Data.Extensions.SqlDateTimeZero.Value) if (result == FeedReadResult.Success && LastUpdated == default)
LastUpdated = DateTime.Now; LastUpdated = DateTimeOffset.Now;
Tracer.DecrementIndentLevel(); Log.Logger.Information("Done reading feed: {0}", result);
Tracer.WriteLine("Done reading feed: {0}", result);
return result; return result;
} }
public async Task<FeedReadResult> ReadAsync(FeedCenterEntities database, bool forceRead = false)
{
return await Task.Run(() => Read(database, forceRead));
}
public Tuple<FeedType, string> DetectFeedType() public Tuple<FeedType, string> DetectFeedType()
{ {
var retrieveResult = RetrieveFeed(); var retrieveResult = RetrieveFeed();
@@ -109,67 +235,83 @@ namespace FeedCenter
return new Tuple<FeedType, string>(FeedType.Unknown, string.Empty); return new Tuple<FeedType, string>(FeedType.Unknown, string.Empty);
} }
return new Tuple<FeedType, string>(FeedParserBase.DetectFeedType(retrieveResult.Item2), retrieveResult.Item2); var feedType = FeedType.Unknown;
try
{
feedType = FeedParserBase.DetectFeedType(retrieveResult.Item2);
}
catch
{
// Ignore
}
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() private Tuple<FeedReadResult, string> RetrieveFeed()
{ {
try try
{ {
// Add extra security protocols // Create and configure the HTTP client if needed
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; if (_httpClient == null)
{
// Create the web request var clientHandler = new HttpClientHandler
var oRequest = WebRequest.Create(new Uri(Source));
// Attempt to cast to a web request
var webRequest = oRequest as HttpWebRequest;
// If this is an http request set some special properties
if (webRequest != null)
{ {
// Make sure to use HTTP version 1.1
webRequest.ProtocolVersion = HttpVersion.Version11;
// Set that we'll accept compressed data // Set that we'll accept compressed data
webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
AllowAutoRedirect = true
};
_httpClient = new HttpClient(clientHandler)
{
// Set a timeout // Set a timeout
webRequest.Timeout = 10000; Timeout = TimeSpan.FromSeconds(10)
};
// If we need to authenticate then set the credentials _httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
if (Authenticate) _httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
webRequest.Credentials = new NetworkCredential(Username, Password, Domain); _httpClient.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.9");
_httpClient.DefaultRequestHeaders.CacheControl = CacheControlHeaderValue.Parse("max-age=0");
// Set a user agent string
if (string.IsNullOrWhiteSpace(Properties.Settings.Default.DefaultUserAgent))
webRequest.UserAgent = "FeedCenter/" + UpdateCheck.LocalVersion;
else
webRequest.UserAgent = Properties.Settings.Default.DefaultUserAgent;
} }
// Set the default encoding // Set a user agent string
var encoding = Encoding.UTF8; 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 // Attempt to get the response
var response = (HttpWebResponse) oRequest.GetResponse(); var response = _httpClient.GetAsync(Source).Result;
// If the response included an encoding then change the encoding response.EnsureSuccessStatusCode();
if (response.ContentEncoding.Length > 0)
encoding = Encoding.GetEncoding(response.ContentEncoding);
// Get the response stream var feedStream = response.Content.ReadAsStream();
var responseStream = response.GetResponseStream();
if (responseStream == null)
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
// Create the text reader // Create the text reader
StreamReader textReader = new XmlSanitizingStream(responseStream, encoding); using StreamReader textReader = new XmlSanitizingStream(feedStream, Encoding.UTF8);
// Get the feed text // Get the feed text
var feedText = textReader.ReadToEnd(); var feedText = textReader.ReadToEnd();
if (string.IsNullOrEmpty(feedText))
return Tuple.Create(FeedReadResult.NoResponse, string.Empty);
// Get rid of any leading and trailing whitespace // Get rid of any leading and trailing whitespace
feedText = feedText.Trim(); feedText = feedText.Trim();
@@ -177,27 +319,51 @@ namespace FeedCenter
feedText = feedText.Replace("&nbsp;", "&#160;"); feedText = feedText.Replace("&nbsp;", "&#160;");
// Find ampersands that aren't properly escaped and replace them with escaped versions // Find ampersands that aren't properly escaped and replace them with escaped versions
var r = new Regex("&(?!(?:[a-z]+|#[0-9]+|#x[0-9a-f]+);)"); var r = UnescapedAmpersandRegex();
feedText = r.Replace(feedText, "&amp;"); feedText = r.Replace(feedText, "&amp;");
return Tuple.Create(FeedReadResult.Success, feedText); return Tuple.Create(FeedReadResult.Success, feedText);
} }
catch (IOException ioException) catch (HttpRequestException httpRequestException)
{ {
Tracer.WriteLine(ioException.Message); if (httpRequestException.StatusCode == HttpStatusCode.NotModified)
{
return Tuple.Create(FeedReadResult.ConnectionFailed, string.Empty); return Tuple.Create(FeedReadResult.NotModified, string.Empty);
} }
catch (WebException webException)
{
var result = FeedReadResult.UnknownError;
var errorResponse = webException.Response as HttpWebResponse; Log.Logger.Error(httpRequestException, "Exception");
if (errorResponse != null) return HandleHttpRequestException(httpRequestException);
}
catch (AggregateException aggregateException)
{ {
switch (errorResponse.StatusCode) Log.Logger.Error(aggregateException, "Exception");
return aggregateException.InnerException switch
{ {
TaskCanceledException => Tuple.Create(FeedReadResult.Timeout, string.Empty),
HttpRequestException httpRequestException => HandleHttpRequestException(httpRequestException),
_ => Tuple.Create(FeedReadResult.UnknownError, string.Empty)
};
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Exception");
return Tuple.Create(FeedReadResult.UnknownError, string.Empty);
}
}
private static Tuple<FeedReadResult, string> HandleHttpRequestException(HttpRequestException httpRequestException)
{
switch (httpRequestException.StatusCode)
{
case HttpStatusCode.TooManyRequests:
return Tuple.Create(FeedReadResult.TooManyRequests, string.Empty);
case HttpStatusCode.ServiceUnavailable:
return Tuple.Create(FeedReadResult.TemporarilyUnavailable, string.Empty);
case HttpStatusCode.InternalServerError: case HttpStatusCode.InternalServerError:
return Tuple.Create(FeedReadResult.ServerError, string.Empty); return Tuple.Create(FeedReadResult.ServerError, string.Empty);
@@ -210,37 +376,24 @@ namespace FeedCenter
case HttpStatusCode.Unauthorized: case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden: case HttpStatusCode.Forbidden:
return Tuple.Create(FeedReadResult.Unauthorized, string.Empty); return Tuple.Create(FeedReadResult.Unauthorized, string.Empty);
}
case HttpStatusCode.Moved:
case HttpStatusCode.Redirect:
return Tuple.Create(FeedReadResult.Moved, string.Empty);
} }
switch (webException.Status) if (httpRequestException.InnerException is not SocketException socketException)
{
case WebExceptionStatus.ConnectFailure:
case WebExceptionStatus.NameResolutionFailure:
result = FeedReadResult.ConnectionFailed;
break;
case WebExceptionStatus.Timeout:
result = FeedReadResult.Timeout;
break;
}
Tracer.WriteException(webException);
if (result == FeedReadResult.UnknownError)
Debug.Print("Unknown error");
return Tuple.Create(result, string.Empty);
}
catch (Exception exception)
{
Tracer.WriteLine(exception.Message);
return Tuple.Create(FeedReadResult.UnknownError, string.Empty); 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)
};
} }
private FeedReadResult ReadFeed(FeedCenterEntities database, bool forceRead) private FeedReadResult ReadFeed(bool forceRead)
{ {
try try
{ {
@@ -252,19 +405,19 @@ namespace FeedCenter
if (!forceRead) if (!forceRead)
{ {
// Figure out how long since we last checked // Figure out how long since we last checked
var timeSpan = DateTime.Now - LastChecked; var timeSpan = DateTimeOffset.Now - LastChecked;
// Check if we are due to read the feed // Check if we are due to read the feed
if (timeSpan.TotalMinutes < CheckInterval) if (timeSpan.TotalMinutes < CheckInterval)
return FeedReadResult.NotDue; return FeedReadResult.NotDue;
} }
// We're checking it now so update the time
LastChecked = DateTime.Now;
// Read the feed text // Read the feed text
var retrieveResult = RetrieveFeed(); var retrieveResult = RetrieveFeed();
// We're checking it now so update the time
LastChecked = DateTimeOffset.Now;
// Get the information out of the async result // Get the information out of the async result
var result = retrieveResult.Item1; var result = retrieveResult.Item1;
var feedText = retrieveResult.Item2; var feedText = retrieveResult.Item2;
@@ -293,66 +446,32 @@ namespace FeedCenter
// Loop over the items to be removed // Loop over the items to be removed
foreach (var itemToRemove in removedItems) foreach (var itemToRemove in removedItems)
{ {
// Delete the item from the database
database.FeedItems.Remove(itemToRemove);
// Remove the item from the list // Remove the item from the list
Items.Remove(itemToRemove); Items.Remove(itemToRemove);
} }
// Process actions on this feed
ProcessActions();
return FeedReadResult.Success; return FeedReadResult.Success;
} }
catch (FeedParseException feedParseException)
{
Log.Logger.Error(feedParseException, "Exception");
return FeedReadResult.InvalidXml;
}
catch (InvalidFeedFormatException exception) catch (InvalidFeedFormatException exception)
{ {
Tracer.WriteException(exception.InnerException); Log.Logger.Error(exception, "Exception");
return FeedReadResult.InvalidXml; return FeedReadResult.InvalidXml;
} }
catch (Exception exception) catch (Exception exception)
{ {
Tracer.WriteLine(exception.Message); Log.Logger.Error(exception, "Exception");
return FeedReadResult.UnknownError; return FeedReadResult.UnknownError;
} }
} }
private void ProcessActions() [GeneratedRegex("&(?!(?:[a-z]+|#[0-9]+|#x[0-9a-f]+);)")]
{ private static partial Regex UnescapedAmpersandRegex();
var sortedActions = from action in Actions orderby action.Sequence ascending select action;
foreach (var feedAction in sortedActions)
{
switch (feedAction.Field)
{
case 0:
Title = Title.Replace(feedAction.Search, feedAction.Replace);
break;
}
}
}
#endregion
// ReSharper disable once UnusedMember.Global
public string LastReadResultDescription
{
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 = $"{typeof(FeedReadResult).Name}_{lastReadResult}";
// Try to get the value from the resources
var resourceValue = Properties.Resources.ResourceManager.GetString(resourceName);
// Return the value or just the enum value if not found
return resourceValue ?? lastReadResult.ToString();
}
}
}
} }

View File

@@ -1,25 +0,0 @@
using System;
namespace FeedCenter
{
public partial class FeedAction
{
#region Constructor
public FeedAction()
{
ID = Guid.NewGuid();
}
#endregion
#region Methods
public override string ToString()
{
return string.Format(Properties.Resources.FeedActionDescription, Field, Search, Replace);
}
#endregion
}
}

View File

@@ -1,47 +1,70 @@
using System; using System;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FeedCenter.Accounts;
using FeedCenter.Options;
using Realms;
namespace FeedCenter namespace FeedCenter.Feeds;
{
public partial class FeedItem 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; }
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() public static FeedItem Create()
{ {
return new FeedItem { ID = System.Guid.NewGuid() }; return new FeedItem { Id = System.Guid.NewGuid() };
} }
#region Methods
public override string ToString() public override string ToString()
{ {
string title = Title; var title = Title;
switch (Properties.Settings.Default.MultipleLineDisplay) switch (Properties.Settings.Default.MultipleLineDisplay)
{ {
case Options.MultipleLineDisplay.SingleLine: case MultipleLineDisplay.SingleLine:
// Strip any newlines from the title // Strip any newlines from the title
title = Regex.Replace(title, @"\n", " "); title = NewlineRegex().Replace(title, " ");
break; break;
case Options.MultipleLineDisplay.FirstLine: case MultipleLineDisplay.FirstLine:
// Find the first newline // Find the first newline
int newlineIndex = title.IndexOf("\n", StringComparison.Ordinal); var newlineIndex = title.IndexOf('\n', StringComparison.Ordinal);
// If a newline was found return everything before it // If a newline was found return everything before it
if (newlineIndex > -1) if (newlineIndex > -1)
title = title.Substring(0, newlineIndex); title = title[..newlineIndex];
break; break;
case MultipleLineDisplay.Normal:
break;
default:
throw new ArgumentOutOfRangeException();
} }
title ??= string.Empty;
// Condense multiple spaces to one space // Condense multiple spaces to one space
title = Regex.Replace(title, @"[ ]{2,}", " "); title = MultipleSpaceRegex().Replace(title, " ");
// Condense tabs to one space // Condense tabs to one space
title = Regex.Replace(title, @"\t", " "); title = TabRegex().Replace(title, " ");
// If the title is blank then put in the "no title" title // If the title is blank then put in the "no title" title
if (title.Length == 0) if (title.Length == 0)
@@ -50,20 +73,28 @@ namespace FeedCenter
return title; return title;
} }
//public void ProcessActions(IEnumerable<FeedAction> feedActions) public async Task MarkAsRead(FeedCenterEntities entities)
//{ {
// foreach (FeedAction feedAction in feedActions) var feed = entities.Feeds.FirstOrDefault(f => f.Id == FeedId);
// {
// switch (feedAction.Field)
// {
// case 1:
// Title = Regex.Replace(Title, feedAction.Search, feedAction.Replace); entities.SaveChanges(() =>
// break; {
// } BeenRead = true;
// } New = false;
//} });
#endregion if (feed == null || feed.Account.Type == AccountType.Local)
return;
await AccountReaderFactory.CreateAccountReader(feed.Account).MarkFeedItemRead(RemoteId);
} }
[GeneratedRegex("\\n")]
private static partial Regex NewlineRegex();
[GeneratedRegex("[ ]{2,}")]
private static partial Regex MultipleSpaceRegex();
[GeneratedRegex("\\t")]
private static partial Regex TabRegex();
} }

View File

@@ -0,0 +1,20 @@
namespace FeedCenter.Feeds;
public enum FeedReadResult
{
Success,
NotModified,
NotDue,
UnknownError,
InvalidXml,
NotEnabled,
Unauthorized,
NoResponse,
NotFound,
Timeout,
ConnectionFailed,
ServerError,
Moved,
TemporarilyUnavailable,
TooManyRequests
}

View File

@@ -0,0 +1,9 @@
namespace FeedCenter.Feeds;
public enum FeedType
{
Unknown,
Rss,
Rdf,
Atom
}

View File

@@ -0,0 +1,7 @@
namespace FeedCenter.Feeds;
public enum MultipleOpenAction
{
IndividualPages,
SinglePage
}

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Realm />
</Weavers>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Realm" minOccurs="0" maxOccurs="1">
<xs:complexType></xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -1,11 +1,12 @@
using System; using FeedCenter.Properties;
using System;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using FeedCenter.Properties; using FeedCenter.Feeds;
namespace FeedCenter;
namespace FeedCenter
{
public partial class MainWindow public partial class MainWindow
{ {
private void DisplayCategory() private void DisplayCategory()
@@ -44,7 +45,7 @@ namespace FeedCenter
Tag = category, Tag = category,
// Set the current item to bold // Set the current item to bold
FontWeight = category.ID == _currentCategory?.ID ? FontWeights.Bold : FontWeights.Normal FontWeight = category.Id == _currentCategory?.Id ? FontWeights.Bold : FontWeights.Normal
}; };
// Handle the click // Handle the click
@@ -70,18 +71,18 @@ namespace FeedCenter
var category = (Category) menuItem.Tag; var category = (Category) menuItem.Tag;
// If the category changed then reset the current feed to the first in the category // If the category changed then reset the current feed to the first in the category
if (_currentCategory?.ID != category?.ID) if (_currentCategory?.Id != category?.Id)
{ {
_currentFeed = category == null ? _database.Feeds.FirstOrDefault() : category.Feeds.FirstOrDefault(); _currentFeed = category == null ? _database.Feeds.FirstOrDefault() : _database.Feeds.FirstOrDefault(f => f.CategoryId == category.Id);
} }
// Set the current category // Set the current category
_currentCategory = category; _currentCategory = category;
// Get the current feed list to match the category // Get the current feed list to match the category
_feedList = _currentCategory == null ? _database.Feeds : _database.Feeds.Where(feed => feed.Category.ID == _currentCategory.ID); _feedList = _currentCategory == null ? _database.Feeds : _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id);
// Reset the feed index // Refresh the feed index
_feedIndex = -1; _feedIndex = -1;
// Get the first feed // Get the first feed
@@ -93,8 +94,8 @@ namespace FeedCenter
// Update the display // Update the display
DisplayCategory(); DisplayCategory();
DisplayFeed(); DisplayFeed();
UpdateToolbarButtonState();
Settings.Default.LastCategoryID = _currentCategory?.ID.ToString() ?? string.Empty; Settings.Default.LastCategoryID = _currentCategory?.Id.ToString() ?? string.Empty;
}
} }
} }

View File

@@ -1,27 +1,24 @@
using Common.IO; using System;
using System;
namespace FeedCenter;
namespace FeedCenter
{
public partial class MainWindow public partial class MainWindow
{ {
private InterprocessMessageListener _commandLineListener; private void HandleCommandLine(string commandLine)
private void HandleCommandLine(object sender, InterprocessMessageListener.InterprocessMessageEventArgs e)
{ {
// If the command line is blank then ignore it // If the command line is blank then ignore it
if (e.Message.Length == 0) if (commandLine.Length == 0)
return; return;
// Pad the command line with a trailing space just to be lazy in parsing // Pad the command line with a trailing space just to be lazy in parsing
var commandLine = e.Message + " "; commandLine += " ";
// Look for the feed URL in the command line // Look for the feed URL in the command line
var startPosition = commandLine.IndexOf("feed://", StringComparison.Ordinal); var startPosition = commandLine.IndexOf("feed://", StringComparison.Ordinal);
// If we found one then we should extract and process it // If nothing was found then exit
if (startPosition > 0) if (startPosition <= 0) return;
{
// Advance past the protocol // Advance past the protocol
startPosition += 7; startPosition += 7;
@@ -29,7 +26,7 @@ namespace FeedCenter
var endPosition = commandLine.IndexOf(" ", startPosition, StringComparison.Ordinal); var endPosition = commandLine.IndexOf(" ", startPosition, StringComparison.Ordinal);
// Extract the feed URL // Extract the feed URL
var feedUrl = commandLine.Substring(startPosition, endPosition - startPosition); var feedUrl = commandLine[startPosition..endPosition];
// Add the HTTP protocol by default // Add the HTTP protocol by default
feedUrl = "http://" + feedUrl; feedUrl = "http://" + feedUrl;
@@ -38,5 +35,3 @@ namespace FeedCenter
HandleNewFeed(feedUrl); HandleNewFeed(feedUrl);
} }
} }
}
}

View File

@@ -3,8 +3,8 @@ using System.Linq;
using System.Net; using System.Net;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
{
public partial class MainWindow public partial class MainWindow
{ {
private readonly string[] _chromeExtensions = { "chrome-extension://ehojfdcmnajoklleckniaifaijfnkpbi/subscribe.html?", "chrome-extension://nlbjncdgjeocebhnmkbbbdekmmmcbfjd/subscribe.html?" }; private readonly string[] _chromeExtensions = { "chrome-extension://ehojfdcmnajoklleckniaifaijfnkpbi/subscribe.html?", "chrome-extension://nlbjncdgjeocebhnmkbbbdekmmmcbfjd/subscribe.html?" };
@@ -35,13 +35,16 @@ namespace FeedCenter
// Get the data as a string // Get the data as a string
var data = (string) e.Data.GetData(DataFormats.Text); var data = (string) e.Data.GetData(DataFormats.Text);
if (string.IsNullOrEmpty(data))
return;
// Check to see if the data starts with any known Chrome extension // Check to see if the data starts with any known Chrome extension
var chromeExtension = _chromeExtensions.FirstOrDefault(c => data.StartsWith(c)); var chromeExtension = _chromeExtensions.FirstOrDefault(data.StartsWith);
// Remove the Chrome extension URL and decode the URL // Remove the Chrome extension URL and decode the URL
if (chromeExtension != null) if (chromeExtension != null)
{ {
data = data.Substring(chromeExtension.Length); data = data[chromeExtension.Length..];
data = WebUtility.UrlDecode(data); data = WebUtility.UrlDecode(data);
} }
@@ -49,4 +52,3 @@ namespace FeedCenter
Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data); Dispatcher.BeginInvoke(new NewFeedDelegate(HandleNewFeed), data);
} }
} }
}

View File

@@ -1,20 +1,29 @@
using System; using FeedCenter.Options;
using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using Common.Internet; using System.Threading;
using FeedCenter.Options; using FeedCenter.Feeds;
namespace FeedCenter;
namespace FeedCenter
{
public partial class MainWindow public partial class MainWindow
{ {
private delegate void NewFeedDelegate(string feedUrl); private delegate void NewFeedDelegate(string feedUrl);
private static string GetAbsoluteUrlString(string baseUrl, string url)
{
var uri = new Uri(url, UriKind.RelativeOrAbsolute);
if (!uri.IsAbsoluteUri)
uri = new Uri(new Uri(baseUrl), uri);
return uri.ToString();
}
private void HandleNewFeed(string feedUrl) private void HandleNewFeed(string feedUrl)
{ {
// Create and configure the new feed // Create and configure the new feed
var feed = Feed.Create(_database); var feed = Feed.Create(_database);
feed.Source = feedUrl; feed.Source = feedUrl;
feed.Category = _database.DefaultCategory;
// Try to detect the feed type // Try to detect the feed type
var feedTypeResult = feed.DetectFeedType(); var feedTypeResult = feed.DetectFeedType();
@@ -22,7 +31,7 @@ namespace FeedCenter
// If we can't figure it out it could be an HTML page // If we can't figure it out it could be an HTML page
if (feedTypeResult.Item1 == FeedType.Unknown) 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) if (feedTypeResult.Item2.Length > 0)
{ {
// Create and load an HTML document with the text // Create and load an HTML document with the text
@@ -32,7 +41,7 @@ namespace FeedCenter
// Look for all RSS or atom links in the document // Look for all RSS or atom links in the document
var rssLinks = htmlDocument.DocumentNode.Descendants("link") var rssLinks = htmlDocument.DocumentNode.Descendants("link")
.Where(n => n.Attributes["type"] != null && (n.Attributes["type"].Value == "application/rss+xml" || n.Attributes["type"].Value == "application/atom+xml")) .Where(n => n.Attributes["type"] != null && (n.Attributes["type"].Value == "application/rss+xml" || n.Attributes["type"].Value == "application/atom+xml"))
.Select(n => new Tuple<string, string>(UrlHelper.GetAbsoluteUrlString(feed.Source, n.Attributes["href"].Value), WebUtility.HtmlDecode(n.Attributes["title"]?.Value ?? feedUrl))) .Select(n => new Tuple<string, string>(GetAbsoluteUrlString(feed.Source, n.Attributes["href"].Value), WebUtility.HtmlDecode(n.Attributes["title"]?.Value ?? feedUrl)))
.Distinct() .Distinct()
.ToList(); .ToList();
@@ -55,7 +64,17 @@ namespace FeedCenter
} }
// Read the feed for the first time // Read the feed for the first time
var feedReadResult = feed.Read(_database); var feedReadResult = feed.Read(true);
// Check to see if this might be rate limited
if (feedReadResult == FeedReadResult.TemporarilyUnavailable)
{
// Wait a second
Thread.Sleep(1000);
// Try to read again
feedReadResult = feed.Read(true);
}
// See if we read the feed okay // See if we read the feed okay
if (feedReadResult == FeedReadResult.Success) if (feedReadResult == FeedReadResult.Success)
@@ -64,37 +83,32 @@ namespace FeedCenter
feed.Name = feed.Title; feed.Name = feed.Title;
// Add the feed to the feed table // Add the feed to the feed table
_database.Feeds.Add(feed); _database.SaveChanges(() => _database.Feeds.Add(feed));
// Save the changes
_database.SaveChanges();
// Show a tip // Show a tip
NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), System.Windows.Forms.ToolTipIcon.Info); NotificationIcon.ShowBalloonTip(string.Format(Properties.Resources.FeedAddedNotification, feed.Name), H.NotifyIcon.Core.NotificationIcon.Info);
// Re-initialize the feed display
DisplayFeed();
} }
else else
{ {
// Feed read failed - ceate a new feed window // Feed read failed - create a new feed window
var feedForm = new FeedWindow(); var feedForm = new FeedWindow(_database);
var dialogResult = feedForm.Display(_database, feed, this); var dialogResult = feedForm.Display(feed, this);
// Display the new feed form // Display the new feed form
if (dialogResult.HasValue && dialogResult.Value) if (!dialogResult.HasValue || !dialogResult.Value)
{ return;
// Add the feed to the feed table
_database.Feeds.Add(feed);
// Save the changes // Add the feed to the feed table
_database.SaveChanges(); _database.SaveChanges(() => _database.Feeds.Add(feed));
}
_currentFeed = feed;
// Refresh the database to current settings
ResetDatabase();
// Re-initialize the feed display // Re-initialize the feed display
DisplayFeed(); DisplayFeed();
} }
} }
}
}
}

View File

@@ -1,11 +1,13 @@
using System; using ChrisKaczor.InstalledBrowsers;
using FeedCenter.Properties;
using System;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
namespace FeedCenter namespace FeedCenter;
{
public partial class MainWindow public partial class MainWindow
{ {
private void HandleLinkTextListMouseUp(object sender, MouseButtonEventArgs e) private void HandleLinkTextListMouseUp(object sender, MouseButtonEventArgs e)
@@ -14,55 +16,50 @@ namespace FeedCenter
{ {
case MouseButton.XButton1: case MouseButton.XButton1:
if (PreviousToolbarButton.IsEnabled)
PreviousFeed(); PreviousFeed();
break; break;
case MouseButton.XButton2: case MouseButton.XButton2:
if (NextToolbarButton.IsEnabled)
NextFeed(); NextFeed();
break; break;
} }
} }
private void HandleItemMouseUp(object sender, MouseButtonEventArgs e) private async void HandleItemMouseUp(object sender, MouseButtonEventArgs e)
{ {
// Only handle the middle button // Only handle the middle button
if (e.ChangedButton != MouseButton.Middle) if (e.ChangedButton != MouseButton.Middle)
return; return;
// Get the feed item // Get the feed item
var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; var feedItem = (Feeds.FeedItem) ((ListBoxItem) sender).DataContext;
// The feed item has been read and is no longer new
feedItem.BeenRead = true;
feedItem.New = false;
// Remove the item from the list // Remove the item from the list
LinkTextList.Items.Remove(feedItem); LinkTextList.Items.Remove(feedItem);
// Save the changes // The feed item has been read and is no longer new
_database.SaveChanges(); await feedItem.MarkAsRead(_database);
} }
private void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e) private async void HandleItemMouseDoubleClick(object sender, MouseButtonEventArgs e)
{ {
// Get the feed item // Get the feed item
var feedItem = (FeedItem) ((ListBoxItem) sender).DataContext; var feedItem = (Feeds.FeedItem) ((ListBoxItem) sender).DataContext;
// Open the item link // Try to open the item link
if (BrowserCommon.OpenLink(feedItem.Link)) if (!InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link))
{ return;
// The feed item has been read and is no longer new
feedItem.BeenRead = true;
feedItem.New = false;
// Remove the item from the list // Remove the item from the list
LinkTextList.Items.Remove(feedItem); LinkTextList.Items.Remove(feedItem);
// Save the changes // The feed item has been read and is no longer new
_database.SaveChanges(); await feedItem.MarkAsRead(_database);
}
} }
private void HandleFeedButtonClick(object sender, RoutedEventArgs e) private void HandleFeedButtonClick(object sender, RoutedEventArgs e)
@@ -83,10 +80,9 @@ namespace FeedCenter
Tag = feed, Tag = feed,
// Set the current item to bold // Set the current item to bold
FontWeight = feed == _currentFeed ? FontWeights.Bold : FontWeights.Normal FontWeight = feed.Id == _currentFeed.Id ? FontWeights.Bold : FontWeights.Normal
}; };
// Handle the click // Handle the click
menuItem.Click += HandleFeedMenuItemClick; menuItem.Click += HandleFeedMenuItemClick;
@@ -107,13 +103,13 @@ namespace FeedCenter
var menuItem = (MenuItem) sender; var menuItem = (MenuItem) sender;
// Get the feed from the menu item tab // Get the feed from the menu item tab
var feed = (Feed) menuItem.Tag; var feed = (Feeds.Feed) menuItem.Tag;
// Loop over all feeds and look for the index of the new one // Loop over all feeds and look for the index of the new one
var feedIndex = 0; var feedIndex = 0;
foreach (var loopFeed in _feedList.OrderBy(loopFeed => loopFeed.Name)) foreach (var loopFeed in _feedList.OrderBy(loopFeed => loopFeed.Name))
{ {
if (loopFeed == feed) if (loopFeed.Id == feed.Id)
{ {
_feedIndex = feedIndex; _feedIndex = feedIndex;
break; break;
@@ -132,4 +128,3 @@ namespace FeedCenter
DisplayFeed(); DisplayFeed();
} }
} }
}

View File

@@ -1,31 +1,28 @@
using Common.Update; using ChrisKaczor.ApplicationUpdate;
using FeedCenter.Feeds;
using FeedCenter.Properties; using FeedCenter.Properties;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using FeedCenter.Accounts;
namespace FeedCenter;
namespace FeedCenter
{
public partial class MainWindow public partial class MainWindow
{ {
private BackgroundWorker _feedReadWorker; private bool _reading;
private class FeedReadWorkerInput private void SetProgressMode(bool showProgress, int maximum)
{ {
public bool ForceRead; // Refresh the progress bar if we need it
public Feed Feed; if (showProgress)
}
private void SetProgressMode(bool value, int feedCount)
{
// Reset the progress bar if we need it
if (value)
{ {
FeedReadProgress.Value = 0; FeedReadProgress.Value = 0;
FeedReadProgress.Maximum = feedCount + 2; FeedReadProgress.Maximum = maximum + 2;
FeedReadProgress.Visibility = Visibility.Visible; FeedReadProgress.Visibility = Visibility.Visible;
} }
else else
@@ -34,55 +31,74 @@ namespace FeedCenter
} }
} }
private void ReadCurrentFeed(bool forceRead = false) private async void ReadCurrentFeed(bool forceRead = false)
{
try
{ {
// Don't read if we're already working // Don't read if we're already working
if (_feedReadWorker.IsBusy) if (_reading)
return; return;
_reading = true;
// Don't read if there is nothing to read // Don't read if there is nothing to read
if (!_database.Feeds.Any()) if (!_database.Feeds.Any())
return; return;
var accountReadInput = new AccountReadInput(_database, _currentFeed.Id, forceRead, () => { });
// Switch to progress mode // Switch to progress mode
SetProgressMode(true, 1); SetProgressMode(true, await _currentFeed.Account.GetProgressSteps(accountReadInput));
// Create the input class // Start reading
var workerInput = new FeedReadWorkerInput { ForceRead = forceRead, Feed = _currentFeed }; await HandleFeedReadWorkerStart(forceRead, _currentFeed.Id);
}
// Start the worker catch (Exception exception)
_feedReadWorker.RunWorkerAsync(workerInput); {
HandleException(exception);
}
} }
private void ReadFeeds(bool forceRead = false) private async Task ReadFeeds(bool forceRead = false)
{ {
// Don't read if we're already working // Don't read if we're already working
if (_feedReadWorker.IsBusy) if (_reading)
return; return;
_reading = true;
// Don't read if there is nothing to read // Don't read if there is nothing to read
if (!_database.Feeds.Any()) if (!_database.Accounts.Any())
return; return;
// Switch to progress mode var accountReadInput = new AccountReadInput(_database, null, forceRead, () => { });
SetProgressMode(true, _database.Feeds.Count());
// Create the input class // Calculate total progress steps
var workerInput = new FeedReadWorkerInput { ForceRead = forceRead, Feed = null }; var progressSteps = 0;
// Start the worker foreach (var account in _database.Accounts)
_feedReadWorker.RunWorkerAsync(workerInput); {
progressSteps += await account.GetProgressSteps(accountReadInput);
} }
private void HandleFeedReadWorkerProgressChanged(object sender, ProgressChangedEventArgs e) // Switch to progress mode
SetProgressMode(true, progressSteps);
// Start reading
await HandleFeedReadWorkerStart(forceRead, null);
}
private void IncrementProgress()
{ {
Debug.Assert(FeedReadProgress.Value + 1 <= FeedReadProgress.Maximum);
// Set progress // Set progress
FeedReadProgress.Value = e.ProgressPercentage; FeedReadProgress.Value++;
} }
private void HandleFeedReadWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) private void CompleteRead()
{ {
// Reset the database to current settings // Refresh the database to current settings
ResetDatabase(); ResetDatabase();
// Save settings // Save settings
@@ -102,6 +118,8 @@ namespace FeedCenter
NewVersionLink.Visibility = Visibility.Visible; NewVersionLink.Visibility = Visibility.Visible;
UpdateErrorLink(); UpdateErrorLink();
_reading = false;
} }
private void UpdateErrorLink() private void UpdateErrorLink()
@@ -117,69 +135,61 @@ namespace FeedCenter
: string.Format(Properties.Resources.FeedErrorsLink, feedErrorCount); : string.Format(Properties.Resources.FeedErrorsLink, feedErrorCount);
} }
private static void HandleFeedReadWorkerStart(object sender, DoWorkEventArgs e) private async Task HandleFeedReadWorkerStart(bool forceRead, Guid? feedId)
{
try
{ {
// Create a new database instance for just this thread // Create a new database instance for just this thread
var database = new FeedCenterEntities(); var database = new FeedCenterEntities();
// Get the worker var accountsToRead = new List<Account>();
var worker = (BackgroundWorker) sender;
// Get the input information // If we have a single feed then get the account for that feed
var workerInput = (FeedReadWorkerInput) e.Argument; if (feedId != null)
// Setup for progress
var currentProgress = 0;
// Create the list of feeds to read
var feedsToRead = new List<Feed>();
// If we have a single feed then add it to the list - otherwise add them all
if (workerInput.Feed != null)
feedsToRead.Add(database.Feeds.First(feed => feed.ID == workerInput.Feed.ID));
else
feedsToRead.AddRange(database.Feeds);
// Loop over each feed and read it
foreach (var feed in feedsToRead)
{ {
// Read the feed var feed = database.Feeds.FirstOrDefault(f => f.Id == feedId);
feed.Read(database, workerInput.ForceRead);
// Increment progress if (feed != null)
currentProgress += 1; accountsToRead.Add(feed.Account);
}
// Report progress else
worker.ReportProgress(currentProgress); {
// Otherwise get all accounts
accountsToRead.AddRange(database.Accounts);
} }
// Save the changes // Loop over each account and read it
database.SaveChanges(); foreach (var account in accountsToRead)
{
var accountReadInput = new AccountReadInput(database, feedId, forceRead, IncrementProgress);
// Increment progress await account.Read(accountReadInput);
currentProgress += 1; }
// Report progress // Report progress
worker.ReportProgress(currentProgress); IncrementProgress();
// See if we're due for a version check // See if we're due for a version check
if (DateTime.Now - Settings.Default.LastVersionCheck >= Settings.Default.VersionCheckInterval) if (UpdateCheck.LocalVersion.Major > 0 && DateTime.Now - Settings.Default.LastVersionCheck >= Settings.Default.VersionCheckInterval)
{ {
// Get the update information // Get the update information
UpdateCheck.CheckForUpdate(); await UpdateCheck.CheckForUpdate(Settings.Default.IncludePrerelease);
// Update the last check time // Update the last check time
Settings.Default.LastVersionCheck = DateTime.Now; Settings.Default.LastVersionCheck = DateTime.Now;
} }
// Increment progress
currentProgress += 1;
// Report progress // Report progress
worker.ReportProgress(currentProgress); IncrementProgress();
// Sleep for a little bit so the user can see the update // Sleep for a little bit so the user can see the update
Thread.Sleep(Settings.Default.ProgressSleepInterval * 3); Thread.Sleep(Settings.Default.ProgressSleepInterval * 3);
CompleteRead();
}
catch (Exception exception)
{
HandleException(exception);
} }
} }
} }

View File

@@ -1,9 +1,10 @@
using FeedCenter.Properties; using ChrisKaczor.InstalledBrowsers;
using FeedCenter.Properties;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
namespace FeedCenter namespace FeedCenter;
{
public partial class MainWindow public partial class MainWindow
{ {
private void HandleHeaderLabelMouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void HandleHeaderLabelMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -26,7 +27,6 @@ namespace FeedCenter
{ {
// Open the link for the current feed on a left double click // Open the link for the current feed on a left double click
if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left)
BrowserCommon.OpenLink(_currentFeed.Link); InstalledBrowser.OpenLink(Settings.Default.Browser, _currentFeed.Link);
}
} }
} }

View File

@@ -2,13 +2,13 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:properties="clr-namespace:FeedCenter.Properties" xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:windows="clr-namespace:Common.Wpf.Windows;assembly=Common.Wpf" xmlns:windows="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.SnappingWindow"
xmlns:toolbar="clr-namespace:Common.Wpf.Toolbar;assembly=Common.Wpf" xmlns:toolbar="clr-namespace:ChrisKaczor.Wpf.Controls.Toolbar;assembly=ChrisKaczor.Wpf.Controls.Toolbar"
xmlns:splitButton="clr-namespace:Common.Wpf.Toolbar.SplitButton;assembly=Common.Wpf" xmlns:splitButton="clr-namespace:ChrisKaczor.Wpf.Controls.Toolbar;assembly=ChrisKaczor.Wpf.Controls.Toolbar"
xmlns:linkControl="clr-namespace:Common.Wpf.LinkControl;assembly=Common.Wpf" xmlns:htmlTextBlock="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.HtmlTextBlock"
xmlns:htmlTextBlock="clr-namespace:Common.Wpf.HtmlTextBlock;assembly=Common.Wpf"
xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:nameBasedGrid="clr-namespace:NameBasedGrid;assembly=NameBasedGrid" xmlns:nameBasedGrid="clr-namespace:NameBasedGrid;assembly=NameBasedGrid"
xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.Link"
Title="MainWindow" Title="MainWindow"
Height="360" Height="360"
Width="252" Width="252"
@@ -80,9 +80,10 @@
FontFamily="Marlett" FontFamily="Marlett"
Content="r" Content="r"
FontSize="8" FontSize="8"
Grid.Column="1"></Button> Grid.Column="1">
</Button>
</Grid> </Grid>
<linkControl:LinkControl Name="NewVersionLink" <controls:Link Name="NewVersionLink"
Height="21" Height="21"
nameBasedGrid:NameBasedGrid.Row="NewVersionRow" nameBasedGrid:NameBasedGrid.Row="NewVersionRow"
Text="{x:Static properties:Resources.NewVersionLink}" Text="{x:Static properties:Resources.NewVersionLink}"
@@ -91,7 +92,7 @@
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Visibility="Collapsed" Visibility="Collapsed"
Click="HandleNewVersionLinkClick"> Click="HandleNewVersionLinkClick">
</linkControl:LinkControl> </controls:Link>
<Grid Name="CategoryGrid" <Grid Name="CategoryGrid"
Height="21" Height="21"
nameBasedGrid:NameBasedGrid.Row="CategoryRow" nameBasedGrid:NameBasedGrid.Row="CategoryRow"
@@ -152,7 +153,6 @@
Foreground="White" Foreground="White"
nameBasedGrid:NameBasedGrid.Row="FeedListRow" nameBasedGrid:NameBasedGrid.Row="FeedListRow"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"> ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBoxItem Content="Test item" />
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<htmlTextBlock:HtmlTextBlock Html="{Binding}" <htmlTextBlock:HtmlTextBlock Html="{Binding}"
@@ -186,12 +186,14 @@
<Setter Property="Panel.Background" <Setter Property="Panel.Background"
TargetName="Bd"> TargetName="Bd">
<Setter.Value> <Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.HighlightBrushKey}" /> <DynamicResource
ResourceKey="{x:Static SystemColors.HighlightBrushKey}" />
</Setter.Value> </Setter.Value>
</Setter> </Setter>
<Setter Property="TextElement.Foreground"> <Setter Property="TextElement.Foreground">
<Setter.Value> <Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.HighlightTextBrushKey}" /> <DynamicResource
ResourceKey="{x:Static SystemColors.HighlightTextBrushKey}" />
</Setter.Value> </Setter.Value>
</Setter> </Setter>
<Setter Property="Panel.Cursor" <Setter Property="Panel.Cursor"
@@ -283,11 +285,13 @@
<MenuItem Header="{x:Static properties:Resources.lockWindowCheckBox}" <MenuItem Header="{x:Static properties:Resources.lockWindowCheckBox}"
IsCheckable="True" IsCheckable="True"
IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=WindowLocked}" /> IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=WindowLocked}" />
<Separator /> <Separator Name="SettingsMenuSeparator" />
<MenuItem Header="{x:Static properties:Resources.CurrentFeed}"> <MenuItem Name="CurrentFeedMenu" Header="{x:Static properties:Resources.CurrentFeed}">
<MenuItem Header="{x:Static properties:Resources.EditMenu}" <MenuItem Name="EditCurrentFeedMenuItem"
Header="{x:Static properties:Resources.EditMenu}"
Click="HandleEditCurrentFeedMenuItemClick" /> Click="HandleEditCurrentFeedMenuItemClick" />
<MenuItem Header="{x:Static properties:Resources.DeleteMenu}" <MenuItem Name="DeleteCurrentFeedMenuItem"
Header="{x:Static properties:Resources.DeleteMenu}"
Click="HandleDeleteCurrentFeedMenuItemClick" /> Click="HandleDeleteCurrentFeedMenuItemClick" />
</MenuItem> </MenuItem>
</ContextMenu> </ContextMenu>
@@ -295,7 +299,7 @@
</splitButton:SplitButton> </splitButton:SplitButton>
</ToolBar> </ToolBar>
</ToolBarTray> </ToolBarTray>
<linkControl:LinkControl Name="FeedErrorsLink" <controls:Link Name="FeedErrorsLink"
Height="21" Height="21"
nameBasedGrid:NameBasedGrid.Row="FeedErrorsRow" nameBasedGrid:NameBasedGrid.Row="FeedErrorsRow"
Text="{x:Static properties:Resources.FeedErrorsLink}" Text="{x:Static properties:Resources.FeedErrorsLink}"
@@ -305,7 +309,7 @@
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Visibility="Collapsed" Visibility="Collapsed"
Click="HandleShowErrorsButtonClick"> Click="HandleShowErrorsButtonClick">
</linkControl:LinkControl> </controls:Link>
</nameBasedGrid:NameBasedGrid> </nameBasedGrid:NameBasedGrid>
</Border> </Border>
</windows:SnappingWindow> </windows:SnappingWindow>

View File

@@ -1,61 +1,77 @@
using Common.Debug; using ChrisKaczor.ApplicationUpdate;
using Common.Helpers; using ChrisKaczor.Wpf.Application;
using Common.IO; using FeedCenter.Feeds;
using Common.Update;
using FeedCenter.Properties; using FeedCenter.Properties;
using Serilog;
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
namespace FeedCenter namespace FeedCenter;
{
public partial class MainWindow public partial class MainWindow : IDisposable
{ {
private Category _currentCategory;
private Feed _currentFeed;
private FeedCenterEntities _database; private FeedCenterEntities _database;
private int _feedIndex; private int _feedIndex;
private IEnumerable<Feed> _feedList;
private Category _currentCategory;
private IQueryable<Feed> _feedList;
private Feed _currentFeed;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
} }
public void Initialize() public void Dispose()
{
_mainTimer?.Dispose();
GC.SuppressFinalize(this);
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
// Initialize the window
Initialize().ContinueWith(_ => { }, TaskScheduler.FromCurrentSynchronizationContext());
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
SingleInstance.Stop().ContinueWith(_ => { }, TaskScheduler.FromCurrentSynchronizationContext());
}
public async Task Initialize()
{ {
// Set up the update handler // Set up the update handler
InitializeUpdate(); InitializeUpdate();
// Show the notification icon
NotificationIcon.Initialize(this);
// Load window settings // Load window settings
LoadWindowSettings(); LoadWindowSettings();
// Set the foreground color to something that can be seen // Set the foreground color to something that can be seen
LinkTextList.Foreground = (System.Drawing.SystemColors.Desktop.GetBrightness() < 0.5) ? Brushes.White : Brushes.Black; LinkTextList.Foreground = System.Drawing.SystemColors.Desktop.GetBrightness() < 0.5
? Brushes.White
: Brushes.Black;
HeaderLabel.Foreground = LinkTextList.Foreground; HeaderLabel.Foreground = LinkTextList.Foreground;
// Create the background worker that does the actual reading
_feedReadWorker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true };
_feedReadWorker.DoWork += HandleFeedReadWorkerStart;
_feedReadWorker.ProgressChanged += HandleFeedReadWorkerProgressChanged;
_feedReadWorker.RunWorkerCompleted += HandleFeedReadWorkerCompleted;
// Set up the database // Set up the database
_database = new FeedCenterEntities(); _database = new FeedCenterEntities();
// Initialize the command line listener // Initialize the single instance listener
_commandLineListener = new InterprocessMessageListener(Properties.Resources.ApplicationName); SingleInstance.MessageReceived += SingleInstance_MessageReceived;
_commandLineListener.MessageReceived += HandleCommandLine; await SingleInstance.StartAsync(App.Name);
// Handle any command line we were started with // Handle any command line we were started with
HandleCommandLine(null, new InterprocessMessageListener.InterprocessMessageEventArgs(Environment.CommandLine)); HandleCommandLine(Environment.CommandLine);
// Create a timer to keep track of things we need to do // Create a timer to keep track of things we need to do
InitializeTimer(); InitializeTimer();
@@ -63,18 +79,34 @@ namespace FeedCenter
// Initialize the feed display // Initialize the feed display
InitializeDisplay(); InitializeDisplay();
if (UpdateCheck.LocalVersion.Major > 0)
{
// Check for update // Check for update
if (Settings.Default.CheckVersionAtStartup) if (Settings.Default.CheckVersionAtStartup)
UpdateCheck.CheckForUpdate(); await UpdateCheck.CheckForUpdate(Settings.Default.IncludePrerelease);
// Show the link if updates are available // Show the link if updates are available
if (UpdateCheck.UpdateAvailable) if (UpdateCheck.UpdateAvailable)
NewVersionLink.Visibility = Visibility.Visible; NewVersionLink.Visibility = Visibility.Visible;
Tracer.WriteLine("MainForm creation finished");
} }
#region Setting events try
{
// Show the notification icon
NotificationIcon.Initialize(this);
}
catch (Exception e)
{
Log.Logger.Error(e, "");
}
Log.Logger.Information("MainForm creation finished");
}
private void SingleInstance_MessageReceived(object sender, string commandLine)
{
HandleCommandLine(commandLine);
}
private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
@@ -85,18 +117,17 @@ namespace FeedCenter
return; return;
} }
if (e.PropertyName == Reflection.GetPropertyName(() => Settings.Default.MultipleLineDisplay)) switch (e.PropertyName)
{ {
case nameof(Settings.Default.MultipleLineDisplay):
// Update the current feed // Update the current feed
DisplayFeed(); DisplayFeed();
} break;
else if (e.PropertyName == Reflection.GetPropertyName(() => Settings.Default.WindowLocked)) case nameof(Settings.Default.WindowLocked):
{
// Update the window for the new window lock value // Update the window for the new window lock value
HandleWindowLockState(); HandleWindowLockState();
} break;
else if (e.PropertyName == Reflection.GetPropertyName(() => Settings.Default.ToolbarLocation)) case nameof(Settings.Default.ToolbarLocation):
{
// Update the window for the toolbar location // Update the window for the toolbar location
switch (Settings.Default.ToolbarLocation) switch (Settings.Default.ToolbarLocation)
{ {
@@ -108,46 +139,97 @@ namespace FeedCenter
NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "BottomToolbarRow"); NameBasedGrid.NameBasedGrid.SetRow(NavigationToolbarTray, "BottomToolbarRow");
break; break;
case Dock.Left:
case Dock.Right:
default:
throw new NotSupportedException();
} }
break;
} }
} }
#endregion private void ResetDatabase()
{
// Get the ID of the current feed
var currentId = _currentFeed?.IsValid ?? false ? _currentFeed.Id : Guid.Empty;
#region Feed display // Create a new database object
_database = new FeedCenterEntities();
_feedList = _currentCategory == null
? _database.Feeds.ToList()
: _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id).ToList();
UpdateToolbarButtonState();
// Get a list of feeds ordered by name
var feedList = _feedList.OrderBy(f => f.Name).ToList();
// First try to find the current feed by ID to see if it is still there
var newIndex = feedList.FindIndex(f => f.Id == currentId);
if (newIndex == -1)
{
// The current feed isn't there anymore so see if we can find a feed at the old index
if (feedList.ElementAtOrDefault(_feedIndex) != null)
newIndex = _feedIndex;
// If there is no feed at the old location then give up and go back to the start
if (newIndex == -1 && feedList.Count > 0)
newIndex = 0;
}
// Set the current index to the new index
_feedIndex = newIndex;
// Re-get the current feed
_currentFeed = _feedIndex == -1
? null
: _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
}
private void UpdateToolbarButtonState() private void UpdateToolbarButtonState()
{ {
// Cache the feed count to save (a little) time // Cache the feed count to save (a little) time
var feedCount = _feedList?.Count() ?? 0; var feedCount = Settings.Default.DisplayEmptyFeeds
? _feedList.Count()
: _feedList.Count(x => x.Items.Any(y => !y.BeenRead));
// Set button states // Set button states
PreviousToolbarButton.IsEnabled = (feedCount > 1); PreviousToolbarButton.IsEnabled = feedCount > 1;
NextToolbarButton.IsEnabled = (feedCount > 1); NextToolbarButton.IsEnabled = feedCount > 1;
RefreshToolbarButton.IsEnabled = (feedCount > 0); RefreshToolbarButton.IsEnabled = feedCount > 0;
FeedButton.IsEnabled = (feedCount > 0); FeedButton.IsEnabled = feedCount > 0;
OpenAllToolbarButton.IsEnabled = (feedCount > 0); OpenAllToolbarButton.IsEnabled = feedCount > 0;
MarkReadToolbarButton.IsEnabled = (feedCount > 0); MarkReadToolbarButton.IsEnabled = feedCount > 0;
FeedLabel.Visibility = (feedCount == 0 ? Visibility.Hidden : Visibility.Visible); FeedLabel.Visibility = feedCount == 0 ? Visibility.Hidden : Visibility.Visible;
FeedButton.Visibility = (feedCount > 1 ? Visibility.Hidden : Visibility.Visible); FeedButton.Visibility = feedCount == 0 ? Visibility.Hidden : Visibility.Visible;
CategoryGrid.Visibility = (_database.Categories.Count() > 1 ? Visibility.Visible : Visibility.Collapsed); 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() private void InitializeDisplay()
{ {
// Get the last category (defaulting to none) // Get the last category (defaulting to none)
_currentCategory = _database.Categories.FirstOrDefault(category => category.ID.ToString() == Settings.Default.LastCategoryID); _currentCategory = _database.Categories.FirstOrDefault(category => category.Id.ToString() == Settings.Default.LastCategoryID);
DisplayCategory(); DisplayCategory();
// Get the current feed list to match the category // Get the current feed list to match the category
_feedList = _currentCategory == null ? _database.Feeds : _database.Feeds.Where(feed => feed.Category.ID == _currentCategory.ID); _feedList = _currentCategory == null
? _database.Feeds
: _database.Feeds.Where(feed => feed.CategoryId == _currentCategory.Id);
UpdateToolbarButtonState(); UpdateToolbarButtonState();
// Clear the link list // Clear the link list
LinkTextList.Items.Clear(); LinkTextList.Items.Clear();
// Reset the feed index // Refresh the feed index
_feedIndex = -1; _feedIndex = -1;
// Start the timer // Start the timer
@@ -182,7 +264,7 @@ namespace FeedCenter
var found = false; var found = false;
// Remember our starting position // Remember our starting position
var startIndex = (_feedIndex == -1 ? 0 : _feedIndex); var startIndex = _feedIndex == -1 ? 0 : _feedIndex;
// Increment the index and adjust if we've gone around the end // Increment the index and adjust if we've gone around the end
_feedIndex = (_feedIndex + 1) % feedCount; _feedIndex = (_feedIndex + 1) % feedCount;
@@ -194,7 +276,7 @@ namespace FeedCenter
_currentFeed = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex); _currentFeed = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
// If the current feed has unread items then we can display it // If the current feed has unread items then we can display it
if (_currentFeed.Items.Count(item => !item.BeenRead) > 0) if (_currentFeed.Items.Any(item => !item.BeenRead))
{ {
found = true; found = true;
break; break;
@@ -202,8 +284,7 @@ namespace FeedCenter
// Increment the index and adjust if we've gone around the end // Increment the index and adjust if we've gone around the end
_feedIndex = (_feedIndex + 1) % feedCount; _feedIndex = (_feedIndex + 1) % feedCount;
} } while (_feedIndex != startIndex);
while (_feedIndex != startIndex);
// If nothing was found then clear the current feed // If nothing was found then clear the current feed
if (!found) if (!found)
@@ -245,7 +326,7 @@ namespace FeedCenter
var found = false; var found = false;
// Remember our starting position // Remember our starting position
var startIndex = (_feedIndex == -1 ? 0 : _feedIndex); var startIndex = _feedIndex == -1 ? 0 : _feedIndex;
// Decrement the feed index // Decrement the feed index
_feedIndex--; _feedIndex--;
@@ -261,7 +342,7 @@ namespace FeedCenter
_currentFeed = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex); _currentFeed = _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex);
// If the current feed has unread items then we can display it // If the current feed has unread items then we can display it
if (_currentFeed.Items.Count(item => !item.BeenRead) > 0) if (_currentFeed.Items.Any(item => !item.BeenRead))
{ {
found = true; found = true;
break; break;
@@ -273,8 +354,7 @@ namespace FeedCenter
// If we've gone below the start of the list then reset to the end // If we've gone below the start of the list then reset to the end
if (_feedIndex < 0) if (_feedIndex < 0)
_feedIndex = feedCount - 1; _feedIndex = feedCount - 1;
} } while (_feedIndex != startIndex);
while (_feedIndex != startIndex);
// If nothing was found then clear the current feed // If nothing was found then clear the current feed
if (!found) if (!found)
@@ -319,7 +399,7 @@ namespace FeedCenter
} }
// Set the header to the feed title // Set the header to the feed title
FeedLabel.Text = (_currentFeed.Name.Length > 0 ? _currentFeed.Name : _currentFeed.Title); FeedLabel.Text = _currentFeed.Name.Length > 0 ? _currentFeed.Name : _currentFeed.Title;
FeedButton.Visibility = _feedList.Count() > 1 ? Visibility.Visible : Visibility.Hidden; FeedButton.Visibility = _feedList.Count() > 1 ? Visibility.Visible : Visibility.Hidden;
// Clear the current list // Clear the current list
@@ -336,61 +416,16 @@ namespace FeedCenter
} }
UpdateOpenAllButton(); UpdateOpenAllButton();
UpdateToolbarButtonState();
} }
private void MarkAllItemsAsRead() private async Task MarkAllItemsAsRead()
{ {
// Loop over all items and mark them as read // Loop over all items and mark them as read
foreach (FeedItem feedItem in LinkTextList.Items) foreach (FeedItem feedItem in LinkTextList.Items)
feedItem.BeenRead = true; await feedItem.MarkAsRead(_database);
// Save the changes
_database.SaveChanges();
// Clear the list // Clear the list
LinkTextList.Items.Clear(); LinkTextList.Items.Clear();
} }
#endregion
#region Database helpers
private void ResetDatabase()
{
// Get the ID of the current feed
var currentId = _currentFeed?.ID ?? Guid.Empty;
// Create a new database object
_database = new FeedCenterEntities();
_feedList = _currentCategory == null ? _database.Feeds : _database.Feeds.Where(feed => feed.Category.ID == _currentCategory.ID);
UpdateToolbarButtonState();
// Get a list of feeds ordered by name
var feedList = _feedList.OrderBy(f => f.Name).ToList();
// First try to find the current feed by ID to see if it is still there
var newIndex = feedList.FindIndex(f => f.ID == currentId);
if (newIndex == -1)
{
// The current feed isn't there anymore so see if we can find a feed at the old index
if (feedList.ElementAtOrDefault(_feedIndex) != null)
newIndex = _feedIndex;
// If there is no feed at the old location then give up and go back to the start
if (newIndex == -1 && feedList.Count > 0)
newIndex = 0;
}
// Set the current index to the new index
_feedIndex = newIndex;
// Re-get the current feed
_currentFeed = (_feedIndex == -1 ? null : _feedList.OrderBy(feed => feed.Name).AsEnumerable().ElementAt(_feedIndex));
}
#endregion
}
} }

View File

@@ -1,42 +1,51 @@
using FeedCenter.Properties; using FeedCenter.Properties;
using System; using System;
using System.Windows.Forms; using System.Timers;
using System.Windows.Threading;
namespace FeedCenter;
namespace FeedCenter
{
public partial class MainWindow public partial class MainWindow
{ {
private Timer _mainTimer; private Timer _mainTimer;
private DateTime _lastFeedRead; private DateTime _lastFeedRead;
private DateTime _lastFeedDisplay; private DateTime _lastFeedDisplay;
private Dispatcher _dispatcher;
private void InitializeTimer() private void InitializeTimer()
{ {
_dispatcher = Dispatcher.CurrentDispatcher;
_mainTimer = new Timer { Interval = 1000 }; _mainTimer = new Timer { Interval = 1000 };
_mainTimer.Tick += HandleMainTimerTick; _mainTimer.Elapsed += HandleMainTimerElapsed;
} }
private void TerminateTimer() private void TerminateTimer()
{ {
StopTimer(); StopTimer();
_mainTimer.Dispose(); _mainTimer?.Dispose();
_mainTimer = null;
} }
private void StartTimer() private void StartTimer()
{ {
_mainTimer.Start(); _mainTimer?.Start();
} }
private void StopTimer() private void StopTimer()
{ {
_mainTimer.Stop(); _mainTimer?.Stop();
} }
private void HandleMainTimerTick(object sender, EventArgs e) private async void HandleMainTimerElapsed(object sender, EventArgs e)
{
try
{
await _dispatcher.Invoke(async () =>
{ {
// If the background worker is busy then don't do anything // If the background worker is busy then don't do anything
if (_feedReadWorker.IsBusy) if (_reading)
return; return;
// Stop the timer for now // Stop the timer for now
@@ -50,10 +59,15 @@ namespace FeedCenter
// Check to see if we should try to read the feeds // Check to see if we should try to read the feeds
if (DateTime.Now - _lastFeedRead >= Settings.Default.FeedCheckInterval) if (DateTime.Now - _lastFeedRead >= Settings.Default.FeedCheckInterval)
ReadFeeds(); await ReadFeeds();
// Get the timer going again // Get the timer going again
StartTimer(); StartTimer();
});
}
catch (Exception exception)
{
HandleException(exception);
} }
} }
} }

View File

@@ -1,14 +1,20 @@
using FeedCenter.Options; using ChrisKaczor.InstalledBrowsers;
using FeedCenter.Options;
using FeedCenter.Properties; using FeedCenter.Properties;
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Web.UI; using System.Web.UI;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using FeedCenter.Feeds;
using Serilog;
using Serilog.Events;
namespace FeedCenter;
namespace FeedCenter
{
public partial class MainWindow public partial class MainWindow
{ {
private void HandlePreviousToolbarButtonClick(object sender, RoutedEventArgs e) private void HandlePreviousToolbarButtonClick(object sender, RoutedEventArgs e)
@@ -21,14 +27,11 @@ namespace FeedCenter
NextFeed(); NextFeed();
} }
private void OpenAllFeedItemsIndividually() private async Task OpenAllFeedItemsIndividually()
{ {
// Create a new list of feed items // Create a new list of feed items
var feedItems = (from FeedItem feedItem in LinkTextList.Items select feedItem).ToList(); var feedItems = (from FeedItem feedItem in LinkTextList.Items select feedItem).ToList();
// Get the browser
var browser = BrowserCommon.FindBrowser(Settings.Default.Browser);
// Cache the settings object // Cache the settings object
var settings = Settings.Default; var settings = Settings.Default;
@@ -39,10 +42,10 @@ namespace FeedCenter
foreach (var feedItem in feedItems) foreach (var feedItem in feedItems)
{ {
// Try to open the link // Try to open the link
if (BrowserCommon.OpenLink(browser, feedItem.Link)) if (InstalledBrowser.OpenLink(Settings.Default.Browser, feedItem.Link))
{ {
// Mark the feed as read // Mark the feed as read
feedItem.BeenRead = true; await feedItem.MarkAsRead(_database);
// Remove the item // Remove the item
LinkTextList.Items.Remove(feedItem); LinkTextList.Items.Remove(feedItem);
@@ -54,9 +57,6 @@ namespace FeedCenter
// Switch to the normal sleep interval // Switch to the normal sleep interval
sleepInterval = settings.OpenAllSleepInterval; sleepInterval = settings.OpenAllSleepInterval;
} }
// Save the changes
_database.SaveChanges();
} }
private void HandleOptionsToolbarButtonClick(object sender, RoutedEventArgs e) private void HandleOptionsToolbarButtonClick(object sender, RoutedEventArgs e)
@@ -64,23 +64,33 @@ namespace FeedCenter
// Create the options form // Create the options form
var optionsWindow = new OptionsWindow { Owner = this }; var optionsWindow = new OptionsWindow { Owner = this };
// Show the options form and get the result // Show the options window
var result = optionsWindow.ShowDialog(); optionsWindow.ShowDialog();
// If okay was selected // Refresh the database to current settings
if (result.HasValue && result.Value)
{
// Reset the database to current settings
ResetDatabase(); ResetDatabase();
// Re-initialize the feed display // Re-initialize the feed display
DisplayFeed(); DisplayFeed();
}
UpdateErrorLink();
} }
private void HandleMarkReadToolbarButtonClick(object sender, RoutedEventArgs e) internal 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) private void HandleShowErrorsButtonClick(object sender, RoutedEventArgs e)
@@ -89,12 +99,9 @@ namespace FeedCenter
var feedErrorWindow = new FeedErrorWindow(); var feedErrorWindow = new FeedErrorWindow();
// Display the window // Display the window
var result = feedErrorWindow.Display(this); feedErrorWindow.Display(this);
// If okay was selected // Refresh the database to current settings
if (result.GetValueOrDefault())
{
// Reset the database to current settings
ResetDatabase(); ResetDatabase();
// Re-initialize the feed display // Re-initialize the feed display
@@ -102,61 +109,88 @@ namespace FeedCenter
UpdateErrorLink(); UpdateErrorLink();
} }
}
private void HandleRefreshMenuItemClick(object sender, RoutedEventArgs e) private async void HandleRefreshMenuItemClick(object sender, RoutedEventArgs e)
{
try
{ {
var menuItem = (MenuItem) e.Source; var menuItem = (MenuItem) e.Source;
if (Equals(menuItem, MenuRefresh)) if (Equals(menuItem, MenuRefresh))
ReadCurrentFeed(true); ReadCurrentFeed(true);
else if (Equals(menuItem, MenuRefreshAll)) else if (Equals(menuItem, MenuRefreshAll))
ReadFeeds(true); await ReadFeeds(true);
} }
catch (Exception exception)
private void HandleRefreshToolbarButtonClick(object sender, RoutedEventArgs e)
{ {
ReadFeeds(true); HandleException(exception);
}
} }
private void HandleOpenAllMenuItemClick(object sender, RoutedEventArgs e) private async void HandleRefreshToolbarButtonClick(object sender, RoutedEventArgs e)
{
try
{
await ReadFeeds(true);
}
catch (Exception exception)
{
HandleException(exception);
}
}
private async void HandleOpenAllMenuItemClick(object sender, RoutedEventArgs e)
{
try
{ {
var menuItem = (MenuItem) e.Source; var menuItem = (MenuItem) e.Source;
if (Equals(menuItem, MenuOpenAllSinglePage)) if (Equals(menuItem, MenuOpenAllSinglePage))
OpenAllFeedItemsOnSinglePage(); await OpenAllFeedItemsOnSinglePage();
else if (Equals(menuItem, MenuOpenAllMultiplePages)) else if (Equals(menuItem, MenuOpenAllMultiplePages))
OpenAllFeedItemsIndividually(); await OpenAllFeedItemsIndividually();
}
catch (Exception exception)
{
HandleException(exception);
}
} }
private void HandleOpenAllToolbarButtonClick(object sender, RoutedEventArgs e) private async void HandleOpenAllToolbarButtonClick(object sender, RoutedEventArgs e)
{
try
{ {
var multipleOpenAction = _currentFeed.MultipleOpenAction; var multipleOpenAction = _currentFeed.MultipleOpenAction;
switch (multipleOpenAction) switch (multipleOpenAction)
{ {
case MultipleOpenAction.IndividualPages: case MultipleOpenAction.IndividualPages:
OpenAllFeedItemsIndividually(); await OpenAllFeedItemsIndividually();
break; break;
case MultipleOpenAction.SinglePage: case MultipleOpenAction.SinglePage:
OpenAllFeedItemsOnSinglePage(); await OpenAllFeedItemsOnSinglePage();
break; break;
} }
} }
catch (Exception exception)
{
HandleException(exception);
}
}
private void HandleEditCurrentFeedMenuItemClick(object sender, RoutedEventArgs e) private void HandleEditCurrentFeedMenuItemClick(object sender, RoutedEventArgs e)
{ {
// Create a new feed window // Create a new feed window
var feedWindow = new FeedWindow(); var feedWindow = new FeedWindow(_database);
// Display the feed window and get the result // Display the feed window and get the result
var result = feedWindow.Display(_database, _currentFeed, this); var result = feedWindow.Display(_currentFeed, this);
// If OK was clicked... // If OK was clicked...
if (result.HasValue && result.Value) if (result.HasValue && result.Value)
{ {
// Save // Save
_database.SaveChanges(); _database.SaveChanges(() => { });
// Update feed // Update feed
DisplayFeed(); DisplayFeed();
@@ -166,7 +200,7 @@ namespace FeedCenter
private void HandleDeleteCurrentFeedMenuItemClick(object sender, RoutedEventArgs e) private void HandleDeleteCurrentFeedMenuItemClick(object sender, RoutedEventArgs e)
{ {
// Confirm this delete since it is for real // Confirm this delete since it is for real
if (MessageBox.Show(this, Properties.Resources.ConfirmDelete, string.Empty, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No) if (MessageBox.Show(this, Properties.Resources.ConfirmDeleteFeed, string.Empty, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return; return;
// Get the current feed // Get the current feed
@@ -175,30 +209,29 @@ namespace FeedCenter
// Move to the next feed // Move to the next feed
NextFeed(); NextFeed();
// Delete all items
foreach (var item in feedToDelete.Items.ToList())
_database.FeedItems.Remove(item);
// Delete the feed // Delete the feed
_database.Feeds.Remove(feedToDelete); _database.SaveChanges(() => _database.Feeds.Remove(feedToDelete));
// Save // Refresh the database to current settings
_database.SaveChanges(); ResetDatabase();
// Re-initialize the feed display
DisplayFeed();
} }
private void OpenAllFeedItemsOnSinglePage() private async Task OpenAllFeedItemsOnSinglePage()
{ {
var fileName = Path.GetTempFileName() + ".html"; var fileName = Path.GetTempFileName() + ".html";
TextWriter textWriter = new StreamWriter(fileName); 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.Html);
htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Head); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Head);
htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Title); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Title);
htmlTextWriter.Write(_currentFeed.Title); await htmlTextWriter.WriteAsync(_currentFeed.Title);
htmlTextWriter.RenderEndTag(); htmlTextWriter.RenderEndTag();
htmlTextWriter.AddAttribute("http-equiv", "Content-Type"); htmlTextWriter.AddAttribute("http-equiv", "Content-Type");
@@ -226,13 +259,13 @@ namespace FeedCenter
htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, item.Link); htmlTextWriter.AddAttribute(HtmlTextWriterAttribute.Href, item.Link);
htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.A); 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.RenderEndTag();
htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Br); htmlTextWriter.RenderBeginTag(HtmlTextWriterTag.Br);
htmlTextWriter.RenderEndTag(); htmlTextWriter.RenderEndTag();
htmlTextWriter.Write(item.Description); await htmlTextWriter.WriteAsync(item.Description);
htmlTextWriter.RenderEndTag(); htmlTextWriter.RenderEndTag();
@@ -243,12 +276,11 @@ namespace FeedCenter
htmlTextWriter.RenderEndTag(); htmlTextWriter.RenderEndTag();
} }
textWriter.Flush(); await textWriter.FlushAsync();
textWriter.Close(); textWriter.Close();
BrowserCommon.OpenLink(fileName); InstalledBrowser.OpenLink(Settings.Default.Browser, fileName);
MarkAllItemsAsRead(); await MarkAllItemsAsRead();
}
} }
} }

View File

@@ -1,19 +1,20 @@
using Common.Update; using ChrisKaczor.ApplicationUpdate;
using FeedCenter.Properties; using FeedCenter.Properties;
using System.Windows; using System.Windows;
namespace FeedCenter namespace FeedCenter;
{
public partial class MainWindow public partial class MainWindow
{ {
private static void InitializeUpdate() private static void InitializeUpdate()
{ {
UpdateCheck.ApplicationName = Properties.Resources.ApplicationDisplayName; UpdateCheck.Initialize(ServerType.GitHub,
UpdateCheck.UpdateServer = Settings.Default.VersionLocation; Settings.Default.VersionLocation,
UpdateCheck.UpdateFile = Settings.Default.VersionFile; string.Empty,
UpdateCheck.ApplicationShutdown = ApplicationShutdown; Properties.Resources.ApplicationDisplayName,
UpdateCheck.ApplicationCurrentMessage = ApplicationCurrentMessage; ApplicationShutdown,
UpdateCheck.ApplicationUpdateMessage = ApplicationUpdateMessage; ApplicationCurrentMessage,
ApplicationUpdateMessage);
} }
private static bool ApplicationUpdateMessage(string title, string message) private static bool ApplicationUpdateMessage(string title, string message)
@@ -33,8 +34,6 @@ namespace FeedCenter
private void HandleNewVersionLinkClick(object sender, RoutedEventArgs e) private void HandleNewVersionLinkClick(object sender, RoutedEventArgs e)
{ {
// Display update information UpdateCheck.DisplayUpdateInformation(true, Settings.Default.IncludePrerelease);
UpdateCheck.DisplayUpdateInformation(true);
}
} }
} }

View File

@@ -1,14 +1,14 @@
using FeedCenter.Properties; using DebounceThrottle;
using FeedCenter.Properties;
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using Common.Helpers;
namespace FeedCenter namespace FeedCenter;
{
public partial class MainWindow public partial class MainWindow
{ {
private void LoadWindowSettings() private void LoadWindowSettings()
@@ -49,14 +49,14 @@ namespace FeedCenter
{ {
// Set the last window location // Set the last window location
Settings.Default.WindowLocation = new Point(Left, Top); Settings.Default.WindowLocation = new Point(Left, Top);
Settings.Default.Save();
// Set the last window size // Set the last window size
Settings.Default.WindowSize = new Size(Width, Height); Settings.Default.WindowSize = new Size(Width, Height);
Settings.Default.Save();
// Save the dock on the navigation tray // Save the dock on the navigation tray
Settings.Default.ToolbarLocation = NameBasedGrid.NameBasedGrid.GetRow(NavigationToolbarTray) == "TopToolbarRow" ? Dock.Top : Dock.Bottom; Settings.Default.ToolbarLocation = NameBasedGrid.NameBasedGrid.GetRow(NavigationToolbarTray) == "TopToolbarRow" ? Dock.Top : Dock.Bottom;
// Save settings
Settings.Default.Save(); Settings.Default.Save();
} }
@@ -76,13 +76,6 @@ namespace FeedCenter
{ {
base.OnClosing(e); base.OnClosing(e);
// Ditch the worker
if (_feedReadWorker != null)
{
_feedReadWorker.CancelAsync();
_feedReadWorker.Dispose();
}
// Get rid of the timer // Get rid of the timer
TerminateTimer(); TerminateTimer();
@@ -90,58 +83,50 @@ namespace FeedCenter
SaveWindowSettings(); SaveWindowSettings();
// Save settings // Save settings
Settings.Default.Save(); _database.SaveChanges(Settings.Default.Save);
// Save options
_database.SaveChanges();
// Get rid of the notification icon // Get rid of the notification icon
NotificationIcon.Dispose(); NotificationIcon.Dispose();
} }
private DelayedMethod _windowStateDelay; private readonly DebounceDispatcher _updateWindowSettingsDispatcher = new(TimeSpan.FromMilliseconds(500));
private void HandleWindowSizeChanged(object sender, SizeChangedEventArgs e) private void HandleWindowSizeChanged(object sender, SizeChangedEventArgs e)
{ {
if (_windowStateDelay == null) _updateWindowSettingsDispatcher.Debounce(() => Dispatcher.Invoke(UpdateWindowSettings));
_windowStateDelay = new DelayedMethod(500, UpdateWindowSettings);
_windowStateDelay.Reset();
} }
private void HandleWindowLocationChanged(object sender, EventArgs e) private void HandleWindowLocationChanged(object sender, EventArgs e)
{ {
if (_windowStateDelay == null) _updateWindowSettingsDispatcher.Debounce(() => Dispatcher.Invoke(UpdateWindowSettings));
_windowStateDelay = new DelayedMethod(500, UpdateWindowSettings);
_windowStateDelay.Reset();
} }
private void UpdateBorder() private void UpdateBorder()
{ {
var windowInteropHelper = new WindowInteropHelper(this); var windowInteropHelper = new WindowInteropHelper(this);
var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle); var screen = WpfScreenHelper.Screen.FromHandle(windowInteropHelper.Handle);
var rectangle = new System.Drawing.Rectangle var rectangle = new Rect
{ {
X = (int) Left, X = Left,
Y = (int) Top, Y = Top,
Width = (int) Width, Width = Width,
Height = (int) Height Height = Height
}; };
var borderThickness = new Thickness(); var borderThickness = new Thickness();
if (rectangle.Right != screen.WorkingArea.Right) if (!rectangle.Right.Equals(screen.WorkingArea.Right))
borderThickness.Right = 1; borderThickness.Right = 1;
if (rectangle.Left != screen.WorkingArea.Left) if (!rectangle.Left.Equals(screen.WorkingArea.Left))
borderThickness.Left = 1; borderThickness.Left = 1;
if (rectangle.Top != screen.WorkingArea.Top) if (!rectangle.Top.Equals(screen.WorkingArea.Top))
borderThickness.Top = 1; borderThickness.Top = 1;
if (rectangle.Bottom != screen.WorkingArea.Bottom) if (!rectangle.Bottom.Equals(screen.WorkingArea.Bottom))
borderThickness.Bottom = 1; borderThickness.Bottom = 1;
WindowBorder.BorderThickness = borderThickness; WindowBorder.BorderThickness = borderThickness;
@@ -157,6 +142,7 @@ namespace FeedCenter
} }
private bool _activated; private bool _activated;
protected override void OnActivated(EventArgs e) protected override void OnActivated(EventArgs e)
{ {
base.OnActivated(e); base.OnActivated(e);
@@ -177,4 +163,3 @@ namespace FeedCenter
Settings.Default.PropertyChanged += HandlePropertyChanged; Settings.Default.PropertyChanged += HandlePropertyChanged;
} }
} }
}

View File

@@ -1,34 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FeedCenter
{
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
public partial class FeedCenterEntities : DbContext
{
public FeedCenterEntities()
: base("name=FeedCenterEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Feed> Feeds { get; set; }
public virtual DbSet<FeedAction> FeedActions { get; set; }
public virtual DbSet<FeedItem> FeedItems { get; set; }
public virtual DbSet<Setting> Settings { get; set; }
}
}

View File

@@ -1,636 +0,0 @@
<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF6.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
const string inputFile = @"Model.edmx";
var textTransform = DynamicTextTransformation.Create(this);
var code = new CodeGenerationTools(this);
var ef = new MetadataTools(this);
var typeMapper = new TypeMapper(code, ef, textTransform.Errors);
var loader = new EdmMetadataLoader(textTransform.Host, textTransform.Errors);
var itemCollection = loader.CreateEdmItemCollection(inputFile);
var modelNamespace = loader.GetModelNamespace(inputFile);
var codeStringGenerator = new CodeStringGenerator(code, typeMapper, ef);
var container = itemCollection.OfType<EntityContainer>().FirstOrDefault();
if (container == null)
{
return string.Empty;
}
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------
<#
var codeNamespace = code.VsNamespaceSuggestion();
if (!String.IsNullOrEmpty(codeNamespace))
{
#>
namespace <#=code.EscapeNamespace(codeNamespace)#>
{
<#
PushIndent(" ");
}
#>
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
<#
if (container.FunctionImports.Any())
{
#>
using System.Data.Entity.Core.Objects;
using System.Linq;
<#
}
#>
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext
{
public <#=code.Escape(container)#>()
: base("name=<#=container.Name#>")
{
<#
if (!loader.IsLazyLoadingEnabled(container))
{
#>
this.Configuration.LazyLoadingEnabled = false;
<#
}
foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
{
// Note: the DbSet members are defined below such that the getter and
// setter always have the same accessibility as the DbSet definition
if (Accessibility.ForReadOnlyProperty(entitySet) != "public")
{
#>
<#=codeStringGenerator.DbSetInitializer(entitySet)#>
<#
}
}
#>
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
<#
foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
{
#>
<#=codeStringGenerator.DbSet(entitySet)#>
<#
}
foreach (var edmFunction in container.FunctionImports)
{
WriteFunctionImport(typeMapper, codeStringGenerator, edmFunction, modelNamespace, includeMergeOption: false);
}
#>
}
<#
if (!String.IsNullOrEmpty(codeNamespace))
{
PopIndent();
#>
}
<#
}
#>
<#+
private void WriteFunctionImport(TypeMapper typeMapper, CodeStringGenerator codeStringGenerator, EdmFunction edmFunction, string modelNamespace, bool includeMergeOption)
{
if (typeMapper.IsComposable(edmFunction))
{
#>
[DbFunction("<#=edmFunction.NamespaceName#>", "<#=edmFunction.Name#>")]
<#=codeStringGenerator.ComposableFunctionMethod(edmFunction, modelNamespace)#>
{
<#+
codeStringGenerator.WriteFunctionParameters(edmFunction, WriteFunctionParameter);
#>
<#=codeStringGenerator.ComposableCreateQuery(edmFunction, modelNamespace)#>
}
<#+
}
else
{
#>
<#=codeStringGenerator.FunctionMethod(edmFunction, modelNamespace, includeMergeOption)#>
{
<#+
codeStringGenerator.WriteFunctionParameters(edmFunction, WriteFunctionParameter);
#>
<#=codeStringGenerator.ExecuteFunction(edmFunction, modelNamespace, includeMergeOption)#>
}
<#+
if (typeMapper.GenerateMergeOptionFunction(edmFunction, includeMergeOption))
{
WriteFunctionImport(typeMapper, codeStringGenerator, edmFunction, modelNamespace, includeMergeOption: true);
}
}
}
public void WriteFunctionParameter(string name, string isNotNull, string notNullInit, string nullInit)
{
#>
var <#=name#> = <#=isNotNull#> ?
<#=notNullInit#> :
<#=nullInit#>;
<#+
}
public const string TemplateId = "CSharp_DbContext_Context_EF6";
public class CodeStringGenerator
{
private readonly CodeGenerationTools _code;
private readonly TypeMapper _typeMapper;
private readonly MetadataTools _ef;
public CodeStringGenerator(CodeGenerationTools code, TypeMapper typeMapper, MetadataTools ef)
{
ArgumentNotNull(code, "code");
ArgumentNotNull(typeMapper, "typeMapper");
ArgumentNotNull(ef, "ef");
_code = code;
_typeMapper = typeMapper;
_ef = ef;
}
public string Property(EdmProperty edmProperty)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2} {{ {3}get; {4}set; }}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
}
public string NavigationProperty(NavigationProperty navProp)
{
var endType = _typeMapper.GetTypeName(navProp.ToEndMember.GetEntityType());
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2} {{ {3}get; {4}set; }}",
AccessibilityAndVirtual(Accessibility.ForNavigationProperty(navProp)),
navProp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many ? ("ICollection<" + endType + ">") : endType,
_code.Escape(navProp),
_code.SpaceAfter(Accessibility.ForGetter(navProp)),
_code.SpaceAfter(Accessibility.ForSetter(navProp)));
}
public string AccessibilityAndVirtual(string accessibility)
{
return accessibility + (accessibility != "private" ? " virtual" : "");
}
public string EntityClassOpening(EntityType entity)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1}partial class {2}{3}",
Accessibility.ForType(entity),
_code.SpaceAfter(_code.AbstractOption(entity)),
_code.Escape(entity),
_code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
}
public string EnumOpening(SimpleType enumType)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} enum {1} : {2}",
Accessibility.ForType(enumType),
_code.Escape(enumType),
_code.Escape(_typeMapper.UnderlyingClrType(enumType)));
}
public void WriteFunctionParameters(EdmFunction edmFunction, Action<string, string, string, string> writeParameter)
{
var parameters = FunctionImportParameter.Create(edmFunction.Parameters, _code, _ef);
foreach (var parameter in parameters.Where(p => p.NeedsLocalVariable))
{
var isNotNull = parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null";
var notNullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", " + parameter.FunctionParameterName + ")";
var nullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", typeof(" + TypeMapper.FixNamespaces(parameter.RawClrTypeName) + "))";
writeParameter(parameter.LocalVariableName, isNotNull, notNullInit, nullInit);
}
}
public string ComposableFunctionMethod(EdmFunction edmFunction, string modelNamespace)
{
var parameters = _typeMapper.GetParameters(edmFunction);
return string.Format(
CultureInfo.InvariantCulture,
"{0} IQueryable<{1}> {2}({3})",
AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction)),
_typeMapper.GetTypeName(_typeMapper.GetReturnType(edmFunction), modelNamespace),
_code.Escape(edmFunction),
string.Join(", ", parameters.Select(p => TypeMapper.FixNamespaces(p.FunctionParameterType) + " " + p.FunctionParameterName).ToArray()));
}
public string ComposableCreateQuery(EdmFunction edmFunction, string modelNamespace)
{
var parameters = _typeMapper.GetParameters(edmFunction);
return string.Format(
CultureInfo.InvariantCulture,
"return ((IObjectContextAdapter)this).ObjectContext.CreateQuery<{0}>(\"[{1}].[{2}]({3})\"{4});",
_typeMapper.GetTypeName(_typeMapper.GetReturnType(edmFunction), modelNamespace),
edmFunction.NamespaceName,
edmFunction.Name,
string.Join(", ", parameters.Select(p => "@" + p.EsqlParameterName).ToArray()),
_code.StringBefore(", ", string.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray())));
}
public string FunctionMethod(EdmFunction edmFunction, string modelNamespace, bool includeMergeOption)
{
var parameters = _typeMapper.GetParameters(edmFunction);
var returnType = _typeMapper.GetReturnType(edmFunction);
var paramList = String.Join(", ", parameters.Select(p => TypeMapper.FixNamespaces(p.FunctionParameterType) + " " + p.FunctionParameterName).ToArray());
if (includeMergeOption)
{
paramList = _code.StringAfter(paramList, ", ") + "MergeOption mergeOption";
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2}({3})",
AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction)),
returnType == null ? "int" : "ObjectResult<" + _typeMapper.GetTypeName(returnType, modelNamespace) + ">",
_code.Escape(edmFunction),
paramList);
}
public string ExecuteFunction(EdmFunction edmFunction, string modelNamespace, bool includeMergeOption)
{
var parameters = _typeMapper.GetParameters(edmFunction);
var returnType = _typeMapper.GetReturnType(edmFunction);
var callParams = _code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray()));
if (includeMergeOption)
{
callParams = ", mergeOption" + callParams;
}
return string.Format(
CultureInfo.InvariantCulture,
"return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction{0}(\"{1}\"{2});",
returnType == null ? "" : "<" + _typeMapper.GetTypeName(returnType, modelNamespace) + ">",
edmFunction.Name,
callParams);
}
public string DbSet(EntitySet entitySet)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} virtual DbSet<{1}> {2} {{ get; set; }}",
Accessibility.ForReadOnlyProperty(entitySet),
_typeMapper.GetTypeName(entitySet.ElementType),
_code.Escape(entitySet));
}
public string DbSetInitializer(EntitySet entitySet)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} = Set<{1}>();",
_code.Escape(entitySet),
_typeMapper.GetTypeName(entitySet.ElementType));
}
public string UsingDirectives(bool inHeader, bool includeCollections = true)
{
return inHeader == string.IsNullOrEmpty(_code.VsNamespaceSuggestion())
? string.Format(
CultureInfo.InvariantCulture,
"{0}using System;{1}" +
"{2}",
inHeader ? Environment.NewLine : "",
includeCollections ? (Environment.NewLine + "using System.Collections.Generic;") : "",
inHeader ? "" : Environment.NewLine)
: "";
}
}
public class TypeMapper
{
private const string ExternalTypeNameAttributeName = @"http://schemas.microsoft.com/ado/2006/04/codegeneration:ExternalTypeName";
private readonly System.Collections.IList _errors;
private readonly CodeGenerationTools _code;
private readonly MetadataTools _ef;
public static string FixNamespaces(string typeName)
{
return typeName.Replace("System.Data.Spatial.", "System.Data.Entity.Spatial.");
}
public TypeMapper(CodeGenerationTools code, MetadataTools ef, System.Collections.IList errors)
{
ArgumentNotNull(code, "code");
ArgumentNotNull(ef, "ef");
ArgumentNotNull(errors, "errors");
_code = code;
_ef = ef;
_errors = errors;
}
public string GetTypeName(TypeUsage typeUsage)
{
return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace: null);
}
public string GetTypeName(EdmType edmType)
{
return GetTypeName(edmType, isNullable: null, modelNamespace: null);
}
public string GetTypeName(TypeUsage typeUsage, string modelNamespace)
{
return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace);
}
public string GetTypeName(EdmType edmType, string modelNamespace)
{
return GetTypeName(edmType, isNullable: null, modelNamespace: modelNamespace);
}
public string GetTypeName(EdmType edmType, bool? isNullable, string modelNamespace)
{
if (edmType == null)
{
return null;
}
var collectionType = edmType as CollectionType;
if (collectionType != null)
{
return String.Format(CultureInfo.InvariantCulture, "ICollection<{0}>", GetTypeName(collectionType.TypeUsage, modelNamespace));
}
var typeName = _code.Escape(edmType.MetadataProperties
.Where(p => p.Name == ExternalTypeNameAttributeName)
.Select(p => (string)p.Value)
.FirstOrDefault())
?? (modelNamespace != null && edmType.NamespaceName != modelNamespace ?
_code.CreateFullName(_code.EscapeNamespace(edmType.NamespaceName), _code.Escape(edmType)) :
_code.Escape(edmType));
if (edmType is StructuralType)
{
return typeName;
}
if (edmType is SimpleType)
{
var clrType = UnderlyingClrType(edmType);
if (!IsEnumType(edmType))
{
typeName = _code.Escape(clrType);
}
typeName = FixNamespaces(typeName);
return clrType.IsValueType && isNullable == true ?
String.Format(CultureInfo.InvariantCulture, "Nullable<{0}>", typeName) :
typeName;
}
throw new ArgumentException("edmType");
}
public Type UnderlyingClrType(EdmType edmType)
{
ArgumentNotNull(edmType, "edmType");
var primitiveType = edmType as PrimitiveType;
if (primitiveType != null)
{
return primitiveType.ClrEquivalentType;
}
if (IsEnumType(edmType))
{
return GetEnumUnderlyingType(edmType).ClrEquivalentType;
}
return typeof(object);
}
public object GetEnumMemberValue(MetadataItem enumMember)
{
ArgumentNotNull(enumMember, "enumMember");
var valueProperty = enumMember.GetType().GetProperty("Value");
return valueProperty == null ? null : valueProperty.GetValue(enumMember, null);
}
public string GetEnumMemberName(MetadataItem enumMember)
{
ArgumentNotNull(enumMember, "enumMember");
var nameProperty = enumMember.GetType().GetProperty("Name");
return nameProperty == null ? null : (string)nameProperty.GetValue(enumMember, null);
}
public System.Collections.IEnumerable GetEnumMembers(EdmType enumType)
{
ArgumentNotNull(enumType, "enumType");
var membersProperty = enumType.GetType().GetProperty("Members");
return membersProperty != null
? (System.Collections.IEnumerable)membersProperty.GetValue(enumType, null)
: Enumerable.Empty<MetadataItem>();
}
public bool EnumIsFlags(EdmType enumType)
{
ArgumentNotNull(enumType, "enumType");
var isFlagsProperty = enumType.GetType().GetProperty("IsFlags");
return isFlagsProperty != null && (bool)isFlagsProperty.GetValue(enumType, null);
}
public bool IsEnumType(GlobalItem edmType)
{
ArgumentNotNull(edmType, "edmType");
return edmType.GetType().Name == "EnumType";
}
public PrimitiveType GetEnumUnderlyingType(EdmType enumType)
{
ArgumentNotNull(enumType, "enumType");
return (PrimitiveType)enumType.GetType().GetProperty("UnderlyingType").GetValue(enumType, null);
}
public string CreateLiteral(object value)
{
if (value == null || value.GetType() != typeof(TimeSpan))
{
return _code.CreateLiteral(value);
}
return string.Format(CultureInfo.InvariantCulture, "new TimeSpan({0})", ((TimeSpan)value).Ticks);
}
public bool VerifyCaseInsensitiveTypeUniqueness(IEnumerable<string> types, string sourceFile)
{
ArgumentNotNull(types, "types");
ArgumentNotNull(sourceFile, "sourceFile");
var hash = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
if (types.Any(item => !hash.Add(item)))
{
_errors.Add(
new CompilerError(sourceFile, -1, -1, "6023",
String.Format(CultureInfo.CurrentCulture, CodeGenerationTools.GetResourceString("Template_CaseInsensitiveTypeConflict"))));
return false;
}
return true;
}
public IEnumerable<SimpleType> GetEnumItemsToGenerate(IEnumerable<GlobalItem> itemCollection)
{
return GetItemsToGenerate<SimpleType>(itemCollection)
.Where(e => IsEnumType(e));
}
public IEnumerable<T> GetItemsToGenerate<T>(IEnumerable<GlobalItem> itemCollection) where T: EdmType
{
return itemCollection
.OfType<T>()
.Where(i => !i.MetadataProperties.Any(p => p.Name == ExternalTypeNameAttributeName))
.OrderBy(i => i.Name);
}
public IEnumerable<string> GetAllGlobalItems(IEnumerable<GlobalItem> itemCollection)
{
return itemCollection
.Where(i => i is EntityType || i is ComplexType || i is EntityContainer || IsEnumType(i))
.Select(g => GetGlobalItemName(g));
}
public string GetGlobalItemName(GlobalItem item)
{
if (item is EdmType)
{
return ((EdmType)item).Name;
}
else
{
return ((EntityContainer)item).Name;
}
}
public IEnumerable<EdmProperty> GetSimpleProperties(EntityType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetSimpleProperties(ComplexType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetComplexProperties(EntityType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetComplexProperties(ComplexType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(EntityType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
}
public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(ComplexType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
}
public IEnumerable<NavigationProperty> GetNavigationProperties(EntityType type)
{
return type.NavigationProperties.Where(np => np.DeclaringType == type);
}
public IEnumerable<NavigationProperty> GetCollectionNavigationProperties(EntityType type)
{
return type.NavigationProperties.Where(np => np.DeclaringType == type && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many);
}
public FunctionParameter GetReturnParameter(EdmFunction edmFunction)
{
ArgumentNotNull(edmFunction, "edmFunction");
var returnParamsProperty = edmFunction.GetType().GetProperty("ReturnParameters");
return returnParamsProperty == null
? edmFunction.ReturnParameter
: ((IEnumerable<FunctionParameter>)returnParamsProperty.GetValue(edmFunction, null)).FirstOrDefault();
}
public bool IsComposable(EdmFunction edmFunction)
{
ArgumentNotNull(edmFunction, "edmFunction");
var isComposableProperty = edmFunction.GetType().GetProperty("IsComposableAttribute");
return isComposableProperty != null && (bool)isComposableProperty.GetValue(edmFunction, null);
}
public IEnumerable<FunctionImportParameter> GetParameters(EdmFunction edmFunction)
{
return FunctionImportParameter.Create(edmFunction.Parameters, _code, _ef);
}
public TypeUsage GetReturnType(EdmFunction edmFunction)
{
var returnParam = GetReturnParameter(edmFunction);
return returnParam == null ? null : _ef.GetElementType(returnParam.TypeUsage);
}
public bool GenerateMergeOptionFunction(EdmFunction edmFunction, bool includeMergeOption)
{
var returnType = GetReturnType(edmFunction);
return !includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType;
}
}
public static void ArgumentNotNull<T>(T arg, string name) where T : class
{
if (arg == null)
{
throw new ArgumentNullException(name);
}
}
#>

View File

@@ -1,10 +0,0 @@
// T4 code generation is enabled for model 'D:\Code\Personal\FeedCenter\Application\Model.edmx'.
// To enable legacy code generation, change the value of the 'Code Generation Strategy' designer
// property to 'Legacy ObjectContext'. This property is available in the Properties Window when the model
// is open in the designer.
// If no context and entity classes have been generated, it may be because you created an empty model but
// have not yet chosen which version of Entity Framework to use. To generate a context class and entity
// classes for your model, open the model in the designer, right-click on the designer surface, and
// select 'Update Model from Database...', 'Generate Database from Model...', or 'Add Code Generation
// Item...'.

View File

@@ -1,9 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

View File

@@ -1,383 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="3.0" xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx">
<!-- EF Runtime content -->
<edmx:Runtime>
<!-- SSDL content -->
<edmx:StorageModels>
<Schema Namespace="FeedCenterModel.Store" Provider="System.Data.SqlServerCe.4.0" ProviderManifestToken="4.0" Alias="Self" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation" xmlns="http://schemas.microsoft.com/ado/2009/11/edm/ssdl">
<EntityType Name="Category">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="uniqueidentifier" Nullable="false" />
<Property Name="Name" Type="nvarchar" MaxLength="1000" Nullable="false" />
</EntityType>
<EntityType Name="Feed">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="uniqueidentifier" Nullable="false" />
<Property Name="Name" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Title" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Source" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Link" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Description" Type="ntext" Nullable="false" />
<Property Name="LastChecked" Type="datetime" Nullable="false" />
<Property Name="CheckInterval" Type="int" Nullable="false" />
<Property Name="Enabled" Type="bit" Nullable="false" />
<Property Name="Authenticate" Type="bit" Nullable="false" />
<Property Name="Username" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Password" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Domain" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="LastReadResult" Type="int" Nullable="false" />
<Property Name="LastUpdated" Type="datetime" Nullable="false" />
<Property Name="ItemComparison" Type="tinyint" Nullable="false" />
<Property Name="CategoryID" Type="uniqueidentifier" Nullable="false" />
<Property Name="MultipleOpenAction" Type="int" Nullable="false" />
</EntityType>
<EntityType Name="FeedAction">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="uniqueidentifier" Nullable="false" />
<Property Name="FeedID" Type="uniqueidentifier" Nullable="false" />
<Property Name="Field" Type="int" Nullable="false" />
<Property Name="Search" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Replace" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Sequence" Type="int" Nullable="false" />
</EntityType>
<EntityType Name="FeedItem">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="uniqueidentifier" Nullable="false" />
<Property Name="FeedID" Type="uniqueidentifier" Nullable="false" />
<Property Name="Title" Type="ntext" Nullable="false" />
<Property Name="Link" Type="nvarchar" MaxLength="1000" Nullable="false" />
<Property Name="Description" Type="ntext" Nullable="false" />
<Property Name="BeenRead" Type="bit" Nullable="false" />
<Property Name="LastFound" Type="datetime" Nullable="false" />
<Property Name="New" Type="bit" Nullable="false" />
<Property Name="Sequence" Type="int" Nullable="false" />
<Property Name="Guid" Type="nvarchar" MaxLength="1000" Nullable="false" />
</EntityType>
<EntityType Name="Setting">
<Key>
<PropertyRef Name="Name" />
<PropertyRef Name="Version" />
</Key>
<Property Name="Name" Type="nvarchar" MaxLength="500" Nullable="false" />
<Property Name="Value" Type="nvarchar" MaxLength="3500" Nullable="false" />
<Property Name="Version" Type="nvarchar" MaxLength="50" Nullable="false" />
</EntityType>
<Association Name="FK_Feed_Category">
<End Role="Category" Type="Self.Category" Multiplicity="1" />
<End Role="Feed" Type="Self.Feed" Multiplicity="*" />
<ReferentialConstraint>
<Principal Role="Category">
<PropertyRef Name="ID" />
</Principal>
<Dependent Role="Feed">
<PropertyRef Name="CategoryID" />
</Dependent>
</ReferentialConstraint>
</Association>
<Association Name="FK_FeedAction_Feed">
<End Role="Feed" Type="Self.Feed" Multiplicity="1">
<OnDelete Action="Cascade" />
</End>
<End Role="FeedAction" Type="Self.FeedAction" Multiplicity="*" />
<ReferentialConstraint>
<Principal Role="Feed">
<PropertyRef Name="ID" />
</Principal>
<Dependent Role="FeedAction">
<PropertyRef Name="FeedID" />
</Dependent>
</ReferentialConstraint>
</Association>
<Association Name="FK_FeedItem_Feed">
<End Role="Feed" Type="Self.Feed" Multiplicity="1">
<OnDelete Action="Cascade" />
</End>
<End Role="FeedItem" Type="Self.FeedItem" Multiplicity="*" />
<ReferentialConstraint>
<Principal Role="Feed">
<PropertyRef Name="ID" />
</Principal>
<Dependent Role="FeedItem">
<PropertyRef Name="FeedID" />
</Dependent>
</ReferentialConstraint>
</Association>
<EntityContainer Name="FeedCenterModelStoreContainer">
<EntitySet Name="Category" EntityType="Self.Category" store:Type="Tables" />
<EntitySet Name="Feed" EntityType="Self.Feed" store:Type="Tables" />
<EntitySet Name="FeedAction" EntityType="Self.FeedAction" store:Type="Tables" />
<EntitySet Name="FeedItem" EntityType="Self.FeedItem" store:Type="Tables" />
<EntitySet Name="Setting" EntityType="Self.Setting" store:Type="Tables" />
<AssociationSet Name="FK_Feed_Category" Association="Self.FK_Feed_Category">
<End Role="Category" EntitySet="Category" />
<End Role="Feed" EntitySet="Feed" />
</AssociationSet>
<AssociationSet Name="FK_FeedAction_Feed" Association="Self.FK_FeedAction_Feed">
<End Role="Feed" EntitySet="Feed" />
<End Role="FeedAction" EntitySet="FeedAction" />
</AssociationSet>
<AssociationSet Name="FK_FeedItem_Feed" Association="Self.FK_FeedItem_Feed">
<End Role="Feed" EntitySet="Feed" />
<End Role="FeedItem" EntitySet="FeedItem" />
</AssociationSet>
</EntityContainer>
</Schema></edmx:StorageModels>
<!-- CSDL content -->
<edmx:ConceptualModels>
<Schema Namespace="FeedCenterModel" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="FeedCenterEntities" annotation:LazyLoadingEnabled="true" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation">
<EntitySet Name="Categories" EntityType="FeedCenterModel.Category" />
<EntitySet Name="Feeds" EntityType="FeedCenterModel.Feed" />
<EntitySet Name="FeedActions" EntityType="FeedCenterModel.FeedAction" />
<EntitySet Name="FeedItems" EntityType="FeedCenterModel.FeedItem" />
<EntitySet Name="Settings" EntityType="FeedCenterModel.Setting" />
<AssociationSet Name="FK_Feed_Category" Association="FeedCenterModel.FK_Feed_Category">
<End Role="Category" EntitySet="Categories" />
<End Role="Feed" EntitySet="Feeds" />
</AssociationSet>
<AssociationSet Name="FK_FeedAction_Feed" Association="FeedCenterModel.FK_FeedAction_Feed">
<End Role="Feed" EntitySet="Feeds" />
<End Role="FeedAction" EntitySet="FeedActions" />
</AssociationSet>
<AssociationSet Name="FK_FeedItem_Feed" Association="FeedCenterModel.FK_FeedItem_Feed">
<End Role="Feed" EntitySet="Feeds" />
<End Role="FeedItem" EntitySet="FeedItems" />
</AssociationSet>
</EntityContainer>
<EntityType Name="Category">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Type="Guid" Name="ID" Nullable="false" />
<Property Type="String" Name="Name" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" />
<NavigationProperty Name="Feeds" Relationship="FeedCenterModel.FK_Feed_Category" FromRole="Category" ToRole="Feed" />
</EntityType>
<EntityType Name="Feed">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Type="Guid" Name="ID" Nullable="false" />
<Property Type="String" Name="Name" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Title" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Source" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Link" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Description" Nullable="false" MaxLength="Max" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="DateTime" Name="LastChecked" Nullable="false" DefaultValue="1900-01-01 00:00:00.000Z" Precision="3" />
<Property Type="Int32" Name="CheckInterval" Nullable="false" DefaultValue="60" />
<Property Type="Boolean" Name="Enabled" Nullable="false" DefaultValue="True" />
<Property Type="Boolean" Name="Authenticate" Nullable="false" DefaultValue="False" />
<Property Type="String" Name="Username" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Password" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Domain" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="FeedCenterModel.FeedReadResult" Name="LastReadResult" Nullable="false" />
<Property Type="DateTime" Name="LastUpdated" Nullable="false" DefaultValue="1900-01-01 00:00:00.000Z" Precision="3" />
<Property Type="FeedCenterModel.FeedItemComparison" Name="ItemComparison" Nullable="false" />
<Property Type="Guid" Name="CategoryID" Nullable="false" a:GetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" a:SetterAccess="Private" />
<NavigationProperty Name="Category" Relationship="FeedCenterModel.FK_Feed_Category" FromRole="Feed" ToRole="Category" />
<NavigationProperty Name="Actions" Relationship="FeedCenterModel.FK_FeedAction_Feed" FromRole="Feed" ToRole="FeedAction" />
<NavigationProperty Name="Items" Relationship="FeedCenterModel.FK_FeedItem_Feed" FromRole="Feed" ToRole="FeedItem" />
<Property Type="FeedCenterModel.MultipleOpenAction" Name="MultipleOpenAction" Nullable="false" />
</EntityType>
<EntityType Name="FeedAction">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Type="Guid" Name="ID" Nullable="false" />
<Property Type="Guid" Name="FeedID" Nullable="false" />
<Property Type="Int32" Name="Field" Nullable="false" />
<Property Type="String" Name="Search" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" />
<Property Type="String" Name="Replace" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" />
<Property Type="Int32" Name="Sequence" Nullable="false" />
<NavigationProperty Name="Feed" Relationship="FeedCenterModel.FK_FeedAction_Feed" FromRole="FeedAction" ToRole="Feed" />
</EntityType>
<EntityType Name="FeedItem">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Type="Guid" Name="ID" Nullable="false" />
<Property Type="Guid" Name="FeedID" Nullable="false" />
<Property Type="String" Name="Title" Nullable="false" MaxLength="Max" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Link" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="String" Name="Description" Nullable="false" MaxLength="Max" FixedLength="false" Unicode="true" DefaultValue="" />
<Property Type="Boolean" Name="BeenRead" Nullable="false" />
<Property Type="DateTime" Name="LastFound" Nullable="false" DefaultValue="1900-01-01 00:00:00.000Z" Precision="3" />
<Property Type="Boolean" Name="New" Nullable="false" />
<NavigationProperty Name="Feed" Relationship="FeedCenterModel.FK_FeedItem_Feed" FromRole="FeedItem" ToRole="Feed" />
<Property Type="String" Name="Guid" Nullable="false" MaxLength="1000" FixedLength="false" Unicode="true" />
<Property Type="Int32" Name="Sequence" Nullable="false" />
</EntityType>
<EntityType Name="Setting">
<Key>
<PropertyRef Name="Name" />
<PropertyRef Name="Version" />
</Key>
<Property Type="String" Name="Name" Nullable="false" MaxLength="500" FixedLength="false" Unicode="true" />
<Property Type="String" Name="Value" Nullable="false" MaxLength="3500" FixedLength="false" Unicode="true" />
<Property Type="String" Name="Version" Nullable="false" MaxLength="50" FixedLength="false" Unicode="true" />
</EntityType>
<Association Name="FK_Feed_Category">
<End Type="FeedCenterModel.Category" Role="Category" Multiplicity="1" />
<End Type="FeedCenterModel.Feed" Role="Feed" Multiplicity="*" />
<ReferentialConstraint>
<Principal Role="Category">
<PropertyRef Name="ID" />
</Principal>
<Dependent Role="Feed">
<PropertyRef Name="CategoryID" />
</Dependent>
</ReferentialConstraint>
</Association>
<Association Name="FK_FeedAction_Feed">
<End Type="FeedCenterModel.Feed" Role="Feed" Multiplicity="1" />
<End Type="FeedCenterModel.FeedAction" Role="FeedAction" Multiplicity="*" />
<ReferentialConstraint>
<Principal Role="Feed">
<PropertyRef Name="ID" />
</Principal>
<Dependent Role="FeedAction">
<PropertyRef Name="FeedID" />
</Dependent>
</ReferentialConstraint>
</Association>
<Association Name="FK_FeedItem_Feed">
<End Type="FeedCenterModel.Feed" Role="Feed" Multiplicity="1" />
<End Type="FeedCenterModel.FeedItem" Role="FeedItem" Multiplicity="*" />
<ReferentialConstraint>
<Principal Role="Feed">
<PropertyRef Name="ID" />
</Principal>
<Dependent Role="FeedItem">
<PropertyRef Name="FeedID" />
</Dependent>
</ReferentialConstraint>
</Association>
<EnumType Name="FeedReadResult" a:ExternalTypeName="FeedCenter.FeedReadResult" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<EnumType Name="FeedItemComparison" UnderlyingType="Byte" a:ExternalTypeName="FeedCenter.FeedItemComparison" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<EnumType Name="MultipleOpenAction" a:ExternalTypeName="FeedCenter.MultipleOpenAction" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
</Schema>
</edmx:ConceptualModels>
<!-- C-S mapping content -->
<edmx:Mappings>
<Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2009/11/mapping/cs">
<EntityContainerMapping StorageEntityContainer="FeedCenterModelStoreContainer" CdmEntityContainer="FeedCenterEntities">
<EntitySetMapping Name="Categories">
<EntityTypeMapping TypeName="FeedCenterModel.Category">
<MappingFragment StoreEntitySet="Category">
<ScalarProperty Name="Name" ColumnName="Name" />
<ScalarProperty Name="ID" ColumnName="ID" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<EntitySetMapping Name="Feeds">
<EntityTypeMapping TypeName="FeedCenterModel.Feed">
<MappingFragment StoreEntitySet="Feed">
<ScalarProperty Name="MultipleOpenAction" ColumnName="MultipleOpenAction" />
<ScalarProperty Name="CategoryID" ColumnName="CategoryID" />
<ScalarProperty Name="ItemComparison" ColumnName="ItemComparison" />
<ScalarProperty Name="LastUpdated" ColumnName="LastUpdated" />
<ScalarProperty Name="LastReadResult" ColumnName="LastReadResult" />
<ScalarProperty Name="Domain" ColumnName="Domain" />
<ScalarProperty Name="Password" ColumnName="Password" />
<ScalarProperty Name="Username" ColumnName="Username" />
<ScalarProperty Name="Authenticate" ColumnName="Authenticate" />
<ScalarProperty Name="Enabled" ColumnName="Enabled" />
<ScalarProperty Name="CheckInterval" ColumnName="CheckInterval" />
<ScalarProperty Name="LastChecked" ColumnName="LastChecked" />
<ScalarProperty Name="Description" ColumnName="Description" />
<ScalarProperty Name="Link" ColumnName="Link" />
<ScalarProperty Name="Source" ColumnName="Source" />
<ScalarProperty Name="Title" ColumnName="Title" />
<ScalarProperty Name="Name" ColumnName="Name" />
<ScalarProperty Name="ID" ColumnName="ID" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<EntitySetMapping Name="FeedActions">
<EntityTypeMapping TypeName="FeedCenterModel.FeedAction">
<MappingFragment StoreEntitySet="FeedAction">
<ScalarProperty Name="Sequence" ColumnName="Sequence" />
<ScalarProperty Name="Replace" ColumnName="Replace" />
<ScalarProperty Name="Search" ColumnName="Search" />
<ScalarProperty Name="Field" ColumnName="Field" />
<ScalarProperty Name="FeedID" ColumnName="FeedID" />
<ScalarProperty Name="ID" ColumnName="ID" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<EntitySetMapping Name="FeedItems">
<EntityTypeMapping TypeName="FeedCenterModel.FeedItem">
<MappingFragment StoreEntitySet="FeedItem">
<ScalarProperty Name="Sequence" ColumnName="Sequence" />
<ScalarProperty Name="Guid" ColumnName="Guid" />
<ScalarProperty Name="New" ColumnName="New" />
<ScalarProperty Name="LastFound" ColumnName="LastFound" />
<ScalarProperty Name="BeenRead" ColumnName="BeenRead" />
<ScalarProperty Name="Description" ColumnName="Description" />
<ScalarProperty Name="Link" ColumnName="Link" />
<ScalarProperty Name="Title" ColumnName="Title" />
<ScalarProperty Name="FeedID" ColumnName="FeedID" />
<ScalarProperty Name="ID" ColumnName="ID" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<EntitySetMapping Name="Settings">
<EntityTypeMapping TypeName="FeedCenterModel.Setting">
<MappingFragment StoreEntitySet="Setting">
<ScalarProperty Name="Version" ColumnName="Version" />
<ScalarProperty Name="Value" ColumnName="Value" />
<ScalarProperty Name="Name" ColumnName="Name" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
</EntityContainerMapping>
</Mapping>
</edmx:Mappings>
</edmx:Runtime>
<!-- EF Designer content (DO NOT EDIT MANUALLY BELOW HERE) -->
<Designer xmlns="http://schemas.microsoft.com/ado/2009/11/edmx">
<Connection>
<DesignerInfoPropertySet>
<DesignerProperty Name="MetadataArtifactProcessing" Value="EmbedInOutputAssembly" />
</DesignerInfoPropertySet>
</Connection>
<Options>
<DesignerInfoPropertySet>
<DesignerProperty Name="ValidateOnBuild" Value="true" />
<DesignerProperty Name="EnablePluralization" Value="True" />
<DesignerProperty Name="IncludeForeignKeysInModel" Value="True" />
<DesignerProperty Name="CodeGenerationStrategy" Value="None" />
<DesignerProperty Name="UseLegacyProvider" Value="False" />
</DesignerInfoPropertySet>
</Options>
<!-- Diagram content (shape and connector positions) -->
<Diagrams>
<Diagram Name="Model">
<EntityTypeShape EntityType="FeedCenterModel.Category" Width="1.5" PointX="0.75" PointY="3.375" Height="1.5956835937499996" />
<EntityTypeShape EntityType="FeedCenterModel.Feed" Width="1.5" PointX="3" PointY="1.625" Height="5.057109375" />
<EntityTypeShape EntityType="FeedCenterModel.FeedAction" Width="1.5" PointX="5.25" PointY="1.125" Height="2.3648893229166674" />
<EntityTypeShape EntityType="FeedCenterModel.FeedItem" Width="1.5" PointX="5.25" PointY="4.25" Height="2.9417936197916656" />
<EntityTypeShape EntityType="FeedCenterModel.Setting" Width="1.5" PointX="7.75" PointY="0.75" Height="1.5956835937499996" />
<AssociationConnector Association="FeedCenterModel.FK_Feed_Category">
<ConnectorPoint PointX="2.25" PointY="4.172841796875" />
<ConnectorPoint PointX="3" PointY="4.172841796875" />
</AssociationConnector>
<AssociationConnector Association="FeedCenterModel.FK_FeedAction_Feed">
<ConnectorPoint PointX="4.5" PointY="2.5574446614583337" />
<ConnectorPoint PointX="5.25" PointY="2.5574446614583337" />
</AssociationConnector>
<AssociationConnector Association="FeedCenterModel.FK_FeedItem_Feed">
<ConnectorPoint PointX="4.5" PointY="5.4660546875" />
<ConnectorPoint PointX="5.25" PointY="5.4660546875" />
</AssociationConnector>
</Diagram>
</Diagrams>
</Designer>
</edmx:Edmx>

View File

@@ -1,726 +0,0 @@
<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF6.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
const string inputFile = @"Model.edmx";
var textTransform = DynamicTextTransformation.Create(this);
var code = new CodeGenerationTools(this);
var ef = new MetadataTools(this);
var typeMapper = new TypeMapper(code, ef, textTransform.Errors);
var fileManager = EntityFrameworkTemplateFileManager.Create(this);
var itemCollection = new EdmMetadataLoader(textTransform.Host, textTransform.Errors).CreateEdmItemCollection(inputFile);
var codeStringGenerator = new CodeStringGenerator(code, typeMapper, ef);
if (!typeMapper.VerifyCaseInsensitiveTypeUniqueness(typeMapper.GetAllGlobalItems(itemCollection), inputFile))
{
return string.Empty;
}
WriteHeader(codeStringGenerator, fileManager);
foreach (var entity in typeMapper.GetItemsToGenerate<EntityType>(itemCollection))
{
fileManager.StartNewFile(entity.Name + ".cs");
BeginNamespace(code);
#>
<#=codeStringGenerator.UsingDirectives(inHeader: false)#>
<#=codeStringGenerator.EntityClassOpening(entity)#>
{
<#
var propertiesWithDefaultValues = typeMapper.GetPropertiesWithDefaultValues(entity);
var collectionNavigationProperties = typeMapper.GetCollectionNavigationProperties(entity);
var complexProperties = typeMapper.GetComplexProperties(entity);
if (propertiesWithDefaultValues.Any() || collectionNavigationProperties.Any() || complexProperties.Any())
{
#>
public <#=code.Escape(entity)#>()
{
<#
foreach (var edmProperty in propertiesWithDefaultValues)
{
#>
this.<#=code.Escape(edmProperty)#> = <#=typeMapper.CreateLiteral(edmProperty.DefaultValue)#>;
<#
}
foreach (var navigationProperty in collectionNavigationProperties)
{
#>
this.<#=code.Escape(navigationProperty)#> = new HashSet<<#=typeMapper.GetTypeName(navigationProperty.ToEndMember.GetEntityType())#>>();
<#
}
foreach (var complexProperty in complexProperties)
{
#>
this.<#=code.Escape(complexProperty)#> = new <#=typeMapper.GetTypeName(complexProperty.TypeUsage)#>();
<#
}
#>
}
<#
}
var simpleProperties = typeMapper.GetSimpleProperties(entity);
if (simpleProperties.Any())
{
foreach (var edmProperty in simpleProperties)
{
#>
<#=codeStringGenerator.Property(edmProperty)#>
<#
}
}
if (complexProperties.Any())
{
#>
<#
foreach(var complexProperty in complexProperties)
{
#>
<#=codeStringGenerator.Property(complexProperty)#>
<#
}
}
var navigationProperties = typeMapper.GetNavigationProperties(entity);
if (navigationProperties.Any())
{
#>
<#
foreach (var navigationProperty in navigationProperties)
{
#>
<#=codeStringGenerator.NavigationProperty(navigationProperty)#>
<#
}
}
#>
}
<#
EndNamespace(code);
}
foreach (var complex in typeMapper.GetItemsToGenerate<ComplexType>(itemCollection))
{
fileManager.StartNewFile(complex.Name + ".cs");
BeginNamespace(code);
#>
<#=codeStringGenerator.UsingDirectives(inHeader: false, includeCollections: false)#>
<#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#>
{
<#
var complexProperties = typeMapper.GetComplexProperties(complex);
var propertiesWithDefaultValues = typeMapper.GetPropertiesWithDefaultValues(complex);
if (propertiesWithDefaultValues.Any() || complexProperties.Any())
{
#>
public <#=code.Escape(complex)#>()
{
<#
foreach (var edmProperty in propertiesWithDefaultValues)
{
#>
this.<#=code.Escape(edmProperty)#> = <#=typeMapper.CreateLiteral(edmProperty.DefaultValue)#>;
<#
}
foreach (var complexProperty in complexProperties)
{
#>
this.<#=code.Escape(complexProperty)#> = new <#=typeMapper.GetTypeName(complexProperty.TypeUsage)#>();
<#
}
#>
}
<#
}
var simpleProperties = typeMapper.GetSimpleProperties(complex);
if (simpleProperties.Any())
{
foreach(var edmProperty in simpleProperties)
{
#>
<#=codeStringGenerator.Property(edmProperty)#>
<#
}
}
if (complexProperties.Any())
{
#>
<#
foreach(var edmProperty in complexProperties)
{
#>
<#=codeStringGenerator.Property(edmProperty)#>
<#
}
}
#>
}
<#
EndNamespace(code);
}
foreach (var enumType in typeMapper.GetEnumItemsToGenerate(itemCollection))
{
fileManager.StartNewFile(enumType.Name + ".cs");
BeginNamespace(code);
#>
<#=codeStringGenerator.UsingDirectives(inHeader: false, includeCollections: false)#>
<#
if (typeMapper.EnumIsFlags(enumType))
{
#>
[Flags]
<#
}
#>
<#=codeStringGenerator.EnumOpening(enumType)#>
{
<#
var foundOne = false;
foreach (MetadataItem member in typeMapper.GetEnumMembers(enumType))
{
foundOne = true;
#>
<#=code.Escape(typeMapper.GetEnumMemberName(member))#> = <#=typeMapper.GetEnumMemberValue(member)#>,
<#
}
if (foundOne)
{
this.GenerationEnvironment.Remove(this.GenerationEnvironment.Length - 3, 1);
}
#>
}
<#
EndNamespace(code);
}
fileManager.Process();
#>
<#+
public void WriteHeader(CodeStringGenerator codeStringGenerator, EntityFrameworkTemplateFileManager fileManager)
{
fileManager.StartHeader();
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=CodeGenerationTools.GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------
<#=codeStringGenerator.UsingDirectives(inHeader: true)#>
<#+
fileManager.EndBlock();
}
public void BeginNamespace(CodeGenerationTools code)
{
var codeNamespace = code.VsNamespaceSuggestion();
if (!String.IsNullOrEmpty(codeNamespace))
{
#>
namespace <#=code.EscapeNamespace(codeNamespace)#>
{
<#+
PushIndent(" ");
}
}
public void EndNamespace(CodeGenerationTools code)
{
if (!String.IsNullOrEmpty(code.VsNamespaceSuggestion()))
{
PopIndent();
#>
}
<#+
}
}
public const string TemplateId = "CSharp_DbContext_Types_EF6";
public class CodeStringGenerator
{
private readonly CodeGenerationTools _code;
private readonly TypeMapper _typeMapper;
private readonly MetadataTools _ef;
public CodeStringGenerator(CodeGenerationTools code, TypeMapper typeMapper, MetadataTools ef)
{
ArgumentNotNull(code, "code");
ArgumentNotNull(typeMapper, "typeMapper");
ArgumentNotNull(ef, "ef");
_code = code;
_typeMapper = typeMapper;
_ef = ef;
}
public string Property(EdmProperty edmProperty)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2} {{ {3}get; {4}set; }}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
}
public string NavigationProperty(NavigationProperty navProp)
{
var endType = _typeMapper.GetTypeName(navProp.ToEndMember.GetEntityType());
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2} {{ {3}get; {4}set; }}",
AccessibilityAndVirtual(Accessibility.ForNavigationProperty(navProp)),
navProp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many ? ("ICollection<" + endType + ">") : endType,
_code.Escape(navProp),
_code.SpaceAfter(Accessibility.ForGetter(navProp)),
_code.SpaceAfter(Accessibility.ForSetter(navProp)));
}
public string AccessibilityAndVirtual(string accessibility)
{
return accessibility + (accessibility != "private" ? " virtual" : "");
}
public string EntityClassOpening(EntityType entity)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1}partial class {2}{3}",
Accessibility.ForType(entity),
_code.SpaceAfter(_code.AbstractOption(entity)),
_code.Escape(entity),
_code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
}
public string EnumOpening(SimpleType enumType)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} enum {1} : {2}",
Accessibility.ForType(enumType),
_code.Escape(enumType),
_code.Escape(_typeMapper.UnderlyingClrType(enumType)));
}
public void WriteFunctionParameters(EdmFunction edmFunction, Action<string, string, string, string> writeParameter)
{
var parameters = FunctionImportParameter.Create(edmFunction.Parameters, _code, _ef);
foreach (var parameter in parameters.Where(p => p.NeedsLocalVariable))
{
var isNotNull = parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null";
var notNullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", " + parameter.FunctionParameterName + ")";
var nullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", typeof(" + TypeMapper.FixNamespaces(parameter.RawClrTypeName) + "))";
writeParameter(parameter.LocalVariableName, isNotNull, notNullInit, nullInit);
}
}
public string ComposableFunctionMethod(EdmFunction edmFunction, string modelNamespace)
{
var parameters = _typeMapper.GetParameters(edmFunction);
return string.Format(
CultureInfo.InvariantCulture,
"{0} IQueryable<{1}> {2}({3})",
AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction)),
_typeMapper.GetTypeName(_typeMapper.GetReturnType(edmFunction), modelNamespace),
_code.Escape(edmFunction),
string.Join(", ", parameters.Select(p => TypeMapper.FixNamespaces(p.FunctionParameterType) + " " + p.FunctionParameterName).ToArray()));
}
public string ComposableCreateQuery(EdmFunction edmFunction, string modelNamespace)
{
var parameters = _typeMapper.GetParameters(edmFunction);
return string.Format(
CultureInfo.InvariantCulture,
"return ((IObjectContextAdapter)this).ObjectContext.CreateQuery<{0}>(\"[{1}].[{2}]({3})\"{4});",
_typeMapper.GetTypeName(_typeMapper.GetReturnType(edmFunction), modelNamespace),
edmFunction.NamespaceName,
edmFunction.Name,
string.Join(", ", parameters.Select(p => "@" + p.EsqlParameterName).ToArray()),
_code.StringBefore(", ", string.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray())));
}
public string FunctionMethod(EdmFunction edmFunction, string modelNamespace, bool includeMergeOption)
{
var parameters = _typeMapper.GetParameters(edmFunction);
var returnType = _typeMapper.GetReturnType(edmFunction);
var paramList = String.Join(", ", parameters.Select(p => TypeMapper.FixNamespaces(p.FunctionParameterType) + " " + p.FunctionParameterName).ToArray());
if (includeMergeOption)
{
paramList = _code.StringAfter(paramList, ", ") + "MergeOption mergeOption";
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2}({3})",
AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction)),
returnType == null ? "int" : "ObjectResult<" + _typeMapper.GetTypeName(returnType, modelNamespace) + ">",
_code.Escape(edmFunction),
paramList);
}
public string ExecuteFunction(EdmFunction edmFunction, string modelNamespace, bool includeMergeOption)
{
var parameters = _typeMapper.GetParameters(edmFunction);
var returnType = _typeMapper.GetReturnType(edmFunction);
var callParams = _code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray()));
if (includeMergeOption)
{
callParams = ", mergeOption" + callParams;
}
return string.Format(
CultureInfo.InvariantCulture,
"return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction{0}(\"{1}\"{2});",
returnType == null ? "" : "<" + _typeMapper.GetTypeName(returnType, modelNamespace) + ">",
edmFunction.Name,
callParams);
}
public string DbSet(EntitySet entitySet)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} virtual DbSet<{1}> {2} {{ get; set; }}",
Accessibility.ForReadOnlyProperty(entitySet),
_typeMapper.GetTypeName(entitySet.ElementType),
_code.Escape(entitySet));
}
public string UsingDirectives(bool inHeader, bool includeCollections = true)
{
return inHeader == string.IsNullOrEmpty(_code.VsNamespaceSuggestion())
? string.Format(
CultureInfo.InvariantCulture,
"{0}using System;{1}" +
"{2}",
inHeader ? Environment.NewLine : "",
includeCollections ? (Environment.NewLine + "using System.Collections.Generic;") : "",
inHeader ? "" : Environment.NewLine)
: "";
}
}
public class TypeMapper
{
private const string ExternalTypeNameAttributeName = @"http://schemas.microsoft.com/ado/2006/04/codegeneration:ExternalTypeName";
private readonly System.Collections.IList _errors;
private readonly CodeGenerationTools _code;
private readonly MetadataTools _ef;
public TypeMapper(CodeGenerationTools code, MetadataTools ef, System.Collections.IList errors)
{
ArgumentNotNull(code, "code");
ArgumentNotNull(ef, "ef");
ArgumentNotNull(errors, "errors");
_code = code;
_ef = ef;
_errors = errors;
}
public static string FixNamespaces(string typeName)
{
return typeName.Replace("System.Data.Spatial.", "System.Data.Entity.Spatial.");
}
public string GetTypeName(TypeUsage typeUsage)
{
return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace: null);
}
public string GetTypeName(EdmType edmType)
{
return GetTypeName(edmType, isNullable: null, modelNamespace: null);
}
public string GetTypeName(TypeUsage typeUsage, string modelNamespace)
{
return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace);
}
public string GetTypeName(EdmType edmType, string modelNamespace)
{
return GetTypeName(edmType, isNullable: null, modelNamespace: modelNamespace);
}
public string GetTypeName(EdmType edmType, bool? isNullable, string modelNamespace)
{
if (edmType == null)
{
return null;
}
var collectionType = edmType as CollectionType;
if (collectionType != null)
{
return String.Format(CultureInfo.InvariantCulture, "ICollection<{0}>", GetTypeName(collectionType.TypeUsage, modelNamespace));
}
var typeName = _code.Escape(edmType.MetadataProperties
.Where(p => p.Name == ExternalTypeNameAttributeName)
.Select(p => (string)p.Value)
.FirstOrDefault())
?? (modelNamespace != null && edmType.NamespaceName != modelNamespace ?
_code.CreateFullName(_code.EscapeNamespace(edmType.NamespaceName), _code.Escape(edmType)) :
_code.Escape(edmType));
if (edmType is StructuralType)
{
return typeName;
}
if (edmType is SimpleType)
{
var clrType = UnderlyingClrType(edmType);
if (!IsEnumType(edmType))
{
typeName = _code.Escape(clrType);
}
typeName = FixNamespaces(typeName);
return clrType.IsValueType && isNullable == true ?
String.Format(CultureInfo.InvariantCulture, "Nullable<{0}>", typeName) :
typeName;
}
throw new ArgumentException("edmType");
}
public Type UnderlyingClrType(EdmType edmType)
{
ArgumentNotNull(edmType, "edmType");
var primitiveType = edmType as PrimitiveType;
if (primitiveType != null)
{
return primitiveType.ClrEquivalentType;
}
if (IsEnumType(edmType))
{
return GetEnumUnderlyingType(edmType).ClrEquivalentType;
}
return typeof(object);
}
public object GetEnumMemberValue(MetadataItem enumMember)
{
ArgumentNotNull(enumMember, "enumMember");
var valueProperty = enumMember.GetType().GetProperty("Value");
return valueProperty == null ? null : valueProperty.GetValue(enumMember, null);
}
public string GetEnumMemberName(MetadataItem enumMember)
{
ArgumentNotNull(enumMember, "enumMember");
var nameProperty = enumMember.GetType().GetProperty("Name");
return nameProperty == null ? null : (string)nameProperty.GetValue(enumMember, null);
}
public System.Collections.IEnumerable GetEnumMembers(EdmType enumType)
{
ArgumentNotNull(enumType, "enumType");
var membersProperty = enumType.GetType().GetProperty("Members");
return membersProperty != null
? (System.Collections.IEnumerable)membersProperty.GetValue(enumType, null)
: Enumerable.Empty<MetadataItem>();
}
public bool EnumIsFlags(EdmType enumType)
{
ArgumentNotNull(enumType, "enumType");
var isFlagsProperty = enumType.GetType().GetProperty("IsFlags");
return isFlagsProperty != null && (bool)isFlagsProperty.GetValue(enumType, null);
}
public bool IsEnumType(GlobalItem edmType)
{
ArgumentNotNull(edmType, "edmType");
return edmType.GetType().Name == "EnumType";
}
public PrimitiveType GetEnumUnderlyingType(EdmType enumType)
{
ArgumentNotNull(enumType, "enumType");
return (PrimitiveType)enumType.GetType().GetProperty("UnderlyingType").GetValue(enumType, null);
}
public string CreateLiteral(object value)
{
if (value == null || value.GetType() != typeof(TimeSpan))
{
return _code.CreateLiteral(value);
}
return string.Format(CultureInfo.InvariantCulture, "new TimeSpan({0})", ((TimeSpan)value).Ticks);
}
public bool VerifyCaseInsensitiveTypeUniqueness(IEnumerable<string> types, string sourceFile)
{
ArgumentNotNull(types, "types");
ArgumentNotNull(sourceFile, "sourceFile");
var hash = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
if (types.Any(item => !hash.Add(item)))
{
_errors.Add(
new CompilerError(sourceFile, -1, -1, "6023",
String.Format(CultureInfo.CurrentCulture, CodeGenerationTools.GetResourceString("Template_CaseInsensitiveTypeConflict"))));
return false;
}
return true;
}
public IEnumerable<SimpleType> GetEnumItemsToGenerate(IEnumerable<GlobalItem> itemCollection)
{
return GetItemsToGenerate<SimpleType>(itemCollection)
.Where(e => IsEnumType(e));
}
public IEnumerable<T> GetItemsToGenerate<T>(IEnumerable<GlobalItem> itemCollection) where T: EdmType
{
return itemCollection
.OfType<T>()
.Where(i => !i.MetadataProperties.Any(p => p.Name == ExternalTypeNameAttributeName))
.OrderBy(i => i.Name);
}
public IEnumerable<string> GetAllGlobalItems(IEnumerable<GlobalItem> itemCollection)
{
return itemCollection
.Where(i => i is EntityType || i is ComplexType || i is EntityContainer || IsEnumType(i))
.Select(g => GetGlobalItemName(g));
}
public string GetGlobalItemName(GlobalItem item)
{
if (item is EdmType)
{
return ((EdmType)item).Name;
}
else
{
return ((EntityContainer)item).Name;
}
}
public IEnumerable<EdmProperty> GetSimpleProperties(EntityType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetSimpleProperties(ComplexType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetComplexProperties(EntityType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetComplexProperties(ComplexType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
}
public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(EntityType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
}
public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(ComplexType type)
{
return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
}
public IEnumerable<NavigationProperty> GetNavigationProperties(EntityType type)
{
return type.NavigationProperties.Where(np => np.DeclaringType == type);
}
public IEnumerable<NavigationProperty> GetCollectionNavigationProperties(EntityType type)
{
return type.NavigationProperties.Where(np => np.DeclaringType == type && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many);
}
public FunctionParameter GetReturnParameter(EdmFunction edmFunction)
{
ArgumentNotNull(edmFunction, "edmFunction");
var returnParamsProperty = edmFunction.GetType().GetProperty("ReturnParameters");
return returnParamsProperty == null
? edmFunction.ReturnParameter
: ((IEnumerable<FunctionParameter>)returnParamsProperty.GetValue(edmFunction, null)).FirstOrDefault();
}
public bool IsComposable(EdmFunction edmFunction)
{
ArgumentNotNull(edmFunction, "edmFunction");
var isComposableProperty = edmFunction.GetType().GetProperty("IsComposableAttribute");
return isComposableProperty != null && (bool)isComposableProperty.GetValue(edmFunction, null);
}
public IEnumerable<FunctionImportParameter> GetParameters(EdmFunction edmFunction)
{
return FunctionImportParameter.Create(edmFunction.Parameters, _code, _ef);
}
public TypeUsage GetReturnType(EdmFunction edmFunction)
{
var returnParam = GetReturnParameter(edmFunction);
return returnParam == null ? null : _ef.GetElementType(returnParam.TypeUsage);
}
public bool GenerateMergeOptionFunction(EdmFunction edmFunction, bool includeMergeOption)
{
var returnType = GetReturnType(edmFunction);
return !includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType;
}
}
public static void ArgumentNotNull<T>(T arg, string name) where T : class
{
if (arg == null)
{
throw new ArgumentNullException(name);
}
}
#>

View File

@@ -1,52 +1,67 @@
using FeedCenter.Properties; using FeedCenter.Properties;
using System.Windows.Forms; using H.NotifyIcon;
using System.Windows.Controls;
namespace FeedCenter;
namespace FeedCenter
{
internal static class NotificationIcon internal static class NotificationIcon
{ {
private static MainWindow _mainForm; private static MainWindow _mainWindow;
private static NotifyIcon _notificationIcon; private static TaskbarIcon _notificationIcon;
private static MenuItem _lockMenuItem;
public static void Initialize(MainWindow mainForm) public static void Initialize(MainWindow mainWindow)
{ {
// Store the main window // Store the main window
_mainForm = mainForm; _mainWindow = mainWindow;
// Create the notification icon // Create the notification icon
_notificationIcon = new NotifyIcon { Icon = Resources.Application }; _notificationIcon = new TaskbarIcon { Icon = Resources.Application };
_notificationIcon.DoubleClick += HandleNotificationIconDoubleClick; _notificationIcon.TrayMouseDoubleClick += HandleNotificationIconDoubleClick;
// Set up the menu // Set up the menu
var contextMenuStrip = new ContextMenuStrip(); var contextMenu = new ContextMenu();
contextMenu.Opened += HandleContextMenuOpened;
var toolStripMenuItem = new ToolStripMenuItem(Resources.NotificationIconContextMenuLocked, null, HandleLockWindowClicked) _lockMenuItem = new MenuItem()
{ {
Checked = Settings.Default.WindowLocked Header = Resources.NotificationIconContextMenuLocked,
IsChecked = Settings.Default.WindowLocked
}; };
contextMenuStrip.Items.Add(toolStripMenuItem); _lockMenuItem.Click += HandleLockWindowClicked;
contextMenu.Items.Add(_lockMenuItem);
contextMenuStrip.Items.Add(new ToolStripSeparator()); contextMenu.Items.Add(new Separator());
contextMenuStrip.Items.Add(Resources.NotificationIconContextMenuExit, null, HandleContextMenuExitClick); var menuItem = new MenuItem()
{
Header = Resources.NotificationIconContextMenuExit
};
menuItem.Click += HandleContextMenuExitClick;
contextMenu.Items.Add(menuItem);
// Set the menu into the icon // Set the menu into the icon
_notificationIcon.ContextMenuStrip = contextMenuStrip; _notificationIcon.ContextMenu = contextMenu;
// Show the icon // Show the icon
_notificationIcon.Visible = true; _notificationIcon.ForceCreate(false);
}
private static void HandleContextMenuOpened(object sender, System.Windows.RoutedEventArgs e)
{
_lockMenuItem.IsChecked = Settings.Default.WindowLocked;
} }
private static void HandleNotificationIconDoubleClick(object sender, System.EventArgs e) private static void HandleNotificationIconDoubleClick(object sender, System.EventArgs e)
{ {
// Bring the main form to the front // Bring the main form to the front
_mainForm.Activate(); _mainWindow.Activate();
} }
private static void HandleContextMenuExitClick(object sender, System.EventArgs e) private static void HandleContextMenuExitClick(object sender, System.EventArgs e)
{ {
// Close the main form // Close the main form
_mainForm.Close(); _mainWindow.Close();
} }
private static void HandleLockWindowClicked(object sender, System.EventArgs e) private static void HandleLockWindowClicked(object sender, System.EventArgs e)
@@ -54,28 +69,21 @@ namespace FeedCenter
// Toggle the lock setting // Toggle the lock setting
Settings.Default.WindowLocked = !Settings.Default.WindowLocked; Settings.Default.WindowLocked = !Settings.Default.WindowLocked;
// Reset the menu choice // Refresh the menu choice
((ToolStripMenuItem) sender).Checked = Settings.Default.WindowLocked; _lockMenuItem.IsChecked = Settings.Default.WindowLocked;
} }
public static void Dispose() public static void Dispose()
{ {
// Get rid of the icon // Get rid of the icon
_notificationIcon.Visible = false; _notificationIcon?.Dispose();
_notificationIcon.Dispose();
_notificationIcon = null; _notificationIcon = null;
_mainForm = null; _mainWindow = null;
} }
public static void ShowBalloonTip(string text, ToolTipIcon icon) public static void ShowBalloonTip(string text, H.NotifyIcon.Core.NotificationIcon icon)
{ {
ShowBalloonTip(text, icon, Settings.Default.BalloonTipTimeout); _notificationIcon.ShowNotification(Resources.ApplicationDisplayName, text, icon);
}
private static void ShowBalloonTip(string text, ToolTipIcon icon, int timeout)
{
_notificationIcon.ShowBalloonTip(timeout, Resources.ApplicationDisplayName, text, icon);
}
} }
} }

View File

@@ -4,21 +4,18 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:options="clr-namespace:FeedCenter.Options" xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:applicationUpdate="clr-namespace:ChrisKaczor.ApplicationUpdate;assembly=ChrisKaczor.ApplicationUpdate"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="300" d:DesignHeight="150"
d:DesignWidth="300"> d:DesignWidth="300">
<Grid> <Grid>
<TextBlock Text="Label" <StackPanel options:Spacing.Vertical="10">
Name="ApplicationNameLabel" <TextBlock Text="{x:Static properties:Resources.ApplicationDisplayName}"
VerticalAlignment="Top"
FontWeight="Bold" /> FontWeight="Bold" />
<TextBlock Text="Label" <TextBlock Text="{Binding Source={x:Static applicationUpdate:UpdateCheck.LocalVersion}, StringFormat={x:Static properties:Resources.Version}}"
Margin="0,22,0,0" Name="VersionLabel" />
Name="VersionLabel" <TextBlock Text="Chris Kaczor" />
VerticalAlignment="Top" /> </StackPanel>
<TextBlock Text="Label"
Margin="0,44,0,0"
Name="CompanyLabel"
VerticalAlignment="Top" />
</Grid> </Grid>
</options:OptionsPanelBase> </options:OptionsPanelBase>

View File

@@ -1,36 +1,13 @@
using Common.Update; using System.Windows;
using System.Reflection;
namespace FeedCenter.Options;
namespace FeedCenter.Options
{
public partial class AboutOptionsPanel public partial class AboutOptionsPanel
{ {
public AboutOptionsPanel() public AboutOptionsPanel(Window parentWindow, FeedCenterEntities entities) : base(parentWindow, entities)
{ {
InitializeComponent(); InitializeComponent();
} }
public override void LoadPanel(FeedCenterEntities database)
{
base.LoadPanel(database);
ApplicationNameLabel.Text = Properties.Resources.ApplicationDisplayName;
string version = UpdateCheck.LocalVersion.ToString();
VersionLabel.Text = string.Format(Properties.Resources.Version, version);
CompanyLabel.Text = ((AssemblyCompanyAttribute) Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false)[0]).Company;
}
public override bool ValidatePanel()
{
return true;
}
public override void SavePanel()
{
}
public override string CategoryName => Properties.Resources.optionCategoryAbout; public override string CategoryName => Properties.Resources.optionCategoryAbout;
} }
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using FeedCenter.Accounts;
namespace FeedCenter.Options;
public class AccountTypeItem
{
public AccountType AccountType { get; set; }
public string Name { get; set; }
public static List<AccountTypeItem> AccountTypes =>
[
new()
{
Name = Properties.Resources.AccountTypeFever,
AccountType = AccountType.Fever
},
//new()
//{
// Name = Properties.Resources.AccountTypeGoogleReader,
// AccountType = AccountType.GoogleReader
//}
new()
{
Name = Properties.Resources.AccountTypeMiniflux,
AccountType = AccountType.Miniflux
}
];
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using FeedCenter.Accounts;
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();
}
}

View File

@@ -0,0 +1,141 @@
<Window x:Class="FeedCenter.Options.AccountWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:feedCenter="clr-namespace:FeedCenter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignInstance Type=accounts:Account}"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:feeds="clr-namespace:FeedCenter.Feeds"
xmlns:accounts="clr-namespace:FeedCenter.Accounts"
mc:Ignorable="d"
Title="AccountWindow"
Height="350"
Width="450"
WindowStartupLocation="CenterOwner"
Icon="/FeedCenter;component/Resources/Application.ico"
FocusManager.FocusedElement="{Binding ElementName=NameTextBox}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TabControl Name="OptionsTabControl"
Grid.Row="0"
Grid.Column="0"
mah:HeaderedControlHelper.HeaderFontSize="16"
mah:TabControlHelper.Underlined="SelectedTabItem">
<TabItem Header="{x:Static properties:Resources.generalTab}">
<StackPanel Margin="0,4"
options:Spacing.Vertical="8">
<ComboBox Name="AccountTypeComboBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.accountTypeLabel}"
DisplayMemberPath="Name"
ItemsSource="{Binding Source={x:Static options:AccountTypeItem.AccountTypes}}"
SelectedValuePath="AccountType"
SelectedValue="{Binding Path=Type, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}" />
<TextBox Name="NameTextBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.accountNameLabel}"
mah:TextBoxHelper.SelectAllOnFocus="True"
Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<TextBox Name="UrlTextBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.accountUrlLabel}"
mah:TextBoxHelper.SelectAllOnFocus="True"
Text="{Binding Path=Url, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<CheckBox Name="ReadIntervalCheckBox"
VerticalContentAlignment="Center"
IsChecked="{Binding Path=Enabled, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}">
<StackPanel Orientation="Horizontal">
<Label Content="{x:Static properties:Resources.accountReadIntervalPrefix}"
HorizontalAlignment="Left"
Margin="0,0,5,0"
VerticalAlignment="Center"
Padding="0" />
<mah:NumericUpDown Width="100"
Maximum="10080"
Minimum="1"
IsEnabled="{Binding ElementName=ReadIntervalCheckBox, Path=IsChecked}"
Value="{Binding CheckInterval, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<Label Content="{x:Static properties:Resources.accountReadIntervalSuffix}"
HorizontalAlignment="Left"
Margin="5,0,0,0"
VerticalAlignment="Center"
Padding="0" />
</StackPanel>
</CheckBox>
</StackPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.authenticationTab}">
<StackPanel Margin="0,4">
<CheckBox Content="{x:Static properties:Resources.accountRequiresAuthenticationCheckBox}"
Margin="0,0,0,4"
Name="RequiresAuthenticationCheckBox"
IsChecked="{Binding Path=Authenticate, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<TextBox Name="AuthenticationUserNameTextBox"
Margin="25,0,0,4"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.authenticationUserNameLabel}"
Text="{Binding Path=Username, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<PasswordBox Name="AuthenticationPasswordTextBox"
Margin="25,0,0,8"
Style="{StaticResource MahApps.Styles.PasswordBox.Button.Revealed}"
mah:PasswordBoxBindingBehavior.Password="{Binding Password, UpdateSourceTrigger=Explicit, ValidatesOnDataErrors=True}"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.authenticationPasswordLabel}"
IsEnabled="{Binding ElementName=RequiresAuthenticationCheckBox, Path=IsChecked}" />
</StackPanel>
</TabItem>
</TabControl>
<Grid Name="AccountReadProgress"
Grid.Row="1"
Height="20"
Visibility="Collapsed">
<ProgressBar Name="AccountReadProgressBar" />
<TextBlock Text="Loading account..."
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
<StackPanel
Name="ButtonPanel"
Grid.Column="0"
Grid.Row="1"
Orientation="Horizontal"
Visibility="Visible"
Margin="0,5,0,0"
HorizontalAlignment="Right">
<Button Content="{x:Static properties:Resources.OkayButton}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
Margin="0,0,5,0"
IsDefault="True"
Click="HandleOkayButtonClick" />
<Button Content="{x:Static properties:Resources.CancelButton}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
IsCancel="True" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,78 @@
using ChrisKaczor.Wpf.Validation;
using System;
using System.Linq;
using System.Windows;
using FeedCenter.Accounts;
namespace FeedCenter.Options;
public partial class AccountWindow
{
private Account _account;
private bool _isNew;
private readonly FeedCenterEntities _entities;
public AccountWindow(FeedCenterEntities entities)
{
_entities = entities;
InitializeComponent();
}
public bool? Display(Account account, Window owner, bool isNew)
{
_account = account;
_isNew = isNew;
DataContext = account;
Title = isNew ? Properties.Resources.AccountWindowAdd : Properties.Resources.AccountWindowEdit;
Owner = owner;
return ShowDialog();
}
private async void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
try
{
var transaction = _entities.BeginTransaction();
if (!this.IsValid(OptionsTabControl))
{
transaction.Rollback();
return;
}
if (_isNew)
{
_entities.Accounts.Add(_account);
}
await transaction.CommitAsync();
var accountId = _account.Id;
var accountReadInput = new AccountReadInput(_entities, null, true, () => AccountReadProgressBar.Value++);
AccountReadProgressBar.Value = 0;
AccountReadProgressBar.Maximum = await _account.GetProgressSteps(accountReadInput);
AccountReadProgress.Visibility = Visibility.Visible;
ButtonPanel.Visibility = Visibility.Collapsed;
//var entities = new FeedCenterEntities();
var account = _entities.Accounts.First(a => a.Id == accountId);
await account.Read(accountReadInput);
DialogResult = true;
Close();
}
catch (Exception exception)
{
MainWindow.HandleException(exception);
}
}
}

View File

@@ -0,0 +1,100 @@
<options:OptionsPanelBase x:Class="FeedCenter.Options.AccountsOptionsPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.Link"
xmlns:feedCenter="clr-namespace:FeedCenter"
xmlns:feeds="clr-namespace:FeedCenter.Feeds"
xmlns:accounts="clr-namespace:FeedCenter.Accounts"
mc:Ignorable="d"
d:DesignHeight="311"
d:DesignWidth="425">
<options:OptionsPanelBase.Resources>
<ResourceDictionary>
<options:AccountTypeToNameConverter x:Key="AccountTypeToNameConverter" />
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary
Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</options:OptionsPanelBase.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<DataGrid Name="AccountDataGrid"
SelectionChanged="HandleAccountDataGridSelectionChanged"
Grid.Row="0"
SelectionMode="Single"
SelectionUnit="FullRow"
Grid.Column="0"
AutoGenerateColumns="False"
GridLinesVisibility="None"
CanUserResizeRows="False"
IsReadOnly="True"
HeadersVisibility="Column"
BorderThickness="1,1,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
AllowDrop="True"
Background="{x:Null}"
d:DataContext="{d:DesignInstance accounts:Account }">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static properties:Resources.AccountNameColumnHeader}"
SortDirection="Ascending"
Width="*" />
<DataGridTextColumn Binding="{Binding Type, Converter={StaticResource AccountTypeToNameConverter}}"
Header="{x:Static properties:Resources.AccountTypeColumnHeader}"
Width="*" />
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow"
BasedOn="{StaticResource MahApps.Styles.DataGridRow}">
<EventSetter Event="MouseDoubleClick"
Handler="HandleAccountDataGridRowMouseDoubleClick" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
<Border Grid.Column="0"
Grid.Row="1"
BorderThickness="1,0,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}" />
<Border Grid.Row="1"
Grid.Column="0"
BorderThickness="1,0,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<StackPanel Orientation="Horizontal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<controls:Link Name="AddAccountButton"
Margin="2"
Click="HandleAddAccountButtonClick"
Text="{x:Static properties:Resources.AddLink}"
ToolTip="{x:Static properties:Resources.AddAccountButton}">
</controls:Link>
<controls:Link Name="EditAccountButton"
Margin="2"
Click="HandleEditAccountButtonClick"
Text="{x:Static properties:Resources.EditLink}"
ToolTip="{x:Static properties:Resources.EditAccountButton}">
</controls:Link>
<controls:Link Name="DeleteAccountButton"
Margin="2"
Click="HandleDeleteAccountButtonClick"
Text="{x:Static properties:Resources.DeleteLink}"
ToolTip="{x:Static properties:Resources.DeleteAccountButton}">
</controls:Link>
</StackPanel>
</Border>
</Grid>
</options:OptionsPanelBase>

View File

@@ -0,0 +1,129 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using FeedCenter.Accounts;
namespace FeedCenter.Options;
public partial class AccountsOptionsPanel
{
private readonly FeedCenterEntities _entities;
public AccountsOptionsPanel(Window parentWindow, FeedCenterEntities entities) : base(parentWindow, entities)
{
_entities = entities;
InitializeComponent();
}
public override string CategoryName => Properties.Resources.optionCategoryAccounts;
public override void LoadPanel()
{
base.LoadPanel();
var collectionViewSource = new CollectionViewSource { Source = _entities.Accounts };
collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
collectionViewSource.IsLiveSortingRequested = true;
collectionViewSource.View.Filter = item =>
{
if (item is not Account account)
return false;
// Filter out local accounts
return account.Type != AccountType.Local;
};
AccountDataGrid.ItemsSource = collectionViewSource.View;
AccountDataGrid.SelectedIndex = 0;
SetAccountButtonStates();
}
private void SetAccountButtonStates()
{
AddAccountButton.IsEnabled = true;
EditAccountButton.IsEnabled = AccountDataGrid.SelectedItem != null;
DeleteAccountButton.IsEnabled = AccountDataGrid.SelectedItem != null;
}
private void AddAccount()
{
var account = new Account(AccountType.Miniflux);
var accountWindow = new AccountWindow(_entities);
var result = accountWindow.Display(account, Window.GetWindow(this), true);
if (!result.HasValue || !result.Value)
return;
AccountDataGrid.SelectedItem = account;
SetAccountButtonStates();
}
private void EditSelectedAccount()
{
if (AccountDataGrid.SelectedItem == null)
return;
var account = (Account) AccountDataGrid.SelectedItem;
var accountWindow = new AccountWindow(_entities);
accountWindow.Display(account, Window.GetWindow(this), false);
}
private void DeleteSelectedAccount()
{
var account = (Account) AccountDataGrid.SelectedItem;
if (MessageBox.Show(ParentWindow, string.Format(Properties.Resources.ConfirmDeleteAccount, account.Name),
Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question,
MessageBoxResult.No) == MessageBoxResult.No)
return;
var index = AccountDataGrid.SelectedIndex;
if (index == AccountDataGrid.Items.Count - 1)
AccountDataGrid.SelectedIndex = index - 1;
else
AccountDataGrid.SelectedIndex = index + 1;
_entities.SaveChanges(() => _entities.Accounts.Remove(account));
SetAccountButtonStates();
}
private void HandleAddAccountButtonClick(object sender, RoutedEventArgs e)
{
AddAccount();
}
private void HandleEditAccountButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedAccount();
}
private void HandleDeleteAccountButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedAccount();
}
private void HandleAccountDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetAccountButtonStates();
}
private void HandleAccountDataGridRowMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (!EditAccountButton.IsEnabled)
return;
EditSelectedAccount();
}
}

View File

@@ -5,36 +5,44 @@
Height="300" Height="300"
Width="500" Width="500"
xmlns:my="clr-namespace:FeedCenter.Properties" xmlns:my="clr-namespace:FeedCenter.Properties"
xmlns:linkControl="clr-namespace:Common.Wpf.LinkControl;assembly=Common.Wpf"
xmlns:feedCenter="clr-namespace:FeedCenter" xmlns:feedCenter="clr-namespace:FeedCenter"
xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.Link"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:feeds="clr-namespace:FeedCenter.Feeds"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Icon="/FeedCenter;component/Resources/Application.ico" Icon="/FeedCenter;component/Resources/Application.ico"
FocusManager.FocusedElement="{Binding ElementName=FeedLinkFilterText}"> FocusManager.FocusedElement="{Binding ElementName=FeedLinkFilterText}">
<Grid> <Window.Resources>
<Grid.ColumnDefinitions> <ResourceDictionary>
<ColumnDefinition Width="Auto" /> <ResourceDictionary.MergedDictionaries>
<ColumnDefinition Width="*" /> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
</Grid.ColumnDefinitions> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="6">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Label Content="{x:Static my:Resources.FeedLinkFilterLabel}" <Grid.ColumnDefinitions>
Margin="6" <ColumnDefinition Width="*" />
Padding="0" </Grid.ColumnDefinitions>
VerticalContentAlignment="Center"
Target="{Binding ElementName=FeedLinkFilterText}" />
<TextBox Grid.Row="0" <TextBox Grid.Row="0"
Grid.Column="1" Grid.Column="0"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static my:Resources.FeedLinkFilterLabel}"
mah:TextBoxHelper.SelectAllOnFocus="True"
Name="FeedLinkFilterText" Name="FeedLinkFilterText"
Margin="6"
TextChanged="HandleFilterTextChanged" /> TextChanged="HandleFilterTextChanged" />
<Border Grid.Row="1" <Border Grid.Row="1"
Grid.ColumnSpan="2"
Grid.Column="0" Grid.Column="0"
Margin="6" Margin="0,6"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<Grid> <Grid>
@@ -45,9 +53,9 @@
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl Name="FilteredFeedsList"> <ItemsControl Name="FilteredFeedsList">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate DataType="options:CheckedFeedListItem">
<CheckBox Margin="2" <CheckBox Content="{Binding Item.Name}"
Content="{Binding Item.Name}" Margin="4"
IsChecked="{Binding IsChecked}" /> IsChecked="{Binding IsChecked}" />
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
@@ -58,70 +66,62 @@
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<TextBlock Margin="2" <TextBlock Margin="4,2,2,2"
Text="{x:Static my:Resources.SelectLabel}"> Text="{x:Static my:Resources.SelectLabel}" />
</TextBlock> <controls:Link Margin="2,2,4,2"
<linkControl:LinkControl Margin="2"
Click="HandleSelectAll" Click="HandleSelectAll"
Text="{x:Static my:Resources.SelectAllLabel}"> Text="{x:Static my:Resources.SelectAllLabel}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Margin="2" <controls:Link Margin="2,2,4,2"
Click="HandleSelectNone" Click="HandleSelectNone"
Text="{x:Static my:Resources.SelectNoneLabel}"> Text="{x:Static my:Resources.SelectNoneLabel}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Margin="2" <controls:Link Margin="2,2,4,2"
Click="HandleSelectInvert" Click="HandleSelectInvert"
Text="{x:Static my:Resources.SelectInvertLabel}"> Text="{x:Static my:Resources.SelectInvertLabel}">
</linkControl:LinkControl> </controls:Link>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
<Grid Grid.Row="2" <Grid Grid.Column="0"
Grid.Column="0" Grid.Row="2">
MouseRightButtonUp="HandleGridMouseRightButtonUp" <Grid.ColumnDefinitions>
ToolTip="{x:Static my:Resources.EnableHint}"> <ColumnDefinition Width="Auto" />
<Label Content="{x:Static my:Resources.openLabel}" <ColumnDefinition Width="*" />
Name="OpenLabel" </Grid.ColumnDefinitions>
Padding="4,0,0,0" <CheckBox Name="OpenCheckBox"
Margin="6,8,6,6" Grid.Column="0" />
ToolTip="{x:Static my:Resources.DisableHint}"
IsEnabled="False" />
</Grid>
<Grid Grid.Column="1"
Grid.Row="2"
MouseRightButtonUp="HandleGridMouseRightButtonUp"
ToolTip="{x:Static my:Resources.EnableHint}">
<ComboBox Name="OpenComboBox" <ComboBox Name="OpenComboBox"
VerticalContentAlignment="Center" Grid.Column="1"
SelectedIndex="0" SelectedIndex="0"
Margin="6" HorizontalContentAlignment="Stretch"
ToolTip="{x:Static my:Resources.DisableHint}" mah:TextBoxHelper.UseFloatingWatermark="True"
IsEnabled="False"> mah:TextBoxHelper.Watermark="{x:Static my:Resources.openLabel}"
IsEnabled="{Binding ElementName=OpenCheckBox, Path=IsChecked}">
<ComboBoxItem Content="{x:Static my:Resources.openAllMultipleToolbarButton}" <ComboBoxItem Content="{x:Static my:Resources.openAllMultipleToolbarButton}"
Tag="{x:Static feedCenter:MultipleOpenAction.IndividualPages}" /> Tag="{x:Static feeds:MultipleOpenAction.IndividualPages}" />
<ComboBoxItem Content="{x:Static my:Resources.openAllSingleToolbarButton}" <ComboBoxItem Content="{x:Static my:Resources.openAllSingleToolbarButton}"
Tag="{x:Static feedCenter:MultipleOpenAction.SinglePage}" /> Tag="{x:Static feeds:MultipleOpenAction.SinglePage}" />
</ComboBox> </ComboBox>
</Grid> </Grid>
<StackPanel Grid.Column="0"
Grid.Row="3"
Orientation="Horizontal"
Margin="0,5,0,0"
HorizontalAlignment="Right">
<Button Content="{x:Static my:Resources.OkayButton}" <Button Content="{x:Static my:Resources.OkayButton}"
Height="23"
HorizontalAlignment="Right" HorizontalAlignment="Right"
IsDefault="True"
Margin="0,6,87,6"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Width="75" Width="75"
Grid.Column="1" Margin="0,0,5,0"
Grid.Row="3" IsDefault="True"
Click="HandleOkButtonClick" /> Click="HandleOkButtonClick" />
<Button Content="{x:Static my:Resources.CancelButton}" <Button Content="{x:Static my:Resources.CancelButton}"
Grid.Column="1"
Height="23"
HorizontalAlignment="Right" HorizontalAlignment="Right"
IsCancel="True"
Margin="0,6,6,6"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Width="75" Width="75"
Grid.Row="3" /> IsCancel="True" />
</StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,28 +1,31 @@
using Common.Wpf; using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using FeedCenter.Feeds;
namespace FeedCenter.Options;
namespace FeedCenter.Options
{
public partial class BulkFeedWindow public partial class BulkFeedWindow
{ {
private List<CheckedListItem<Feed>> _checkedListBoxItems; private List<CheckedListItem<Feed>> _checkedListBoxItems;
private CollectionViewSource _collectionViewSource; private CollectionViewSource _collectionViewSource;
private readonly FeedCenterEntities _entities;
public BulkFeedWindow() public BulkFeedWindow(FeedCenterEntities entities)
{ {
_entities = entities;
InitializeComponent(); InitializeComponent();
} }
public void Display(Window window, FeedCenterEntities database) public void Display(Window window)
{ {
_checkedListBoxItems = new List<CheckedListItem<Feed>>(); _checkedListBoxItems = new List<CheckedListItem<Feed>>();
foreach (var feed in database.AllFeeds) foreach (var feed in _entities.Feeds)
_checkedListBoxItems.Add(new CheckedListItem<Feed> { Item = feed }); _checkedListBoxItems.Add(new CheckedListItem<Feed> { Item = feed });
_collectionViewSource = new CollectionViewSource { Source = _checkedListBoxItems }; _collectionViewSource = new CollectionViewSource { Source = _checkedListBoxItems };
@@ -36,7 +39,7 @@ namespace FeedCenter.Options
ShowDialog(); ShowDialog();
} }
void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e) private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{ {
var checkedListBoxItem = (CheckedListItem<Feed>) e.Item; var checkedListBoxItem = (CheckedListItem<Feed>) e.Item;
@@ -51,12 +54,15 @@ namespace FeedCenter.Options
} }
private void HandleOkButtonClick(object sender, RoutedEventArgs e) private void HandleOkButtonClick(object sender, RoutedEventArgs e)
{
_entities.SaveChanges(() =>
{ {
foreach (var item in _checkedListBoxItems.Where(i => i.IsChecked)) foreach (var item in _checkedListBoxItems.Where(i => i.IsChecked))
{ {
if (OpenComboBox.IsEnabled) if (OpenComboBox.IsEnabled)
item.Item.MultipleOpenAction = (MultipleOpenAction) ((ComboBoxItem) OpenComboBox.SelectedItem).Tag; item.Item.MultipleOpenAction = (MultipleOpenAction) ((ComboBoxItem) OpenComboBox.SelectedItem).Tag;
} }
});
DialogResult = true; DialogResult = true;
Close(); Close();
@@ -91,11 +97,4 @@ namespace FeedCenter.Options
checkedListItem.IsChecked = !checkedListItem.IsChecked; checkedListItem.IsChecked = !checkedListItem.IsChecked;
} }
} }
private void HandleGridMouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
OpenLabel.IsEnabled = !OpenLabel.IsEnabled;
OpenComboBox.IsEnabled = !OpenComboBox.IsEnabled;
}
}
} }

View File

@@ -2,44 +2,54 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:properties="clr-namespace:FeedCenter.Properties" xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:feedCenter="clr-namespace:FeedCenter"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Windows;assembly=ChrisKaczor.Wpf.Windows.ControlBox"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:feeds="clr-namespace:FeedCenter.Feeds"
d:DataContext="{d:DesignInstance Type=feeds:Category}"
Title="CategoryWindow" Title="CategoryWindow"
Height="119" Width="300"
Width="339" ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Icon="/FeedCenter;component/Resources/Application.ico" Icon="/FeedCenter;component/Resources/Application.ico"
FocusManager.FocusedElement="{Binding ElementName=NameTextBox}"> FocusManager.FocusedElement="{Binding ElementName=NameTextBox}"
<Grid> controls:ControlBox.HasMinimizeButton="False"
<Grid.ColumnDefinitions> controls:ControlBox.HasMaximizeButton="False">
<ColumnDefinition Width="Auto" /> <Window.Resources>
<ColumnDefinition Width="367*" /> <ResourceDictionary>
</Grid.ColumnDefinitions> <ResourceDictionary.MergedDictionaries>
<Label Content="{x:Static properties:Resources.feedCategoryLabel}" <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
HorizontalAlignment="Left" <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
Target="{Binding ElementName=NameTextBox}" <ResourceDictionary
VerticalAlignment="Top" Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
VerticalContentAlignment="Center" <ResourceDictionary
Margin="12,12,0,0" /> Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
<TextBox Margin="7,14,12,0" </ResourceDictionary.MergedDictionaries>
Name="NameTextBox" </ResourceDictionary>
Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" </Window.Resources>
VerticalAlignment="Top" <StackPanel Margin="6"
Grid.Column="1" /> options:Spacing.Vertical="5">
<TextBox Name="NameTextBox"
mah:TextBoxHelper.UseFloatingWatermark="True"
mah:TextBoxHelper.Watermark="{x:Static properties:Resources.categoryNameLabel}"
mah:TextBoxHelper.SelectAllOnFocus="True"
Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True}" />
<StackPanel
options:Spacing.Horizontal="5"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="{x:Static properties:Resources.OkayButton}" <Button Content="{x:Static properties:Resources.OkayButton}"
Height="23"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75" Width="75"
IsDefault="True" IsDefault="True"
Margin="0,0,93,12" Click="HandleOkayButtonClick" />
Click="HandleOkayButtonClick"
Grid.Column="1" />
<Button Content="{x:Static properties:Resources.CancelButton}" <Button Content="{x:Static properties:Resources.CancelButton}"
Height="23"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75" Width="75"
IsCancel="True" IsCancel="True" />
Margin="0,0,12,12" </StackPanel>
Grid.Column="1" /> </StackPanel>
</Grid>
</Window> </Window>

View File

@@ -1,15 +1,17 @@
using Common.Wpf.Extensions; using ChrisKaczor.Wpf.Validation;
using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using FeedCenter.Feeds;
using System.Windows.Data;
namespace FeedCenter.Options;
namespace FeedCenter.Options
{
public partial class CategoryWindow public partial class CategoryWindow
{ {
public CategoryWindow() private readonly FeedCenterEntities _entities;
public CategoryWindow(FeedCenterEntities entities)
{ {
_entities = entities;
InitializeComponent(); InitializeComponent();
} }
@@ -19,7 +21,9 @@ namespace FeedCenter.Options
DataContext = category; DataContext = category;
// Set the title based on the state of the category // Set the title based on the state of the category
Title = string.IsNullOrWhiteSpace(category.Name) ? Properties.Resources.CategoryWindowAdd : Properties.Resources.CategoryWindowEdit; Title = string.IsNullOrWhiteSpace(category.Name)
? Properties.Resources.CategoryWindowAdd
: Properties.Resources.CategoryWindowEdit;
// Set the window owner // Set the window owner
Owner = owner; Owner = owner;
@@ -30,30 +34,16 @@ namespace FeedCenter.Options
private void HandleOkayButtonClick(object sender, RoutedEventArgs e) private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{ {
// Get a list of all explicit binding expressions var transaction = _entities.BeginTransaction();
var bindingExpressions = this.GetBindingExpressions(new[] { UpdateSourceTrigger.Explicit });
// Loop over each binding expression and clear any existing error if (!this.IsValid())
bindingExpressions.ForEach(b => Validation.ClearInvalid(b.BindingExpression));
// Force all explicit bindings to update the source
bindingExpressions.ForEach(bindingExpression => bindingExpression.BindingExpression.UpdateSource());
// See if there are any errors
var hasError = bindingExpressions.Exists(bindingExpression => bindingExpression.BindingExpression.HasError);
// If there was an error then set focus to the bad controls
if (hasError)
{ {
// Get the first framework element with an error transaction.Rollback();
var firstErrorElement = bindingExpressions.First(b => b.BindingExpression.HasError).FrameworkElement;
// Set focus
firstErrorElement.Focus();
return; return;
} }
transaction.Commit();
// Dialog is good // Dialog is good
DialogResult = true; DialogResult = true;
@@ -61,4 +51,3 @@ namespace FeedCenter.Options
Close(); Close();
} }
} }
}

View File

@@ -0,0 +1,3 @@
namespace FeedCenter.Options;
public class CheckedFeedListItem : CheckedListItem<Feeds.Feed>;

View File

@@ -0,0 +1,41 @@
using System.ComponentModel;
namespace FeedCenter.Options;
public class CheckedListItem<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool _isChecked;
private readonly T _item;
public CheckedListItem() { }
public CheckedListItem(T item, bool isChecked = false)
{
_item = item;
_isChecked = isChecked;
}
public T Item
{
get => _item;
init
{
_item = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Item)));
}
}
public bool IsChecked
{
get => _isChecked;
set
{
_isChecked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsChecked)));
}
}
}

View File

@@ -5,57 +5,38 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:options="clr-namespace:FeedCenter.Options" xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:properties="clr-namespace:FeedCenter.Properties" xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="300" d:DesignHeight="150"
d:DesignWidth="300"> d:DesignWidth="400">
<Grid> <StackPanel options:Spacing.Vertical="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Content="{x:Static properties:Resources.lockWindowCheckBox}" <CheckBox Content="{x:Static properties:Resources.lockWindowCheckBox}"
Height="16"
HorizontalAlignment="Left"
Name="LockWindowCheckBox" Name="LockWindowCheckBox"
VerticalAlignment="Top" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=WindowLocked}"
Width="300" Click="OnSaveSettings" />
Grid.ColumnSpan="2" />
<CheckBox Content="{x:Static properties:Resources.displayEmptyFeedsCheckBox}" <CheckBox Content="{x:Static properties:Resources.displayEmptyFeedsCheckBox}"
Height="16"
HorizontalAlignment="Left"
Margin="0,22,0,0"
Name="DisplayEmptyFeedsCheckBox" Name="DisplayEmptyFeedsCheckBox"
VerticalAlignment="Top" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=DisplayEmptyFeeds}"
Width="300" Click="OnSaveSettings" />
Grid.ColumnSpan="2" <ComboBox
Grid.Column="0" />
<Label Grid.Column="0"
Content="{x:Static properties:Resources.toolbarLocationLabel}"
VerticalAlignment="Top"
Margin="0,50,0,0"
Padding="0,5,5,5"
Target="{Binding ElementName=ToolbarLocationComboBox}"
Width="97" />
<ComboBox Margin="8,53,0,0"
Name="ToolbarLocationComboBox" Name="ToolbarLocationComboBox"
VerticalAlignment="Top" mah:TextBoxHelper.UseFloatingWatermark="True"
Grid.Column="1"> mah:TextBoxHelper.Watermark="{x:Static properties:Resources.toolbarLocationLabel}"
SelectedValue="{Binding Source={x:Static properties:Settings.Default}, Path=ToolbarLocation}"
SelectedValuePath="Tag"
SelectionChanged="OnSaveSettings">
<ComboBoxItem Content="{x:Static properties:Resources.Top}" <ComboBoxItem Content="{x:Static properties:Resources.Top}"
Tag="{x:Static Dock.Top}" /> Tag="{x:Static Dock.Top}" />
<ComboBoxItem Content="{x:Static properties:Resources.Bottom}" <ComboBoxItem Content="{x:Static properties:Resources.Bottom}"
Tag="{x:Static Dock.Bottom}" /> Tag="{x:Static Dock.Bottom}" />
</ComboBox> </ComboBox>
<Label Grid.Column="0" <ComboBox
Content="{x:Static properties:Resources.multipleLineDisplayLabel}"
VerticalAlignment="Top"
Margin="0,82,0,0"
Padding="0,5,5,5"
Target="{Binding ElementName=MultipleLineDisplayComboBox}"
Width="97" />
<ComboBox Margin="8,86,0,0"
Name="MultipleLineDisplayComboBox" Name="MultipleLineDisplayComboBox"
VerticalAlignment="Top" mah:TextBoxHelper.UseFloatingWatermark="True"
Grid.Column="1"> mah:TextBoxHelper.Watermark="{x:Static properties:Resources.multipleLineDisplayLabel}"
SelectedValue="{Binding Source={x:Static properties:Settings.Default}, Path=MultipleLineDisplay}"
SelectedValuePath="Tag"
SelectionChanged="OnSaveSettings">
<ComboBoxItem Content="{x:Static properties:Resources.multipleLineDisplayNormal}" <ComboBoxItem Content="{x:Static properties:Resources.multipleLineDisplayNormal}"
Tag="{x:Static options:MultipleLineDisplay.Normal}" /> Tag="{x:Static options:MultipleLineDisplay.Normal}" />
<ComboBoxItem Content="{x:Static properties:Resources.multipleLineDisplaySingleLine}" <ComboBoxItem Content="{x:Static properties:Resources.multipleLineDisplaySingleLine}"
@@ -63,5 +44,5 @@
<ComboBoxItem Content="{x:Static properties:Resources.multipleLineDisplayFirstLine}" <ComboBoxItem Content="{x:Static properties:Resources.multipleLineDisplayFirstLine}"
Tag="{x:Static options:MultipleLineDisplay.FirstLine}" /> Tag="{x:Static options:MultipleLineDisplay.FirstLine}" />
</ComboBox> </ComboBox>
</Grid> </StackPanel>
</options:OptionsPanelBase> </options:OptionsPanelBase>

View File

@@ -1,46 +1,33 @@
using FeedCenter.Properties; using FeedCenter.Properties;
using System.Linq; using System.Windows;
using System.Windows.Controls;
namespace FeedCenter.Options;
namespace FeedCenter.Options
{
public partial class DisplayOptionsPanel public partial class DisplayOptionsPanel
{ {
public DisplayOptionsPanel() public DisplayOptionsPanel(Window parentWindow, FeedCenterEntities entities) : base(parentWindow, entities)
{ {
InitializeComponent(); InitializeComponent();
} }
public override void LoadPanel(FeedCenterEntities database)
{
base.LoadPanel(database);
LockWindowCheckBox.IsChecked = Settings.Default.WindowLocked;
DisplayEmptyFeedsCheckBox.IsChecked = Settings.Default.DisplayEmptyFeeds;
ToolbarLocationComboBox.SelectedItem = ToolbarLocationComboBox.Items.Cast<ComboBoxItem>().First(comboBoxItem => (Dock) comboBoxItem.Tag == Settings.Default.ToolbarLocation);
MultipleLineDisplayComboBox.SelectedItem = MultipleLineDisplayComboBox.Items.Cast<ComboBoxItem>().First(comboBoxItem => (MultipleLineDisplay) comboBoxItem.Tag == Settings.Default.MultipleLineDisplay);
}
public override bool ValidatePanel()
{
return true;
}
public override void SavePanel()
{
if (LockWindowCheckBox.IsChecked.HasValue && Settings.Default.WindowLocked != LockWindowCheckBox.IsChecked.Value)
Settings.Default.WindowLocked = LockWindowCheckBox.IsChecked.Value;
if (DisplayEmptyFeedsCheckBox.IsChecked.HasValue && Settings.Default.DisplayEmptyFeeds != DisplayEmptyFeedsCheckBox.IsChecked.Value)
Settings.Default.DisplayEmptyFeeds = DisplayEmptyFeedsCheckBox.IsChecked.Value;
var dock = (Dock) ((ComboBoxItem) ToolbarLocationComboBox.SelectedItem).Tag;
Settings.Default.ToolbarLocation = dock;
var multipleLineDisplay = (MultipleLineDisplay) ((ComboBoxItem) MultipleLineDisplayComboBox.SelectedItem).Tag;
Settings.Default.MultipleLineDisplay = multipleLineDisplay;
}
public override string CategoryName => Properties.Resources.optionCategoryDisplay; public override string CategoryName => Properties.Resources.optionCategoryDisplay;
public override void LoadPanel()
{
base.LoadPanel();
MarkLoaded();
}
private void OnSaveSettings(object sender, RoutedEventArgs e)
{
SaveSettings();
}
private void SaveSettings()
{
if (!HasLoaded) return;
Settings.Default.Save();
} }
} }

View File

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

View File

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

View File

@@ -4,11 +4,23 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:options="clr-namespace:FeedCenter.Options" xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:linkControl="clr-namespace:Common.Wpf.LinkControl;assembly=Common.Wpf"
xmlns:properties="clr-namespace:FeedCenter.Properties" xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:controls="clr-namespace:ChrisKaczor.Wpf.Controls;assembly=ChrisKaczor.Wpf.Controls.Link"
xmlns:feedCenter="clr-namespace:FeedCenter"
xmlns:feeds="clr-namespace:FeedCenter.Feeds"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="311" d:DesignHeight="311"
d:DesignWidth="425"> d:DesignWidth="425">
<options:OptionsPanelBase.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</options:OptionsPanelBase.Resources>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -19,7 +31,43 @@
<ColumnDefinition Width="5" /> <ColumnDefinition Width="5" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<DataGrid Name="FeedListBox" <DataGrid Name="CategoryDataGrid"
SelectionChanged="HandleCategoryDataGridSelectionChanged"
Grid.Row="0"
SelectionMode="Single"
SelectionUnit="FullRow"
Grid.Column="0"
AutoGenerateColumns="False"
GridLinesVisibility="None"
CanUserResizeRows="False"
IsReadOnly="True"
HeadersVisibility="Column"
BorderThickness="1,1,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
AllowDrop="True"
Background="{x:Null}"
d:DataContext="{d:DesignInstance feeds:Category }">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static properties:Resources.CategoryNameColumnHeader}"
SortDirection="Ascending"
Width="*" />
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow"
BasedOn="{StaticResource MahApps.Styles.DataGridRow}">
<EventSetter Event="Drop"
Handler="HandleCategoryDataGridRowDrop" />
<EventSetter Event="DragEnter"
Handler="HandleCategoryDataGridRowDragEnter" />
<EventSetter Event="DragLeave"
Handler="HandleCategoryDataGridRowDragLeave" />
<EventSetter Event="MouseDoubleClick"
Handler="HandleCategoryDataGridRowMouseDoubleClick" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
<DataGrid Name="FeedDataGrid"
SelectionMode="Extended" SelectionMode="Extended"
Grid.Column="2" Grid.Column="2"
Grid.Row="0" Grid.Row="0"
@@ -27,9 +75,13 @@
GridLinesVisibility="None" GridLinesVisibility="None"
CanUserResizeRows="False" CanUserResizeRows="False"
IsReadOnly="True" IsReadOnly="True"
SelectionUnit="FullRow"
HeadersVisibility="Column" HeadersVisibility="Column"
BorderThickness="1,1,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Background="{x:Null}" Background="{x:Null}"
PreviewMouseLeftButtonDown="HandleFeedListPreviewMouseLeftButtonDown"> SelectionChanged="HandleFeedDataGridSelectionChanged"
d:DataContext="{d:DesignInstance feeds:Feed }">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}" <DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static properties:Resources.FeedNameColumnHeader}" Header="{x:Static properties:Resources.FeedNameColumnHeader}"
@@ -39,55 +91,15 @@
Header="{x:Static properties:Resources.LastUpdatedColumnHeader}" Header="{x:Static properties:Resources.LastUpdatedColumnHeader}"
Width="Auto" /> Width="Auto" />
</DataGrid.Columns> </DataGrid.Columns>
<DataGrid.ItemContainerStyle> <DataGrid.RowStyle>
<Style TargetType="DataGridRow"> <Style TargetType="DataGridRow"
BasedOn="{StaticResource MahApps.Styles.DataGridRow}">
<EventSetter Event="MouseDoubleClick" <EventSetter Event="MouseDoubleClick"
Handler="HandleListBoxItemMouseDoubleClick" /> Handler="HandleFeedDataGridRowMouseDoubleClick" />
<EventSetter Event="PreviewMouseMove" <EventSetter Event="PreviewMouseLeftButtonDown"
Handler="HandleListBoxItemPreviewMouseMove" /> Handler="HandleFeedDataGridRowPreviewMouseLeftButtonDown" />
</Style> </Style>
</DataGrid.ItemContainerStyle> </DataGrid.RowStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness"
Value="0" />
</Style>
</DataGrid.CellStyle>
</DataGrid>
<DataGrid Name="CategoryListBox"
SelectionChanged="HandleCategoryListBoxSelectionChanged"
Grid.Row="0"
SelectionMode="Extended"
Grid.Column="0"
AutoGenerateColumns="False"
GridLinesVisibility="None"
CanUserResizeRows="False"
IsReadOnly="True"
HeadersVisibility="Column"
AllowDrop="True"
Background="{x:Null}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static properties:Resources.CategoryNameColumnHeader}"
SortDirection="Ascending"
Width="*" />
</DataGrid.Columns>
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="Drop"
Handler="HandleTextBlockDrop" />
<EventSetter Event="DragEnter"
Handler="HandleTextBlockDragEnter" />
<EventSetter Event="DragLeave"
Handler="HandleTextBlockDragLeave" />
</Style>
</DataGrid.ItemContainerStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness"
Value="0" />
</Style>
</DataGrid.CellStyle>
</DataGrid> </DataGrid>
<Border Grid.Column="2" <Border Grid.Column="2"
Grid.Row="1" Grid.Row="1"
@@ -95,39 +107,39 @@
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<linkControl:LinkControl Name="AddFeedButton" <controls:Link Name="AddFeedButton"
Margin="2" Margin="2"
Click="HandleAddFeedButtonClick" Click="HandleAddFeedButtonClick"
Text="{x:Static properties:Resources.AddLink}" Text="{x:Static properties:Resources.AddLink}"
ToolTip="{x:Static properties:Resources.AddFeedButton}"> ToolTip="{x:Static properties:Resources.AddFeedButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Name="EditFeedButton" <controls:Link Name="EditFeedButton"
Margin="2" Margin="2"
Click="HandleEditFeedButtonClick" Click="HandleEditFeedButtonClick"
Text="{x:Static properties:Resources.EditLink}" Text="{x:Static properties:Resources.EditLink}"
ToolTip="{x:Static properties:Resources.EditFeedButton}"> ToolTip="{x:Static properties:Resources.EditFeedButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Name="DeleteFeedButton" <controls:Link Name="DeleteFeedButton"
Margin="2" Margin="2"
Click="HandleDeleteFeedButtonClick" Click="HandleDeleteFeedButtonClick"
Text="{x:Static properties:Resources.DeleteLink}" Text="{x:Static properties:Resources.DeleteLink}"
ToolTip="{x:Static properties:Resources.DeleteFeedButton}"> ToolTip="{x:Static properties:Resources.DeleteFeedButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Margin="6,2,2,2" <controls:Link Margin="6,2,2,2"
Click="HandleImportButtonClick" Click="HandleImportButtonClick"
Text="{x:Static properties:Resources.ImportLink}" Text="{x:Static properties:Resources.ImportLink}"
ToolTip="{x:Static properties:Resources.ImportFeedsButton}"> ToolTip="{x:Static properties:Resources.ImportFeedsButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Margin="2" <controls:Link Margin="2"
Click="HandleExportButtonClick" Click="HandleExportButtonClick"
Text="{x:Static properties:Resources.ExportLink}" Text="{x:Static properties:Resources.ExportLink}"
ToolTip="{x:Static properties:Resources.ExportFeedsButton}"> ToolTip="{x:Static properties:Resources.ExportFeedsButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Margin="6,2,2,2" <controls:Link Margin="6,2,2,2"
Click="HandleMultipleEditClick" Click="HandleMultipleEditClick"
Text="{x:Static properties:Resources.MultipleEditLink}" Text="{x:Static properties:Resources.MultipleEditLink}"
ToolTip="{x:Static properties:Resources.MultipleEditButton}"> ToolTip="{x:Static properties:Resources.MultipleEditButton}">
</linkControl:LinkControl> </controls:Link>
</StackPanel> </StackPanel>
</Border> </Border>
<Border Grid.Row="1" <Border Grid.Row="1"
@@ -136,24 +148,24 @@
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<linkControl:LinkControl Name="AddCategoryButton" <controls:Link Name="AddCategoryButton"
Margin="2" Margin="2"
Click="HandleAddCategoryButtonClick" Click="HandleAddCategoryButtonClick"
Text="{x:Static properties:Resources.AddLink}" Text="{x:Static properties:Resources.AddLink}"
ToolTip="{x:Static properties:Resources.AddCategoryButton}"> ToolTip="{x:Static properties:Resources.AddCategoryButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Name="EditCategoryButton" <controls:Link Name="EditCategoryButton"
Margin="2" Margin="2"
Click="HandleEditCategoryButtonClick" Click="HandleEditCategoryButtonClick"
Text="{x:Static properties:Resources.EditLink}" Text="{x:Static properties:Resources.EditLink}"
ToolTip="{x:Static properties:Resources.EditCategoryButton}"> ToolTip="{x:Static properties:Resources.EditCategoryButton}">
</linkControl:LinkControl> </controls:Link>
<linkControl:LinkControl Name="DeleteCategoryButton" <controls:Link Name="DeleteCategoryButton"
Margin="2" Margin="2"
Click="HandleDeleteCategoryButtonClick" Click="HandleDeleteCategoryButtonClick"
Text="{x:Static properties:Resources.DeleteLink}" Text="{x:Static properties:Resources.DeleteLink}"
ToolTip="{x:Static properties:Resources.DeleteCategoryButton}"> ToolTip="{x:Static properties:Resources.DeleteCategoryButton}">
</linkControl:LinkControl> </controls:Link>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>

View File

@@ -6,104 +6,95 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Xml; using System.Xml;
using FeedCenter.Accounts;
using FeedCenter.Feeds;
namespace FeedCenter.Options;
namespace FeedCenter.Options
{
public partial class FeedsOptionsPanel public partial class FeedsOptionsPanel
{ {
#region Constructor private CollectionViewSource _collectionViewSource;
private readonly FeedCenterEntities _entities;
public FeedsOptionsPanel() public FeedsOptionsPanel(Window parentWindow, FeedCenterEntities entities) : base(parentWindow, entities)
{ {
_entities = entities;
InitializeComponent(); InitializeComponent();
} }
#endregion
#region OptionsPanelBase overrides
public override void LoadPanel(FeedCenterEntities database)
{
base.LoadPanel(database);
var collectionViewSource = new CollectionViewSource { Source = Database.AllCategories };
collectionViewSource.SortDescriptions.Add(new SortDescription("SortKey", ListSortDirection.Ascending));
collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
CategoryListBox.ItemsSource = collectionViewSource.View;
CategoryListBox.SelectedIndex = 0;
}
public override bool ValidatePanel()
{
return true;
}
public override void SavePanel()
{ }
public override string CategoryName => Properties.Resources.optionCategoryFeeds; public override string CategoryName => Properties.Resources.optionCategoryFeeds;
#endregion public override void LoadPanel()
{
base.LoadPanel();
#region Feed list management var collectionViewSource = new CollectionViewSource { Source = _entities.Categories };
collectionViewSource.SortDescriptions.Add(new SortDescription("SortKey", ListSortDirection.Ascending));
collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
collectionViewSource.IsLiveSortingRequested = true;
CategoryDataGrid.ItemsSource = collectionViewSource.View;
CategoryDataGrid.SelectedIndex = 0;
}
private void SetFeedButtonStates() private void SetFeedButtonStates()
{ {
AddFeedButton.IsEnabled = true; AddFeedButton.IsEnabled = true;
EditFeedButton.IsEnabled = (FeedListBox.SelectedItem != null); EditFeedButton.IsEnabled = FeedDataGrid.SelectedItems.Count == 1;
DeleteFeedButton.IsEnabled = (FeedListBox.SelectedItem != null); DeleteFeedButton.IsEnabled = FeedDataGrid.SelectedItems.Count > 0;
} }
private void AddFeed() private void AddFeed()
{ {
var feed = Feed.Create(Database); var feed = Feed.Create(_entities);
var category = (Category) CategoryListBox.SelectedItem; var category = (Category) CategoryDataGrid.SelectedItem;
feed.Category = category; feed.CategoryId = category.Id;
var feedWindow = new FeedWindow(); var feedWindow = new FeedWindow(_entities);
var result = feedWindow.Display(Database, feed, Window.GetWindow(this)); var result = feedWindow.Display(feed, Window.GetWindow(this));
if (result.HasValue && result.Value) if (!result.HasValue || !result.Value)
{ return;
Database.Feeds.Add(feed);
FeedListBox.SelectedItem = feed; _entities.SaveChanges(() => _entities.Feeds.Add(feed));
FeedDataGrid.SelectedItem = feed;
SetFeedButtonStates(); SetFeedButtonStates();
} }
}
private void EditSelectedFeed() private void EditSelectedFeed()
{ {
if (FeedListBox.SelectedItem == null) if (FeedDataGrid.SelectedItem == null)
return; return;
var feed = (Feed) FeedListBox.SelectedItem; var feed = (Feed) FeedDataGrid.SelectedItem;
var feedWindow = new FeedWindow(); var feedWindow = new FeedWindow(_entities);
feedWindow.Display(Database, feed, Window.GetWindow(this)); feedWindow.Display(feed, Window.GetWindow(this));
} }
private void DeleteSelectedFeed() private void DeleteSelectedFeeds()
{ {
var feed = (Feed) FeedListBox.SelectedItem; if (MessageBox.Show(ParentWindow, Properties.Resources.ConfirmDeleteFeeds, Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
Database.Feeds.Remove(feed); var selectedItems = new Feed[FeedDataGrid.SelectedItems.Count];
FeedDataGrid.SelectedItems.CopyTo(selectedItems, 0);
foreach (var feed in selectedItems)
_entities.SaveChanges(() => _entities.Feeds.Remove(feed));
SetFeedButtonStates(); SetFeedButtonStates();
} }
#endregion
#region Feed event handlers
private void HandleAddFeedButtonClick(object sender, RoutedEventArgs e) private void HandleAddFeedButtonClick(object sender, RoutedEventArgs e)
{ {
AddFeed(); AddFeed();
@@ -116,7 +107,7 @@ namespace FeedCenter.Options
private void HandleDeleteFeedButtonClick(object sender, RoutedEventArgs e) private void HandleDeleteFeedButtonClick(object sender, RoutedEventArgs e)
{ {
DeleteSelectedFeed(); DeleteSelectedFeeds();
} }
private void HandleImportButtonClick(object sender, RoutedEventArgs e) private void HandleImportButtonClick(object sender, RoutedEventArgs e)
@@ -129,15 +120,11 @@ namespace FeedCenter.Options
ExportFeeds(); ExportFeeds();
} }
#endregion
#region Feed import and export
private void ExportFeeds() private void ExportFeeds()
{ {
// Setup the save file dialog
var saveFileDialog = new SaveFileDialog var saveFileDialog = new SaveFileDialog
{ {
FileName = Properties.Resources.ApplicationName,
Filter = Properties.Resources.ImportExportFilter, Filter = Properties.Resources.ImportExportFilter,
FilterIndex = 0, FilterIndex = 0,
OverwritePrompt = true OverwritePrompt = true
@@ -148,7 +135,6 @@ namespace FeedCenter.Options
if (!result.GetValueOrDefault(false)) if (!result.GetValueOrDefault(false))
return; return;
// Setup the writer settings
var writerSettings = new XmlWriterSettings var writerSettings = new XmlWriterSettings
{ {
Indent = true, Indent = true,
@@ -156,48 +142,32 @@ namespace FeedCenter.Options
ConformanceLevel = ConformanceLevel.Document ConformanceLevel = ConformanceLevel.Document
}; };
// Create an XML writer for the file chosen
var xmlWriter = XmlWriter.Create(saveFileDialog.FileName, writerSettings); var xmlWriter = XmlWriter.Create(saveFileDialog.FileName, writerSettings);
// Start the opml element
xmlWriter.WriteStartElement("opml"); xmlWriter.WriteStartElement("opml");
// Start the body element
xmlWriter.WriteStartElement("body"); xmlWriter.WriteStartElement("body");
// Loop over each feed foreach (var feed in _entities.Feeds.OrderBy(feed => feed.Name))
foreach (var feed in Database.Feeds.OrderBy(feed => feed.Name))
{ {
// Start the outline element
xmlWriter.WriteStartElement("outline"); xmlWriter.WriteStartElement("outline");
// Write the title
xmlWriter.WriteAttributeString("title", feed.Title); xmlWriter.WriteAttributeString("title", feed.Title);
// Write the HTML link
xmlWriter.WriteAttributeString("htmlUrl", feed.Link); xmlWriter.WriteAttributeString("htmlUrl", feed.Link);
// Write the XML link
xmlWriter.WriteAttributeString("xmlUrl", feed.Source); xmlWriter.WriteAttributeString("xmlUrl", feed.Source);
// End the outline element
xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement();
} }
// End the body element
xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement();
// End the opml element
xmlWriter.WriteEndElement(); xmlWriter.WriteEndElement();
// Flush and close the writer
xmlWriter.Flush(); xmlWriter.Flush();
xmlWriter.Close(); xmlWriter.Close();
} }
private void ImportFeeds() private void ImportFeeds()
{ {
// Setup the open file dialog
var openFileDialog = new OpenFileDialog var openFileDialog = new OpenFileDialog
{ {
Filter = Properties.Resources.ImportExportFilter, Filter = Properties.Resources.ImportExportFilter,
@@ -209,44 +179,35 @@ namespace FeedCenter.Options
if (!result.GetValueOrDefault(false)) if (!result.GetValueOrDefault(false))
return; return;
// Setup the reader settings
var xmlReaderSettings = new XmlReaderSettings { IgnoreWhitespace = true }; var xmlReaderSettings = new XmlReaderSettings { IgnoreWhitespace = true };
// Create an XML reader for the file chosen
var xmlReader = XmlReader.Create(openFileDialog.FileName, xmlReaderSettings); var xmlReader = XmlReader.Create(openFileDialog.FileName, xmlReaderSettings);
try try
{ {
// Read the first node
xmlReader.Read(); xmlReader.Read();
// Read the OPML node
xmlReader.ReadStartElement("opml"); xmlReader.ReadStartElement("opml");
// Read the body node
xmlReader.ReadStartElement("body"); xmlReader.ReadStartElement("body");
// Read all outline nodes
while (xmlReader.NodeType != XmlNodeType.EndElement) while (xmlReader.NodeType != XmlNodeType.EndElement)
{ {
// Create a new feed var feed = Feed.Create(_entities);
var feed = Feed.Create(Database);
feed.Category = Database.Categories.First(c => c.IsDefault);
// Loop over all attributes
while (xmlReader.MoveToNextAttribute()) while (xmlReader.MoveToNextAttribute())
{ {
// Handle the attibute
switch (xmlReader.Name.ToLower()) switch (xmlReader.Name.ToLower())
{ {
case "title": case "title":
feed.Title = xmlReader.Value; feed.Title = xmlReader.Value;
break; break;
// ReSharper disable once StringLiteralTypo
case "htmlurl": case "htmlurl":
feed.Link = xmlReader.Value; feed.Link = xmlReader.Value;
break; break;
// ReSharper disable once StringLiteralTypo
case "xmlurl": case "xmlurl":
feed.Source = xmlReader.Value; feed.Source = xmlReader.Value;
break; break;
@@ -257,24 +218,18 @@ namespace FeedCenter.Options
} }
} }
// Fill in defaults for optional fields
if (string.IsNullOrEmpty(feed.Name)) if (string.IsNullOrEmpty(feed.Name))
feed.Name = feed.Title; feed.Name = feed.Title;
// Add the feed to the main list _entities.SaveChanges(() => _entities.Feeds.Add(feed));
Database.Feeds.Add(feed);
// Move back to the element node
xmlReader.MoveToElement(); xmlReader.MoveToElement();
// Skip to the next node
xmlReader.Skip(); xmlReader.Skip();
} }
// End the body node
xmlReader.ReadEndElement(); xmlReader.ReadEndElement();
// End the OPML node
xmlReader.ReadEndElement(); xmlReader.ReadEndElement();
} }
finally finally
@@ -283,71 +238,72 @@ namespace FeedCenter.Options
} }
} }
#endregion
#region Category list management
private void SetCategoryButtonStates() private void SetCategoryButtonStates()
{ {
AddCategoryButton.IsEnabled = true; AddCategoryButton.IsEnabled = true;
EditCategoryButton.IsEnabled = (CategoryListBox.SelectedItem != null && CategoryListBox.SelectedItem != Database.DefaultCategory);
DeleteCategoryButton.IsEnabled = (CategoryListBox.SelectedItem != null && CategoryListBox.SelectedItem != Database.DefaultCategory); var selectedId = ((Category) CategoryDataGrid.SelectedItem).Id;
EditCategoryButton.IsEnabled = CategoryDataGrid.SelectedItem != null &&
selectedId != _entities.DefaultCategory.Id;
DeleteCategoryButton.IsEnabled = CategoryDataGrid.SelectedItem != null &&
selectedId != _entities.DefaultCategory.Id;
} }
private void AddCategory() private void AddCategory()
{ {
var category = Category.Create(); var category = new Category();
var categoryWindow = new CategoryWindow(); var categoryWindow = new CategoryWindow(_entities);
var result = categoryWindow.Display(category, Window.GetWindow(this)); var result = categoryWindow.Display(category, Window.GetWindow(this));
if (result.HasValue && result.Value) if (!result.HasValue || !result.Value)
{ return;
Database.Categories.Add(category);
CategoryListBox.SelectedItem = category; _entities.SaveChanges(() => _entities.Categories.Add(category));
CategoryDataGrid.SelectedItem = category;
SetCategoryButtonStates(); SetCategoryButtonStates();
} }
}
private void EditSelectedCategory() private void EditSelectedCategory()
{ {
if (CategoryListBox.SelectedItem == null) if (CategoryDataGrid.SelectedItem == null)
return; return;
var category = (Category) CategoryListBox.SelectedItem; var category = (Category) CategoryDataGrid.SelectedItem;
var categoryWindow = new CategoryWindow(); var categoryWindow = new CategoryWindow(_entities);
categoryWindow.Display(category, Window.GetWindow(this)); categoryWindow.Display(category, Window.GetWindow(this));
} }
private void DeleteSelectedCategory() private void DeleteSelectedCategory()
{ {
var defaultCategory = Database.DefaultCategory; var category = (Category) CategoryDataGrid.SelectedItem;
var category = (Category) CategoryListBox.SelectedItem; if (MessageBox.Show(ParentWindow, string.Format(Properties.Resources.ConfirmDeleteCategory, category.Name), Properties.Resources.ConfirmDeleteTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.No)
return;
category.Feeds.ToList().ForEach(feed => feed.Category = defaultCategory); var defaultCategory = _entities.DefaultCategory;
var index = CategoryListBox.SelectedIndex; foreach (var feed in _entities.Feeds.Where(f => f.CategoryId == category.Id))
_entities.SaveChanges(() => feed.CategoryId = defaultCategory.Id);
if (index == CategoryListBox.Items.Count - 1) var index = CategoryDataGrid.SelectedIndex;
CategoryListBox.SelectedIndex = index - 1;
if (index == CategoryDataGrid.Items.Count - 1)
CategoryDataGrid.SelectedIndex = index - 1;
else else
CategoryListBox.SelectedIndex = index + 1; CategoryDataGrid.SelectedIndex = index + 1;
Database.Categories.Remove(category); _entities.SaveChanges(() => _entities.Categories.Remove(category));
SetCategoryButtonStates(); SetCategoryButtonStates();
} }
#endregion
#region Category event handlers
private void HandleAddCategoryButtonClick(object sender, RoutedEventArgs e) private void HandleAddCategoryButtonClick(object sender, RoutedEventArgs e)
{ {
AddCategory(); AddCategory();
@@ -363,25 +319,21 @@ namespace FeedCenter.Options
DeleteSelectedCategory(); DeleteSelectedCategory();
} }
#endregion private void HandleCategoryDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
private CollectionViewSource _collectionViewSource;
private void HandleCategoryListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
if (_collectionViewSource == null) if (_collectionViewSource == null)
{ {
_collectionViewSource = new CollectionViewSource { Source = Database.AllFeeds }; _collectionViewSource = new CollectionViewSource { Source = _entities.Feeds };
_collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending)); _collectionViewSource.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
_collectionViewSource.Filter += HandleCollectionViewSourceFilter; _collectionViewSource.Filter += HandleCollectionViewSourceFilter;
FeedListBox.ItemsSource = _collectionViewSource.View; FeedDataGrid.ItemsSource = _collectionViewSource.View;
} }
_collectionViewSource.View.Refresh(); _collectionViewSource.View.Refresh();
if (FeedListBox.Items.Count > 0) if (FeedDataGrid.Items.Count > 0)
FeedListBox.SelectedIndex = 0; FeedDataGrid.SelectedIndex = 0;
SetFeedButtonStates(); SetFeedButtonStates();
SetCategoryButtonStates(); SetCategoryButtonStates();
@@ -389,21 +341,21 @@ namespace FeedCenter.Options
private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e) private void HandleCollectionViewSourceFilter(object sender, FilterEventArgs e)
{ {
var selectedCategory = (Category) CategoryListBox.SelectedItem; var selectedCategory = (Category) CategoryDataGrid.SelectedItem;
var feed = (Feed) e.Item; var feed = (Feed) e.Item;
e.Accepted = (feed.Category.ID == selectedCategory.ID); e.Accepted = feed.CategoryId == selectedCategory.Id && feed.Account.Type == AccountType.Local;
} }
private void HandleTextBlockDrop(object sender, DragEventArgs e) private void HandleCategoryDataGridRowDrop(object sender, DragEventArgs e)
{ {
var feedList = (List<Feed>) e.Data.GetData(typeof(List<Feed>)); var feedList = (List<Feed>) e.Data.GetData(typeof(List<Feed>));
var category = (Category) ((DataGridRow) sender).Item; var category = (Category) ((DataGridRow) sender).Item;
foreach (var feed in feedList) foreach (var feed in feedList!)
feed.Category = category; _entities.SaveChanges(() => feed.CategoryId = category.Id);
_collectionViewSource.View.Refresh(); _collectionViewSource.View.Refresh();
@@ -412,52 +364,48 @@ namespace FeedCenter.Options
dataGridRow.FontWeight = FontWeights.Normal; dataGridRow.FontWeight = FontWeights.Normal;
} }
private void HandleListBoxItemPreviewMouseMove(object sender, MouseEventArgs e) private void HandleCategoryDataGridRowDragEnter(object sender, DragEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
var selectedItems = FeedListBox.SelectedItems.Cast<Feed>().ToList();
DragDrop.DoDragDrop(FeedListBox, selectedItems, DragDropEffects.Move);
}
}
private void HandleTextBlockDragEnter(object sender, DragEventArgs e)
{ {
var dataGridRow = (DataGridRow) sender; var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Bold; dataGridRow.FontWeight = FontWeights.Bold;
} }
private void HandleTextBlockDragLeave(object sender, DragEventArgs e) private void HandleCategoryDataGridRowDragLeave(object sender, DragEventArgs e)
{ {
var dataGridRow = (DataGridRow) sender; var dataGridRow = (DataGridRow) sender;
dataGridRow.FontWeight = FontWeights.Normal; dataGridRow.FontWeight = FontWeights.Normal;
} }
private void HandleListBoxItemMouseDoubleClick(object sender, MouseButtonEventArgs e) private void HandleFeedDataGridRowMouseDoubleClick(object sender, MouseButtonEventArgs e)
{ {
EditSelectedFeed(); EditSelectedFeed();
} }
private void HandleMultipleEditClick(object sender, RoutedEventArgs e) private void HandleMultipleEditClick(object sender, RoutedEventArgs e)
{ {
var bulkFeedWindow = new BulkFeedWindow(); var bulkFeedWindow = new BulkFeedWindow(_entities);
bulkFeedWindow.Display(Window.GetWindow(this), Database); bulkFeedWindow.Display(Window.GetWindow(this));
} }
private void HandleFeedListPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void HandleFeedDataGridRowPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
// Get the object that was clicked on var selectedItems = FeedDataGrid.SelectedItems.Cast<Feed>().ToList();
var originalSource = (DependencyObject) e.OriginalSource;
// Look for a row that contains the object DragDrop.DoDragDrop(FeedDataGrid, selectedItems, DragDropEffects.Move);
var dataGridRow = (DataGridRow) FeedListBox.ContainerFromElement(originalSource); }
// If the selection already contains this row then ignore it private void HandleCategoryDataGridRowMouseDoubleClick(object sender, MouseButtonEventArgs e)
if (dataGridRow != null && FeedListBox.SelectedItems.Contains(dataGridRow.Item)) {
e.Handled = true; if (!EditCategoryButton.IsEnabled)
} return;
EditSelectedCategory();
}
private void HandleFeedDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetFeedButtonStates();
} }
} }

View File

@@ -5,54 +5,30 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:options="clr-namespace:FeedCenter.Options" xmlns:options="clr-namespace:FeedCenter.Options"
xmlns:properties="clr-namespace:FeedCenter.Properties" xmlns:properties="clr-namespace:FeedCenter.Properties"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:installedBrowsers="clr-namespace:ChrisKaczor.InstalledBrowsers;assembly=ChrisKaczor.InstalledBrowsers"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="300" d:DesignHeight="300"
d:DesignWidth="300"> d:DesignWidth="300">
<Grid> <StackPanel options:Spacing.Vertical="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="15" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Content="{x:Static properties:Resources.startWithWindowsCheckBox}" <CheckBox Content="{x:Static properties:Resources.startWithWindowsCheckBox}"
Name="StartWithWindowsCheckBox" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=StartWithWindows}"
VerticalAlignment="Top" Click="OnSaveSettings" />
VerticalContentAlignment="Center" <ComboBox mah:TextBoxHelper.UseFloatingWatermark="True"
Margin="0,5" mah:TextBoxHelper.Watermark="{x:Static properties:Resources.defaultBrowserLabel}"
Grid.ColumnSpan="2" /> d:DataContext="{d:DesignInstance Type=installedBrowsers:InstalledBrowser}"
<Label Content="{x:Static properties:Resources.defaultBrowserLabel}" DisplayMemberPath="Name"
Target="{Binding ElementName=BrowserComboBox}" ItemsSource="{Binding Source={x:Static installedBrowsers:InstalledBrowser.InstalledBrowsers}}"
Grid.Column="0" SelectedValuePath="Key"
Grid.Row="2" SelectedValue="{Binding Source={x:Static properties:Settings.Default}, Path=Browser}"
Padding="0" SelectionChanged="OnSaveSettings" />
VerticalContentAlignment="Center" <ComboBox mah:TextBoxHelper.UseFloatingWatermark="True"
Margin="0,0,5,0" /> mah:TextBoxHelper.Watermark="{x:Static properties:Resources.defaultUserAgentLabel}"
<ComboBox Name="BrowserComboBox" d:DataContext="{d:DesignInstance Type=options:UserAgentItem}"
Grid.Row="2" DisplayMemberPath="Caption"
Grid.Column="1" ItemsSource="{Binding Source={x:Static options:UserAgentItem.DefaultUserAgents}}"
VerticalContentAlignment="Center"> SelectedValuePath="UserAgent"
<ComboBoxItem Content="{x:Static properties:Resources.DefaultBrowserCaption}" SelectedValue="{Binding Source={x:Static properties:Settings.Default}, Path=DefaultUserAgent}"
Tag="" /> SelectionChanged="OnSaveSettings" />
</ComboBox> </StackPanel>
<Label Content="{x:Static properties:Resources.defaultUserAgentLabel}"
Target="{Binding ElementName=BrowserComboBox}"
Grid.Column="0"
Grid.Row="4"
Padding="0"
VerticalContentAlignment="Center"
Margin="0,0,5,0" />
<ComboBox Name="UserAgentComboBox"
Grid.Row="4"
Grid.Column="1"
VerticalContentAlignment="Center">
</ComboBox>
</Grid>
</options:OptionsPanelBase> </options:OptionsPanelBase>

View File

@@ -1,132 +1,36 @@
using System.Collections.Generic; using ChrisKaczor.Wpf.Application;
using FeedCenter.Properties;
using System.Windows; using System.Windows;
using Common.Wpf.Extensions;
using Common.Internet;
using System.Windows.Controls;
using System.Windows.Data;
internal class UserAgentItem namespace FeedCenter.Options;
{
internal string Caption { get; set; }
internal string UserAgent { get; set; }
}
namespace FeedCenter.Options
{
public partial class GeneralOptionsPanel public partial class GeneralOptionsPanel
{ {
public GeneralOptionsPanel() public GeneralOptionsPanel(Window parentWindow, FeedCenterEntities entities) : base(parentWindow, entities)
{ {
InitializeComponent(); InitializeComponent();
} }
public override void LoadPanel(FeedCenterEntities database)
{
base.LoadPanel(database);
var settings = Properties.Settings.Default;
StartWithWindowsCheckBox.IsChecked = settings.StartWithWindows;
LoadBrowserComboBox(BrowserComboBox, settings.Browser);
LoadUserAgentComboBox(UserAgentComboBox, settings.DefaultUserAgent);
}
public override bool ValidatePanel()
{
return true;
}
public override void SavePanel()
{
var settings = Properties.Settings.Default;
if (StartWithWindowsCheckBox.IsChecked.HasValue &&
settings.StartWithWindows != StartWithWindowsCheckBox.IsChecked.Value)
settings.StartWithWindows = StartWithWindowsCheckBox.IsChecked.Value;
Application.Current.SetStartWithWindows(settings.StartWithWindows);
settings.Browser = (string) ((ComboBoxItem) BrowserComboBox.SelectedItem).Tag;
settings.DefaultUserAgent = (string) ((ComboBoxItem) UserAgentComboBox.SelectedItem).Tag;
var expressions = this.GetBindingExpressions(new[] { UpdateSourceTrigger.Explicit });
this.UpdateAllSources(expressions);
}
public override string CategoryName => Properties.Resources.optionCategoryGeneral; public override string CategoryName => Properties.Resources.optionCategoryGeneral;
private static void LoadBrowserComboBox(ComboBox comboBox, string selected) public override void LoadPanel()
{ {
comboBox.SelectedIndex = 0; base.LoadPanel();
ComboBoxItem selectedItem = null; MarkLoaded();
var browsers = Browser.DetectInstalledBrowsers();
foreach (var browser in browsers)
{
var item = new ComboBoxItem { Content = browser.Value.Name, Tag = browser.Key };
comboBox.Items.Add(item);
if (browser.Key == selected)
selectedItem = item;
} }
if (selectedItem != null) private void OnSaveSettings(object sender, RoutedEventArgs e)
comboBox.SelectedItem = selectedItem; {
SaveSettings();
} }
private static void LoadUserAgentComboBox(ComboBox comboBox, string selected) private void SaveSettings()
{ {
comboBox.SelectedIndex = 0; if (!HasLoaded) return;
ComboBoxItem selectedItem = null; Settings.Default.Save();
var userAgents = GetUserAgents(); Application.Current.SetStartWithWindows(Settings.Default.StartWithWindows);
foreach (var userAgent in userAgents)
{
var item = new ComboBoxItem { Content = userAgent.Caption, Tag = userAgent.UserAgent };
comboBox.Items.Add(item);
if (userAgent.UserAgent == selected)
selectedItem = item;
}
if (selectedItem != null)
comboBox.SelectedItem = selectedItem;
}
private static List<UserAgentItem> GetUserAgents()
{
var userAgents = new List<UserAgentItem>
{
new UserAgentItem
{
Caption = Properties.Resources.DefaultUserAgentCaption,
UserAgent = string.Empty
},
new UserAgentItem
{
Caption = "Windows RSS Platform 2.0",
UserAgent = "Windows-RSS-Platform/2.0 (MSIE 9.0; Windows NT 6.1)"
},
new UserAgentItem
{
Caption = "Feedly 1.0",
UserAgent = "Feedly/1.0"
},
new UserAgentItem
{
Caption = "curl",
UserAgent = "curl/7.47.0"
}
};
return userAgents;
}
} }
} }

View File

@@ -0,0 +1,73 @@
using JetBrains.Annotations;
using System.Windows;
using System.Windows.Controls;
namespace FeedCenter.Options;
public class MarginSetter
{
private static Thickness GetLastItemMargin(DependencyObject obj)
{
return (Thickness) obj.GetValue(LastItemMarginProperty);
}
[UsedImplicitly]
public static Thickness GetMargin(DependencyObject obj)
{
return (Thickness) obj.GetValue(MarginProperty);
}
private static void MarginChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
// Make sure this is put on a panel
if (sender is not Panel panel)
return;
// Avoid duplicate registrations
panel.Loaded -= OnPanelLoaded;
panel.Loaded += OnPanelLoaded;
if (panel.IsLoaded)
{
OnPanelLoaded(panel, null);
}
}
private static void OnPanelLoaded(object sender, RoutedEventArgs e)
{
var panel = (Panel) sender;
// Go over the children and set margin for them:
for (var i = 0; i < panel.Children.Count; i++)
{
var child = panel.Children[i];
if (child is not FrameworkElement fe)
continue;
var isLastItem = i == panel.Children.Count - 1;
fe.Margin = isLastItem ? GetLastItemMargin(panel) : GetMargin(panel);
}
}
[UsedImplicitly]
public static void SetLastItemMargin(DependencyObject obj, Thickness value)
{
obj.SetValue(LastItemMarginProperty, value);
}
[UsedImplicitly]
public static void SetMargin(DependencyObject obj, Thickness value)
{
obj.SetValue(MarginProperty, value);
}
// Using a DependencyProperty as the backing store for Margin. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MarginProperty =
DependencyProperty.RegisterAttached("Margin", typeof(Thickness), typeof(MarginSetter),
new UIPropertyMetadata(new Thickness(), MarginChangedCallback));
public static readonly DependencyProperty LastItemMarginProperty =
DependencyProperty.RegisterAttached("LastItemMargin", typeof(Thickness), typeof(MarginSetter),
new UIPropertyMetadata(new Thickness(), MarginChangedCallback));
}

View File

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

View File

@@ -1,27 +1,29 @@
using System; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace FeedCenter.Options namespace FeedCenter.Options;
{
public class OptionsPanelBase : UserControl public class OptionsPanelBase : UserControl
{ {
protected FeedCenterEntities Database { get; private set; } protected readonly Window ParentWindow;
protected readonly FeedCenterEntities Entities;
public virtual void LoadPanel(FeedCenterEntities database) protected OptionsPanelBase(Window parentWindow, FeedCenterEntities entities)
{ {
Database = database; ParentWindow = parentWindow;
} Entities = entities;
public virtual bool ValidatePanel()
{
throw new NotImplementedException();
}
public virtual void SavePanel()
{
throw new NotImplementedException();
} }
public virtual string CategoryName => null; public virtual string CategoryName => null;
protected bool HasLoaded { get; private set; }
public virtual void LoadPanel()
{
}
protected void MarkLoaded()
{
HasLoaded = true;
} }
} }

View File

@@ -8,29 +8,44 @@
ResizeMode="CanResize" ResizeMode="CanResize"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Icon="/FeedCenter;component/Resources/Application.ico"> Icon="/FeedCenter;component/Resources/Application.ico">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.FlatButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/light.cobalt.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid> <Grid>
<ListBox HorizontalAlignment="Left" <Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="130" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
Grid.Row="0"
Name="CategoryListBox" Name="CategoryListBox"
Width="126" SelectionChanged="HandleSelectedCategoryChanged" />
SelectionChanged="HandleSelectedCategoryChanged" <ContentControl
Margin="12,12,0,41" /> Grid.Column="2"
<ContentControl Margin="144,12,12,41" Grid.Row="0"
Margin="0,6,6,6"
Name="ContentControl" Name="ContentControl"
IsTabStop="False" /> IsTabStop="False" />
<Button Content="{x:Static properties:Resources.OkayButton}" <Button
Height="23" Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0,6,6,6"
Content="{x:Static properties:Resources.CloseButton}"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="0,0,93,12"
VerticalAlignment="Bottom"
Width="75"
IsDefault="True"
Click="HandleOkayButtonClick" />
<Button Content="{x:Static properties:Resources.CancelButton}"
Margin="0,0,12,12"
Height="23"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Width="75"
IsCancel="True" /> IsCancel="True" />
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,20 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace FeedCenter.Options namespace FeedCenter.Options;
{
public partial class OptionsWindow public partial class OptionsWindow
{ {
#region Member variables private readonly List<OptionsPanelBase> _optionPanels = new();
private readonly FeedCenterEntities _entities = new();
private readonly List<OptionsPanelBase> _optionPanels = new List<OptionsPanelBase>();
private readonly FeedCenterEntities _database = new FeedCenterEntities();
#endregion
#region Constructor
public OptionsWindow() public OptionsWindow()
{ {
@@ -27,26 +19,23 @@ namespace FeedCenter.Options
LoadCategories(); LoadCategories();
} }
#endregion
#region Category handling
private void AddCategories() private void AddCategories()
{ {
_optionPanels.Add(new GeneralOptionsPanel()); _optionPanels.Add(new GeneralOptionsPanel(this, _entities));
_optionPanels.Add(new DisplayOptionsPanel()); _optionPanels.Add(new DisplayOptionsPanel(this, _entities));
_optionPanels.Add(new FeedsOptionsPanel()); _optionPanels.Add(new FeedsOptionsPanel(this, _entities));
_optionPanels.Add(new UpdateOptionsPanel()); _optionPanels.Add(new AccountsOptionsPanel(this, _entities));
_optionPanels.Add(new AboutOptionsPanel()); _optionPanels.Add(new UpdateOptionsPanel(this, _entities));
_optionPanels.Add(new AboutOptionsPanel(this, _entities));
} }
private void LoadCategories() private void LoadCategories()
{ {
// Loop over each panel // Loop over each panel
foreach (OptionsPanelBase optionsPanel in _optionPanels) foreach (var optionsPanel in _optionPanels)
{ {
// Tell the panel to load itself // Tell the panel to load itself
optionsPanel.LoadPanel(_database); optionsPanel.LoadPanel();
// Add the panel to the category ist // Add the panel to the category ist
CategoryListBox.Items.Add(new CategoryListItem(optionsPanel)); CategoryListBox.Items.Add(new CategoryListItem(optionsPanel));
@@ -71,58 +60,18 @@ namespace FeedCenter.Options
SelectCategory(((CategoryListItem) CategoryListBox.SelectedItem).Panel); SelectCategory(((CategoryListItem) CategoryListBox.SelectedItem).Panel);
} }
#endregion
#region Category list item
private class CategoryListItem private class CategoryListItem
{ {
public OptionsPanelBase Panel { get; }
public CategoryListItem(OptionsPanelBase panel) public CategoryListItem(OptionsPanelBase panel)
{ {
Panel = panel; Panel = panel;
} }
public OptionsPanelBase Panel { get; }
public override string ToString() public override string ToString()
{ {
return Panel.CategoryName; return Panel.CategoryName;
} }
} }
#endregion
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
// Loop over each panel and ask them to validate
foreach (OptionsPanelBase optionsPanel in _optionPanels)
{
// If validation fails...
if (!optionsPanel.ValidatePanel())
{
// ...select the right category
SelectCategory(optionsPanel);
// Stop validation
return;
}
}
// Loop over each panel and ask them to save
foreach (OptionsPanelBase optionsPanel in _optionPanels)
{
// Save!
optionsPanel.SavePanel();
}
// Save the actual settings
_database.SaveChanges();
Properties.Settings.Default.Save();
DialogResult = true;
// Close the window
Close();
}
}
} }

Some files were not shown because too many files have changed in this diff Show More