mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 16:04:23 +00:00
Add support for images
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -36,7 +37,24 @@ func GetFileContent(fs *filesystem.FileSystem) http.HandlerFunc {
|
||||
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 {
|
||||
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
padding: 0 $padding;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
GitCommit,
|
||||
Plus,
|
||||
Trash,
|
||||
Image,
|
||||
} from '@geist-ui/icons';
|
||||
|
||||
const FileTree = ({
|
||||
@@ -37,8 +38,15 @@ const FileTree = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderIcon = ({ type }) =>
|
||||
type === 'directory' ? <Folder /> : <File />;
|
||||
const isImageFile = (fileName) => {
|
||||
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 (
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Breadcrumbs,
|
||||
@@ -14,7 +14,17 @@ import { Code, Eye } from '@geist-ui/icons';
|
||||
import Editor from './Editor';
|
||||
import FileTree from './FileTree';
|
||||
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 = ({
|
||||
content,
|
||||
@@ -33,6 +43,21 @@ const MainContent = ({
|
||||
const { setToast } = useToasts();
|
||||
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
||||
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 () => {
|
||||
try {
|
||||
@@ -153,13 +178,17 @@ const MainContent = ({
|
||||
>
|
||||
<div className="content-header">
|
||||
{renderBreadcrumbs()}
|
||||
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.Item label={<Code />} value="source" />
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.Item
|
||||
label={<Code />}
|
||||
value="source"
|
||||
disabled={isCurrentFileImage}
|
||||
/>
|
||||
<Tabs.Item label={<Eye />} value="preview" />
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="content-body">
|
||||
{activeTab === 'source' ? (
|
||||
{activeTab === 'source' && !isCurrentFileImage ? (
|
||||
<Editor
|
||||
content={content}
|
||||
onChange={onContentChange}
|
||||
@@ -167,8 +196,23 @@ const MainContent = ({
|
||||
filePath={selectedFile}
|
||||
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>
|
||||
</Grid>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
const MarkdownPreview = ({ content }) => {
|
||||
const MarkdownPreview = ({ content, baseUrl }) => {
|
||||
return (
|
||||
<div className="markdown-preview">
|
||||
<ReactMarkdown
|
||||
@@ -30,6 +30,14 @@ const MarkdownPreview = ({ content }) => {
|
||||
</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}
|
||||
|
||||
@@ -13,6 +13,11 @@ const DEFAULT_FILE = {
|
||||
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 [content, setContent] = useState(DEFAULT_FILE.content);
|
||||
const [files, setFiles] = useState([]);
|
||||
@@ -76,8 +81,12 @@ const useFileManagement = (gitEnabled = false) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = await fetchFileContent(filePath);
|
||||
setContent(fileContent);
|
||||
if (!isImageFile(filePath)) {
|
||||
const fileContent = await fetchFileContent(filePath);
|
||||
setContent(fileContent);
|
||||
} else {
|
||||
setContent(''); // Set empty content for image files
|
||||
}
|
||||
setSelectedFile(filePath);
|
||||
setIsNewFile(false);
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
@@ -127,3 +127,7 @@ export const commitAndPush = async (message) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileUrl = (filePath) => {
|
||||
return `${API_BASE_URL}/files/${filePath}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user