Merge branch 'feature/cleanup'

This commit is contained in:
Oliver Booth 2024-05-06 15:02:19 +01:00
commit 9991ecf173
Signed by: oliverbooth
GPG Key ID: E60B570D1B7557B5
142 changed files with 1140 additions and 742 deletions

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents the author of a blog post.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents a blog post.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents a comment that was posted on a legacy comment framework.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents a user which can log in to the blog.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Mastodon;
namespace OliverBooth.Common.Data.Mastodon;
public enum AttachmentType
{

View File

@ -0,0 +1,31 @@
namespace OliverBooth.Common.Data.Mastodon;
/// <summary>
/// Represents a status on Mastodon.
/// </summary>
public interface IMastodonStatus
{
/// <summary>
/// Gets the content of the status.
/// </summary>
/// <value>The content.</value>
string Content { get; }
/// <summary>
/// Gets the date and time at which this status was posted.
/// </summary>
/// <value>The post timestamp.</value>
DateTimeOffset CreatedAt { get; }
/// <summary>
/// Gets the media attachments for this status.
/// </summary>
/// <value>The media attachments.</value>
IReadOnlyList<MediaAttachment> MediaAttachments { get; }
/// <summary>
/// Gets the original URI of the status.
/// </summary>
/// <value>The original URI.</value>
Uri OriginalUri { get; }
}

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Mastodon;
namespace OliverBooth.Common.Data.Mastodon;
public sealed class MediaAttachment
{

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data;
namespace OliverBooth.Common.Data;
/// <summary>
/// An enumeration of the possible visibilities of a blog post.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents the state of a book.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents an entry in the blacklist.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a book.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a code snippet.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a programming language.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a project.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a template.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a tutorial article.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a folder for tutorial articles.

View File

@ -1,6 +1,6 @@
using System.ComponentModel;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents the status of a project.

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.59"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="ZString" Version="2.5.1"/>
</ItemGroup>
</Project>

View File

@ -1,7 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Blog;
using OliverBooth.Common.Data;
using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for managing blog posts.
@ -22,8 +23,9 @@ public interface IBlogPostService
/// <summary>
/// Returns the total number of blog posts.
/// </summary>
/// <param name="visibility">The post visibility filter.</param>
/// <returns>The total number of blog posts.</returns>
int GetBlogPostCount();
int GetBlogPostCount(Visibility visibility = Visibility.None);
/// <summary>
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
@ -62,6 +64,15 @@ public interface IBlogPostService
/// <returns>The next blog post from the specified blog post.</returns>
IBlogPost? GetNextPost(IBlogPost blogPost);
/// <summary>
/// Returns the number of pages needed to render all blog posts, using the specified <paramref name="pageSize" /> as an
/// indicator of how many posts are allowed per page.
/// </summary>
/// <param name="pageSize">The page size. Defaults to 10.</param>
/// <param name="visibility">The post visibility filter.</param>
/// <returns>The page count.</returns>
int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None);
/// <summary>
/// Returns the previous blog post from the specified blog post.
/// </summary>

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Blog;
using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for managing users.

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service which can fetch multi-language code snippets.

View File

@ -1,6 +1,6 @@
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for managing contact information.

View File

@ -1,6 +1,6 @@
using OliverBooth.Data.Mastodon;
using OliverBooth.Common.Data.Mastodon;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
public interface IMastodonService
{
@ -8,5 +8,5 @@ public interface IMastodonService
/// Gets the latest status posted to Mastodon.
/// </summary>
/// <returns>The latest status.</returns>
MastodonStatus GetLatestStatus();
IMastodonStatus GetLatestStatus();
}

View File

@ -0,0 +1,14 @@
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service which can perform programming language lookup.
/// </summary>
public interface IProgrammingLanguageService
{
/// <summary>
/// Returns the human-readable name of a language.
/// </summary>
/// <param name="alias">The alias of the language.</param>
/// <returns>The human-readable name, or <paramref name="alias" /> if the name could not be found.</returns>
string GetLanguageName(string alias);
}

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for interacting with projects.

View File

@ -1,6 +1,6 @@
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service which fetches books from the reading list.

View File

@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service which can retrieve tutorial articles.

View File

@ -1,7 +1,7 @@
using Markdig.Helpers;
using Markdig.Syntax;
namespace OliverBooth.Markdown.Callout;
namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
/// <summary>
/// Represents a callout block.

View File

@ -3,7 +3,7 @@ using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace OliverBooth.Markdown.Callout;
namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
/// <summary>
/// Extension for adding Obsidian-style callouts to a Markdown pipeline.

View File

@ -5,7 +5,7 @@ using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace OliverBooth.Markdown.Callout;
namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
/// <summary>
/// An inline parser for Obsidian-style callouts (<c>[!NOTE]</c> etc.)

View File

@ -4,7 +4,7 @@ using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace OliverBooth.Markdown.Callout;
namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
/// <summary>
/// Represents an HTML renderer which renders a <see cref="CalloutBlock" />.
@ -96,7 +96,7 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer<CalloutBlock>
private static void WriteTitle(TextRendererBase renderer, MarkdownPipeline pipeline, string calloutTitle)
{
string html = Markdig.Markdown.ToHtml(calloutTitle, pipeline);
string html = global::Markdig.Markdown.ToHtml(calloutTitle, pipeline);
var document = new HtmlDocument();
document.LoadHtml(html);
if (document.DocumentNode.FirstChild is { Name: "p" } child)

View File

@ -1,8 +1,8 @@
using Markdig;
using Markdig.Renderers;
using OliverBooth.Services;
using OliverBooth.Extensions.Markdig.Services;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Extensions.Markdig.Markdown.Template;
/// <summary>
/// Represents a Markdown extension that adds support for MediaWiki-style templates.

View File

@ -1,6 +1,6 @@
using Markdig.Syntax.Inlines;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Extensions.Markdig.Markdown.Template;
/// <summary>
/// Represents a Markdown inline element that represents a MediaWiki-style template.

View File

@ -2,7 +2,7 @@ using Cysharp.Text;
using Markdig.Helpers;
using Markdig.Parsers;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Extensions.Markdig.Markdown.Template;
/// <summary>
/// Represents a Markdown inline parser that handles MediaWiki-style templates.

View File

@ -1,8 +1,8 @@
using Markdig.Renderers;
using Markdig.Renderers.Html;
using OliverBooth.Services;
using OliverBooth.Extensions.Markdig.Services;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Extensions.Markdig.Markdown.Template;
/// <summary>
/// Represents a Markdown object renderer that handles <see cref="TemplateInline" /> elements.

View File

@ -1,7 +1,7 @@
using Markdig;
using Markdig.Renderers;
namespace OliverBooth.Markdown.Timestamp;
namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
/// <summary>
/// Represents a Markdig extension that supports Discord-style timestamps.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Markdown.Timestamp;
namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
/// <summary>
/// An enumeration of timestamp formats.

View File

@ -1,6 +1,6 @@
using Markdig.Syntax.Inlines;
namespace OliverBooth.Markdown.Timestamp;
namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
/// <summary>
/// Represents a Markdown inline element that contains a timestamp.

View File

@ -1,7 +1,7 @@
using Markdig.Helpers;
using Markdig.Parsers;
namespace OliverBooth.Markdown.Timestamp;
namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
/// <summary>
/// Represents a Markdown inline parser that matches Discord-style timestamps.

View File

@ -3,7 +3,7 @@ using Humanizer;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace OliverBooth.Markdown.Timestamp;
namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
/// <summary>
/// Represents a Markdown object renderer that renders <see cref="TimestampInline" /> elements.

View File

@ -0,0 +1,56 @@
using Markdig;
using OliverBooth.Extensions.Markdig.Markdown.Callout;
using OliverBooth.Extensions.Markdig.Markdown.Template;
using OliverBooth.Extensions.Markdig.Services;
namespace OliverBooth.Extensions.Markdig;
/// <summary>
/// Extension methods for <see cref="MarkdownPipelineBuilder" />.
/// </summary>
public static class MarkdownPipelineExtensions
{
/// <summary>
/// Enables the use of Obsidian-style callouts in this pipeline.
/// </summary>
/// <param name="builder">The Markdig markdown pipeline builder.</param>
/// <returns>The modified Markdig markdown pipeline builder.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder" /> is <see langword="null" />.</exception>
public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Extensions.AddIfNotAlready<CalloutExtension>();
return builder;
}
/// <summary>
/// Enables the use of Wiki-style templates in this pipeline.
/// </summary>
/// <param name="builder">The Markdig markdown pipeline builder.</param>
/// <param name="templateService">The template service responsible for fetching and rendering templates.</param>
/// <returns>The modified Markdig markdown pipeline builder.</returns>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="builder" /> is <see langword="null" />.</para>
/// -or-
/// <para><paramref name="templateService" /> is <see langword="null" />.</para>
/// </exception>
public static MarkdownPipelineBuilder UseTemplates(this MarkdownPipelineBuilder builder, ITemplateService templateService)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (templateService is null)
{
throw new ArgumentNullException(nameof(templateService));
}
builder.Use(new TemplateExtension(templateService));
return builder;
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.36.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
</ItemGroup>
</Project>

View File

@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web;
using OliverBooth.Markdown.Template;
using OliverBooth.Common.Data.Web;
using OliverBooth.Extensions.Markdig.Markdown.Template;
namespace OliverBooth.Services;
namespace OliverBooth.Extensions.Markdig.Services;
/// <summary>
/// Represents a service that renders MediaWiki-style templates.

View File

@ -1,7 +1,7 @@
using System.Globalization;
using SmartFormat.Core.Extensions;
namespace OliverBooth.Formatting;
namespace OliverBooth.Extensions.SmartFormat;
/// <summary>
/// Represents a SmartFormat formatter that formats a date.

View File

@ -1,7 +1,8 @@
using Markdig;
using Microsoft.Extensions.DependencyInjection;
using SmartFormat.Core.Extensions;
namespace OliverBooth.Formatting;
namespace OliverBooth.Extensions.SmartFormat;
/// <summary>
/// Represents a SmartFormat formatter that formats markdown.

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.36.2"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="SmartFormat.NET" Version="3.3.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
</ItemGroup>
</Project>

View File

@ -13,6 +13,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.Markdig", "OliverBooth.Extensions.Markdig\OliverBooth.Extensions.Markdig.csproj", "{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Common", "OliverBooth.Common\OliverBooth.Common.csproj", "{AD231E0F-FAED-4661-963F-EB22F858E148}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.SmartFormat", "OliverBooth.Extensions.SmartFormat\OliverBooth.Extensions.SmartFormat.csproj", "{9D56FA9B-B95B-460D-8745-41AABAA8BF61}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -23,6 +29,18 @@ Global
{A58A6FA3-480C-400B-822A-3786741BF39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = Release|Any CPU
{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Release|Any CPU.Build.0 = Release|Any CPU
{AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.Build.0 = Release|Any CPU
{9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection

View File

@ -1,102 +0,0 @@
using Humanizer;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
namespace OliverBooth.Controllers.Blog;
/// <summary>
/// Represents a controller for the blog API.
/// </summary>
[ApiController]
[Route("api/blog")]
[Produces("application/json")]
public sealed class BlogApiController : ControllerBase
{
private readonly IBlogPostService _blogPostService;
private readonly IBlogUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="BlogApiController" /> class.
/// </summary>
/// <param name="blogPostService">The <see cref="IBlogPostService" />.</param>
/// <param name="userService">The <see cref="IBlogUserService" />.</param>
public BlogApiController(IBlogPostService blogPostService, IBlogUserService userService)
{
_blogPostService = blogPostService;
_userService = userService;
}
[Route("count")]
public IActionResult Count()
{
return Ok(new { count = _blogPostService.GetBlogPostCount() });
}
[HttpGet("posts/{page:int?}")]
public IActionResult GetAllBlogPosts(int page = 0)
{
const int itemsPerPage = 10;
IReadOnlyList<IBlogPost> allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage);
return Ok(allPosts.Select(post => CreatePostObject(post)));
}
[HttpGet("posts/tagged/{tag}/{page:int?}")]
public IActionResult GetTaggedBlogPosts(string tag, int page = 0)
{
const int itemsPerPage = 10;
tag = tag.Replace('-', ' ').ToLowerInvariant();
IReadOnlyList<IBlogPost> allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage);
allPosts = allPosts.Where(post => post.Tags.Contains(tag)).ToList();
return Ok(allPosts.Select(post => CreatePostObject(post)));
}
[HttpGet("author/{id:guid}")]
public IActionResult GetAuthor(Guid id)
{
if (!_userService.TryGetUser(id, out IUser? author)) return NotFound();
return Ok(new
{
id = author.Id,
name = author.DisplayName,
avatarUrl = author.AvatarUrl,
});
}
[HttpGet("post/{id:guid?}")]
public IActionResult GetPost(Guid id)
{
if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound();
return Ok(CreatePostObject(post, true));
}
private object CreatePostObject(IBlogPost post, bool includeContent = false)
{
return new
{
id = post.Id,
commentsEnabled = post.EnableComments,
identifier = post.GetDisqusIdentifier(),
author = post.Author.Id,
title = post.Title,
published = post.Published.ToUnixTimeSeconds(),
updated = post.Updated?.ToUnixTimeSeconds(),
formattedPublishDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"),
formattedUpdateDate = post.Updated?.ToString("dddd, d MMMM yyyy HH:mm"),
humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed),
content = includeContent ? _blogPostService.RenderPost(post) : null,
trimmed,
tags = post.Tags.Select(t => t.Replace(' ', '-')),
url = new
{
year = post.Published.ToString("yyyy"),
month = post.Published.ToString("MM"),
day = post.Published.ToString("dd"),
slug = post.Slug
}
};
}
}

View File

@ -1,8 +1,8 @@
using System.Xml.Serialization;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Blog;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
using OliverBooth.Data.Blog.Rss;
using OliverBooth.Services;
namespace OliverBooth.Controllers.Blog;

View File

@ -1,7 +1,7 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Services;
namespace OliverBooth.Controllers;

View File

@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema;
using OliverBooth.Common.Data;
using OliverBooth.Common.Data.Blog;
using SmartFormat;
namespace OliverBooth.Data.Blog;

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data;
namespace OliverBooth.Data.Blog.Configuration;

View File

@ -1,4 +1,5 @@
using System.Web;
using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Data.Blog;

View File

@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography;
using System.Text;
using Cysharp.Text;
using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Data.Blog;

View File

@ -1,34 +1,24 @@
using System.Text.Json.Serialization;
using OliverBooth.Common.Data.Mastodon;
namespace OliverBooth.Data.Mastodon;
public sealed class MastodonStatus
/// <inheritdoc />
internal sealed class MastodonStatus : IMastodonStatus
{
/// <summary>
/// Gets the content of the status.
/// </summary>
/// <value>The content.</value>
/// <inheritdoc />
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
/// <summary>
/// Gets the date and time at which this status was posted.
/// </summary>
/// <value>The post timestamp.</value>
/// <inheritdoc />
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets the media attachments for this status.
/// </summary>
/// <value>The media attachments.</value>
/// <inheritdoc />
[JsonPropertyName("media_attachments")]
public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; } = ArraySegment<MediaAttachment>.Empty;
public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; } = ArraySegment<MediaAttachment>.Empty;
/// <summary>
/// Gets the original URI of the status.
/// </summary>
/// <value>The original URI.</value>
/// <inheritdoc />
[JsonPropertyName("url")]
public Uri OriginalUri { get; set; } = null!;
}

View File

@ -1,3 +1,5 @@
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
/// <inheritdoc cref="IBlacklistEntry"/>

View File

@ -1,4 +1,5 @@
using NetBarcode;
using OliverBooth.Common.Data.Web;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;

View File

@ -1,3 +1,5 @@
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
/// <inheritdoc />

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web.Configuration;

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web.Configuration;

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data;
namespace OliverBooth.Data.Web.Configuration;

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data;
namespace OliverBooth.Data.Web.Configuration;

View File

@ -1,3 +1,5 @@
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
/// <inheritdoc cref="IProgrammingLanguage" />

View File

@ -1,3 +1,5 @@
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
/// <summary>

View File

@ -1,3 +1,5 @@
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
/// <summary>

View File

@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema;
using OliverBooth.Common.Data;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;

View File

@ -1,3 +1,6 @@
using OliverBooth.Common.Data;
using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
/// <summary>

View File

@ -1,8 +1,8 @@
using System.Web;
using Cysharp.Text;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Services;
namespace OliverBooth.Extensions;

View File

@ -1,21 +0,0 @@
using Markdig;
using OliverBooth.Markdown.Callout;
namespace OliverBooth.Markdown;
/// <summary>
/// Extension methods for <see cref="MarkdownPipelineBuilder" />.
/// </summary>
internal static class MarkdownExtensions
{
/// <summary>
/// Uses this extension to enable Obsidian-style callouts.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline.</returns>
public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<CalloutExtension>();
return pipeline;
}
}

View File

@ -1,8 +1,9 @@
using System.Diagnostics;
using System.Text;
using Markdig;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Services;
using OliverBooth.Extensions.Markdig.Markdown.Template;
namespace OliverBooth.Markdown.Template;

View File

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
using OliverBooth.Extensions.Markdig.Markdown.Template;
namespace OliverBooth.Markdown.Template;

View File

@ -30,13 +30,9 @@
<PackageReference Include="Alexinea.Extensions.Configuration.Toml" Version="7.0.0"/>
<PackageReference Include="AspNetCore.ReCaptcha" Version="1.8.1"/>
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0"/>
<PackageReference Include="HtmlAgilityPack" Version="1.11.59"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="MailKit" Version="4.4.0"/>
<PackageReference Include="MailKitSimplified.Sender" Version="2.9.0"/>
<PackageReference Include="Markdig" Version="0.36.2"/>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.3"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.3"/>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.3"/>
<PackageReference Include="NetBarcode" Version="1.7.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2"/>
@ -45,10 +41,20 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="SmartFormat.NET" Version="3.3.2"/>
<PackageReference Include="X10D" Version="3.3.1"/>
<PackageReference Include="X10D.Hosting" Version="3.3.1"/>
<PackageReference Include="ZString" Version="2.5.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
<ProjectReference Include="..\OliverBooth.Extensions.Markdig\OliverBooth.Extensions.Markdig.csproj"/>
<ProjectReference Include="..\OliverBooth.Extensions.SmartFormat\OliverBooth.Extensions.SmartFormat.csproj"/>
</ItemGroup>
<ItemGroup>
<Compile Update="Pages\Shared\Partials\PageTabsUtility.cs">
<DependentUpon>_PageTabs.cshtml</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@ -1,9 +1,10 @@
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
@using Humanizer
@using Markdig
@using OliverBooth.Data
@using OliverBooth.Data.Blog
@using OliverBooth.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OliverBooth.Common.Data
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Services
@inject IBlogPostService BlogPostService
@inject MarkdownPipeline MarkdownPipeline
@model Article
@ -77,12 +78,7 @@
</abbr>
}
</p>
<div class="post-tags">
@foreach (string tag in post.Tags)
{
<a asp-page="Index" asp-route-tag="@tag" class="badge bg-secondary">@tag</a>
}
</div>
<hr>
<article>
@ -91,6 +87,18 @@
<hr>
<div class="d-flex align-items-center mb-3">
<i data-lucide="tag"></i>
<ul class="ms-2 post-tags">
@foreach (string tag in post.Tags)
{
<li class="post-tag">
<a asp-page="Index" asp-route-tag="@Html.UrlEncoder.Encode(tag)">@tag</a>
</li>
}
</ul>
</div>
<div class="row">
<div class="col-sm-12 col-md-6">
@if (BlogPostService.GetPreviousPost(post) is { } previousPost)

