mirror of
https://github.com/thejakubruzicka/MNotes.git
synced 2025-07-10 06:24:04 +02:00
2925 lines
95 KiB
HTML
2925 lines
95 KiB
HTML
<!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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
|
|
// 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>
|