diff --git a/src/components/SlashMenu.tsx b/src/components/SlashMenu.tsx index afea61b..8358dd8 100644 --- a/src/components/SlashMenu.tsx +++ b/src/components/SlashMenu.tsx @@ -9,9 +9,7 @@ import { AlignLeft, AlignCenter, AlignRight, Trash2, AppWindow } from "lucide-solid"; -import { cn, convertImageToWebpFile } from "@/lib/utils"; -import { activeTaskId, uploadTaskAttachment, activeNoteId, uploadNoteAttachment } from "@/store"; -import { toast } from "solid-sonner"; +import { cn } from "@/lib/utils"; export interface CommandItem { title: string; @@ -122,54 +120,7 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[] description: "Upload an image.", icon: ImageIcon, command: ({ editor, range }: { editor: any, range: any }) => { - editor.chain().focus().deleteRange(range).run(); - - const taskId = activeTaskId(); - const noteId = activeNoteId(); - - if (!taskId && !noteId) { - toast.error("Cannot upload image outside of a task or note."); - return; - } - - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async () => { - if (input.files?.length) { - const originalFile = input.files[0]; - toast.promise( - (async () => { - const webpFile = await convertImageToWebpFile(originalFile); - if (taskId) { - return uploadTaskAttachment(taskId, webpFile); - } else { - return uploadNoteAttachment(noteId!, webpFile); - } - })(), - { - loading: "Processing & uploading image...", - success: (url) => { - // Insert image, then run enter to add a newline (paragraph) below it. - // Without this, the image stays selected and the next keypress deletes it. - editor.chain() - .focus() - .setImage({ src: url }) - .run(); - - // Manually force a new paragraph at the end so the user can keep typing - editor.chain() - .focus('end') - .insertContent('

') - .run(); - return "Image uploaded."; - }, - error: "Failed to upload image." - } - ); - } - }; - input.click(); + editor.chain().focus().deleteRange(range).uploadImage().run(); }, }, { diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx index 26a2d2d..53d299d 100644 --- a/src/components/TaskDetail.tsx +++ b/src/components/TaskDetail.tsx @@ -3,7 +3,7 @@ import { Sheet, SheetContent } from "@/components/ui/sheet"; import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag } from "@/store"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings, Star } from "lucide-solid"; +import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings, Star, Image as ImageIcon } from "lucide-solid"; import { StatusCircle } from "./StatusCircle"; import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store"; import { Button } from "./ui/button"; @@ -363,7 +363,7 @@ export const TaskDetail: Component = (props) => { {/* Favorite */}
- +
+ + {/* Image Upload Shortcut */} +
+ +
{/* Metadata Row (Dates/Timestamps) */} diff --git a/src/components/TaskEditor.tsx b/src/components/TaskEditor.tsx index 2538e79..5ab8812 100644 --- a/src/components/TaskEditor.tsx +++ b/src/components/TaskEditor.tsx @@ -27,6 +27,7 @@ import { Dropcursor } from "@tiptap/extension-dropcursor"; import { Mention } from "@tiptap/extension-mention"; import { userSuggestion, noteSuggestion } from "@/lib/mention-suggestion"; import { FileViewer } from "@/lib/extensions/file-viewer"; +import { ImageUpload } from "@/lib/extensions/image-upload"; interface TaskEditorProps { content?: string; @@ -161,6 +162,7 @@ export const TaskEditor: Component = (props) => { width: 2, }), FileViewer, + ImageUpload, ], // Use untrack so the editor doesn't re-initialize when props.content changes content: untrack(() => props.content) || "", diff --git a/src/lib/extensions/image-upload.ts b/src/lib/extensions/image-upload.ts new file mode 100644 index 0000000..3715f3a --- /dev/null +++ b/src/lib/extensions/image-upload.ts @@ -0,0 +1,70 @@ +import { Extension } from '@tiptap/core'; +import { activeTaskId, uploadTaskAttachment, activeNoteId, uploadNoteAttachment } from "@/store"; +import { convertImageToWebpFile } from "@/lib/utils"; +import { toast } from "solid-sonner"; + +declare module '@tiptap/core' { + interface Commands { + imageUpload: { + uploadImage: () => ReturnType; + }; + } +} + +export const ImageUpload = Extension.create({ + name: 'imageUpload', + + addCommands() { + return { + uploadImage: () => ({ editor }) => { + const taskId = activeTaskId(); + const noteId = activeNoteId(); + + if (!taskId && !noteId) { + toast.error("Cannot upload image outside of a task or note."); + return false; + } + + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.onchange = async () => { + if (input.files?.length) { + const originalFile = input.files[0]; + toast.promise( + (async () => { + const webpFile = await convertImageToWebpFile(originalFile); + if (taskId) { + return uploadTaskAttachment(taskId, webpFile); + } else { + return uploadNoteAttachment(noteId!, webpFile); + } + })(), + { + loading: "Processing & uploading image...", + success: (url) => { + // Insert image + editor.chain() + .focus() + .setImage({ src: url }) + .run(); + + // Manually force a new paragraph at the end so the user can keep typing + editor.chain() + .focus('end') + .insertContent('

') + .run(); + + return "Image uploaded."; + }, + error: "Failed to upload image." + } + ); + } + }; + input.click(); + return true; + }, + }; + }, +}); diff --git a/src/views/NotepadView.tsx b/src/views/NotepadView.tsx index 879ade7..68a1e5b 100644 --- a/src/views/NotepadView.tsx +++ b/src/views/NotepadView.tsx @@ -2,7 +2,7 @@ import { type Component, createSignal, createMemo, createEffect, onCleanup, For, import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag } from "@/store"; import { pb } from "@/lib/pocketbase"; import { type Note } from "@/store"; -import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus, Star, MoreHorizontal } from "lucide-solid"; +import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus, Star, MoreHorizontal, Type, Image as ImageIcon } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { DragDropProvider, DragDropSensors, SortableProvider, createSortable, closestCenter } from "@thisbeyond/solid-dnd"; @@ -22,6 +22,7 @@ export const NotepadView: Component<{ const [linkSearchQuery, setLinkSearchQuery] = createSignal(""); const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(false); const [isDragging, setIsDragging] = createSignal(false); + const [editorInstance, setEditorInstance] = createSignal(null); const currentUserId = pb.authStore.model?.id; @@ -432,9 +433,38 @@ export const NotepadView: Component<{ /> )} + + {/* Editor Commands Row */} +
+ + +
- {/* Mobile Tabs Dropdown Moved to Footer */}