Auto-save drafts to localStorage
- Drafts saved 1s after last keystroke (debounced) - 'Draft saved' indicator in toolbar - On file open: prompts to restore unsaved draft if found - Draft cleared on successful save (Ctrl+S) - Works across disconnects/logouts — draft persists in browser
This commit is contained in:
+44
-2
@@ -74,6 +74,7 @@
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<span class="file-name">{{ currentFile || 'No file open' }}</span>
|
||||
<span v-if="draftSaved" class="draft-indicator">Draft saved</span>
|
||||
<span class="file-meta" v-if="fileMeta">{{ fileMeta }}</span>
|
||||
<button v-if="currentFile" class="toolbar-btn" @click="toggleCollab" :title="collabActive ? 'Disconnect collab' : 'Enable collab'">
|
||||
{{ collabActive ? '👥 Live' : '👤 Solo' }}
|
||||
@@ -99,7 +100,7 @@
|
||||
<textarea
|
||||
ref="editor"
|
||||
v-model="content"
|
||||
@input="isDirty = true"
|
||||
@input="onEdit"
|
||||
@keydown="handleKeydown"
|
||||
@drop.prevent="handleImageDrop"
|
||||
@dragover.prevent
|
||||
@@ -110,7 +111,7 @@
|
||||
<MilkdownEditor
|
||||
:key="currentFile"
|
||||
v-model="content"
|
||||
@update:modelValue="isDirty = true"
|
||||
@update:modelValue="onEdit"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mode === 'split'" class="preview-pane">
|
||||
@@ -514,14 +515,53 @@ async function openFile(path) {
|
||||
}
|
||||
}
|
||||
isDirty.value = false
|
||||
draftSaved.value = false
|
||||
view.value = 'files'
|
||||
showHistory.value = false
|
||||
showShareDialog.value = false
|
||||
aiResult.value = ''
|
||||
|
||||
// Check for unsaved draft
|
||||
const draft = loadDraft()
|
||||
if (draft && draft !== content.value) {
|
||||
if (confirm('You have an unsaved draft for this file. Restore it?')) {
|
||||
content.value = draft
|
||||
isDirty.value = true
|
||||
} else {
|
||||
clearDraft()
|
||||
}
|
||||
}
|
||||
loadHistory()
|
||||
checkGitStatus()
|
||||
}
|
||||
|
||||
// ─── Auto-draft ──────────────────────────────────────────────────────────────
|
||||
|
||||
const draftSaved = ref(false)
|
||||
let draftTimer = null
|
||||
|
||||
function onEdit() {
|
||||
isDirty.value = true
|
||||
draftSaved.value = false
|
||||
clearTimeout(draftTimer)
|
||||
draftTimer = setTimeout(() => {
|
||||
const key = currentFile.value || '_unsaved'
|
||||
localStorage.setItem('mh_draft_' + key, content.value)
|
||||
draftSaved.value = true
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function clearDraft() {
|
||||
const key = currentFile.value || '_unsaved'
|
||||
localStorage.removeItem('mh_draft_' + key)
|
||||
draftSaved.value = false
|
||||
}
|
||||
|
||||
function loadDraft() {
|
||||
const key = currentFile.value || '_unsaved'
|
||||
return localStorage.getItem('mh_draft_' + key)
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if (!currentFile.value) {
|
||||
const name = prompt('Save as (e.g. notes.md):')
|
||||
@@ -551,6 +591,7 @@ async function saveFile() {
|
||||
}
|
||||
cacheFile(currentFile.value, content.value)
|
||||
isDirty.value = false
|
||||
clearDraft()
|
||||
setTimeout(checkGitStatus, 1000)
|
||||
}
|
||||
|
||||
@@ -1200,6 +1241,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
||||
}
|
||||
|
||||
.file-name { color: var(--text-muted); font-size: 13px; }
|
||||
.draft-indicator { color: var(--success); font-size: 11px; opacity: 0.8; }
|
||||
|
||||
.export-actions { display: flex; gap: 4px; }
|
||||
.export-actions button {
|
||||
|
||||
Reference in New Issue
Block a user