MNotes/index-new.html

2925 lines
95 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="MNotes - A modern Markdown note-taking application">
<title>MNotes</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/material-components-web/14.0.0/material-components-web.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,1,0">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap">
<style>
/* Theme Variables */
:root {
/* Material 3 (You/Expressive) Colors - Light Theme */
--primary-color: #006A6A;
--on-primary-color: #FFFFFF;
--primary-container-color: #6FF7F7;
--on-primary-container-color: #002020;
--secondary-color: #4A6363;
--on-secondary-color: #FFFFFF;
--secondary-container-color: #CCE8E7;
--on-secondary-container-color: #051F1F;
--tertiary-color: #4B607C;
--on-tertiary-color: #FFFFFF;
--tertiary-container-color: #D3E4FF;
--on-tertiary-container-color: #041C35;
--error-color: #BA1A1A;
--on-error-color: #FFFFFF;
--error-container-color: #FFDAD6;
--on-error-container-color: #410002;
--surface-color: #FAFDFC;
--on-surface-color: #191C1C;
--surface-variant-color: #DAE5E3;
--on-surface-variant-color: #3F4948;
--outline-color: #6F7978;
--background-color: #FAFDFC;
--on-background-color: #191C1C;
--elevation-level-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
--elevation-level-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
--elevation-level-3: 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
--surface-tint-color: #006A6A;
}
.theme-dark {
/* Material 3 (You/Expressive) Colors - Dark Theme */
--primary-color: #4CDADA;
--on-primary-color: #003737;
--primary-container-color: #004F4F;
--on-primary-container-color: #6FF7F7;
--secondary-color: #B0CCCC;
--on-secondary-color: #1B3534;
--secondary-container-color: #324B4B;
--on-secondary-container-color: #CCE8E7;
--tertiary-color: #B4C8E9;
--on-tertiary-color: #1C314B;
--tertiary-container-color: #334863;
--on-tertiary-container-color: #D3E4FF;
--error-color: #FFB4AB;
--on-error-color: #690005;
--error-container-color: #93000A;
--on-error-container-color: #FFDAD6;
--surface-color: #191C1C;
--on-surface-color: #E0E3E2;
--surface-variant-color: #3F4948;
--on-surface-variant-color: #BEC9C7;
--outline-color: #899392;
--background-color: #191C1C;
--on-background-color: #E0E3E2;
--elevation-level-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.35);
--elevation-level-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.35);
--elevation-level-3: 0px 4px 8px 3px rgba(0, 0, 0, 0.35);
--surface-tint-color: #4CDADA;
}
.theme-sepia {
/* Material 3 (You/Expressive) Colors - Sepia Theme */
--primary-color: #8B5000;
--on-primary-color: #FFFFFF;
--primary-container-color: #FFDDB7;
--on-primary-container-color: #2C1700;
--secondary-color: #735A41;
--on-secondary-color: #FFFFFF;
--secondary-container-color: #FFDDBD;
--on-secondary-container-color: #291805;
--tertiary-color: #5B6237;
--on-tertiary-color: #FFFFFF;
--tertiary-container-color: #DFE8B2;
--on-tertiary-container-color: #181E00;
--error-color: #BA1A1A;
--on-error-color: #FFFFFF;
--error-container-color: #FFDAD6;
--on-error-container-color: #410002;
--surface-color: #FFFBFF;
--on-surface-color: #201B16;
--surface-variant-color: #F0E0CF;
--on-surface-variant-color: #4E4539;
--outline-color: #7F7667;
--background-color: #FFF8F3;
--on-background-color: #201B16;
--elevation-level-1: 0px 1px 3px 1px rgba(139, 80, 0, 0.15);
--elevation-level-2: 0px 2px 6px 2px rgba(139, 80, 0, 0.15);
--elevation-level-3: 0px 4px 8px 3px rgba(139, 80, 0, 0.15);
--surface-tint-color: #8B5000;
}
/* Base Styles */
body {
font-family: 'Google Sans', 'Roboto', sans-serif;
margin: 0;
padding: 0;
background-color: var(--background-color);
color: var(--on-background-color);
transition: background-color 0.3s ease, color 0.3s ease;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
/* Accessibility - Focus States */
button:focus,
a:focus,
input:focus,
textarea:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Layout Components */
.header {
background-color: var(--primary-color);
color: var(--on-primary-color);
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--elevation-level-2);
z-index: 10;
transition: background-color 0.3s ease, color 0.3s ease;
}
.left-section {
display: flex;
align-items: center;
gap: 16px;
}
.app-title {
font-size: 1.5rem;
font-weight: 500;
margin: 0;
font-family: 'Google Sans', sans-serif;
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 280px;
background-color: var(--surface-color);
border-right: 1px solid var(--outline-color);
display: flex;
flex-direction: column;
overflow-y: auto;
box-shadow: var(--elevation-level-1);
transition: background-color 0.3s ease, color 0.3s ease, left 0.3s ease;
}
.sidebar-header {
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--outline-color);
background-color: var(--surface-variant-color);
color: var(--on-surface-variant-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.note-list {
list-style: none;
padding: 0;
margin: 0;
flex-grow: 1;
overflow-y: auto;
}
.note-item {
padding: 16px;
cursor: pointer;
border-bottom: 1px solid var(--outline-color);
transition: background-color 0.2s, transform 0.1s;
border-radius: 12px;
margin: 8px;
}
.note-item:hover {
background-color: var(--surface-variant-color);
transform: translateY(-2px);
}
.note-item:focus {
outline: 2px solid var(--primary-color);
outline-offset: -2px;
}
.note-item.active {
background-color: var(--primary-container-color);
color: var(--on-primary-container-color);
box-shadow: var(--elevation-level-1);
}
.note-item-title {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.note-item-preview {
font-size: 0.85rem;
opacity: 0.75;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-date {
font-size: 0.75rem;
opacity: 0.6;
margin-top: 4px;
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: var(--background-color);
transition: background-color 0.3s ease;
}
.toolbar {
background-color: var(--surface-color);
border-bottom: 1px solid var(--outline-color);
padding: 8px 16px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
transition: background-color 0.3s ease;
}
.toolbar-group {
display: flex;
gap: 4px;
border-right: 1px solid var(--outline-color);
padding-right: 8px;
margin-right: 8px;
}
.toolbar-group:last-child {
border-right: none;
}
.editor-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.editor {
flex: 1;
padding: 16px;
overflow-y: auto;
outline: none;
font-size: 1rem;
line-height: 1.6;
border: none;
background-color: var(--surface-color);
color: var(--on-surface-color);
resize: none;
font-family: 'Roboto Mono', monospace, 'Google Sans', sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
}
.markdown-preview {
flex: 1;
padding: 16px;
overflow-y: auto;
background-color: var(--surface-color);
color: var(--on-surface-color);
border-left: 1px solid var(--outline-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.markdown-preview.hidden {
display: none;
}
/* UI Components */
.button {
background-color: var(--primary-container-color);
color: var(--on-primary-container-color);
border: none;
border-radius: 20px;
padding: 10px 16px;
font-family: 'Google Sans', sans-serif;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
box-shadow: var(--elevation-level-1);
position: relative;
}
.button:hover {
background-color: var(--primary-container-color);
opacity: 0.9;
box-shadow: var(--elevation-level-2);
}
.button:active {
transform: translateY(1px);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button-icon {
font-size: 1.25rem;
}
.icon-button {
background-color: transparent;
color: var(--on-surface-color);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s;
}
.icon-button:hover {
background-color: var(--surface-variant-color);
}
.icon-button.active {
color: var(--primary-color);
background-color: var(--primary-container-color);
}
.theme-selector {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
background-color: var(--surface-variant-color);
border-radius: 24px;
transition: background-color 0.3s ease;
}
.theme-button {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.theme-button:focus {
outline: 2px solid var(--primary-color);
}
.theme-button.active {
border-color: var(--primary-color);
transform: scale(1.2);
}
.theme-light {
background-color: #FFFBFE;
}
.theme-dark {
background-color: #1C1B1F;
}
.theme-sepia {
background-color: #F5F5DC;
}
.status-bar {
padding: 8px 16px;
font-size: 0.75rem;
background-color: var(--surface-color);
border-top: 1px solid var(--outline-color);
display: flex;
justify-content: space-between;
transition: background-color 0.3s ease, color 0.3s ease;
}
.toast {
position: fixed;
bottom: 16px;
right: 16px;
padding: 12px 16px;
background-color: var(--surface-color);
color: var(--on-surface-color);
border-radius: 8px;
box-shadow: var(--elevation-level-2);
z-index: 1000;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s, transform 0.3s;
max-width: 300px;
}
.toast.visible {
opacity: 1;
transform: translateY(0);
}
.toast-success {
border-left: 4px solid #4CAF50;
}
.toast-error {
border-left: 4px solid var(--error-color);
}
.toast-info {
border-left: 4px solid var(--primary-color);
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(4px);
opacity: 0;
transition: opacity 0.3s;
}
.modal.visible {
opacity: 1;
}
.modal-content {
background-color: var(--surface-color);
padding: 32px;
border-radius: 28px;
width: 100%;
max-width: 400px;
max-height: 80vh; /* Limit height to 80% of viewport height */
box-shadow: var(--elevation-level-3);
transform: scale(0.9);
transition: transform 0.3s;
color: var(--on-surface-color);
display: flex;
flex-direction: column; /* Organize content vertically */
overflow: hidden; /* Hide overflow but allow internal scrolling */
}
.modal.visible .modal-content {
transform: scale(1);
}
/* Ensuring modal content areas are properly scrollable */
.markdown-content {
overflow-y: auto; /* Make content scrollable */
max-height: 60vh; /* Limit height to leave room for buttons */
margin-bottom: 16px; /* Space before buttons */
padding-right: 8px; /* Room for scrollbar */
}
/* News modal specific styles */
#newsModal .modal-content {
display: flex;
flex-direction: column;
}
#newsContent {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
}
.modal-title {
font-size: 1.5rem;
font-weight: 500;
margin-top: 0;
margin-bottom: 24px;
color: var(--on-surface-color);
font-family: 'Google Sans', sans-serif;
}
.form-field {
margin-bottom: 24px;
}
.form-field-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--on-surface-color);
}
.input-field {
width: 100%;
padding: 16px;
font-family: 'Google Sans', 'Roboto', sans-serif;
font-size: 1rem;
border: 2px solid var(--outline-color);
border-radius: 16px;
background-color: var(--surface-color);
color: var(--on-surface-color);
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.input-field:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 2px rgba(0, 106, 106, 0.2);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.info-message {
background-color: var(--surface-variant-color);
color: var(--on-surface-variant-color);
padding: 12px 16px;
margin: 8px 0;
border-radius: 12px;
display: none;
font-size: 0.875rem;
border-left: 4px solid var(--tertiary-color);
}
.search-container {
position: relative;
margin-bottom: 8px;
padding: 0 16px;
}
.search-input {
width: 100%;
padding: 10px 16px 10px 40px;
border-radius: 20px;
border: 1px solid var(--outline-color);
background-color: var(--surface-color);
color: var(--on-surface-color);
font-family: 'Google Sans', sans-serif;
font-size: 0.875rem;
transition: all 0.2s;
box-sizing: border-box;
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(0, 106, 106, 0.2);
}
.search-icon {
position: absolute;
left: 26px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: var(--on-surface-variant-color);
pointer-events: none;
}
.search-clear {
position: absolute;
right: 26px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: var(--on-surface-variant-color);
background: none;
border: none;
cursor: pointer;
display: none;
}
.search-clear.visible {
display: block;
}
/* Utility Classes */
.hidden {
display: none !important;
}
.material-symbols-rounded {
font-variation-settings:
'FILL' 1,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
/* Markdown Preview Styling */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
color: var(--on-surface-color);
margin-top: 1.5em;
margin-bottom: 0.5em;
font-family: 'Google Sans', sans-serif;
}
.markdown-content h1 {
font-size: 2em;
border-bottom: 1px solid var(--outline-color);
padding-bottom: 0.3em;
}
.markdown-content h2 {
font-size: 1.5em;
border-bottom: 1px solid var(--outline-color);
padding-bottom: 0.3em;
}
.markdown-content a {
color: var(--primary-color);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.markdown-content code {
background-color: var(--surface-variant-color);
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: 'Roboto Mono', monospace;
font-size: 0.9em;
}
.markdown-content pre {
background-color: var(--surface-variant-color);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
}
.markdown-content blockquote {
border-left: 4px solid var(--primary-color);
margin-left: 0;
padding-left: 16px;
color: var(--on-surface-variant-color);
}
.markdown-content img {
max-width: 100%;
border-radius: 8px;
}
.markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
}
.markdown-content th,
.markdown-content td {
border: 1px solid var(--outline-color);
padding: 8px 12px;
text-align: left;
}
.markdown-content th {
background-color: var(--surface-variant-color);
}
.markdown-content input[type="checkbox"] {
margin-right: 8px;
}
/* Auto-save Indicator */
.auto-save-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
opacity: 0;
transition: opacity 0.3s;
}
.auto-save-indicator.visible {
opacity: 1;
}
.auto-save-indicator .material-symbols-rounded {
font-size: 14px;
}
/* Mobile Styles */
@media (max-width: 768px) {
.sidebar {
position: absolute;
left: -280px;
height: 100%;
transition: left 0.3s ease;
z-index: 100;
}
.sidebar.open {
left: 0;
}
.toggle-sidebar {
display: block;
}
.theme-selector {
margin-left: 8px;
}
.toolbar {
overflow-x: auto;
justify-content: flex-start;
padding: 8px;
}
.button {
padding: 8px 12px;
font-size: 0.8rem;
}
.editor-wrapper {
flex-direction: column;
}
.markdown-preview:not(.hidden) {
border-left: none;
border-top: 1px solid var(--outline-color);
max-height: 50%;
}
}
</style>
</head>
<body>
<div class="header">
<div class="left-section">
<button id="toggleSidebar" class="button" aria-label="Toggle sidebar">
<span class="material-symbols-rounded">menu</span>
</button>
<div class="logo-container">
<img src="logo/mnotes.png" alt="MNotes Logo" class="app-logo" id="appLogo" onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 40%22><text x=%220%22 y=%2230%22 font-family=%22Google Sans, Arial%22 font-size=%2230%22 fill=%22white%22>MNotes</text></svg>'; this.onerror=null;">
</div>
</div>
<div class="header-controls">
<button id="aboutBtn" class="icon-button" aria-label="About MNotes">
<span class="material-symbols-rounded">info</span>
</button>
<div class="theme-selector">
<button class="theme-button theme-light active" data-theme="light" title="Light theme" aria-label="Switch to light theme"></button>
<button class="theme-button theme-dark" data-theme="dark" title="Dark theme" aria-label="Switch to dark theme"></button>
<button class="theme-button theme-sepia" data-theme="sepia" title="Sepia theme" aria-label="Switch to sepia theme"></button>
<button id="customizeThemeBtn" class="icon-button" aria-label="Customize theme colors">
<span class="material-symbols-rounded">palette</span>
</button>
</div>
</div>
</div>
<div class="main-container">
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>Notes</h2>
<button id="newNoteBtn" class="button" aria-label="Create new note">
<span class="material-symbols-rounded">add</span>
</button>
</div>
<div class="search-container">
<span class="material-symbols-rounded search-icon">search</span>
<input type="text" id="searchNotes" class="search-input" placeholder="Search notes..." aria-label="Search notes">
<button id="clearSearch" class="search-clear" aria-label="Clear search">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<ul class="note-list" id="noteList" role="list" aria-label="Notes list">
<!-- Note items will be added here -->
</ul>
</div>
<div class="editor-container">
<div class="toolbar">
<div class="toolbar-group">
<button id="saveNote" class="button" aria-label="Save note">
<span class="material-symbols-rounded">save</span>
<span class="button-text">Save</span>
</button>
<button id="downloadNote" class="button" aria-label="Download note">
<span class="material-symbols-rounded">download</span>
<span class="button-text">Download</span>
</button>
</div>
<div class="toolbar-group">
<button id="formatBold" class="icon-button" aria-label="Bold text">
<span class="material-symbols-rounded">format_bold</span>
</button>
<button id="formatItalic" class="icon-button" aria-label="Italic text">
<span class="material-symbols-rounded">format_italic</span>
</button>
<button id="formatCode" class="icon-button" aria-label="Code block">
<span class="material-symbols-rounded">code</span>
</button>
<button id="formatList" class="icon-button" aria-label="Bullet list">
<span class="material-symbols-rounded">format_list_bulleted</span>
</button>
</div>
<div class="toolbar-group">
<button id="togglePreview" class="button" aria-label="Toggle preview">
<span class="material-symbols-rounded">preview</span>
<span class="button-text">Preview</span>
</button>
</div>
<div class="toolbar-group">
<button id="githubSave" class="button" aria-label="Save to GitHub">
<span class="material-symbols-rounded">cloud_upload</span>
<span class="button-text">GitHub</span>
</button>
</div>
<div class="toolbar-group">
<button id="deleteNote" class="button" aria-label="Delete note">
<span class="material-symbols-rounded">delete</span>
<span class="button-text">Delete</span>
</button>
</div>
</div>
<div class="editor-wrapper">
<textarea id="editor" class="editor" placeholder="Start typing your note here..." aria-label="Note editor"></textarea>
<div id="markdownPreview" class="markdown-preview hidden markdown-content" aria-label="Markdown preview"></div>
</div>
<div class="status-bar">
<div>
<span id="wordCount">0 words</span>
<span class="auto-save-indicator" id="autoSaveIndicator">
<span class="material-symbols-rounded">sync</span>
Auto-saving...
</span>
</div>
<span id="lastSaved">Not saved yet</span>
</div>
</div>
</div>
<!-- GitHub Auth Modal -->
<div class="modal" id="githubModal">
<div class="modal-content">
<h2 class="modal-title">GitHub Authentication</h2>
<div class="form-field">
<label for="githubUsername" class="form-field-label">GitHub Username</label>
<input type="text" id="githubUsername" class="input-field" placeholder="Your GitHub username">
</div>
<div class="form-field">
<label for="githubToken" class="form-field-label">Personal Access Token</label>
<input type="password" id="githubToken" class="input-field" placeholder="GitHub personal access token">
<div class="info-message" style="display: block; font-size: 0.75rem;">
<strong>Security Note:</strong> Tokens are stored in your browser's localStorage.
For better security, use a token with minimal permissions.
</div>
</div>
<div class="form-field">
<label for="githubRepo" class="form-field-label">Repository Name</label>
<input type="text" id="githubRepo" class="input-field" placeholder="Existing repository name">
</div>
<div class="info-message" id="githubMessage"></div>
<div class="modal-actions">
<button id="clearGithubAuth" class="button" aria-label="Clear GitHub credentials">
<span class="material-symbols-rounded">delete</span>
Clear
</button>
<button id="cancelGithubAuth" class="button" aria-label="Cancel">Cancel</button>
<button id="confirmGithubAuth" class="button" aria-label="Connect to GitHub">Connect</button>
</div>
</div>
</div>
<!-- New Note Modal -->
<div class="modal" id="newNoteModal">
<div class="modal-content">
<h2 class="modal-title">Create New Note</h2>
<div class="form-field">
<label for="newNoteTitle" class="form-field-label">Note Title</label>
<input type="text" id="newNoteTitle" class="input-field" placeholder="Enter a title for your note">
</div>
<div class="modal-actions">
<button id="cancelNewNote" class="button" aria-label="Cancel">Cancel</button>
<button id="confirmNewNote" class="button" aria-label="Create new note">Create</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal" id="deleteConfirmModal">
<div class="modal-content">
<h2 class="modal-title">Delete Note</h2>
<p>Are you sure you want to delete this note? This action cannot be undone.</p>
<div class="modal-actions">
<button id="cancelDelete" class="button" aria-label="Cancel">Cancel</button>
<button id="confirmDelete" class="button" aria-label="Delete note">Delete</button>
</div>
</div>
</div>
<!-- Unsaved Changes Modal -->
<div class="modal" id="unsavedChangesModal">
<div class="modal-content">
<h2 class="modal-title">Unsaved Changes</h2>
<p>You have unsaved changes. What would you like to do?</p>
<div class="modal-actions">
<button id="discardChanges" class="button" aria-label="Discard changes">Discard</button>
<button id="saveChanges" class="button" aria-label="Save changes">Save</button>
<button id="cancelNavigation" class="button" aria-label="Cancel">Cancel</button>
</div>
</div>
</div>
<!-- Theme Customization Modal -->
<div class="modal" id="themeCustomizationModal">
<div class="modal-content">
<h2 class="modal-title">Customize Theme Colors</h2>
<div class="theme-tabs">
<button class="theme-tab active" data-theme-tab="light">Light</button>
<button class="theme-tab" data-theme-tab="dark">Dark</button>
<button class="theme-tab" data-theme-tab="sepia">Sepia</button>
</div>
<div class="form-field">
<label for="primaryColorPicker" class="form-field-label">Primary Color</label>
<div class="color-picker-container">
<input type="color" id="primaryColorPicker" class="color-picker">
<input type="text" id="primaryColorHex" class="input-field color-hex" placeholder="#000000">
</div>
</div>
<div class="modal-actions">
<button id="resetThemeColors" class="button" aria-label="Reset to default">Reset</button>
<button id="cancelThemeCustomization" class="button" aria-label="Cancel">Cancel</button>
<button id="saveThemeCustomization" class="button" aria-label="Save changes">Save</button>
</div>
</div>
</div>
<!-- About Modal -->
<div class="modal" id="aboutModal">
<div class="modal-content">
<h2 class="modal-title">About MNotes</h2>
<div class="about-content">
<div class="about-header">
<img src="logo/mnotes.png" alt="MNotes Logo" class="about-logo" onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 40%22><text x=%220%22 y=%2230%22 font-family=%22Google Sans, Arial%22 font-size=%2230%22 fill=%22black%22>MNotes</text></svg>'; this.onerror=null;">
<div>
<h3 id="appVersion">Loading version...</h3>
<p>A modern Markdown note-taking application</p>
</div>
</div>
<div class="about-section">
<h4>Credits</h4>
<ul>
<li><strong>Jakub Ruzicka / korozelife</strong> (GitHub: <a href="https://github.com/thejakubruzicka" target="_blank">@thejakubruzicka</a>) - Main idea</li>
<li><strong>mxnticek</strong> (GitHub: <a href="https://github.com/VlastikYoutubeKo" target="_blank">@VlastikYoutubeKo</a>) - Adding some features</li>
<li><strong>Gemini AI</strong> (Google AI) - Rough example of the app</li>
<li><strong>Claude AI</strong> - Finishing touches of the app</li>
</ul>
</div>
<div class="about-section">
<h4>License</h4>
<p>This project is open source and available under the MIT License.</p>
</div>
</div>
<div class="modal-actions">
<button id="closeAbout" class="button" aria-label="Close">Close</button>
</div>
</div>
</div>
<!-- News Modal -->
<div class="modal" id="newsModal">
<div class="modal-content">
<h2 class="modal-title">News & Updates</h2>
<div id="newsContent" class="markdown-content">
<!-- News content will be loaded here -->
</div>
<div class="modal-actions" style="margin-top: auto; position: sticky; bottom: 0; background: var(--surface-color); padding-top: 8px;">
<div class="checkbox-container">
<input type="checkbox" id="dontShowAgain" class="styled-checkbox">
<label for="dontShowAgain">Don't show again</label>
</div>
<button id="closeNews" class="button" aria-label="Close">Close</button>
</div>
</div>
</div>
<!-- Toast notification -->
<div id="toast" class="toast"></div>
<script>
/**
* MNotes Application
* A modern Markdown note-taking application
*/
document.addEventListener('DOMContentLoaded', function() {
/* ================= STATE MANAGEMENT ================= */
// State variables
const state = {
notes: [],
currentNoteId: null,
githubAuth: {
username: '',
token: '',
repo: ''
},
editor: {
hasUnsavedChanges: false,
lastSavedContent: '',
isPreviewActive: false
},
ui: {
isSidebarOpen: window.innerWidth > 768,
searchQuery: '',
pendingNoteToOpen: null,
lastSaveTimestamp: null,
theme: 'light'
}
};
// Tracks if the editor content has changed since last save
let editorChangeTimeout = null;
/* ================= DOM ELEMENTS ================= */
// Editor elements
const editor = document.getElementById('editor');
const noteList = document.getElementById('noteList');
const wordCount = document.getElementById('wordCount');
const lastSaved = document.getElementById('lastSaved');
const markdownPreview = document.getElementById('markdownPreview');
const autoSaveIndicator = document.getElementById('autoSaveIndicator');
// Sidebar elements
const toggleSidebarBtn = document.getElementById('toggleSidebar');
const sidebar = document.getElementById('sidebar');
const newNoteBtn = document.getElementById('newNoteBtn');
const searchInput = document.getElementById('searchNotes');
const clearSearchBtn = document.getElementById('clearSearch');
// Toolbar buttons
const saveNoteBtn = document.getElementById('saveNote');
const downloadNoteBtn = document.getElementById('downloadNote');
const togglePreviewBtn = document.getElementById('togglePreview');
const githubSaveBtn = document.getElementById('githubSave');
const deleteNoteBtn = document.getElementById('deleteNote');
const formatBoldBtn = document.getElementById('formatBold');
const formatItalicBtn = document.getElementById('formatItalic');
const formatCodeBtn = document.getElementById('formatCode');
const formatListBtn = document.getElementById('formatList');
// Theme buttons
const themeButtons = document.querySelectorAll('.theme-button');
// Modals
const githubModal = document.getElementById('githubModal');
const githubUsername = document.getElementById('githubUsername');
const githubToken = document.getElementById('githubToken');
const githubRepo = document.getElementById('githubRepo');
const githubMessage = document.getElementById('githubMessage');
const confirmGithubAuth = document.getElementById('confirmGithubAuth');
const cancelGithubAuth = document.getElementById('cancelGithubAuth');
const clearGithubAuth = document.getElementById('clearGithubAuth');
const newNoteModal = document.getElementById('newNoteModal');
const newNoteTitle = document.getElementById('newNoteTitle');
const confirmNewNote = document.getElementById('confirmNewNote');
const cancelNewNote = document.getElementById('cancelNewNote');
const deleteConfirmModal = document.getElementById('deleteConfirmModal');
const confirmDelete = document.getElementById('confirmDelete');
const cancelDelete = document.getElementById('cancelDelete');
const unsavedChangesModal = document.getElementById('unsavedChangesModal');
const discardChangesBtn = document.getElementById('discardChanges');
const saveChangesBtn = document.getElementById('saveChanges');
const cancelNavigationBtn = document.getElementById('cancelNavigation');
// Toast
const toast = document.getElementById('toast');
/* ================= INITIALIZATION ================= */
/**
* Initialize the application
*/
function initialize() {
loadFromLocalStorage();
attachEventListeners();
// Create first note if none exist
if (state.notes.length === 0) {
createNewNote('Welcome to MNotes', 'Start typing to create your first note!');
} else {
renderNoteList();
// Open the most recently updated note
const mostRecentNote = [...state.notes].sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt);
})[0];
openNote(mostRecentNote.id);
}
// Set initial UI state
updateSidebarVisibility();
updateThemeUI();
}
/**
* Load data from localStorage
*/
function loadFromLocalStorage() {
try {
// Load notes
const savedNotes = localStorage.getItem('notes');
if (savedNotes) {
state.notes = JSON.parse(savedNotes);
}
// Load theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
state.ui.theme = savedTheme;
}
// Load GitHub auth
const savedGithubAuth = localStorage.getItem('githubAuth');
if (savedGithubAuth) {
state.githubAuth = JSON.parse(savedGithubAuth);
}
} catch (error) {
showToast('Error loading data from localStorage: ' + error.message, 'error');
console.error('Error loading from localStorage:', error);
}
}
/**
* Attach event listeners to DOM elements
*/
function attachEventListeners() {
// Editor events
editor.addEventListener('input', handleEditorInput);
editor.addEventListener('keydown', handleEditorKeydown);
// Sidebar events
toggleSidebarBtn.addEventListener('click', toggleSidebar);
newNoteBtn.addEventListener('click', showNewNoteModal);
searchInput.addEventListener('input', handleSearchInput);
clearSearchBtn.addEventListener('click', clearSearch);
// Toolbar events
saveNoteBtn.addEventListener('click', saveCurrentNote);
downloadNoteBtn.addEventListener('click', downloadNoteAsMarkdown);
togglePreviewBtn.addEventListener('click', togglePreview);
githubSaveBtn.addEventListener('click', handleGitHubSave);
deleteNoteBtn.addEventListener('click', showDeleteConfirmation);
// Formatting events
formatBoldBtn.addEventListener('click', () => insertFormatting('**', '**', 'bold text'));
formatItalicBtn.addEventListener('click', () => insertFormatting('*', '*', 'italic text'));
formatCodeBtn.addEventListener('click', () => insertFormatting('```\n', '\n```', 'code'));
formatListBtn.addEventListener('click', () => insertFormatting('- ', '', 'list item'));
// Theme events
themeButtons.forEach(button => {
button.addEventListener('click', () => setTheme(button.dataset.theme));
});
// GitHub modal events
confirmGithubAuth.addEventListener('click', saveGithubAuth);
cancelGithubAuth.addEventListener('click', hideGithubModal);
clearGithubAuth.addEventListener('click', clearGithubCredentials);
// New note modal events
confirmNewNote.addEventListener('click', handleCreateNewNote);
cancelNewNote.addEventListener('click', hideNewNoteModal);
newNoteTitle.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
handleCreateNewNote();
}
});
// Delete modal events
confirmDelete.addEventListener('click', confirmDeleteNote);
cancelDelete.addEventListener('click', hideDeleteConfirmModal);
// Unsaved changes modal events
discardChangesBtn.addEventListener('click', () => handleUnsavedChangesAction('discard'));
saveChangesBtn.addEventListener('click', () => handleUnsavedChangesAction('save'));
cancelNavigationBtn.addEventListener('click', () => handleUnsavedChangesAction('cancel'));
// Window events
window.addEventListener('beforeunload', handleBeforeUnload);
window.addEventListener('resize', updateSidebarVisibility);
}
/* ================= NOTE OPERATIONS ================= */
/**
* Create a new note
* @param {string} title - The title of the note
* @param {string} content - The content of the note
*/
function createNewNote(title, content = '') {
const newNote = {
id: Date.now().toString(),
title: title || 'Untitled Note',
content: content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
state.notes.push(newNote);
saveNotesToLocalStorage();
renderNoteList();
openNote(newNote.id);
showToast(`Note "${title}" created`, 'success');
}
/**
* Open a note
* @param {string} noteId - The ID of the note to open
*/
function openNote(noteId) {
// Check for unsaved changes before switching notes
if (state.editor.hasUnsavedChanges && state.currentNoteId !== noteId) {
state.ui.pendingNoteToOpen = noteId;
showUnsavedChangesModal();
return;
}
const note = state.notes.find(n => n.id === noteId);
if (!note) {
showToast('Note not found', 'error');
return;
}
state.currentNoteId = noteId;
editor.value = note.content;
state.editor.lastSavedContent = note.content;
state.editor.hasUnsavedChanges = false;
updateWordCount();
updateLastSaved(note.updatedAt);
renderMarkdownPreview();
// Update active state in note list
updateActiveNoteInList();
// Enable/disable buttons based on note selection
updateButtonStates();
}
/**
* Save the current note
* @param {boolean} silent - Whether to show notifications
* @returns {boolean} - Whether the save was successful
*/
function saveCurrentNote(event, silent = false) {
if (event) event.preventDefault();
if (!state.currentNoteId) {
if (!silent) showToast('No note selected', 'error');
return false;
}
try {
const noteIndex = state.notes.findIndex(n => n.id === state.currentNoteId);
if (noteIndex === -1) {
if (!silent) showToast('Note not found', 'error');
return false;
}
// Only save if content has changed
if (state.notes[noteIndex].content !== editor.value) {
state.notes[noteIndex].content = editor.value;
state.notes[noteIndex].updatedAt = new Date().toISOString();
state.editor.lastSavedContent = editor.value;
state.editor.hasUnsavedChanges = false;
saveNotesToLocalStorage();
updateLastSaved(state.notes[noteIndex].updatedAt);
renderNoteList();
renderMarkdownPreview();
if (!silent) showToast('Note saved', 'success');
}
return true;
} catch (error) {
console.error('Error saving note:', error);
if (!silent) showToast('Error saving note: ' + error.message, 'error');
return false;
}
}
/**
* Auto-save the current note
*/
function autoSaveNote() {
if (state.editor.hasUnsavedChanges && state.currentNoteId) {
// Show auto-save indicator
autoSaveIndicator.classList.add('visible');
// Save the note silently
const saveSuccess = saveCurrentNote(null, true);
// Hide auto-save indicator after a delay
setTimeout(() => {
autoSaveIndicator.classList.remove('visible');
}, 1500);
return saveSuccess;
}
return false;
}
/**
* Delete the current note
*/
function confirmDeleteNote() {
if (!state.currentNoteId) {
showToast('No note selected', 'error');
return;
}
try {
const noteToDelete = state.notes.find(n => n.id === state.currentNoteId);
if (!noteToDelete) {
showToast('Note not found', 'error');
return;
}
const noteTitle = noteToDelete.title;
// Remove note from array
state.notes = state.notes.filter(n => n.id !== state.currentNoteId);
saveNotesToLocalStorage();
// Hide the delete confirmation modal
hideDeleteConfirmModal();
// Show success message
showToast(`Note "${noteTitle}" deleted`, 'success');
// Open another note if available, or clear the editor
if (state.notes.length > 0) {
openNote(state.notes[0].id);
} else {
state.currentNoteId = null;
editor.value = '';
state.editor.lastSavedContent = '';
state.editor.hasUnsavedChanges = false;
updateWordCount();
updateLastSaved(null);
updateButtonStates();
}
renderNoteList();
} catch (error) {
console.error('Error deleting note:', error);
showToast('Error deleting note: ' + error.message, 'error');
}
}
/**
* Download the current note as Markdown
*/
function downloadNoteAsMarkdown(event) {
if (event) event.preventDefault();
if (!state.currentNoteId) {
showToast('No note selected', 'error');
return;
}
try {
const note = state.notes.find(n => n.id === state.currentNoteId);
if (!note) {
showToast('Note not found', 'error');
return;
}
// Generate a filename based on the note title
const filename = `${note.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.md`;
const content = note.content;
// Create a blob and download it
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
showToast(`Note downloaded as ${filename}`, 'success');
} catch (error) {
console.error('Error downloading note:', error);
showToast('Error downloading note: ' + error.message, 'error');
}
}
/* ================= GITHUB INTEGRATION ================= */
/**
* Handle GitHub save button click
*/
function handleGitHubSave(event) {
if (event) event.preventDefault();
// If no auth details or missing fields, show the auth modal
if (!state.githubAuth.username || !state.githubAuth.token || !state.githubAuth.repo) {
showGithubModal();
return;
}
// If no note is selected, show error
if (!state.currentNoteId) {
showToast('No note selected', 'error');
return;
}
// Save any unsaved changes first
if (state.editor.hasUnsavedChanges) {
const saveSuccess = saveCurrentNote(null, true);
if (!saveSuccess) {
showToast('Failed to save note before GitHub upload', 'error');
return;
}
}
// Now save to GitHub
saveToGithub();
}
/**
* Save the current note to GitHub
*/
async function saveToGithub() {
if (!state.currentNoteId) {
showToast('No note selected', 'error');
return;
}
try {
// Find the note
const note = state.notes.find(n => n.id === state.currentNoteId);
if (!note) {
showToast('Note not found', 'error');
return;
}
// Ensure the notes directory path exists by creating directory path notation
const dirPath = 'notes';
const filename = `${note.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.md`;
const fullPath = `${dirPath}/${filename}`;
const content = note.content;
// Show loading toast
showToast('Saving to GitHub...', 'info');
// Step 1: First, try to get the file to see if it exists using the GitHub REST API
let sha;
try {
const getResponse = await fetch(`https://api.github.com/repos/${state.githubAuth.username}/${state.githubAuth.repo}/contents/${fullPath}`, {
method: 'GET',
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${state.githubAuth.token}`,
'X-GitHub-Api-Version': '2022-11-28'
}
});
if (getResponse.status === 200) {
const fileData = await getResponse.json();
sha = fileData.sha;
console.log('File exists, got SHA:', sha);
} else if (getResponse.status === 404) {
console.log('File does not exist, will create new file');
} else {
// Handle other error statuses
const errorData = await getResponse.json();
throw new Error(`GitHub API error (GET): ${errorData.message}`);
}
} catch (error) {
// Only log the error, we'll continue with the PUT request
console.warn('Error checking file existence:', error);
// If it's not a 404, we should show a warning to the user
if (error.message && !error.message.includes('404')) {
showToast(`Warning: ${error.message}`, 'error');
}
}
// Step 2: Create or update the file using the GitHub REST API
// Properly encode content to Base64 as required by the API
// Note: btoa() doesn't handle Unicode characters well, so we need to use encodeURIComponent
const base64Content = btoa(unescape(encodeURIComponent(content)));
const requestBody = {
message: `Update note: ${note.title}`,
content: base64Content,
committer: {
name: state.githubAuth.username || 'MNotes User',
email: `${state.githubAuth.username || 'user'}@users.noreply.github.com`
}
};
// Add SHA if we're updating an existing file
if (sha) {
requestBody.sha = sha;
}
const response = await fetch(`https://api.github.com/repos/${state.githubAuth.username}/${state.githubAuth.repo}/contents/${fullPath}`, {
method: 'PUT',
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${state.githubAuth.token}`,
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
// Handle the response
if (response.status === 200 || response.status === 201) {
const responseData = await response.json();
console.log('File saved successfully:', responseData);
showToast(`Note "${note.title}" saved to GitHub successfully!`, 'success');
// Make the HTML URL clickable
if (responseData.content && responseData.content.html_url) {
const htmlUrl = responseData.content.html_url;
// Show a clickable link in a modal or toast
const msg = `<div>File saved! <a href="${htmlUrl}" target="_blank">View on GitHub</a></div>`;
document.getElementById('toast').innerHTML = msg;
}
} else {
const errorData = await response.json();
throw new Error(`GitHub API error (PUT): ${errorData.message}`);
}
} catch (error) {
console.error('Error saving to GitHub:', error);
showToast(`Error saving to GitHub: ${error.message}`, 'error');
}
}
/**
* Save GitHub authentication credentials
*/
function saveGithubAuth(event) {
if (event) event.preventDefault();
// Validate inputs
const username = githubUsername.value.trim();
const token = githubToken.value.trim();
const repo = githubRepo.value.trim();
if (!username || !token || !repo) {
githubMessage.textContent = 'All fields are required';
githubMessage.style.display = 'block';
return;
}
// Save to state and localStorage
state.githubAuth = { username, token, repo };
saveGithubAuthToLocalStorage();
hideGithubModal();
// Success message
showToast('GitHub credentials saved', 'success');
// Try to save the current note to GitHub
if (state.currentNoteId) {
saveToGithub();
}
}
/**
* Clear GitHub credentials
*/
function clearGithubCredentials(event) {
if (event) event.preventDefault();
// Clear form fields
githubUsername.value = '';
githubToken.value = '';
githubRepo.value = '';
// Clear state and localStorage
state.githubAuth = { username: '', token: '', repo: '' };
localStorage.removeItem('githubAuth');
// Success message
githubMessage.textContent = 'Credentials cleared';
githubMessage.className = 'info-message';
githubMessage.style.display = 'block';
// Hide message after a delay
setTimeout(() => {
githubMessage.style.display = 'none';
}, 3000);
}
/**
* Save GitHub auth to localStorage
*/
function saveGithubAuthToLocalStorage() {
try {
localStorage.setItem('githubAuth', JSON.stringify(state.githubAuth));
} catch (error) {
console.error('Error saving GitHub auth to localStorage:', error);
showToast('Error saving GitHub credentials', 'error');
}
}
/* ================= UI OPERATIONS ================= */
/**
* Toggle the sidebar visibility
*/
function toggleSidebar(event) {
if (event) event.preventDefault();
state.ui.isSidebarOpen = !state.ui.isSidebarOpen;
updateSidebarVisibility();
}
/**
* Update sidebar visibility based on state
*/
function updateSidebarVisibility() {
if (state.ui.isSidebarOpen) {
sidebar.classList.add('open');
} else {
sidebar.classList.remove('open');
}
}
/**
* Toggle markdown preview
*/
function togglePreview(event) {
if (event) event.preventDefault();
state.editor.isPreviewActive = !state.editor.isPreviewActive;
if (state.editor.isPreviewActive) {
renderMarkdownPreview();
markdownPreview.classList.remove('hidden');
togglePreviewBtn.querySelector('.button-text').textContent = 'Editor';
} else {
markdownPreview.classList.add('hidden');
togglePreviewBtn.querySelector('.button-text').textContent = 'Preview';
}
// For mobile, adjust the editor wrapper
if (window.innerWidth <= 768) {
const editorWrapper = document.querySelector('.editor-wrapper');
editorWrapper.style.flexDirection = state.editor.isPreviewActive ? 'column' : 'row';
}
}
/**
* Render the markdown preview
*/
function renderMarkdownPreview() {
if (!state.editor.isPreviewActive) return;
try {
// Simple markdown to HTML conversion
let html = simpleMarkdownToHtml(editor.value);
markdownPreview.innerHTML = html;
// Add classes for styling
markdownPreview.querySelectorAll('pre code').forEach(block => {
block.className = 'language-js';
});
} catch (error) {
console.error('Error rendering markdown:', error);
markdownPreview.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
}
}
/**
* Simple markdown to HTML conversion
* @param {string} markdown - The markdown text to convert
* @returns {string} - The converted HTML
*/
function simpleMarkdownToHtml(markdown) {
if (!markdown) return '';
let html = markdown;
// Escape HTML
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Headers
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
html = html.replace(/^##### (.*$)/gm, '<h5>$1</h5>');
html = html.replace(/^###### (.*$)/gm, '<h6>$1</h6>');
// Bold
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
// Code blocks
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Lists
html = html.replace(/^\s*\- (.*$)/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n)+/g, '<ul>$&</ul>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// Images
html = html.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
// Blockquotes
html = html.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>');
// Paragraphs
html = html.replace(/\n\n(.*?)\n\n/g, '<p>$1</p>');
// Line breaks
html = html.replace(/\n/g, '<br>');
return html;
}
/**
* Update the active note in the list
*/
function updateActiveNoteInList() {
const noteItems = document.querySelectorAll('.note-item');
noteItems.forEach(item => {
if (item.dataset.id === state.currentNoteId) {
item.classList.add('active');
item.setAttribute('aria-current', 'true');
// Scroll into view if necessary
if (!isElementInViewport(item)) {
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} else {
item.classList.remove('active');
item.setAttribute('aria-current', 'false');
}
});
}
/**
* Check if an element is visible in the viewport
* @param {HTMLElement} element - The element to check
* @returns {boolean} - Whether the element is in the viewport
*/
function isElementInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
/**
* Update the word count
*/
function updateWordCount() {
try {
const text = editor.value.trim();
const words = text ? text.split(/\s+/).length : 0;
wordCount.textContent = `${words} word${words === 1 ? '' : 's'}`;
} catch (error) {
console.error('Error updating word count:', error);
}
}
/**
* Update the last saved timestamp
* @param {string|null} timestamp - ISO timestamp string or null
*/
function updateLastSaved(timestamp) {
if (!timestamp) {
lastSaved.textContent = 'Not saved yet';
return;
}
try {
const date = new Date(timestamp);
lastSaved.textContent = `Last saved: ${date.toLocaleString()}`;
state.ui.lastSaveTimestamp = timestamp;
} catch (error) {
console.error('Error updating last saved:', error);
lastSaved.textContent = 'Last saved: Unknown';
}
}
/**
* Update UI elements based on current state
*/
function updateButtonStates() {
const hasCurrentNote = !!state.currentNoteId;
// Disable buttons if no note is selected
saveNoteBtn.disabled = !hasCurrentNote;
downloadNoteBtn.disabled = !hasCurrentNote;
deleteNoteBtn.disabled = !hasCurrentNote;
formatBoldBtn.disabled = !hasCurrentNote;
formatItalicBtn.disabled = !hasCurrentNote;
formatCodeBtn.disabled = !hasCurrentNote;
formatListBtn.disabled = !hasCurrentNote;
togglePreviewBtn.disabled = !hasCurrentNote;
// GitHub button is only disabled if no note is selected
// It's always enabled if a note is selected, regardless of auth status
githubSaveBtn.disabled = !hasCurrentNote;
// Editor is disabled if no note is selected
editor.disabled = !hasCurrentNote;
}
/**
* Set the theme
* @param {string} theme - 'light', 'dark', or 'sepia'
*/
function setTheme(theme) {
if (!theme || !['light', 'dark', 'sepia'].includes(theme)) {
theme = 'light';
}
state.ui.theme = theme;
updateThemeUI();
try {
localStorage.setItem('theme', theme);
} catch (error) {
console.error('Error saving theme to localStorage:', error);
}
}
/**
* Update the UI based on current theme
*/
function updateThemeUI() {
// Remove all theme classes from body
document.body.classList.remove('theme-light', 'theme-dark', 'theme-sepia');
// Add the current theme class if not light (default)
if (state.ui.theme !== 'light') {
document.body.classList.add(`theme-${state.ui.theme}`);
}
// Update theme buttons
themeButtons.forEach(button => {
if (button.dataset.theme === state.ui.theme) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
// Force repaint for some browsers
document.body.style.display = 'none';
document.body.offsetHeight; // Trigger a reflow
document.body.style.display = '';
}
/**
* Handle search input
*/
function handleSearchInput() {
state.ui.searchQuery = searchInput.value.trim().toLowerCase();
// Show/hide clear button
if (state.ui.searchQuery) {
clearSearchBtn.classList.add('visible');
} else {
clearSearchBtn.classList.remove('visible');
}
renderNoteList();
}
/**
* Clear the search
*/
function clearSearch() {
searchInput.value = '';
state.ui.searchQuery = '';
clearSearchBtn.classList.remove('visible');
renderNoteList();
searchInput.focus();
}
/**
* Show a toast notification
* @param {string} message - The message to display
* @param {string} type - 'success', 'error', or 'info'
*/
function showToast(message, type = 'info') {
// Clear any existing toast timeout
if (window.toastTimeout) {
clearTimeout(window.toastTimeout);
}
// Set type class
toast.className = 'toast toast-' + type;
toast.textContent = message;
toast.classList.add('visible');
// Hide after 3 seconds
window.toastTimeout = setTimeout(() => {
toast.classList.remove('visible');
}, 3000);
}
/* ================= EVENT HANDLERS ================= */
/**
* Handle editor input
*/
function handleEditorInput() {
updateWordCount();
// Mark as having unsaved changes if content differs from last saved
const hasChanges = editor.value !== state.editor.lastSavedContent;
if (hasChanges !== state.editor.hasUnsavedChanges) {
state.editor.hasUnsavedChanges = hasChanges;
}
// Update preview if active
if (state.editor.isPreviewActive) {
// Debounce preview rendering
if (window.previewTimeout) {
clearTimeout(window.previewTimeout);
}
window.previewTimeout = setTimeout(renderMarkdownPreview, 300);
}
// Debounce auto-save
if (editorChangeTimeout) {
clearTimeout(editorChangeTimeout);
}
editorChangeTimeout = setTimeout(() => {
if (state.editor.hasUnsavedChanges) {
autoSaveNote();
}
}, 3000);
}
/**
* Handle keyboard shortcuts in the editor
* @param {KeyboardEvent} event - The keyboard event
*/
function handleEditorKeydown(event) {
// Save on Ctrl+S or Cmd+S
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
saveCurrentNote();
return;
}
// Preview on Ctrl+P or Cmd+P
if ((event.ctrlKey || event.metaKey) && event.key === 'p') {
event.preventDefault();
togglePreview();
return;
}
// Bold on Ctrl+B or Cmd+B
if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
event.preventDefault();
insertFormatting('**', '**', 'bold text');
return;
}
// Italic on Ctrl+I or Cmd+I
if ((event.ctrlKey || event.metaKey) && event.key === 'i') {
event.preventDefault();
insertFormatting('*', '*', 'italic text');
return;
}
// Tab key inside the editor should insert spaces
if (event.key === 'Tab') {
event.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
// Insert 2 spaces at cursor position
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
// Move cursor position after the inserted spaces
editor.selectionStart = editor.selectionEnd = start + 2;
// Trigger input event for auto-save
editor.dispatchEvent(new Event('input'));
}
}
/**
* Insert formatting into the editor
* @param {string} before - Text to insert before selection
* @param {string} after - Text to insert after selection
* @param {string} placeholder - Placeholder if nothing is selected
*/
function insertFormatting(before, after, placeholder) {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
const replacement = selectedText.length > 0 ? selectedText : placeholder;
// Insert the formatted text
editor.value = editor.value.substring(0, start)
+ before
+ replacement
+ after
+ editor.value.substring(end);
// Set the selection to include the new text
const newCursorPos = selectedText.length > 0
? end + before.length + after.length
: start + before.length + placeholder.length;
editor.focus();
editor.selectionStart = selectedText.length > 0 ? start : start + before.length;
editor.selectionEnd = selectedText.length > 0 ? newCursorPos : start + before.length + placeholder.length;
// Trigger input event for auto-save and preview update
editor.dispatchEvent(new Event('input'));
}
/**
* Handle creating a new note from the modal
*/
function handleCreateNewNote() {
const title = newNoteTitle.value.trim();
if (!title) {
// Show error in modal
const errorMsg = document.createElement('div');
errorMsg.className = 'info-message';
errorMsg.textContent = 'Please enter a title';
errorMsg.style.display = 'block';
// Add after the input field
const formField = newNoteTitle.closest('.form-field');
// Remove any existing error message
const existingError = formField.querySelector('.info-message');
if (existingError) {
formField.removeChild(existingError);
}
formField.appendChild(errorMsg);
// Focus the input field
newNoteTitle.focus();
return;
}
createNewNote(title);
hideNewNoteModal();
}
/**
* Handle actions from the unsaved changes modal
* @param {string} action - 'save', 'discard', or 'cancel'
*/
function handleUnsavedChangesAction(action) {
hideUnsavedChangesModal();
switch (action) {
case 'save':
// Save changes, then open the pending note
if (saveCurrentNote(null, true) && state.ui.pendingNoteToOpen) {
openNote(state.ui.pendingNoteToOpen);
state.ui.pendingNoteToOpen = null;
}
break;
case 'discard':
// Discard changes and open the pending note
state.editor.hasUnsavedChanges = false;
if (state.ui.pendingNoteToOpen) {
openNote(state.ui.pendingNoteToOpen);
state.ui.pendingNoteToOpen = null;
}
break;
case 'cancel':
// Do nothing, keep editing the current note
state.ui.pendingNoteToOpen = null;
break;
}
}
/**
* Handle beforeunload event to warn about unsaved changes
* @param {BeforeUnloadEvent} event - The beforeunload event
*/
function handleBeforeUnload(event) {
if (state.editor.hasUnsavedChanges) {
const message = 'You have unsaved changes. Are you sure you want to leave?';
event.returnValue = message;
return message;
}
}
/* ================= RENDERING ================= */
/**
* Render the note list
*/
function renderNoteList() {
// Clear current list
noteList.innerHTML = '';
// Get notes to display based on search
const notesToDisplay = state.ui.searchQuery
? state.notes.filter(note => {
return note.title.toLowerCase().includes(state.ui.searchQuery) ||
note.content.toLowerCase().includes(state.ui.searchQuery);
})
: state.notes;
if (notesToDisplay.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.className = 'note-item';
if (state.ui.searchQuery) {
emptyItem.textContent = 'No matching notes found';
} else {
emptyItem.textContent = 'No notes yet';
}
noteList.appendChild(emptyItem);
return;
}
// Sort notes by updated date (newest first)
const sortedNotes = [...notesToDisplay].sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt);
});
// Create list items
sortedNotes.forEach(note => {
const noteItem = document.createElement('li');
noteItem.className = 'note-item';
noteItem.setAttribute('role', 'listitem');
noteItem.setAttribute('tabindex', '0');
noteItem.setAttribute('aria-label', note.title);
if (note.id === state.currentNoteId) {
noteItem.classList.add('active');
noteItem.setAttribute('aria-current', 'true');
} else {
noteItem.setAttribute('aria-current', 'false');
}
noteItem.dataset.id = note.id;
const titleElement = document.createElement('div');
titleElement.className = 'note-item-title';
titleElement.textContent = note.title;
const previewElement = document.createElement('div');
previewElement.className = 'note-item-preview';
// Highlight matching text if searching
if (state.ui.searchQuery && note.content.toLowerCase().includes(state.ui.searchQuery)) {
const content = note.content;
const query = state.ui.searchQuery;
// Find the first occurrence of the query
const index = content.toLowerCase().indexOf(query);
// Get some context around the match
const start = Math.max(0, index - 20);
const end = Math.min(content.length, index + query.length + 20);
let preview = content.substring(start, end);
// Add ellipsis if truncated
if (start > 0) preview = '...' + preview;
if (end < content.length) preview = preview + '...';
previewElement.textContent = preview;
} else {
previewElement.textContent = note.content.substring(0, 50) || 'Empty note';
}
const dateElement = document.createElement('div');
dateElement.className = 'note-date';
// Format the date
const date = new Date(note.updatedAt);
const now = new Date();
let dateStr = '';
// If same day, show time
if (date.toDateString() === now.toDateString()) {
dateStr = 'Today at ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
// Show date
dateStr = date.toLocaleDateString([], { month: 'short', day: 'numeric', year: now.getFullYear() !== date.getFullYear() ? 'numeric' : undefined });
}
dateElement.textContent = dateStr;
noteItem.appendChild(titleElement);
noteItem.appendChild(previewElement);
noteItem.appendChild(dateElement);
// Add event listeners
noteItem.addEventListener('click', () => {
openNote(note.id);
});
noteItem.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openNote(note.id);
}
});
noteList.appendChild(noteItem);
});
}
/**
* Save notes to localStorage
*/
function saveNotesToLocalStorage() {
try {
localStorage.setItem('notes', JSON.stringify(state.notes));
} catch (error) {
console.error('Error saving notes to localStorage:', error);
showToast('Error saving notes: ' + error.message, 'error');
}
}
/* ================= MODALS ================= */
/**
* Show the GitHub auth modal
*/
function showGithubModal() {
// Set form values
githubUsername.value = state.githubAuth.username || '';
githubToken.value = state.githubAuth.token || '';
githubRepo.value = state.githubAuth.repo || '';
// Clear any error messages
githubMessage.style.display = 'none';
// Show the modal
githubModal.style.display = 'flex';
githubModal.classList.add('visible');
// Focus the first empty field
if (!state.githubAuth.username) {
githubUsername.focus();
} else if (!state.githubAuth.token) {
githubToken.focus();
} else if (!state.githubAuth.repo) {
githubRepo.focus();
} else {
githubUsername.focus();
}
}
/**
* Hide the GitHub auth modal
*/
function hideGithubModal() {
githubModal.classList.remove('visible');
setTimeout(() => {
githubModal.style.display = 'none';
}, 300);
}
/**
* Show the new note modal
*/
function showNewNoteModal() {
newNoteTitle.value = '';
// Remove any error messages
const errorMsg = newNoteTitle.closest('.form-field').querySelector('.info-message');
if (errorMsg) {
errorMsg.remove();
}
newNoteModal.style.display = 'flex';
newNoteModal.classList.add('visible');
newNoteTitle.focus();
}
/**
* Hide the new note modal
*/
function hideNewNoteModal() {
newNoteModal.classList.remove('visible');
setTimeout(() => {
newNoteModal.style.display = 'none';
}, 300);
}
/**
* Show delete confirmation modal
*/
function showDeleteConfirmation() {
if (!state.currentNoteId) {
showToast('No note selected', 'error');
return;
}
deleteConfirmModal.style.display = 'flex';
deleteConfirmModal.classList.add('visible');
}
/**
* Hide delete confirmation modal
*/
function hideDeleteConfirmModal() {
deleteConfirmModal.classList.remove('visible');
setTimeout(() => {
deleteConfirmModal.style.display = 'none';
}, 300);
}
/**
* Show unsaved changes modal
*/
function showUnsavedChangesModal() {
unsavedChangesModal.style.display = 'flex';
unsavedChangesModal.classList.add('visible');
}
/**
* Hide unsaved changes modal
*/
function hideUnsavedChangesModal() {
unsavedChangesModal.classList.remove('visible');
setTimeout(() => {
unsavedChangesModal.style.display = 'none';
}, 300);
}
/* ================= STARTUP ================= */
/* ================= THEME CUSTOMIZATION ================= */
/**
* Apply custom theme colors
*/
function applyCustomThemeColors() {
// Get the current theme
const currentTheme = state.ui.theme;
// Get custom colors for this theme
const themeColors = state.customThemeColors[currentTheme];
if (themeColors && themeColors.primary) {
// Set CSS variable
document.documentElement.style.setProperty('--primary-color', themeColors.primary);
// Derive other colors based on primary (simplified version)
if (currentTheme === 'light') {
document.documentElement.style.setProperty('--primary-container-color', adjustColor(themeColors.primary, 80));
document.documentElement.style.setProperty('--on-primary-color', isDarkColor(themeColors.primary) ? '#FFFFFF' : '#000000');
document.documentElement.style.setProperty('--on-primary-container-color', isDarkColor(adjustColor(themeColors.primary, 80)) ? '#FFFFFF' : '#000000');
} else if (currentTheme === 'dark') {
document.documentElement.style.setProperty('--primary-container-color', adjustColor(themeColors.primary, -40));
document.documentElement.style.setProperty('--on-primary-color', isDarkColor(themeColors.primary) ? '#FFFFFF' : '#000000');
document.documentElement.style.setProperty('--on-primary-container-color', isDarkColor(adjustColor(themeColors.primary, -40)) ? '#FFFFFF' : '#000000');
} else if (currentTheme === 'sepia') {
document.documentElement.style.setProperty('--primary-container-color', adjustColor(themeColors.primary, 80));
document.documentElement.style.setProperty('--on-primary-color', isDarkColor(themeColors.primary) ? '#FFFFFF' : '#000000');
document.documentElement.style.setProperty('--on-primary-container-color', isDarkColor(adjustColor(themeColors.primary, 80)) ? '#FFFFFF' : '#000000');
}
}
}
/**
* Check if a color is dark
* @param {string} color - CSS color value
* @returns {boolean} - Whether the color is dark
*/
function isDarkColor(color) {
// Convert hex to RGB
let r, g, b;
if (color.startsWith('#')) {
color = color.substring(1);
r = parseInt(color.substring(0, 2), 16);
g = parseInt(color.substring(2, 4), 16);
b = parseInt(color.substring(4, 6), 16);
} else if (color.startsWith('rgb')) {
const matches = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (matches) {
r = parseInt(matches[1]);
g = parseInt(matches[2]);
b = parseInt(matches[3]);
} else {
return false;
}
} else {
return false;
}
// Calculate relative luminance using WCAG formula
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
// If luminance < 128, color is dark
return luminance < 128;
}
/**
* Adjust a color by a percentage
* @param {string} color - CSS color value (hex)
* @param {number} percent - Percentage to adjust (positive lightens, negative darkens)
* @returns {string} - Adjusted hex color
*/
function adjustColor(color, percent) {
if (!color.startsWith('#')) {
return color;
}
color = color.substring(1);
let r = parseInt(color.substring(0, 2), 16);
let g = parseInt(color.substring(2, 4), 16);
let b = parseInt(color.substring(4, 6), 16);
r = Math.min(255, Math.max(0, r + (percent * 2.55)));
g = Math.min(255, Math.max(0, g + (percent * 2.55)));
b = Math.min(255, Math.max(0, b + (percent * 2.55)));
const rr = Math.round(r).toString(16).padStart(2, '0');
const gg = Math.round(g).toString(16).padStart(2, '0');
const bb = Math.round(b).toString(16).padStart(2, '0');
return `#${rr}${gg}${bb}`;
}
/**
* Show theme customization modal
*/
function showThemeCustomizationModal() {
// Set the active theme tab
document.querySelectorAll('.theme-tab').forEach(tab => {
if (tab.dataset.themeTab === state.ui.theme) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
// Set the color picker to the current theme's color
const currentThemeColor = state.customThemeColors[state.ui.theme]?.primary ||
defaultThemeColors[state.ui.theme]?.primary ||
'#000000';
document.getElementById('primaryColorPicker').value = currentThemeColor;
document.getElementById('primaryColorHex').value = currentThemeColor;
// Show the modal
const modal = document.getElementById('themeCustomizationModal');
modal.style.display = 'flex';
modal.classList.add('visible');
}
/**
* Save theme customization
*/
function saveThemeCustomization() {
// Get the active theme tab
const activeThemeTab = document.querySelector('.theme-tab.active').dataset.themeTab;
// Get the selected color
const selectedColor = document.getElementById('primaryColorPicker').value;
// Update the state
state.customThemeColors[activeThemeTab] = {
primary: selectedColor
};
// Save to localStorage
localStorage.setItem('customThemeColors', JSON.stringify(state.customThemeColors));
// Apply the new colors
applyCustomThemeColors();
// Hide the modal
const modal = document.getElementById('themeCustomizationModal');
modal.classList.remove('visible');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
// Show success message
showToast('Theme colors saved', 'success');
}
/**
* Reset theme colors to defaults
*/
function resetThemeColors() {
// Get the active theme tab
const activeThemeTab = document.querySelector('.theme-tab.active').dataset.themeTab;
// Reset to default color
const defaultColor = defaultThemeColors[activeThemeTab]?.primary || '#000000';
// Update inputs
document.getElementById('primaryColorPicker').value = defaultColor;
document.getElementById('primaryColorHex').value = defaultColor;
// Update state
state.customThemeColors[activeThemeTab] = {
primary: defaultColor
};
// Save to localStorage
localStorage.setItem('customThemeColors', JSON.stringify(state.customThemeColors));
// Apply the new colors
applyCustomThemeColors();
// Show success message
showToast('Theme colors reset to default', 'success');
}
/* ================= ABOUT DIALOG ================= */
/**
* Show the about dialog
*/
function showAboutDialog() {
// Load version from README.md
fetchVersionFromReadme();
// Show the modal
const modal = document.getElementById('aboutModal');
modal.style.display = 'flex';
modal.classList.add('visible');
}
/**
* Hide the about dialog
*/
function hideAboutDialog() {
const modal = document.getElementById('aboutModal');
modal.classList.remove('visible');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
}
/**
* Fetch version information from README.md
*/
function fetchVersionFromReadme() {
fetch('README.md')
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch README.md: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then(content => {
// Extract version using regex
const versionMatch = content.match(/Version(?:\s+|:\s*)(v?\d+\.\d+\.\d+(?:-\w+)?)/i);
if (versionMatch && versionMatch[1]) {
// Update version element
document.getElementById('appVersion').textContent = `Version ${versionMatch[1]}`;
} else {
console.warn('Could not extract version from README.md');
document.getElementById('appVersion').textContent = 'Version Unknown';
}
})
.catch(error => {
console.error('Error fetching README.md:', error);
document.getElementById('appVersion').textContent = 'Version Unknown';
});
}
/* ================= NEWS SYSTEM ================= */
/**
* Show news modal
*/
function showNewsModal() {
// Load news content
fetchNewsContent();
// Show the modal
const modal = document.getElementById('newsModal');
modal.style.display = 'flex';
modal.classList.add('visible');
}
/**
* Hide news modal
*/
function hideNewsModal() {
const modal = document.getElementById('newsModal');
modal.classList.remove('visible');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
// Save "don't show again" preference if checked
const dontShowAgain = document.getElementById('dontShowAgain').checked;
if (dontShowAgain) {
saveNewsPreference();
}
}
/**
* Save news preference
*/
function saveNewsPreference() {
try {
// Get current news hash to identify if there are new updates
fetch('news.md')
.then(response => response.text())
.then(content => {
// Create a simple hash of the content
const newsHash = hashString(content);
localStorage.setItem('newsLastShown', newsHash);
console.log('Saved news preference with hash:', newsHash);
})
.catch(error => {
console.error('Error saving news preference:', error);
});
} catch (error) {
console.error('Error saving news preference:', error);
}
}
/**
* Check if news should be shown
*/
function checkShouldShowNews() {
try {
// If user has never seen news or there are new updates, show news
fetch('news.md')
.then(response => response.text())
.then(content => {
// Create a simple hash of the content
const currentNewsHash = hashString(content);
const lastShownHash = localStorage.getItem('newsLastShown');
console.log('News check:', { currentNewsHash, lastShownHash });
if (!lastShownHash || currentNewsHash !== lastShownHash) {
// Show news dialog
showNewsModal();
}
})
.catch(error => {
console.error('Error checking news:', error);
});
} catch (error) {
console.error('Error checking news status:', error);
}
}
/**
* Create a simple hash of a string
* @param {string} str - The string to hash
* @returns {string} - A simple hash of the string
*/
function hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return hash.toString(16);
}
/**
* Fetch and render news content
*/
function fetchNewsContent() {
const newsContent = document.getElementById('newsContent');
newsContent.innerHTML = '<p>Loading news...</p>';
fetch('news.md')
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch news.md: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then(content => {
// Convert markdown to HTML
const html = simpleMarkdownToHtml(content);
newsContent.innerHTML = html;
})
.catch(error => {
console.error('Error fetching news:', error);
newsContent.innerHTML = `<p>Error loading news: ${error.message}</p>`;
});
}
/* ================= INITIALIZATION ================= */
/**
* Initialize the application
*/
function initialize() {
// Initialize state values
state.customThemeColors = JSON.parse(localStorage.getItem('customThemeColors') || '{}');
// Define default theme colors
window.defaultThemeColors = {
light: { primary: '#006A6A' },
dark: { primary: '#4CDADA' },
sepia: { primary: '#8B5000' }
};
// Load data from localStorage
loadFromLocalStorage();
// Attach all event listeners including new ones
attachAllEventListeners();
// Create first note if none exist
if (state.notes.length === 0) {
createNewNote('Welcome to MNotes', 'Start typing to create your first note!');
} else {
renderNoteList();
// Open the most recently updated note
const mostRecentNote = [...state.notes].sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt);
})[0];
openNote(mostRecentNote.id);
}
// Set initial UI state
updateSidebarVisibility();
updateThemeUI();
applyCustomThemeColors();
// Check for news
setTimeout(checkShouldShowNews, 1000);
// Fetch version information
fetchVersionFromReadme();
}
/**
* Attach all event listeners
* Extends attachEventListeners to include new UI elements
*/
function attachAllEventListeners() {
// Call the original attachEventListeners function
attachEventListeners();
// Add event listeners for new components
// About dialog
const aboutBtn = document.getElementById('aboutBtn');
const closeAbout = document.getElementById('closeAbout');
aboutBtn.addEventListener('click', showAboutDialog);
closeAbout.addEventListener('click', hideAboutDialog);
// News modal
const closeNews = document.getElementById('closeNews');
closeNews.addEventListener('click', hideNewsModal);
// Theme customization
const customizeThemeBtn = document.getElementById('customizeThemeBtn');
const saveThemeCustomizationBtn = document.getElementById('saveThemeCustomization');
const cancelThemeCustomizationBtn = document.getElementById('cancelThemeCustomization');
const resetThemeColorsBtn = document.getElementById('resetThemeColors');
const themeTabs = document.querySelectorAll('.theme-tab');
const primaryColorPicker = document.getElementById('primaryColorPicker');
const primaryColorHex = document.getElementById('primaryColorHex');
customizeThemeBtn.addEventListener('click', showThemeCustomizationModal);
saveThemeCustomizationBtn.addEventListener('click', saveThemeCustomization);
cancelThemeCustomizationBtn.addEventListener('click', () => {
const modal = document.getElementById('themeCustomizationModal');
modal.classList.remove('visible');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
});
resetThemeColorsBtn.addEventListener('click', resetThemeColors);
// Theme tabs
themeTabs.forEach(tab => {
tab.addEventListener('click', () => {
// Update active tab
themeTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update color picker
const themeId = tab.dataset.themeTab;
const themeColor = state.customThemeColors[themeId]?.primary ||
defaultThemeColors[themeId]?.primary ||
'#000000';
primaryColorPicker.value = themeColor;
primaryColorHex.value = themeColor;
});
});
// Sync color picker with hex input
primaryColorPicker.addEventListener('input', () => {
primaryColorHex.value = primaryColorPicker.value;
});
primaryColorHex.addEventListener('input', () => {
// Check if valid hex color
if (/^#[0-9A-F]{6}$/i.test(primaryColorHex.value)) {
primaryColorPicker.value = primaryColorHex.value;
}
});
}
// Initialize the application
initialize();
});
</script>
</body>
</html>