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
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
/content.txt
|
/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
|
// 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"
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN mkdir notes
|
||||||
COPY --from=build /app/publish .
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
EXPOSE 8080
|
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;
|
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
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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="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>
|
||||||
|
var noteName = '@Model.CurrentNote';
|
||||||
|
</script>
|
||||||
|
|
||||||
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
|
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
|
||||||
<script src="~/js/site.js" asp-append-version="true"></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>
|
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
@RenderSection("scripts", false)
|
@RenderSection("scripts", false)
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -6,5 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ContentFilePath": "content.txt"
|
"FileNoteService": {
|
||||||
|
"ContentFolder": "notes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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%;
|
||||||
|
@ -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');
|
||||||
|
Loading…
Reference in New Issue
Block a user