Merge dev: collapsible settings and resizable notes list (v0.1.2)

This commit is contained in:
drelich
2026-03-21 08:45:45 +01:00
6 changed files with 161 additions and 84 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "nextcloud-notes-tauri", "name": "nextcloud-notes-tauri",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Nextcloud Notes", "productName": "Nextcloud Notes",
"version": "0.1.0", "version": "0.1.1",
"identifier": "com.davidrelich.nextcloud-notes", "identifier": "com.davidrelich.nextcloud-notes",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -77,7 +77,7 @@ export class NextcloudAPI {
webdavPath += `/${path}`; webdavPath += `/${path}`;
const url = `${this.serverURL}${webdavPath}`; const url = `${this.serverURL}${webdavPath}`;
console.log('Fetching attachment via WebDAV:', url); console.log(`[Note ${_noteId}] Fetching attachment via WebDAV:`, url);
const response = await tauriFetch(url, { const response = await tauriFetch(url, {
headers: { headers: {

View File

@@ -58,6 +58,7 @@ export function CategoriesSidebar({
}: CategoriesSidebarProps) { }: CategoriesSidebarProps) {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [newCategoryName, setNewCategoryName] = useState(''); const [newCategoryName, setNewCategoryName] = useState('');
const [isSettingsCollapsed, setIsSettingsCollapsed] = useState(true);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@@ -185,14 +186,24 @@ export function CategoriesSidebar({
</div> </div>
{/* User Info and Settings */} {/* User Info and Settings */}
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-white dark:bg-gray-900"> <div className="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between p-4 pb-3">
<div className="flex items-center space-x-2 min-w-0"> <div className="flex items-center space-x-2 min-w-0">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0"> <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0">
{username.charAt(0).toUpperCase()} {username.charAt(0).toUpperCase()}
</div> </div>
<span className="text-sm text-gray-700 dark:text-gray-200 truncate font-medium">{username}</span> <span className="text-sm text-gray-700 dark:text-gray-200 truncate font-medium">{username}</span>
</div> </div>
<div className="flex items-center gap-1">
<button
onClick={() => setIsSettingsCollapsed(!isSettingsCollapsed)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
title={isSettingsCollapsed ? "Show Settings" : "Hide Settings"}
>
<svg className={`w-4 h-4 text-gray-600 dark:text-gray-300 transition-transform ${isSettingsCollapsed ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<button <button
onClick={onLogout} onClick={onLogout}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0" className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
@@ -203,7 +214,10 @@ export function CategoriesSidebar({
</svg> </svg>
</button> </button>
</div> </div>
</div>
{!isSettingsCollapsed && (
<div className="px-4 pb-4">
{/* Theme Toggle */} {/* Theme Toggle */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-500 dark:text-gray-400">Theme</span> <span className="text-xs text-gray-500 dark:text-gray-400">Theme</span>
@@ -322,6 +336,8 @@ export function CategoriesSidebar({
</div> </div>
</div> </div>
</div> </div>
)}
</div>
</div> </div>
); );
} }

View File

@@ -77,13 +77,23 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
return; return;
} }
// Guard: Only process if localContent has been updated for the current note
// This prevents processing stale content from the previous note
if (previousNoteIdRef.current !== note.id) {
console.log(`[Note ${note.id}] Skipping image processing - waiting for content to sync (previousNoteIdRef: ${previousNoteIdRef.current})`);
return;
}
const processImages = async () => { const processImages = async () => {
console.log(`[Note ${note.id}] Processing images in preview mode. Content length: ${localContent.length}`);
setIsLoadingImages(true); setIsLoadingImages(true);
setProcessedContent(''); // Clear old content immediately
// Find all image references in markdown: ![alt](path) // Find all image references in markdown: ![alt](path)
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
let content = localContent; let content = localContent;
const matches = [...localContent.matchAll(imageRegex)]; const matches = [...localContent.matchAll(imageRegex)];
console.log(`[Note ${note.id}] Found ${matches.length} images to process`);
for (const match of matches) { for (const match of matches) {
const [fullMatch, alt, imagePath] = match; const [fullMatch, alt, imagePath] = match;
@@ -121,11 +131,13 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
useEffect(() => { useEffect(() => {
const loadNewNote = () => { const loadNewNote = () => {
if (note) { if (note) {
console.log(`[Note ${note.id}] Loading note. Title: "${note.title}", Content length: ${note.content.length}`);
setLocalTitle(note.title); setLocalTitle(note.title);
setLocalContent(note.content); setLocalContent(note.content);
setLocalCategory(note.category || ''); setLocalCategory(note.category || '');
setLocalFavorite(note.favorite); setLocalFavorite(note.favorite);
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
setProcessedContent(''); // Clear preview content immediately
const firstLine = note.content.split('\n')[0].replace(/^#+\s*/, '').trim(); const firstLine = note.content.split('\n')[0].replace(/^#+\s*/, '').trim();
const titleMatchesFirstLine = note.title === firstLine || note.title === firstLine.substring(0, 50); const titleMatchesFirstLine = note.title === firstLine || note.title === firstLine.substring(0, 50);
@@ -136,10 +148,13 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
}; };
if (previousNoteIdRef.current !== null && previousNoteIdRef.current !== note?.id) { if (previousNoteIdRef.current !== null && previousNoteIdRef.current !== note?.id) {
console.log(`Switching from note ${previousNoteIdRef.current} to note ${note?.id}`);
// Clear preview content immediately when switching notes
setProcessedContent('');
if (hasUnsavedChanges) { if (hasUnsavedChanges) {
handleSave(); handleSave();
} }
setTimeout(loadNewNote, 100); loadNewNote();
} else { } else {
loadNewNote(); loadNewNote();
} }

View File

@@ -30,6 +30,9 @@ export function NotesList({
}: NotesListProps) { }: NotesListProps) {
const [isSyncing, setIsSyncing] = React.useState(false); const [isSyncing, setIsSyncing] = React.useState(false);
const [deleteClickedId, setDeleteClickedId] = React.useState<number | null>(null); const [deleteClickedId, setDeleteClickedId] = React.useState<number | null>(null);
const [width, setWidth] = React.useState(320);
const [isResizing, setIsResizing] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
const handleSync = async () => { const handleSync = async () => {
setIsSyncing(true); setIsSyncing(true);
@@ -37,6 +40,34 @@ export function NotesList({
setTimeout(() => setIsSyncing(false), 500); setTimeout(() => setIsSyncing(false), 500);
}; };
React.useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing) return;
const newWidth = e.clientX - (containerRef.current?.getBoundingClientRect().left || 0);
if (newWidth >= 240 && newWidth <= 600) {
setWidth(newWidth);
}
};
const handleMouseUp = () => {
setIsResizing(false);
};
if (isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isResizing]);
const handleDeleteClick = (note: Note, e: React.MouseEvent) => { const handleDeleteClick = (note: Note, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
@@ -79,7 +110,11 @@ export function NotesList({
}; };
return ( return (
<div className="w-80 bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col"> <div
ref={containerRef}
className="bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col relative"
style={{ width: `${width}px`, minWidth: '240px', maxWidth: '600px' }}
>
<div className="p-4 border-b border-gray-200 dark:border-gray-700"> <div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Notes</h2> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Notes</h2>
@@ -207,6 +242,17 @@ export function NotesList({
)) ))
)} )}
</div> </div>
{/* Resize Handle */}
<div
className="absolute top-0 right-0 w-1 h-full cursor-ew-resize hover:bg-blue-500 transition-colors group"
onMouseDown={(e) => {
e.preventDefault();
setIsResizing(true);
}}
>
<div className="absolute inset-y-0 -right-1 w-3" />
</div>
</div> </div>
); );
} }