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>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<span class="file-name">{{ currentFile || 'No file open' }}</span>
|
<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>
|
<span class="file-meta" v-if="fileMeta">{{ fileMeta }}</span>
|
||||||
<button v-if="currentFile" class="toolbar-btn" @click="toggleCollab" :title="collabActive ? 'Disconnect collab' : 'Enable collab'">
|
<button v-if="currentFile" class="toolbar-btn" @click="toggleCollab" :title="collabActive ? 'Disconnect collab' : 'Enable collab'">
|
||||||
{{ collabActive ? '👥 Live' : '👤 Solo' }}
|
{{ collabActive ? '👥 Live' : '👤 Solo' }}
|
||||||
@@ -99,7 +100,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
ref="editor"
|
ref="editor"
|
||||||
v-model="content"
|
v-model="content"
|
||||||
@input="isDirty = true"
|
@input="onEdit"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
@drop.prevent="handleImageDrop"
|
@drop.prevent="handleImageDrop"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
<MilkdownEditor
|
<MilkdownEditor
|
||||||
:key="currentFile"
|
:key="currentFile"
|
||||||
v-model="content"
|
v-model="content"
|
||||||
@update:modelValue="isDirty = true"
|
@update:modelValue="onEdit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === 'split'" class="preview-pane">
|
<div v-if="mode === 'split'" class="preview-pane">
|
||||||
@@ -514,14 +515,53 @@ async function openFile(path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
isDirty.value = false
|
isDirty.value = false
|
||||||
|
draftSaved.value = false
|
||||||
view.value = 'files'
|
view.value = 'files'
|
||||||
showHistory.value = false
|
showHistory.value = false
|
||||||
showShareDialog.value = false
|
showShareDialog.value = false
|
||||||
aiResult.value = ''
|
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()
|
loadHistory()
|
||||||
checkGitStatus()
|
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() {
|
async function saveFile() {
|
||||||
if (!currentFile.value) {
|
if (!currentFile.value) {
|
||||||
const name = prompt('Save as (e.g. notes.md):')
|
const name = prompt('Save as (e.g. notes.md):')
|
||||||
@@ -551,6 +591,7 @@ async function saveFile() {
|
|||||||
}
|
}
|
||||||
cacheFile(currentFile.value, content.value)
|
cacheFile(currentFile.value, content.value)
|
||||||
isDirty.value = false
|
isDirty.value = false
|
||||||
|
clearDraft()
|
||||||
setTimeout(checkGitStatus, 1000)
|
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; }
|
.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 { display: flex; gap: 4px; }
|
||||||
.export-actions button {
|
.export-actions button {
|
||||||
|
|||||||
Reference in New Issue
Block a user