Implement hybrid WebDAV + API favorite sync
- Keep WebDAV for reliable content sync - Add REST API calls for favorite status only - Match notes by modified timestamp + category (titles can differ) - Sync favorites from API after WebDAV sync completes - Update favorite via API when user toggles star - Tested and working with mobile app sync
This commit is contained in:
13
src/App.tsx
13
src/App.tsx
@@ -198,6 +198,18 @@ function App() {
|
||||
localStorage.setItem('previewFontSize', size.toString());
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async (note: Note, favorite: boolean) => {
|
||||
try {
|
||||
await syncManager.updateFavoriteStatus(note, favorite);
|
||||
// Update local state
|
||||
setNotes(prevNotes =>
|
||||
prevNotes.map(n => n.id === note.id ? { ...n, favorite } : n)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Toggle favorite failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNote = async () => {
|
||||
try {
|
||||
const timestamp = new Date().toLocaleString('en-US', {
|
||||
@@ -355,6 +367,7 @@ function App() {
|
||||
<NoteEditor
|
||||
note={selectedNote}
|
||||
onUpdateNote={handleUpdateNote}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onUnsavedChanges={setHasUnsavedChanges}
|
||||
categories={categories}
|
||||
isFocusMode={isFocusMode}
|
||||
|
||||
@@ -61,6 +61,51 @@ export class NextcloudAPI {
|
||||
await this.request<void>(`/notes/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Fetch lightweight note list with IDs and favorites for hybrid sync
|
||||
async fetchNotesMetadata(): Promise<Array<{id: number, title: string, category: string, favorite: boolean, modified: number}>> {
|
||||
const notes = await this.request<Note[]>('/notes');
|
||||
return notes.map(note => ({
|
||||
id: note.id as number,
|
||||
title: note.title,
|
||||
category: note.category,
|
||||
favorite: note.favorite,
|
||||
modified: note.modified,
|
||||
}));
|
||||
}
|
||||
|
||||
// Update only favorite status via API
|
||||
async updateFavoriteStatus(noteId: number, favorite: boolean): Promise<void> {
|
||||
await this.request<Note>(`/notes/${noteId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ favorite }),
|
||||
});
|
||||
}
|
||||
|
||||
// Map WebDAV note to API ID by matching modified timestamp and category
|
||||
// We can't use title because API title and WebDAV first-line title can differ
|
||||
async findApiIdForNote(title: string, category: string, modified: number): Promise<number | null> {
|
||||
try {
|
||||
const metadata = await this.fetchNotesMetadata();
|
||||
|
||||
// First try exact title + category match
|
||||
let match = metadata.find(note =>
|
||||
note.title === title && note.category === category
|
||||
);
|
||||
|
||||
// If no title match, try modified timestamp + category (more reliable)
|
||||
if (!match) {
|
||||
match = metadata.find(note =>
|
||||
note.modified === modified && note.category === category
|
||||
);
|
||||
}
|
||||
|
||||
return match ? match.id : null;
|
||||
} catch (error) {
|
||||
console.error('Failed to find API ID for note:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAttachment(_noteId: number | string, path: string, noteCategory?: string): Promise<string> {
|
||||
// Build WebDAV path: /remote.php/dav/files/{username}/Notes/{category}/.attachments.{noteId}/{filename}
|
||||
// The path from markdown is like: .attachments.38479/Screenshot.png
|
||||
|
||||
@@ -10,6 +10,7 @@ import { InsertToolbar } from './InsertToolbar';
|
||||
interface NoteEditorProps {
|
||||
note: Note | null;
|
||||
onUpdateNote: (note: Note) => void;
|
||||
onToggleFavorite?: (note: Note, favorite: boolean) => void;
|
||||
onUnsavedChanges?: (hasChanges: boolean) => void;
|
||||
categories: string[];
|
||||
isFocusMode?: boolean;
|
||||
@@ -24,7 +25,7 @@ interface NoteEditorProps {
|
||||
const imageCache = new Map<string, string>();
|
||||
|
||||
|
||||
export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, isFocusMode, onToggleFocusMode, editorFont = 'Source Code Pro', editorFontSize = 14, previewFont = 'Merriweather', previewFontSize = 16, api }: NoteEditorProps) {
|
||||
export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChanges, categories, isFocusMode, onToggleFocusMode, editorFont = 'Source Code Pro', editorFontSize = 14, previewFont = 'Merriweather', previewFontSize = 16, api }: NoteEditorProps) {
|
||||
const [localContent, setLocalContent] = useState('');
|
||||
const [localCategory, setLocalCategory] = useState('');
|
||||
const [localFavorite, setLocalFavorite] = useState(false);
|
||||
@@ -399,8 +400,14 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
||||
};
|
||||
|
||||
const handleFavoriteToggle = () => {
|
||||
setLocalFavorite(!localFavorite);
|
||||
if (note) {
|
||||
const newFavorite = !localFavorite;
|
||||
setLocalFavorite(newFavorite);
|
||||
|
||||
if (note && onToggleFavorite) {
|
||||
// Use dedicated favorite toggle callback if provided
|
||||
onToggleFavorite(note, newFavorite);
|
||||
} else if (note) {
|
||||
// Fallback to full update if no callback provided
|
||||
const firstLine = localContent.split('\n')[0].replace(/^#+\s*/, '').trim();
|
||||
const title = firstLine || 'Untitled';
|
||||
|
||||
@@ -409,7 +416,7 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
||||
title,
|
||||
content: localContent,
|
||||
category: localCategory,
|
||||
favorite: !localFavorite,
|
||||
favorite: newFavorite,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,6 +117,9 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync favorite status from API
|
||||
await this.syncFavoriteStatus();
|
||||
|
||||
this.notifyStatus('idle', 0);
|
||||
|
||||
// Notify that sync is complete so UI can reload
|
||||
@@ -131,6 +134,41 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync favorite status from API to local cache
|
||||
private async syncFavoriteStatus(): Promise<void> {
|
||||
if (!this.api) return;
|
||||
|
||||
try {
|
||||
console.log('Syncing favorite status from API...');
|
||||
const apiMetadata = await this.api.fetchNotesMetadata();
|
||||
const cachedNotes = await localDB.getAllNotes();
|
||||
|
||||
// Map API notes by title and category for matching
|
||||
const apiMap = new Map<string, {id: number, favorite: boolean}>();
|
||||
for (const apiNote of apiMetadata) {
|
||||
const key = `${apiNote.category}/${apiNote.title}`;
|
||||
apiMap.set(key, { id: apiNote.id, favorite: apiNote.favorite });
|
||||
}
|
||||
|
||||
// Update favorite status in cache for matching notes
|
||||
for (const cachedNote of cachedNotes) {
|
||||
const key = `${cachedNote.category}/${cachedNote.title}`;
|
||||
const apiData = apiMap.get(key);
|
||||
|
||||
if (apiData && cachedNote.favorite !== apiData.favorite) {
|
||||
console.log(`Updating favorite status for "${cachedNote.title}": ${cachedNote.favorite} -> ${apiData.favorite}`);
|
||||
cachedNote.favorite = apiData.favorite;
|
||||
await localDB.saveNote(cachedNote);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Favorite status sync complete');
|
||||
} catch (error) {
|
||||
console.error('Failed to sync favorite status:', error);
|
||||
// Don't throw - favorite sync is non-critical
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all notes and cache them
|
||||
private async fetchAndCacheNotes(): Promise<Note[]> {
|
||||
if (!this.api) throw new Error('API not initialized');
|
||||
@@ -197,6 +235,42 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Update favorite status via API
|
||||
async updateFavoriteStatus(note: Note, favorite: boolean): Promise<void> {
|
||||
if (!this.api) {
|
||||
throw new Error('API not initialized');
|
||||
}
|
||||
|
||||
if (!this.isOnline) {
|
||||
// Update locally, will sync when back online
|
||||
note.favorite = favorite;
|
||||
await localDB.saveNote(note);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find API ID for this note
|
||||
const apiId = await this.api.findApiIdForNote(note.title, note.category, note.modified);
|
||||
|
||||
if (apiId) {
|
||||
// Update via API
|
||||
await this.api.updateFavoriteStatus(apiId, favorite);
|
||||
console.log(`Updated favorite status for "${note.title}" (API ID: ${apiId})`);
|
||||
} else {
|
||||
console.warn(`Could not find API ID for note: "${note.title}"`);
|
||||
}
|
||||
|
||||
// Update local cache
|
||||
note.favorite = favorite;
|
||||
await localDB.saveNote(note);
|
||||
} catch (error) {
|
||||
console.error('Failed to update favorite status:', error);
|
||||
// Still update locally
|
||||
note.favorite = favorite;
|
||||
await localDB.saveNote(note);
|
||||
}
|
||||
}
|
||||
|
||||
// Update note on server and cache
|
||||
async updateNote(note: Note): Promise<Note> {
|
||||
if (!this.api) {
|
||||
|
||||
Reference in New Issue
Block a user