Implemented db backend base, added UI features

This commit is contained in:
Max 2023-05-18 20:15:31 +02:00
parent 66acd6a1c2
commit 24d62d79dc
11 changed files with 147 additions and 136 deletions

View File

@ -13,7 +13,6 @@ namespace SharpRss
public static class FeedCache public static class FeedCache
{ {
private static readonly Dictionary<string, Feed> CachedFeeds = new Dictionary<string, Feed>(); private static readonly Dictionary<string, Feed> CachedFeeds = new Dictionary<string, Feed>();
public static async Task<Feed> GetFeed(string urlKey) public static async Task<Feed> GetFeed(string urlKey)
{ {
Log.Verbose("Fetching feed: {UrlKey}", urlKey); Log.Verbose("Fetching feed: {UrlKey}", urlKey);
@ -33,9 +32,11 @@ namespace SharpRss
feedUrl = urls.First().Url; feedUrl = urls.First().Url;
Feed feed = await FeedReader.ReadAsync(feedUrl); Feed feed = await FeedReader.ReadAsync(feedUrl);
if (feed == null) if (feed != null)
{
Log.Warning("Could not get feed: {FeedUrl}", feedUrl); Log.Warning("Could not get feed: {FeedUrl}", feedUrl);
CachedFeeds.Add(urlKey, feed); CachedFeeds[feedUrl] = feed;
}
return feed; return feed;
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using ToolQit.Extensions;
namespace SharpRss.Models namespace SharpRss.Models
{ {
@ -28,6 +27,7 @@ namespace SharpRss.Models
public string Name { get; set; } public string Name { get; set; }
public string HexColor { get; set; } public string HexColor { get; set; }
public string PathIcon { get; set; }
public string CategoryId { get; private set; } public string CategoryId { get; private set; }
} }
} }

View File

@ -9,13 +9,15 @@ namespace SharpRss.Models
{ {
} }
public FeedModel(Feed feed) public FeedModel(string rssFeedUrl, CategoryModel? category = null)
{ {
if (category != null)
CategoryId = category.CategoryId;
FeedId = Guid.NewGuid().ToString(); FeedId = Guid.NewGuid().ToString();
Url = feed.Link; FeedUrl = rssFeedUrl;
} }
public string Url { get; set; } public string FeedUrl { get; set; }
public string FeedId { get; private set; } public string FeedId { get; private set; }
public string CategoryId { get; set; } = ""; public string CategoryId { get; set; } = "";
@ -23,29 +25,11 @@ namespace SharpRss.Models
{ {
FeedModel feedModel = new FeedModel() FeedModel feedModel = new FeedModel()
{ {
Url = url, FeedUrl = url,
FeedId = feedId, FeedId = feedId,
CategoryId = categoryId CategoryId = categoryId
}; };
return feedModel; return feedModel;
} }
//private readonly Task _fetchTask;
//public async Task FetchAsync() => _feed = await FeedCache.GetFeed(Url);
/*private Feed? _feed;
public Feed Base {
get
{
if (_feed == null)
{
if (_fetchTask.IsFaulted)
{ return new Feed(); }
if (_fetchTask.Status is not (TaskStatus.Running or TaskStatus.WaitingForActivation))
_fetchTask.Start();
_fetchTask.Wait();
}
return _feed ?? new Feed();
}
}*/
} }
} }

View File

