Merge branch 'dev/multiple-notes'

This commit is contained in:
Ryan Peters 2024-01-06 21:10:06 -05:00
commit b5a4ce68a5
16 changed files with 161 additions and 60 deletions

2
.gitignore vendored
View File

@ -362,3 +362,5 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
/content.txt /content.txt
/Notes

2
.vscode/launch.json vendored
View File

@ -17,7 +17,7 @@
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": { "serverReadyAction": {
"action": "openExternally", "action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)" "pattern": "\\bNow listening on:\\s+(http?://\\S+)"
}, },
"env": { "env": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -3,12 +3,14 @@ using BinaryDad.Notes.Services;
namespace BinaryDad.Notes.Controllers namespace BinaryDad.Notes.Controllers
{ {
[ApiController]
public class ApiController : ControllerBase public class ApiController : ControllerBase
{ {
private readonly INoteService noteService; private readonly INoteService noteService;
public ApiController(INoteService noteService) => this.noteService = noteService; public ApiController(INoteService noteService) => this.noteService = noteService;
public string Note() => noteService.Get(); [Route("note/{noteName}")]
public string Note(string noteName) => noteService.GetText(noteName);
} }
} }

View File

@ -1,4 +1,5 @@
using BinaryDad.Notes.Services; using BinaryDad.Notes.Models;
using BinaryDad.Notes.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -11,10 +12,24 @@ public class NoteController : Controller
public NoteController(INoteService noteService) => this.noteService = noteService; public NoteController(INoteService noteService) => this.noteService = noteService;
public IActionResult Index() [Route("{noteName=default}")]
public IActionResult Index(string noteName)
{ {
var content = noteService.Get(); var model = new ContentModel
{
CurrentNote = noteName,
Text = noteService.GetText(noteName),
NoteNames = noteService.GetNoteNames()
};
return View((object)content); return View(model);
}
[Route("{noteName}/delete")]
public IActionResult Delete(string noteName)
{
noteService.DeleteNote(noteName);
return Redirect("/");
} }
} }

8
Models/ContentModel.cs Normal file
View File

@ -0,0 +1,8 @@
namespace BinaryDad.Notes.Models;
public class ContentModel
{
public string CurrentNote { get; set; } = string.Empty;
public ICollection<string> NoteNames { get; set; } = new List<string>();
public string Text { get; set; } = string.Empty;
}

View File

@ -1,8 +0,0 @@
namespace BinaryDad.Notes.Models;
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}

7
NoteContext.cs Normal file
View File

@ -0,0 +1,7 @@
namespace BinaryDad.Notes
{
public static class NoteContext
{
public static IDictionary<string, string> ClientNotes { get; } = new Dictionary<string, string>();
}
}

View File

@ -14,11 +14,29 @@ namespace BinaryDad.Notes
this.noteService = noteService; this.noteService = noteService;
} }
public async Task SaveNote(string content) public override Task OnConnectedAsync()
{ {
noteService.Save(content); var noteName = Context.GetHttpContext().Request.Query["noteName"];
await Clients.Others.SendAsync("updateNote", content); NoteContext.ClientNotes[Context.ConnectionId] = noteName;
return base.OnConnectedAsync();
}
public async Task SaveNote(string content, string? noteName)
{
noteService.SaveText(content, noteName);
// find all other connections except for the current one
var clientConnections = NoteContext.ClientNotes
.Where(c => c.Value == noteName && c.Key != Context.ConnectionId)
.Select(c => c.Key)
.ToList();
// update note for all other clients
await Clients
.Clients(clientConnections)
.SendAsync("updateNote", content);
} }
} }
} }

View File

