Add support for images

This commit is contained in:
2024-09-29 23:44:24 +02:00
parent 879c1d96ff
commit 43d647c9ea
7 changed files with 118 additions and 12 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -36,7 +37,24 @@ func GetFileContent(fs *filesystem.FileSystem) http.HandlerFunc {
return return
} }
w.Header().Set("Content-Type", "text/plain") // Determine content type based on file extension
contentType := "text/plain"
switch filepath.Ext(filePath) {
case ".png":
contentType = "image/png"
case ".jpg", ".jpeg":
contentType = "image/jpeg"
case ".webp":
contentType = "image/webp"
case ".gif":
contentType = "image/gif"
case ".svg":
contentType = "image/svg+xml"
case ".md":
contentType = "text/markdown"
}
w.Header().Set("Content-Type", contentType)
if _, err := w.Write(content); err != nil { if _, err := w.Write(content); err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError) http.Error(w, "Failed to write response", http.StatusInternalServerError)
} }

View File

@@ -22,6 +22,21 @@ $navbar-height: 64px;
} }
} }
.image-preview {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
}
.image-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.page-content { .page-content {
padding: 0 $padding; padding: 0 $padding;
} }

View File

@@ -7,6 +7,7 @@ import {
GitCommit, GitCommit,
Plus, Plus,
Trash, Trash,
Image,
} from '@geist-ui/icons'; } from '@geist-ui/icons';
const FileTree = ({ const FileTree = ({
@@ -37,8 +38,15 @@ const FileTree = ({
); );
}; };
const renderIcon = ({ type }) => const isImageFile = (fileName) => {
type === 'directory' ? <Folder /> : <File />; const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
return imageExtensions.some((ext) => fileName.toLowerCase().endsWith(ext));
};
const renderIcon = ({ type, name }) => {
if (type === 'directory') return <Folder />;
return isImageFile(name) ? <Image /> : <File />;
};
return ( return (
<div> <div>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
Grid, Grid,
Breadcrumbs, Breadcrumbs,
@@ -14,7 +14,17 @@ import { Code, Eye } from '@geist-ui/icons';
import Editor from './Editor'; import Editor from './Editor';
import FileTree from './FileTree'; import FileTree from './FileTree';
import MarkdownPreview from './MarkdownPreview'; import MarkdownPreview from './MarkdownPreview';
import { commitAndPush, saveFileContent, deleteFile } from '../services/api'; import {
commitAndPush,
saveFileContent,
deleteFile,
getFileUrl,
} from '../services/api';
const isImageFile = (filePath) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
return imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext));
};
const MainContent = ({ const MainContent = ({
content, content,
@@ -33,6 +43,21 @@ const MainContent = ({
const { setToast } = useToasts(); const { setToast } = useToasts();
const [newFileModalVisible, setNewFileModalVisible] = useState(false); const [newFileModalVisible, setNewFileModalVisible] = useState(false);
const [newFileName, setNewFileName] = useState(''); const [newFileName, setNewFileName] = useState('');
const [isCurrentFileImage, setIsCurrentFileImage] = useState(false);
useEffect(() => {
const currentFileIsImage = isImageFile(selectedFile);
setIsCurrentFileImage(currentFileIsImage);
if (currentFileIsImage) {
setActiveTab('preview');
}
}, [selectedFile]);
const handleTabChange = (value) => {
if (!isCurrentFileImage || value === 'preview') {
setActiveTab(value);
}
};
const handlePull = async () => { const handlePull = async () => {
try { try {
@@ -153,13 +178,17 @@ const MainContent = ({
> >
<div className="content-header"> <div className="content-header">
{renderBreadcrumbs()} {renderBreadcrumbs()}
<Tabs value={activeTab} onChange={setActiveTab}> <Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.Item label={<Code />} value="source" /> <Tabs.Item
label={<Code />}
value="source"
disabled={isCurrentFileImage}
/>
<Tabs.Item label={<Eye />} value="preview" /> <Tabs.Item label={<Eye />} value="preview" />
</Tabs> </Tabs>
</div> </div>
<div className="content-body"> <div className="content-body">
{activeTab === 'source' ? ( {activeTab === 'source' && !isCurrentFileImage ? (
<Editor <Editor
content={content} content={content}
onChange={onContentChange} onChange={onContentChange}
@@ -167,8 +196,23 @@ const MainContent = ({
filePath={selectedFile} filePath={selectedFile}
themeType={themeType} themeType={themeType}
/> />
) : isCurrentFileImage ? (
<div className="image-preview">
<img
src={getFileUrl(selectedFile)}
alt={selectedFile}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
/>
</div>
) : ( ) : (
<MarkdownPreview content={content} /> <MarkdownPreview
content={content}
baseUrl={window.API_BASE_URL}
/>
)} )}
</div> </div>
</Grid> </Grid>

View File

@@ -6,7 +6,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
const MarkdownPreview = ({ content }) => { const MarkdownPreview = ({ content, baseUrl }) => {
return ( return (
<div className="markdown-preview"> <div className="markdown-preview">
<ReactMarkdown <ReactMarkdown
@@ -30,6 +30,14 @@ const MarkdownPreview = ({ content }) => {
</code> </code>
); );
}, },
img({ src, alt, ...props }) {
// Check if the src is a relative path
if (src && !src.startsWith('http') && !src.startsWith('data:')) {
// Prepend the baseUrl to create an absolute path
src = `${baseUrl}/files/${src}`;
}
return <img src={src} alt={alt} {...props} />;
},
}} }}
> >
{content} {content}

View File

@@ -13,6 +13,11 @@ const DEFAULT_FILE = {
content: '# Welcome to NovaMD\n\nStart editing here!', content: '# Welcome to NovaMD\n\nStart editing here!',
}; };
const isImageFile = (filePath) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
return imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext));
};
const useFileManagement = (gitEnabled = false) => { const useFileManagement = (gitEnabled = false) => {
const [content, setContent] = useState(DEFAULT_FILE.content); const [content, setContent] = useState(DEFAULT_FILE.content);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
@@ -76,8 +81,12 @@ const useFileManagement = (gitEnabled = false) => {
} }
try { try {
const fileContent = await fetchFileContent(filePath); if (!isImageFile(filePath)) {
setContent(fileContent); const fileContent = await fetchFileContent(filePath);
setContent(fileContent);
} else {
setContent(''); // Set empty content for image files
}
setSelectedFile(filePath); setSelectedFile(filePath);
setIsNewFile(false); setIsNewFile(false);
setHasUnsavedChanges(false); setHasUnsavedChanges(false);

View File

@@ -127,3 +127,7 @@ export const commitAndPush = async (message) => {
throw error; throw error;
} }
}; };
export const getFileUrl = (filePath) => {
return `${API_BASE_URL}/files/${filePath}`;
};