@ -10,7 +10,7 @@ using SharpRss.Models;
namespace SharpRss.Services namespace SharpRss.Services
{ {
public class DatabaseService : IDisposable internal class DatabaseService : IDisposable
{ {
private string _connectionString => $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.db")};"; private string _connectionString => $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.db")};";
internal DatabaseService() internal DatabaseService()
@ -24,22 +24,11 @@ namespace SharpRss.Services
{ {
bool result = true; bool result = true;
_sqlConn.Open(); _sqlConn.Open();
/*var queryResult = await _sqlConn.QueryAsync("SELECT * FROM category_data WHERE name=@catName", new { catName = category.Name });
var enumerable = queryResult.ToList();
if (queryResult != null && enumerable.Any()) // Category already exists!
result = false;*/
foreach (var categoryModel in categories) foreach (var categoryModel in categories)
{ {
await _sqlConn.QueryAsync("INSERT OR IGNORE INTO category_data (name, hex_color, category_id) VALUES(@catName, @hexColor, @categoryId)", await _sqlConn.QueryAsync("INSERT INTO category_data (name, hex_color, path_icon, category_id) VALUES(@catName, @hexColor, @pathIcon, @categoryId) ON CONFLICT(name) DO UPDATE SET hex_color=@hexColor, path_icon=@pathIcon",
new { catName = categoryModel.Name, hexColor = categoryModel.HexColor, categoryId = categoryModel.CategoryId }); new { catName = categoryModel.Name, hexColor = categoryModel.HexColor, pathIcon = categoryModel.PathIcon, categoryId = categoryModel.CategoryId });
} }
/*if (result)
{
if (createResult.Any()) // Did not create the category
result = false;
}*/
_sqlConn.Close(); _sqlConn.Close();
return result; return result;
} }
@ -50,7 +39,7 @@ namespace SharpRss.Services
_sqlConn.Open(); _sqlConn.Open();
foreach (var feedModel in feeds) foreach (var feedModel in feeds)
{ {
await _sqlConn.QueryAsync("INSERT OR IGNORE INTO feed_data(url, feed_id, category_id) VALUES(@url, @feedId, @categoryId)", new { url = feedModel.Url, feedId = feedModel.FeedId, categoryId = feedModel.CategoryId }); await _sqlConn.QueryAsync("INSERT OR REPLACE INTO feed_data(url, feed_id, category_id) VALUES(@url, @feedId, @categoryId) ON CONFLICT(url) DO UPDATE SET category_id=@categoryId", new { url = feedModel.FeedUrl, feedId = feedModel.FeedId, categoryId = feedModel.CategoryId });
} }
_sqlConn.Close(); _sqlConn.Close();
return result; return result;
@ -90,14 +79,14 @@ namespace SharpRss.Services
{ {
Log.Verbose("Checking database..."); Log.Verbose("Checking database...");
_sqlConn.Open(); _sqlConn.Open();
var queryResponse = await _sqlConn.QueryAsync("CREATE TABLE IF NOT EXISTS category_data (name STRING NOT NULL, hex_color STRING NOT NULL, category_id STRING PRIMARY KEY)"); var queryResponse = await _sqlConn.QueryAsync("CREATE TABLE IF NOT EXISTS category_data (name STRING NOT NULL, hex_color STRING NOT NULL, path_icon STRING, category_id STRING PRIMARY KEY, CONSTRAINT name UNIQUE (name))");
if (queryResponse.Any()) if (queryResponse.Any())
{ {
_sqlConn.Close(); _sqlConn.Close();
_sqlConn.Dispose(); _sqlConn.Dispose();
throw new SqliteException("Error initializing database!", 0); throw new SqliteException("Error initializing database!", 0);
} }
queryResponse = await _sqlConn.QueryAsync("CREATE TABLE IF NOT EXISTS feed_data (url STRING NOT NULL, feed_id STRING PRIMARY KEY, category_id STRING DEFAULT '')"); queryResponse = await _sqlConn.QueryAsync("CREATE TABLE IF NOT EXISTS feed_data (url STRING NOT NULL, feed_id STRING PRIMARY KEY, category_id STRING NOT NULL DEFAULT '', CONSTRAINT url, UNIQUE (url))");
if (queryResponse.Any()) if (queryResponse.Any())
{ {
_sqlConn.Close(); _sqlConn.Close();

View File

@ -1,6 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CodeHollow.FeedReader;
using SharpRss.Models; using SharpRss.Models;
using ToolQit.Extensions;
namespace SharpRss.Services namespace SharpRss.Services
{ {
@ -11,64 +14,76 @@ namespace SharpRss.Services
{ {
public RssService() public RssService()
{ {
_dbService = new DatabaseService(); //SetupTestCategoriesAndFeedsAsync();
Initialize();
} }
private readonly DatabaseService _dbService; private readonly DatabaseService _dbService = new DatabaseService();
private async void Initialize() private async void SetupTestCategoriesAndFeedsAsync()
{ {
//HashSet<CategoryModel> categoryModels = await _dbService.GetCategoriesAsync(); await _dbService.AddCategoriesAsync(new HashSet<CategoryModel>()
{
new CategoryModel() { Name = "All" },
new CategoryModel() { Name = "RSS" },
new CategoryModel() { Name = "Tech" },
new CategoryModel() { Name = "News" }
});
await _dbService.AddFeedsAsync(new HashSet<FeedModel>()
{
new FeedModel("http://fedoramagazine.org/feed/"),
new FeedModel("https://www.nasa.gov/rss/dyn/breaking_news.rss"),
new FeedModel("https://journals.plos.org/plosone/feed/atom"),
new FeedModel("https://itsfoss.com/feed")
});
} }
public async Task<HashSet<object>> GetAllAsync() public async Task<Feed> GetFeedAsync(string rssUrl)
{
return await FeedCache.GetFeed(rssUrl);
}
public async Task<HashSet<object>> GetAllUnsortedAsync()
{ {
HashSet<object> items = new HashSet<object>(); HashSet<object> items = new HashSet<object>();
var categories = await _dbService.GetCategoriesAsync();
var feeds = await _dbService.GetFeedsAsync(string.Empty);
items.UnionWith(categories);
items.UnionWith(feeds);
return items; return items;
} }
public async Task<HashSet<CategoryModel>> GetCategoriesAsync() public async Task<HashSet<CategoryModel>> GetCategoriesAsync()
{ {
return new HashSet<CategoryModel>(); var result = await _dbService.GetCategoriesAsync();
return result.OrderBy(x => x.Name).ToHashSet();
} }
public async Task<HashSet<FeedModel>> GetFeedsAsync(CategoryModel? categoryModel = null) public async Task<HashSet<FeedModel>> GetFeedsAsync(CategoryModel? categoryModel = null)
{ {
HashSet<FeedModel> feeds = new HashSet<FeedModel>(); HashSet<FeedModel> feeds;
if (categoryModel != null) if (categoryModel != null)
{ feeds = await _dbService.GetFeedsAsync(categoryModel.CategoryId);
// Get feeds from the category.
}
else else
{ feeds = await _dbService.GetFeedsAsync();
// Get all the feeds.
}
return feeds; return feeds;
} }
public async void AddCategoryAsync(string name) public async void AddCategoryAsync(string name, string? icon, string hexColor)
{ {
CategoryModel categoryModel = new CategoryModel()
{
Name = name
};
if (icon != null)
categoryModel.PathIcon = icon;
if (!hexColor.IsNullEmptyWhiteSpace())
categoryModel.HexColor = hexColor;
await _dbService.AddCategoriesAsync(new HashSet<CategoryModel>() { categoryModel });
} }
public async void AddFeedAsync(string rssUrl) public async void AddFeedAsync(string rssUrl, CategoryModel? category = null)
{ {
FeedModel feedModel = new FeedModel(rssUrl, category);
await _dbService.AddFeedsAsync(new HashSet<FeedModel>() { feedModel });
} }
/*private static HashSet<FeedModel> feedSet = new HashSet<FeedModel>()
{
new FeedModel("http://fedoramagazine.org/feed/"),
new FeedModel("https://www.nasa.gov/rss/dyn/breaking_news.rss"),
};
private static HashSet<FeedModel> feedSet2 = new HashSet<FeedModel>()
{
new FeedModel("https://journals.plos.org/plosone/feed/atom"),
new FeedModel("https://itsfoss.com/feed")
};*/
/*HashSet<CategoryModel> set = new HashSet<CategoryModel>()
{
new CategoryModel("RSS", feedSet),
new CategoryModel("Tech", feedSet2)
};*/
} }
} }

View File

@ -1,16 +1,14 @@
using System; using System;
using System.Collections.Generic;
using SharpRss.Models;
namespace WebSharpRSS.Models namespace WebSharpRSS.Models
{ {
public class FeedStateContainer public class FeedStateContainer
{ {
public HashSet<FeedModel> Feeds { get; set; } public TreeItemData? TreeItem { get; set; }
public event Action StateChanged; public event Action? StateChanged;
public void SetValue(HashSet<FeedModel> feedSet) public void SetValue(TreeItemData treeItemSet)
{ {
Feeds = feedSet; TreeItem = treeItemSet;
Invoke(); Invoke();
} }

View File

@ -3,32 +3,33 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using CodeHollow.FeedReader; using CodeHollow.FeedReader;
using MudBlazor; using MudBlazor;
using Serilog;
using SharpRss.Models; using SharpRss.Models;
using SharpRss.Services;
using ToolQit; using ToolQit;
namespace WebSharpRSS.Models namespace WebSharpRSS.Models
{ {
public class TreeItemData public class TreeItemData
{ {
public TreeItemData(CategoryModel catModel) public TreeItemData(CategoryModel catModel, RssService rssService)
{ {
_service = rssService;
CategoryModel = catModel; CategoryModel = catModel;
//Feeds = CategoryModel.Feeds.Where(x => x.Base != null).Select(x => new TreeItemData(x)).ToHashSet(); Initialize();
Title = CategoryModel.Name;
Icon = Icons.Material.Filled.RssFeed;
} }
public TreeItemData(FeedModel feedModel) public TreeItemData(FeedModel feedModel, RssService rssService)
{ {
_service = rssService;
FeedModel = feedModel; FeedModel = feedModel;
//Feed = FeedModel.Base; Initialize();
Title = Feed.Title;
string faviconAddress = Feed.Link.Remove(Feed.Link.IndexOf("http", StringComparison.Ordinal), Feed.Link.IndexOf("://", StringComparison.Ordinal) + 3);
FaviconUrl = string.Format(Caretaker.Settings["Paths"].GetString("FaviconResolveUrl"), faviconAddress);
} }
private readonly RssService _service;
public readonly CategoryModel? CategoryModel; public readonly CategoryModel? CategoryModel;
public readonly FeedModel? FeedModel; public readonly FeedModel? FeedModel;
private HashSet<FeedModel> _feedModels;
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public bool IsSelected { get; set; } public bool IsSelected { get; set; }
@ -36,11 +37,11 @@ namespace WebSharpRSS.Models
public string? FaviconUrl { get; set; } public string? FaviconUrl { get; set; }
// Category // Category
public bool HasChild => Feeds != null; public bool HasChild => _feedModels != null && _feedModels.Any();
public bool IsExpanded { get; set; } public bool IsExpanded { get; set; }
public HashSet<TreeItemData>? Feeds { get; set; } public HashSet<TreeItemData>? Feeds { get; set; }
// Feed // Feed
public Feed? Feed { get; set; } public Feed? Feed { get; private set; }
public int FeeditemCount public int FeeditemCount
{ {
get get
@ -60,5 +61,35 @@ namespace WebSharpRSS.Models
return 0; return 0;
} }
} }
private async void Initialize()
{
if (CategoryModel != null)
{
Title = CategoryModel.Name;
Icon = Icons.Material.Filled.Category;
_feedModels = await _service.GetFeedsAsync(CategoryModel);
if (_feedModels.Any())
Feeds = _feedModels.Select(x => new TreeItemData(x, _service)).OrderBy(x => x.Title).ToHashSet();
}
if (FeedModel != null)
{
try
{
Feed = await _service.GetFeedAsync(FeedModel.FeedUrl);
}
catch (Exception e)
{
Log.Error(e, "Error fetching feed: {FeedUrl}", FeedModel.FeedUrl);
Feed = null;
return;
}
Icon = Icons.Material.Filled.RssFeed;
Title = Feed.Title;
string faviconAddress = Feed.Link.Remove(Feed.Link.IndexOf("http", StringComparison.Ordinal), Feed.Link.IndexOf("://", StringComparison.Ordinal) + 3);
FaviconUrl = string.Format(Caretaker.Settings["Paths"].GetString("FaviconResolveUrl"), faviconAddress);
}
}
} }
} }

View File

@ -4,33 +4,28 @@
@using WebSharpRSS.Models; @using WebSharpRSS.Models;
@using SharpRss.Services @using SharpRss.Services
@inject RssService _rssService; @*@inject RssService _rssService;*@
@inject FeedStateContainer _stateContainer; @inject FeedStateContainer _stateContainer;
<MudGrid Spacing="3" Justify="Justify.FlexStart"> <MudGrid Spacing="3" Justify="Justify.FlexStart">
@if (Feeds != null) @foreach (var feedItem in _items)
{ {
foreach (var feedItem in _items) <MudItem xs="6">
{ <MudCard>
<MudItem xs="6"> <MudCardContent>
<MudCard> <MudText>@feedItem.Title</MudText>
<MudCardContent> <MudText Typo="Typo.body2">@feedItem.Description</MudText>
<MudText>@feedItem.Title</MudText> <MudText Typo="Typo.overline">@feedItem.PublishingDate.ToString()</MudText>
<MudText Typo="Typo.body2">@feedItem.Description</MudText> </MudCardContent>
<MudText Typo="Typo.overline">@feedItem.PublishingDate.ToString()</MudText> </MudCard>
</MudCardContent> </MudItem>
</MudCard>
</MudItem>
}
} }
</MudGrid> </MudGrid>
@code { @code {
[Parameter]
public HashSet<FeedModel>? Feeds { get; set; }
private HashSet<FeedItem> _items = new HashSet<FeedItem>(); private HashSet<FeedItem> _items = new HashSet<FeedItem>();
protected override async void OnInitialized() protected override void OnInitialized()
{ {
UpdateFeeds(); UpdateFeeds();
_stateContainer.StateChanged += FeedsChanged; _stateContainer.StateChanged += FeedsChanged;
@ -43,11 +38,17 @@
private void UpdateFeeds() private void UpdateFeeds()
{ {
Feeds = _stateContainer.Feeds; if (_stateContainer.TreeItem == null) return;
if (Feeds == null) return; if (_stateContainer.TreeItem.Feed != null)
foreach (var feedmodel in Feeds) _items = _stateContainer.TreeItem.Feed.Items.ToHashSet();
if (_stateContainer.TreeItem.Feeds != null)
{ {
//_items = feedmodel.Base.Items.OrderBy(x => x.PublishingDate).Reverse().ToHashSet(); _items = new HashSet<FeedItem>();
foreach (var itemData in _stateContainer.TreeItem.Feeds)
{
if (itemData.Feed == null) continue;
_items.UnionWith(itemData.Feed.Items);
}
} }
} }
} }

View File

@ -4,8 +4,6 @@ using Microsoft.Extensions.Hosting;
using MudBlazor; using MudBlazor;
using MudBlazor.Services; using MudBlazor.Services;
using Serilog; using Serilog;
using SharpRss;
using SharpRss.Models;
using SharpRss.Services; using SharpRss.Services;
using ToolQit; using ToolQit;
using WebSharpRSS; using WebSharpRSS;
@ -13,12 +11,12 @@ using WebSharpRSS.Models;
Caretaker.Settings.SetAppDefaultSettings(); Caretaker.Settings.SetAppDefaultSettings();
Bootstrapper.SetupLogging(); Bootstrapper.SetupLogging();
Log.Information("Starting application...."); Log.Information("Starting...");
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); builder.Services.AddServerSideBlazor();
builder.Services.AddTransient<RssService>(); builder.Services.AddSingleton<RssService>();
builder.Services.AddSingleton<FeedStateContainer>(); builder.Services.AddSingleton<FeedStateContainer>();
builder.Services.AddMudServices(config => builder.Services.AddMudServices(config =>
{ {

View File

@ -11,16 +11,16 @@
@inject RssService _rssService @inject RssService _rssService
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudTreeView Items="GuideItems" @bind-SelectedValue="SelectedItem" Hover="true"> <MudTreeView Items="_guideItems" @bind-SelectedValue="SelectedItem" Hover="true">
<ItemTemplate> <ItemTemplate>
<MudTreeViewItem @bind-Expanded="@context.IsExpanded" Items="@context.Feeds" Value="@context"> <MudTreeViewItem @bind-Expanded="@context.IsExpanded" Items="@context.Feeds" Value="@context" CanExpand="@context.HasChild" @onclick="ItemClicked">
<Content> <Content>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%"> <div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<div style="justify-self: start;" class="d-flex align-center"> <div style="justify-self: start;" class="d-flex align-center">
<MudTreeViewItemToggleButton @bind-Expanded="@context.IsExpanded" Visible="@context.HasChild" /> <MudTreeViewItemToggleButton @bind-Expanded="@context.IsExpanded" Visible="@context.HasChild" LoadingIconColor="Color.Info"/>
@if (context.FaviconUrl.IsNullEmptyWhiteSpace() && context.Icon != null) @if (context.FaviconUrl.IsNullEmptyWhiteSpace() && context.Icon != null)
{ {
<MudIcon Icon="@context.Icon"/> <MudIcon Icon="@context.Icon" Style="@($"color:{context.CategoryModel?.HexColor ?? Theme.Palette.Primary.Value}")"/>
} }
else else
{ {
@ -39,36 +39,30 @@
</MudStack> </MudStack>
@code { @code {
public HashSet<TreeItemData> GuideItems = new HashSet<TreeItemData>(); private MudTheme Theme = new MudTheme();
private TreeItemData? _selecteditem; private readonly HashSet<TreeItemData> _guideItems = new HashSet<TreeItemData>();
private TreeItemData? _selectedItem;
private TreeItemData? SelectedItem private TreeItemData? SelectedItem
{ {
get => _selecteditem; get => _selectedItem;
set set
{ {
_selecteditem = value; _selectedItem = value;
ItemClicked(); ItemClicked();
} }
} }
private void ItemClicked() private void ItemClicked()
{ {
if (SelectedItem == null) return; if (SelectedItem == null) return;
if (SelectedItem.FeedModel != null) _stateContainer.SetValue(SelectedItem);
_stateContainer.SetValue(new HashSet<FeedModel>() { SelectedItem.FeedModel });
if (SelectedItem.Feeds != null)
{
}
} }
protected override async void OnInitialized() protected override async void OnInitialized()
{ {
Log.Verbose("Loading guide data..."); Log.Verbose("Loading guide data...");
HashSet<object> items = await _rssService.GetAllUnsortedAsync();
/*HashSet<CategoryModel> cats = await _rssService.GetAllAsync(); _guideItems.UnionWith(items.Select(x => x is CategoryModel model ? new TreeItemData(model, _rssService) : x is FeedModel feedModel ? new TreeItemData(feedModel, _rssService) : throw new ArgumentException("Arg x is invalid!")));
await Task.Run(() => Categories.UnionWith(cats.Select(x => new TreeItemData(x)).ToHashSet()));*/
StateHasChanged(); StateHasChanged();
Log.Verbose(" Guide initialized!"); Log.Verbose(" Guide initialized!");
//await Task.Run(() => Categories = cats.Select(x => new TreeItemData(x)).ToHashSet());
} }
} }

Binary file not shown.