@ -2,21 +2,64 @@
{ {
public class FileNoteService : INoteService public class FileNoteService : INoteService
{ {
private readonly string? filePath; private readonly string folderPath;
private static readonly string defaultFolderPath = "notes";
public FileNoteService(IConfiguration configuration) public FileNoteService(IConfiguration configuration)
{ {
filePath = configuration["ContentFilePath"]; folderPath = configuration["FileNoteService:ContentFolder"];
if (string.IsNullOrWhiteSpace(folderPath))
{
folderPath = defaultFolderPath;
}
}
public string GetText(string noteName)
{
CheckFile(noteName);
return File.ReadAllText(GetFilePath(noteName));
}
public ICollection<string> GetNoteNames()
{
return Directory.GetFiles(folderPath)
.Select(f => Path.GetFileName(f))
.ToList();
}
public void SaveText(string content, string noteName)
{
File.WriteAllText(GetFilePath(noteName), content);
}
public void DeleteNote(string noteName)
{
var filePath = GetFilePath(noteName);
File.Delete(filePath);
}
private void CheckFile(string noteName)
{
var filePath = GetFilePath(noteName);
// ensure initialized // ensure initialized
if (!File.Exists(filePath)) if (!File.Exists(filePath))
{ {
Save("Hi! Feel free to start typing. Everything will be saved soon after you are done typing."); Directory.CreateDirectory(folderPath);
SaveText("Hi! Feel free to start typing. Everything will be saved soon after you are done typing.", noteName);
} }
} }
public string Get() => File.ReadAllText(filePath); private string GetFilePath(string noteName)
{
noteName = noteName.Trim().ToLower();
public void Save(string content) => File.WriteAllText(filePath, content); return Path.Combine(folderPath, noteName);
}
} }
} }

View File

@ -2,7 +2,9 @@
{ {
public interface INoteService public interface INoteService
{ {
string Get(); ICollection<string> GetNoteNames();
void Save(string content); string GetText(string noteName);
void SaveText(string content, string noteName);
void DeleteNote(string noteName);
} }
} }

View File

@ -1,11 +1,22 @@
@model string @model ContentModel
<textarea id="content" name="content" spellcheck="false">@Model</textarea> <textarea id="content" name="content" spellcheck="false">@Model.Text</textarea>
<div class="note-names">
@foreach (var note in Model.NoteNames.Order())
{
var css = note.Equals(Model.CurrentNote, StringComparison.OrdinalIgnoreCase) ? "current" : null;
<a href="@note" class="@css">@note</a>
}
</div>
<div class="toast" id="saved-indicator">Saved</div> <div class="toast" id="saved-indicator">Saved</div>
<div class="toast" id="update-indicator">Updated</div> <div class="toast" id="update-indicator">Updated</div>
@section scripts { @section Scripts
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script> {
<script src="~/js/site.js" asp-append-version="true"></script> <script>
var noteName = '@Model.CurrentNote';
</script>
} }

View File

@ -1,25 +0,0 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -11,9 +11,10 @@
<body> <body>
@RenderBody() @RenderBody()
@RenderSection("scripts", false)
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
@RenderSection("scripts", false)
</body> </body>
</html> </html>

View File

@ -6,5 +6,7 @@
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ContentFilePath": "content.txt" "FileNoteService": {
"ContentFolder": "notes"
}
} }

View File

@ -20,6 +20,28 @@ body {
min-height: 100%; min-height: 100%;
} }
div.note-names {
position: fixed;
bottom: 5px;
left: 0;
font-size: 14px;
opacity: 0.5;
}
div.note-names a {
color: #666;
padding-left: 10px;
text-decoration: none;
}
div.note-names a.current {
font-weight: bold;
}
div .note-names a:not(:last-of-type) {
border-right: 1px solid #666;
}
textarea { textarea {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -1,6 +1,6 @@
let connection = new signalR.HubConnectionBuilder() let connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect() .withAutomaticReconnect()
.withUrl("/noteHub") .withUrl(`/noteHub?noteName=${noteName}`)
.build(); .build();
function start() { function start() {
@ -29,7 +29,7 @@ function saveContent($textarea) {
var content = $textarea.val(); var content = $textarea.val();
connection.invoke('SaveNote', content).then(function () { connection.invoke('SaveNote', content, noteName).then(function () {
showToast('#saved-indicator'); showToast('#saved-indicator');
}).catch(function (err) { }).catch(function (err) {
console.error(err.toString()); console.error(err.toString());
@ -53,9 +53,10 @@ $(function () {
showToast('#update-indicator'); showToast('#update-indicator');
}); });
// update content after reconnected // update content after reconnected
connection.onreconnected(function() { connection.onreconnected(function() {
$.get('api/note', function(content) { $.get(`api/note/${noteName}`, function(content) {
$textarea.val(content); $textarea.val(content);
showToast('#update-indicator'); showToast('#update-indicator');
console.log('Refreshed after disconnect'); console.log('Refreshed after disconnect');