Merge dev: collapsible settings and resizable notes list (v0.1.2)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nextcloud-notes-tauri",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Nextcloud Notes",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"identifier": "com.davidrelich.nextcloud-notes",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -77,7 +77,7 @@ export class NextcloudAPI {
|
||||
webdavPath += `/${path}`;
|
||||
|
||||
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, {
|
||||
headers: {
|
||||
|
||||
@@ -58,6 +58,7 @@ export function CategoriesSidebar({
|
||||
}: CategoriesSidebarProps) {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newCategoryName, setNewCategoryName] = useState('');
|
||||
const [isSettingsCollapsed, setIsSettingsCollapsed] = useState(true);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -185,27 +186,40 @@ export function CategoriesSidebar({
|
||||
</div>
|
||||
|
||||
{/* 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="flex items-center justify-between mb-3">
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||
<div className="flex items-center justify-between p-4 pb-3">
|
||||
<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">
|
||||
{username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200 truncate font-medium">{username}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
|
||||
title="Logout"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
<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
|
||||
onClick={onLogout}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
|
||||
title="Logout"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
{!isSettingsCollapsed && (
|
||||
<div className="px-4 pb-4">
|
||||
{/* Theme Toggle */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Theme</span>
|
||||
<div className="flex items-center space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
|
||||
<button
|
||||
@@ -250,77 +264,79 @@ export function CategoriesSidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Font Settings */}
|
||||
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700 space-y-3">
|
||||
{/* Font Settings */}
|
||||
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700 space-y-3">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Fonts</span>
|
||||
|
||||
{/* Editor Font */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">Editor</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={editorFont}
|
||||
onChange={(e) => onEditorFontChange(e.target.value)}
|
||||
className="flex-1 min-w-0 text-sm px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
|
||||
style={{ fontFamily: editorFont }}
|
||||
>
|
||||
{EDITOR_FONTS.map((font) => (
|
||||
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>
|
||||
{font.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={editorFontSize}
|
||||
onChange={(e) => onEditorFontSizeChange(parseInt(e.target.value, 10))}
|
||||
className="w-16 flex-shrink-0 text-sm px-2 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer text-center"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 17, 18, 20, 22, 24].map((size) => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/* Editor Font */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">Editor</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={editorFont}
|
||||
onChange={(e) => onEditorFontChange(e.target.value)}
|
||||
className="flex-1 min-w-0 text-sm px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
|
||||
style={{ fontFamily: editorFont }}
|
||||
>
|
||||
{EDITOR_FONTS.map((font) => (
|
||||
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>
|
||||
{font.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={editorFontSize}
|
||||
onChange={(e) => onEditorFontSizeChange(parseInt(e.target.value, 10))}
|
||||
className="w-16 flex-shrink-0 text-sm px-2 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer text-center"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 17, 18, 20, 22, 24].map((size) => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Font */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">Preview</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={previewFont}
|
||||
onChange={(e) => onPreviewFontChange(e.target.value)}
|
||||
className="flex-1 min-w-0 text-sm px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
|
||||
style={{ fontFamily: previewFont }}
|
||||
>
|
||||
{PREVIEW_FONTS.map((font) => (
|
||||
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>
|
||||
{font.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={previewFontSize}
|
||||
onChange={(e) => onPreviewFontSizeChange(parseInt(e.target.value, 10))}
|
||||
className="w-16 flex-shrink-0 text-sm px-2 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer text-center"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 17, 18, 20, 22, 24].map((size) => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Preview Font */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">Preview</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={previewFont}
|
||||
onChange={(e) => onPreviewFontChange(e.target.value)}
|
||||
className="flex-1 min-w-0 text-sm px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
|
||||
style={{ fontFamily: previewFont }}
|
||||
>
|
||||
{PREVIEW_FONTS.map((font) => (
|
||||
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>
|
||||
{font.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={previewFontSize}
|
||||
onChange={(e) => onPreviewFontSizeChange(parseInt(e.target.value, 10))}
|
||||
className="w-16 flex-shrink-0 text-sm px-2 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer text-center"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 17, 18, 20, 22, 24].map((size) => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,13 +77,23 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
||||
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 () => {
|
||||
console.log(`[Note ${note.id}] Processing images in preview mode. Content length: ${localContent.length}`);
|
||||
setIsLoadingImages(true);
|
||||
setProcessedContent(''); // Clear old content immediately
|
||||
|
||||
// Find all image references in markdown: 
|
||||
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||
let content = localContent;
|
||||
const matches = [...localContent.matchAll(imageRegex)];
|
||||
console.log(`[Note ${note.id}] Found ${matches.length} images to process`);
|
||||
|
||||
for (const match of matches) {
|
||||
const [fullMatch, alt, imagePath] = match;
|
||||
@@ -121,11 +131,13 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
||||
useEffect(() => {
|
||||
const loadNewNote = () => {
|
||||
if (note) {
|
||||
console.log(`[Note ${note.id}] Loading note. Title: "${note.title}", Content length: ${note.content.length}`);
|
||||
setLocalTitle(note.title);
|
||||
setLocalContent(note.content);
|
||||
setLocalCategory(note.category || '');
|
||||
setLocalFavorite(note.favorite);
|
||||
setHasUnsavedChanges(false);
|
||||
setProcessedContent(''); // Clear preview content immediately
|
||||
|
||||
const firstLine = note.content.split('\n')[0].replace(/^#+\s*/, '').trim();
|
||||
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) {
|
||||
console.log(`Switching from note ${previousNoteIdRef.current} to note ${note?.id}`);
|
||||
// Clear preview content immediately when switching notes
|
||||
setProcessedContent('');
|
||||
if (hasUnsavedChanges) {
|
||||
handleSave();
|
||||
}
|
||||
setTimeout(loadNewNote, 100);
|
||||
loadNewNote();
|
||||
} else {
|
||||
loadNewNote();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ export function NotesList({
|
||||
}: NotesListProps) {
|
||||
const [isSyncing, setIsSyncing] = React.useState(false);
|
||||
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 () => {
|
||||
setIsSyncing(true);
|
||||
@@ -37,6 +40,34 @@ export function NotesList({
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -79,7 +110,11 @@ export function NotesList({
|
||||
};
|
||||
|
||||
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="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Notes</h2>
|
||||
@@ -207,6 +242,17 @@ export function NotesList({
|
||||
))
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user