Merge branch 'master' of https://git.binarydad.com/ryan/Notes
This commit is contained in:
commit
6563838738
2
.gitignore
vendored
2
.gitignore
vendored
@ -365,3 +365,5 @@ MigrationBackup/
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
/content.txt
|
||||
|
||||
/Notes
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -17,7 +17,7 @@
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
"pattern": "\\bNow listening on:\\s+(http?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
|
@ -3,12 +3,14 @@ using BinaryDad.Notes.Services;
|
||||
|
||||
namespace BinaryDad.Notes.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class ApiController : ControllerBase
|
||||
{
|
||||
private readonly INoteService noteService;
|
||||
|
||||
public ApiController(INoteService noteService) => this.noteService = noteService;
|
||||
|
||||
public string Note() => noteService.Get();
|
||||
[Route("note/{noteName}")]
|
||||
public string Note(string noteName) => noteService.GetText(noteName);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BinaryDad.Notes.Services;
|
||||
using BinaryDad.Notes.Models;
|
||||
using BinaryDad.Notes.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -11,10 +12,24 @@ public class NoteController : Controller
|
||||
|
||||
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("/");
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ RUN dotnet publish "BinaryDad.Notes.csproj" -c Release -o /app/publish /p:UseApp
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
RUN mkdir notes
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
EXPOSE 8080
|
||||
|
8
Models/ContentModel.cs
Normal file
8
Models/ContentModel.cs
Normal 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;
|
||||
}
|
@ -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
7
NoteContext.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BinaryDad.Notes
|
||||
{
|
||||
public static class NoteContext
|
||||
{
|
||||
public static IDictionary<string, string> ClientNotes { get; } = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
24
NoteHub.cs
24
NoteHub.cs
@ -14,11 +14,29 @@ namespace BinaryDad.Notes
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,64 @@
|
||||
{
|
||||
public class FileNoteService : INoteService
|
||||
{
|
||||
private readonly string? filePath;
|
||||
private readonly string folderPath;
|
||||
|
||||
private static readonly string defaultFolderPath = "notes";
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
{
|
||||
public interface INoteService
|
||||
{
|
||||
string Get();
|
||||
void Save(string content);
|
||||
ICollection<string> GetNoteNames();
|
||||
string GetText(string noteName);
|
||||
void SaveText(string content, string noteName);
|
||||
void DeleteNote(string noteName);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,25 @@
|
||||
@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="update-indicator">Updated</div>
|
||||
|
||||
@section scripts {
|
||||
@section Scripts
|
||||
{
|
||||
<script>
|
||||
var noteName = '@Model.CurrentNote';
|
||||
</script>
|
||||
|
||||
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -14,6 +14,7 @@
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
@RenderSection("scripts", false)
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -6,5 +6,7 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ContentFilePath": "content.txt"
|
||||
"FileNoteService": {
|
||||
"ContentFolder": "notes"
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,28 @@ body {
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -1,6 +1,6 @@
|
||||
let connection = new signalR.HubConnectionBuilder()
|
||||
.withAutomaticReconnect()
|
||||
.withUrl("/noteHub")
|
||||
.withUrl(`/noteHub?noteName=${noteName}`)
|
||||
.build();
|
||||
|
||||
function start() {
|
||||
@ -29,7 +29,7 @@ function saveContent($textarea) {
|
||||
|
||||
var content = $textarea.val();
|
||||
|
||||
connection.invoke('SaveNote', content).then(function () {
|
||||
connection.invoke('SaveNote', content, noteName).then(function () {
|
||||
showToast('#saved-indicator');
|
||||
}).catch(function (err) {
|
||||
console.error(err.toString());
|
||||
@ -52,10 +52,11 @@ $(function () {
|
||||
$textarea.val(content);
|
||||
showToast('#update-indicator');
|
||||
});
|
||||
|
||||
|
||||
// update content after reconnected
|
||||
connection.onreconnected(function() {
|
||||
$.get('api/note', function(content) {
|
||||
$.get(`api/note/${noteName}`, function(content) {
|
||||
$textarea.val(content);
|
||||
showToast('#update-indicator');
|
||||
console.log('Refreshed after disconnect');
|
||||
|
Loading…
x
Reference in New Issue
Block a user