feat: WebDAV file access and category color sync (v0.2.0)

Major Changes:
- Switch from Nextcloud Notes API to direct WebDAV file access
- Notes stored as .txt files with filename-based IDs for reliability
- Implement safer sync strategy without clearNotes() to prevent data loss
- Add ETag-based conflict detection for concurrent edits
- Add category color sync to .category-colors.json on server
- Show neutral gray badges for categories without assigned colors

Technical Improvements:
- Replace numeric IDs with filename-based string IDs
- Update Note type to support both number and string IDs
- Implement WebDAV methods: fetchNotesWebDAV, createNoteWebDAV, updateNoteWebDAV, deleteNoteWebDAV
- Add CategoryColorsSync service for server synchronization
- Remove hash-based color fallback (only show colors when explicitly set)

Bug Fixes:
- Fix category badge rendering to show all categories
- Prevent note loss during sync operations
- Improve offline-first functionality with better merge strategy
This commit is contained in:
drelich
2026-03-25 20:12:00 +01:00
parent 861eb1e103
commit 5a925dc50e
11 changed files with 490 additions and 97 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { categoryColorsSync } from '../services/categoryColorsSync';
const EDITOR_FONTS = [
{ name: 'Source Code Pro', value: 'Source Code Pro' },
@@ -75,32 +76,28 @@ export function CategoriesSidebar({
const [newCategoryName, setNewCategoryName] = useState('');
const [renamingCategory, setRenamingCategory] = useState<string | null>(null);
const [renameCategoryValue, setRenameCategoryValue] = useState('');
const [categoryColors, setCategoryColors] = useState<Record<string, number>>({});
const [categoryColors, setCategoryColors] = useState<Record<string, number>>(() => categoryColorsSync.getAllColors());
const [colorPickerCategory, setColorPickerCategory] = useState<string | null>(null);
const [isSettingsCollapsed, setIsSettingsCollapsed] = useState(true);
const inputRef = useRef<HTMLInputElement>(null);
const renameInputRef = useRef<HTMLInputElement>(null);
// Load category colors from localStorage
useEffect(() => {
const saved = localStorage.getItem('categoryColors');
if (saved) {
setCategoryColors(JSON.parse(saved));
}
const handleColorChange = () => {
setCategoryColors(categoryColorsSync.getAllColors());
};
categoryColorsSync.setChangeCallback(handleColorChange);
window.addEventListener('categoryColorChanged', handleColorChange);
return () => {
window.removeEventListener('categoryColorChanged', handleColorChange);
};
}, []);
const setCategoryColor = (category: string, colorIndex: number | null) => {
const updated = { ...categoryColors };
if (colorIndex === null) {
delete updated[category];
} else {
updated[category] = colorIndex;
}
setCategoryColors(updated);
localStorage.setItem('categoryColors', JSON.stringify(updated));
const setCategoryColor = async (category: string, colorIndex: number | null) => {
await categoryColorsSync.setColor(category, colorIndex);
setColorPickerCategory(null);
// Dispatch event to notify other components
window.dispatchEvent(new Event('categoryColorChanged'));
};
useEffect(() => {