|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import Utils from "./utils.js";
|
|
|
import DB from "./db.js";
|
|
|
import Editor from "./editor.js";
|
|
|
import Preview from "./preview.js";
|
|
|
import Documents from "./documents.js";
|
|
|
import AITools from "./ai-tools.js";
|
|
|
import Export from "./export.js";
|
|
|
import Templates from "./templates.js";
|
|
|
|
|
|
class App {
|
|
|
constructor() {
|
|
|
this.currentDoc = null;
|
|
|
this.currentDocId = null;
|
|
|
this.unsavedChanges = false;
|
|
|
|
|
|
|
|
|
this.editor = Editor;
|
|
|
this.preview = Preview;
|
|
|
this.documents = Documents;
|
|
|
this.aiTools = AITools;
|
|
|
this.export = Export;
|
|
|
this.templates = Templates;
|
|
|
}
|
|
|
|
|
|
async init() {
|
|
|
console.log("π Elysia Markdown Studio v1.3.0 initializing...");
|
|
|
|
|
|
|
|
|
this.editor.init();
|
|
|
this.preview.init();
|
|
|
this.documents.init();
|
|
|
this.aiTools.init();
|
|
|
this.export.init();
|
|
|
this.templates.init();
|
|
|
|
|
|
|
|
|
this.loadSettings();
|
|
|
|
|
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
|
|
|
|
this.showWelcome();
|
|
|
|
|
|
|
|
|
const autoSave = Utils.storage.get("autoSave", true);
|
|
|
if (autoSave) {
|
|
|
this.editor.startAutoSave();
|
|
|
}
|
|
|
|
|
|
console.log("β¨ Elysia Markdown Studio ready!");
|
|
|
}
|
|
|
|
|
|
setupEventListeners() {
|
|
|
|
|
|
document.getElementById("btn-new-doc").addEventListener("click", () => {
|
|
|
this.templates.showTemplatesModal();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById("btn-save").addEventListener("click", () => {
|
|
|
this.saveDocument();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById("btn-settings").addEventListener("click", () => {
|
|
|
Utils.modal.open("modal-settings");
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById("btn-save-settings").addEventListener("click", () => {
|
|
|
this.saveSettings();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById("doc-title").addEventListener("input", e => {
|
|
|
if (this.currentDoc) {
|
|
|
this.currentDoc.title = e.target.value || "Untitled Document";
|
|
|
this.unsavedChanges = true;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
const mobileToggle = document.getElementById("mobile-view-toggle");
|
|
|
if (mobileToggle) {
|
|
|
mobileToggle.addEventListener("click", () => {
|
|
|
this.toggleMobileView();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
document.addEventListener("keydown", e => {
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
|
if (e.key === "s") {
|
|
|
e.preventDefault();
|
|
|
this.saveDocument();
|
|
|
} else if (e.key === "n") {
|
|
|
e.preventDefault();
|
|
|
this.templates.showTemplatesModal();
|
|
|
} else if (e.key === "/") {
|
|
|
e.preventDefault();
|
|
|
this.showKeyboardShortcuts();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
window.addEventListener("beforeunload", e => {
|
|
|
if (this.unsavedChanges) {
|
|
|
e.preventDefault();
|
|
|
e.returnValue = "";
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async newDocument(templateName = null) {
|
|
|
if (this.unsavedChanges) {
|
|
|
const save = confirm("Save current document before creating new one?");
|
|
|
if (save) {
|
|
|
await this.saveDocument();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
let content = "";
|
|
|
if (templateName) {
|
|
|
const template = await this.templates.getTemplate(templateName);
|
|
|
content = template?.content || "";
|
|
|
}
|
|
|
|
|
|
|
|
|
this.currentDoc = {
|
|
|
id: Utils.uuid(),
|
|
|
title: "Untitled Document",
|
|
|
content,
|
|
|
tags: [],
|
|
|
favorite: false,
|
|
|
createdAt: Date.now(),
|
|
|
updatedAt: Date.now(),
|
|
|
wordCount: 0,
|
|
|
charCount: 0
|
|
|
};
|
|
|
|
|
|
this.currentDocId = null;
|
|
|
this.unsavedChanges = false;
|
|
|
|
|
|
|
|
|
document.getElementById("doc-title").value = this.currentDoc.title;
|
|
|
this.editor.setContent(content);
|
|
|
this.editor.currentDoc = this.currentDoc;
|
|
|
|
|
|
Utils.toast.info("New document created");
|
|
|
}
|
|
|
|
|
|
async loadDocument(id) {
|
|
|
if (this.unsavedChanges) {
|
|
|
const save = confirm("Save current document before loading another?");
|
|
|
if (save) {
|
|
|
await this.saveDocument();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const doc = await DB.getDocument(id);
|
|
|
if (!doc) {
|
|
|
Utils.toast.error("Document not found");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.currentDoc = doc;
|
|
|
this.currentDocId = id;
|
|
|
this.unsavedChanges = false;
|
|
|
|
|
|
|
|
|
document.getElementById("doc-title").value = doc.title;
|
|
|
this.editor.setContent(doc.content);
|
|
|
this.editor.currentDoc = doc;
|
|
|
|
|
|
|
|
|
this.documents.updateActiveDoc(id);
|
|
|
|
|
|
Utils.toast.success(`Loaded: ${doc.title}`);
|
|
|
}
|
|
|
|
|
|
async saveDocument(silent = false) {
|
|
|
const content = this.editor.getContent();
|
|
|
const title = document.getElementById("doc-title").value || "Untitled Document";
|
|
|
|
|
|
if (!content && !silent) {
|
|
|
Utils.toast.warning("Document is empty. Add some content before saving!");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
if (this.currentDocId) {
|
|
|
|
|
|
await DB.updateDocument(this.currentDocId, {
|
|
|
title,
|
|
|
content,
|
|
|
updatedAt: Date.now()
|
|
|
});
|
|
|
|
|
|
if (!silent) Utils.toast.success("Document saved!");
|
|
|
} else {
|
|
|
|
|
|
const existingDocs = await DB.getAllDocuments();
|
|
|
const duplicate = existingDocs.find(
|
|
|
doc => doc.title.toLowerCase() === title.toLowerCase() && doc.content === content
|
|
|
);
|
|
|
|
|
|
if (duplicate && !silent) {
|
|
|
const shouldCreate = confirm(
|
|
|
`A document with the title "${title}" and identical content already exists.\n\n` +
|
|
|
"Do you want to create it anyway?"
|
|
|
);
|
|
|
if (!shouldCreate) return;
|
|
|
}
|
|
|
|
|
|
|
|
|
const doc = await DB.createDocument({
|
|
|
title,
|
|
|
content
|
|
|
});
|
|
|
|
|
|
this.currentDocId = doc.id;
|
|
|
this.currentDoc = doc;
|
|
|
|
|
|
if (!silent) Utils.toast.success("Document created and saved!");
|
|
|
}
|
|
|
|
|
|
this.unsavedChanges = false;
|
|
|
|
|
|
|
|
|
this.documents.loadDocuments();
|
|
|
} catch (err) {
|
|
|
console.error("Save failed:", err);
|
|
|
Utils.toast.error("Failed to save document");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
loadSettings() {
|
|
|
const apiKey = Utils.storage.get("apiKey");
|
|
|
const model = Utils.storage.get("model", "anthropic/claude-sonnet-4.5");
|
|
|
const previewTheme = Utils.storage.get("previewTheme", "elysia");
|
|
|
const autoSave = Utils.storage.get("autoSave", true);
|
|
|
const livePreview = Utils.storage.get("livePreview", true);
|
|
|
|
|
|
|
|
|
if (document.getElementById("api-key")) {
|
|
|
document.getElementById("api-key").value = apiKey || "";
|
|
|
}
|
|
|
if (document.getElementById("model-select")) {
|
|
|
document.getElementById("model-select").value = model;
|
|
|
}
|
|
|
if (document.getElementById("preview-theme")) {
|
|
|
document.getElementById("preview-theme").value = previewTheme;
|
|
|
}
|
|
|
if (document.getElementById("auto-save")) {
|
|
|
document.getElementById("auto-save").checked = autoSave;
|
|
|
}
|
|
|
if (document.getElementById("live-preview")) {
|
|
|
document.getElementById("live-preview").checked = livePreview;
|
|
|
}
|
|
|
|
|
|
|
|
|
this.preview.setTheme(previewTheme);
|
|
|
}
|
|
|
|
|
|
saveSettings() {
|
|
|
const apiKey = document.getElementById("api-key").value.trim();
|
|
|
const model = document.getElementById("model-select").value;
|
|
|
const previewTheme = document.getElementById("preview-theme").value;
|
|
|
const autoSave = document.getElementById("auto-save").checked;
|
|
|
const livePreview = document.getElementById("live-preview").checked;
|
|
|
|
|
|
|
|
|
if (apiKey && !apiKey.startsWith("sk-or-")) {
|
|
|
Utils.toast.warning("API key should start with 'sk-or-'. Please check your key.");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
Utils.storage.set("apiKey", apiKey);
|
|
|
Utils.storage.set("model", model);
|
|
|
Utils.storage.set("previewTheme", previewTheme);
|
|
|
Utils.storage.set("autoSave", autoSave);
|
|
|
Utils.storage.set("livePreview", livePreview);
|
|
|
|
|
|
|
|
|
this.preview.setTheme(previewTheme);
|
|
|
|
|
|
|
|
|
if (autoSave) {
|
|
|
this.editor.startAutoSave();
|
|
|
} else {
|
|
|
this.editor.stopAutoSave();
|
|
|
}
|
|
|
|
|
|
|
|
|
if (livePreview) {
|
|
|
this.preview.update();
|
|
|
}
|
|
|
|
|
|
Utils.toast.success("Settings saved!");
|
|
|
Utils.modal.close("modal-settings");
|
|
|
}
|
|
|
|
|
|
|
|
|
toggleMobileView() {
|
|
|
const previewPane = document.querySelector(".preview-pane");
|
|
|
const editorPane = document.querySelector(".editor-pane");
|
|
|
const toggleBtn = document.getElementById("mobile-view-toggle");
|
|
|
const toggleIcon = toggleBtn?.querySelector(".toggle-icon");
|
|
|
const toggleText = toggleBtn?.querySelector(".toggle-text");
|
|
|
|
|
|
if (!previewPane || !editorPane) return;
|
|
|
|
|
|
const isPreviewActive = previewPane.classList.contains("active");
|
|
|
|
|
|
if (isPreviewActive) {
|
|
|
|
|
|
previewPane.classList.remove("active");
|
|
|
editorPane.style.display = "block";
|
|
|
if (toggleIcon) toggleIcon.textContent = "ποΈ";
|
|
|
if (toggleText) toggleText.textContent = "Preview";
|
|
|
} else {
|
|
|
|
|
|
this.preview.update();
|
|
|
previewPane.classList.add("active");
|
|
|
editorPane.style.display = "none";
|
|
|
if (toggleIcon) toggleIcon.textContent = "βοΈ";
|
|
|
if (toggleText) toggleText.textContent = "Editor";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
showWelcome() {
|
|
|
const welcomeMessage = `# Welcome to Elysia Markdown Studio π
|
|
|
|
|
|
Your AI-powered writing companion is ready!
|
|
|
|
|
|
## β¨ Features
|
|
|
|
|
|
- **Live Preview** - See your markdown rendered in real-time
|
|
|
- **AI Tools** - Summarize, improve, merge documents with Elysia's intelligence
|
|
|
- **Rich Markdown** - Support for tables, math (KaTeX), diagrams (Mermaid), code highlighting
|
|
|
- **Smart Export** - Export to Markdown, HTML, Artifact, JSON, Plain Text
|
|
|
- **Document Management** - Organize with tags, favorites, collections
|
|
|
- **Templates** - Quick start with README, Blog, Meeting Notes, and more!
|
|
|
|
|
|
## π Quick Start
|
|
|
|
|
|
1. Click **βοΈ Settings** to add your OpenRouter API key (for AI features)
|
|
|
2. Start writing in the left pane
|
|
|
3. See live preview on the right
|
|
|
4. Use toolbar for quick formatting
|
|
|
5. Save with **πΎ** or Ctrl+S
|
|
|
6. Access AI tools with **π§ **
|
|
|
|
|
|
## β¨οΈ Keyboard Shortcuts
|
|
|
|
|
|
| Shortcut | Action |
|
|
|
|----------|--------|
|
|
|
| **Ctrl+S** | Save document |
|
|
|
| **Ctrl+N** | New document |
|
|
|
| **Ctrl+B** | Bold text |
|
|
|
| **Ctrl+I** | Italic text |
|
|
|
| **Ctrl+/** | Show all shortcuts |
|
|
|
|
|
|
Press **Ctrl+/** anytime to see the full shortcuts guide!
|
|
|
|
|
|
## π‘ Tips
|
|
|
|
|
|
- Right-click documents in the sidebar for quick actions (rename, delete, favorite)
|
|
|
- AI tools work best with content > 100 words
|
|
|
- Auto-save runs every 30 seconds (configurable in Settings)
|
|
|
- Export to "Artifact" format for beautiful standalone HTML pages
|
|
|
|
|
|
Start writing your masterpiece! Delete this text and create something amazing! π
|
|
|
|
|
|
---
|
|
|
|
|
|
*Built with love by Jean & Elysia* ππ`;
|
|
|
|
|
|
this.editor.setContent(welcomeMessage);
|
|
|
this.currentDoc = {
|
|
|
title: "Welcome to Elysia Markdown Studio",
|
|
|
content: welcomeMessage
|
|
|
};
|
|
|
|
|
|
document.getElementById("doc-title").value = this.currentDoc.title;
|
|
|
}
|
|
|
|
|
|
showKeyboardShortcuts() {
|
|
|
const shortcuts = `# β¨οΈ Keyboard Shortcuts - Elysia Markdown Studio
|
|
|
|
|
|
## Document Management
|
|
|
- **Ctrl+S** - Save current document
|
|
|
- **Ctrl+N** - Create new document
|
|
|
- **Ctrl+/** - Show this shortcuts guide
|
|
|
|
|
|
## Text Formatting
|
|
|
- **Ctrl+B** - **Bold** text
|
|
|
- **Ctrl+I** - *Italic* text
|
|
|
|
|
|
## Navigation
|
|
|
- **Tab** - Indent / Next field
|
|
|
- **Shift+Tab** - Outdent / Previous field
|
|
|
- **Ctrl+F** - Search in document (browser default)
|
|
|
|
|
|
## Pro Tips π‘
|
|
|
- Right-click documents for context menu
|
|
|
- Use toolbar buttons for advanced formatting (tables, links, images)
|
|
|
- Drag & drop images into editor (coming soon!)
|
|
|
|
|
|
---
|
|
|
|
|
|
Press **Esc** to close this guide and continue writing!`;
|
|
|
|
|
|
|
|
|
const modal = document.createElement("div");
|
|
|
modal.className = "modal active";
|
|
|
modal.id = "modal-shortcuts";
|
|
|
modal.innerHTML = `
|
|
|
<div class="modal-content">
|
|
|
<div class="modal-header">
|
|
|
<h2>β¨οΈ Keyboard Shortcuts</h2>
|
|
|
<button class="modal-close" onclick="this.closest('.modal').remove()">Γ</button>
|
|
|
</div>
|
|
|
<div class="modal-body">
|
|
|
<div class="markdown-preview">${marked.parse(shortcuts)}</div>
|
|
|
</div>
|
|
|
<div class="modal-footer">
|
|
|
<button class="btn-primary" onclick="this.closest('.modal').remove()">Got it!</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
|
|
|
const handleEsc = e => {
|
|
|
if (e.key === "Escape") {
|
|
|
modal.remove();
|
|
|
document.removeEventListener("keydown", handleEsc);
|
|
|
}
|
|
|
};
|
|
|
document.addEventListener("keydown", handleEsc);
|
|
|
|
|
|
|
|
|
modal.addEventListener("click", e => {
|
|
|
if (e.target === modal) {
|
|
|
modal.remove();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
const app = new App();
|
|
|
window.app = app;
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
app.init();
|
|
|
});
|
|
|
|
|
|
export default app;
|
|
|
|