View File

@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Primitives;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
using BC = BCrypt.Net.BCrypt;
namespace OliverBooth.Pages.Blog;
@ -79,7 +79,6 @@ public class Article : PageModel
var date = new DateOnly(year, month, day);
if (!_blogPostService.TryGetPost(date, slug, out IBlogPost? post))
{
Response.StatusCode = 404;
return NotFound();
}

View File

@ -1,75 +1,26 @@
@page
@using Humanizer
@using OliverBooth.Data.Mastodon
@using OliverBooth.Services
@using OliverBooth.Common.Data
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Services
@model Index
@inject IMastodonService MastodonService
@inject IBlogPostService BlogPostService
@{
ViewData["Title"] = "Blog";
MastodonStatus latestStatus = MastodonService.GetLatestStatus();
}
<div class="card text-center mastodon-update-card">
<div class="card-body">
@Html.Raw(latestStatus.Content)
@foreach (MediaAttachment attachment in latestStatus.MediaAttachments)
{
switch (attachment.Type)
{
case AttachmentType.Audio:
<p><audio controls="controls" src="@attachment.Url"></audio></p>
break;
case AttachmentType.Video:
<p><video controls="controls" class="figure-img img-fluid" src="@attachment.Url"></video></p>
break;
case AttachmentType.Image:
case AttachmentType.GifV:
<p><img class="figure-img img-fluid" src="@attachment.Url"></p>
break;
}
}
</div>
<div class="card-footer text-muted">
<abbr title="@latestStatus.CreatedAt.ToString("F")">@latestStatus.CreatedAt.Humanize()</abbr>
&bull;
<a href="@latestStatus.OriginalUri" target="_blank">View on Mastodon</a>
</div>
</div>
@await Html.PartialAsync("Partials/_MastodonStatus")
<div id="all-blog-posts">
@await Html.PartialAsync("_LoadingSpinner")
@foreach (IBlogPost post in BlogPostService.GetBlogPosts(0))
{
@await Html.PartialAsync("Partials/_BlogCard", post)
}
</div>
<script id="blog-post-template" type="text/x-handlebars-template">
<div class="card-header">
<span class="text-muted">
<img class="blog-author-icon" src="{{author.avatar}}" alt="{{author.name}}">
<span>{{author.name}}<span>
<span> &bull; </span>
<abbr title="{{ post.formattedDate }}">{{ post.date_humanized }}</abbr>
</span>
</div>
<div class="card-body">
<h2>
<a href="{{post.url}}"> {{post.title}}</a>
</h2>
<p>{{{post.excerpt}}}</p>
{{#if post.trimmed}}
<p>
<a href="{{post.url}}">
Read more...
</a>
</p>
{{/if}}
</div>
<div class="card-footer">
{{#each post.tags}}
<a href="?tag={{urlEncode this}}" class="badge text-bg-dark">{{this}}</a>
{{/each}}
</div>
</script>
@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData)
{
["UrlRoot"] = "/blog",
["Page"] = 1,
["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published)
})

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog;
@ -36,7 +36,7 @@ public class Index : PageModel
return _blogPostService.TryGetPost(wpPostId, out IBlogPost? post) ? RedirectToPost(post) : NotFound();
}
private IActionResult RedirectToPost(IBlogPost post)
private RedirectResult RedirectToPost(IBlogPost post)
{
var route = new
{

View File

@ -0,0 +1,23 @@
@page "/blog/page/{pageNumber:int}"
@model List
@using OliverBooth.Common.Data
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Services
@inject IBlogPostService BlogPostService
@await Html.PartialAsync("Partials/_MastodonStatus")
<div id="all-blog-posts">
@foreach (IBlogPost post in BlogPostService.GetBlogPosts(Model.PageNumber))
{
@await Html.PartialAsync("Partials/_BlogCard", post)
}
</div>
@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData)
{
["UrlRoot"] = "/blog",
["Page"] = Model.PageNumber,
["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published)
})

View File

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace OliverBooth.Pages.Blog;
/// <summary>
/// Represents a class which defines the model for the <c>/blog/page/#</c> route.
/// </summary>
public class List : PageModel
{
/// <summary>
/// Gets the requested page number.
/// </summary>
/// <value>The requested page number.</value>
public int PageNumber { get; private set; }
/// <summary>
/// Handles the incoming GET request to the page.
/// </summary>
/// <param name="page">The requested page number, starting from 1.</param>
/// <returns></returns>
public IActionResult OnGet([FromRoute(Name = "pageNumber")] int page = 1)
{
if (page < 2)
{
return RedirectToPage("Index");
}
PageNumber = page;
return Page();
}
}

View File

@ -1,8 +1,8 @@
using Cysharp.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog;

View File

@ -1,5 +1,5 @@
@page
@using OliverBooth.Data.Web
@using OliverBooth.Common.Data.Web
@model OliverBooth.Pages.Books
@{
ViewData["Title"] = "Reading List";

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages;

View File

@ -1,6 +1,7 @@
@page
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OliverBooth.Common.Data.Web
@using OliverBooth.Common.Services
@inject IContactService ContactService
@{
ViewData["Title"] = "Blacklist";

View File

@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Contact";
}

View File

@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model OliverBooth.Pages.Contact.Result
@{

View File

@ -1,34 +0,0 @@
@page "/error/{code:int?}"
@model OliverBooth.Pages.ErrorModel
@{
Layout = "_MinimalLayout";
ViewData["Title"] = "Error";
}
<h2 class="text-danger">
@switch (Model.HttpStatusCode)
{
case 403:
<span>403 Forbidden</span>
break;
case 404:
<span>404 Page not found</span>
break;
case 500:
<span>Internal server error</span>
break;
default:
<span>Something went wrong</span>
break;
}
</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

View File

@ -1,28 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace OliverBooth.Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public int HttpStatusCode { get; private set; }
public IActionResult OnGet(int? code = null)
{
HttpStatusCode = code ?? HttpContext.Response.StatusCode;
if (HttpStatusCode == 200)
{
return RedirectToPage("/Index");
}
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
return Page();
}
}

View File

@ -0,0 +1,17 @@
@page "/error/400"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">400 Bad Request</h1>
</div>
<div class="p-2">
<p class="text-center">Received invalid request message. Check your request and try again.</p>
</div>
</div>
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/400-bad-request.png">
</div>
</div>
</article>

View File

@ -0,0 +1,15 @@
@page "/error/403"
<article>
<div class="text-center d-flex flex-column align-items-center">
<div class="p-2">
<h1 class="text-center">403 Forbidden</h1>
</div>
<div class="p-2">
<img class="img-fluid" src="~/img/error/403-forbidden.png">
</div>
<div class="p-2">
<p class="text-center">Access to the requested page is forbidden.</p>
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/504"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/504-gateway-timeout.png">
</div>
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">504 Gateway Timeout</h1>
</div>
<div class="p-2">
<p class="text-center">The server is slacking. Give it more coffee.</p>
</div>
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/400"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">410 Gone</h1>
</div>
<div class="p-2">
<p class="text-center">The requested page has mysteriously disappeared.</p>
</div>
</div>
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/410-gone.png">
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/418"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/418-im-a-teapot.png">
</div>
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">418 I'm A Teapot</h1>
</div>
<div class="p-2">
<p class="text-center">No coffee available. I am only capable of brewing tea.</p>
</div>
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/500"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/500-internal-server-error.png">
</div>
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">500 Internal Server Error</h1>
</div>
<div class="p-2">
<p class="text-center">This is my fault, not yours.</p>
</div>
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/404"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/404-not-found.png">
</div>
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">404 Not Found</h1>
</div>
<div class="p-2">
<p class="text-center">The requested page could not be found.</p>
</div>
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/503"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">503 Service Unavailable</h1>
</div>
<div class="p-2">
<p class="text-center">The server is currently unable to process your request. Please try again later.</p>
</div>
</div>
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/503-service-unavailable.png">
</div>
</div>
</article>

View File

@ -0,0 +1,17 @@
@page "/error/429"
<article>
<div class="d-flex align-items-center justify-content-center">
<div class="d-flex flex-column align-items-center justify-content-center">
<div class="p-2">
<h1 class="text-center">429 Too Many Requests</h1>
</div>
<div class="p-2">
<p class="text-center">You are being rate limited.</p>
</div>
</div>
<div class="align-self-stretch">
<img class="img-fluid" src="~/img/error/429-too-many-requests.png">
</div>
</div>
</article>

View File

@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
<main class="container">
<div class="row align-items-center mb-3">

View File

@ -1,4 +1,5 @@
@page "/privacy/five-oclock-somewhere"
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "It's 5 O'Clock Somewhere Privacy Policy";
}

View File

@ -1,4 +1,5 @@
@page "/privacy/google-play"
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Google Play Privacy Policy";
}

View File

@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Privacy Policy";
}

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