diff --git a/.gitignore b/.gitignore
index 4da40ba..6f4ad01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -362,3 +362,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/Notemark/data/shares
+/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md
diff --git a/Notemark/Components/Pages/Editor.razor b/Notemark/Components/Pages/Editor.razor
index eb46d67..6aa804d 100644
--- a/Notemark/Components/Pages/Editor.razor
+++ b/Notemark/Components/Pages/Editor.razor
@@ -24,6 +24,9 @@
+
@@ -128,7 +131,6 @@
{
markdownContent = e.Value?.ToString() ?? "";
hasUnsavedChanges = true;
- saveStatus = "Saving...";
// Update preview if visible
if (showPreview)
@@ -136,25 +138,24 @@
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
}
- // Debounce auto-save
+ // Debounced autosave - cancel previous timer and start new one
autoSaveTimer?.Dispose();
- autoSaveTimer = new Timer(async _ => await AutoSave(), null, 1000, Timeout.Infinite);
-
- await InvokeAsync(StateHasChanged);
- }
-
- private async Task OnKeyDown(KeyboardEventArgs e)
- {
- // Ctrl+S or Cmd+S for manual save
- if ((e.CtrlKey || e.MetaKey) && e.Key == "s")
+ autoSaveTimer = new Timer(async _ =>
{
- await SaveContent();
+ await InvokeAsync(async () => await AutoSave());
+ }, null, 2000, Timeout.Infinite); // Increased to 2 seconds for production
+
+ // Don't show "Saving..." immediately, wait for actual save
+ if (saveStatus == "Saved")
+ {
+ saveStatus = "Modified";
+ await InvokeAsync(StateHasChanged);
}
}
private async Task AutoSave()
{
- if (hasUnsavedChanges)
+ if (hasUnsavedChanges && !string.IsNullOrEmpty(sessionId))
{
await SaveContent();
}
@@ -162,18 +163,46 @@
private async Task SaveContent()
{
- if (string.IsNullOrEmpty(sessionId))
+ if (string.IsNullOrEmpty(sessionId) || !hasUnsavedChanges)
return;
+
+ // Set saving status only when actually starting to save
+ saveStatus = "Saving...";
+ await InvokeAsync(StateHasChanged);
try
{
+ // Longer timeout for large content (30 seconds)
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
await SessionService.SaveSessionContentAsync(sessionId, markdownContent);
+
hasUnsavedChanges = false;
saveStatus = "Saved";
}
- catch
+ catch (OperationCanceledException)
+ {
+ saveStatus = "Timeout";
+ // Retry after 5 seconds
+ _ = Task.Delay(5000).ContinueWith(async _ =>
+ {
+ if (hasUnsavedChanges)
+ {
+ await InvokeAsync(async () => await SaveContent());
+ }
+ });
+ }
+ catch (Exception)
{
saveStatus = "Error";
+ // Retry after 3 seconds
+ _ = Task.Delay(3000).ContinueWith(async _ =>
+ {
+ if (hasUnsavedChanges)
+ {
+ await InvokeAsync(async () => await SaveContent());
+ }
+ });
}
await InvokeAsync(StateHasChanged);
@@ -189,6 +218,20 @@
}
}
+ private async Task OnKeyDown(KeyboardEventArgs e)
+ {
+ // Ctrl+S or Cmd+S for manual save
+ if ((e.CtrlKey || e.MetaKey) && e.Key == "s")
+ {
+ await ManualSave();
+ }
+ }
+
+ private async Task ManualSave()
+ {
+ await SaveContent();
+ }
+
private async Task ToggleShareDropdown()
{
if (!showShareDropdown)
diff --git a/Notemark/Components/Pages/Editor.razor.css b/Notemark/Components/Pages/Editor.razor.css
index f583412..5365ff9 100644
--- a/Notemark/Components/Pages/Editor.razor.css
+++ b/Notemark/Components/Pages/Editor.razor.css
@@ -35,6 +35,7 @@
/* Buttons */
.btn-toggle,
.btn-share,
+.btn-save,
.btn-copy,
.btn-close {
background: none;
@@ -48,6 +49,33 @@
transition: all 0.2s ease;
}
+.btn-save {
+ padding: 0.5rem;
+ min-width: 2.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1rem;
+}
+
+.btn-save:enabled:hover {
+ background-color: var(--success);
+ color: white;
+ border-color: var(--success);
+ transform: translateY(-1px);
+}
+
+.btn-save:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background-color: transparent;
+}
+
+.btn-save:disabled:hover {
+ background-color: transparent;
+ transform: none;
+}
+
.btn-toggle:hover,
.btn-share:hover,
.btn-copy:hover {
@@ -87,14 +115,22 @@
color: var(--success);
}
-.save-status.saving {
+.save-status.modified {
color: var(--secondary-text);
}
+.save-status.saving {
+ color: var(--accent);
+}
+
.save-status.error {
color: var(--error);
}
+.save-status.timeout {
+ color: var(--error);
+}
+
/* Editor Layout */
.editor-layout {
flex: 1;
@@ -396,11 +432,18 @@
}
.btn-toggle,
- .btn-share {
+ .btn-share,
+ .btn-save {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
+ .btn-save {
+ padding: 0.375rem;
+ min-width: 2rem;
+ font-size: 0.875rem;
+ }
+
.markdown-editor,
.preview-content {
padding: 1rem;
diff --git a/Notemark/Components/Pages/SharedEditor.razor b/Notemark/Components/Pages/SharedEditor.razor
index eefbefc..8d0cecf 100644
--- a/Notemark/Components/Pages/SharedEditor.razor
+++ b/Notemark/Components/Pages/SharedEditor.razor
@@ -141,7 +141,7 @@ else
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
}
}
- catch (Exception ex)
+ catch (Exception)
{
errorMessage = "An error occurred while loading the document.";
}
diff --git a/Notemark/Components/Pages/SharedPreview.razor b/Notemark/Components/Pages/SharedPreview.razor
index 7821b04..7ab8b05 100644
--- a/Notemark/Components/Pages/SharedPreview.razor
+++ b/Notemark/Components/Pages/SharedPreview.razor
@@ -113,7 +113,7 @@ else
// Render to HTML
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
}
- catch (Exception ex)
+ catch (Exception)
{
errorMessage = "An error occurred while loading the document.";
}
diff --git a/Notemark/Services/SessionService.cs b/Notemark/Services/SessionService.cs
index f159fa9..5fdecd7 100644
--- a/Notemark/Services/SessionService.cs
+++ b/Notemark/Services/SessionService.cs
@@ -1,3 +1,5 @@
+using System.Text;
+
namespace Notemark.Services;
public class SessionService : ISessionService
@@ -50,7 +52,49 @@ public class SessionService : ISessionService
public async Task SaveSessionContentAsync(string sessionId, string content)
{
var filePath = GetSessionFilePath(sessionId);
- await File.WriteAllTextAsync(filePath, content ?? string.Empty);
+
+ // Create directory if it doesn't exist
+ var directory = Path.GetDirectoryName(filePath)!;
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ // For large content, use streaming to avoid memory issues
+ try
+ {
+ if (string.IsNullOrEmpty(content))
+ {
+ await File.WriteAllTextAsync(filePath, string.Empty, Encoding.UTF8);
+ return;
+ }
+
+ // Use FileStream for large content to avoid memory pressure
+ await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true);
+ await using var writer = new StreamWriter(fileStream, Encoding.UTF8);
+
+ // Write in chunks for very large content
+ if (content.Length > 50000) // > 50KB
+ {
+ const int chunkSize = 8192;
+ for (int i = 0; i < content.Length; i += chunkSize)
+ {
+ int length = Math.Min(chunkSize, content.Length - i);
+ await writer.WriteAsync(content.AsMemory(i, length));
+ await writer.FlushAsync(); // Ensure data is written
+ }
+ }
+ else
+ {
+ await writer.WriteAsync(content);
+ }
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // Retry after creating directory
+ Directory.CreateDirectory(directory);
+ await File.WriteAllTextAsync(filePath, content ?? string.Empty, Encoding.UTF8);
+ }
}
public async Task SessionExistsAsync(string sessionId)
diff --git a/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md b/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md
deleted file mode 100644
index 992cd91..0000000
--- a/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Notemark — .NET 10 Blazor Server Specification (Markdown Editor + Preview + Share)
-
-## 0) Goal
-Notemark is a minimalist Markdown editor web app built with **.NET 10 Blazor Server**.
-
-- First visit generates a **GUID sessionId**
-- Markdown is saved as a **file on disk** (no database)
-- Fullscreen editor, optional preview split view
-- Share links:
- - **Readonly** → preview only
- - **Editable** → preview only + Edit button
-
----
-
-## 1) Tech Stack
-- .NET 10
-- Blazor Server
-- Markdown rendering: Markdig
-- HTML sanitization: Ganss.XSS (HtmlSanitizer)
-- Persistence: File system only
-
-