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"
|
"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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -127,3 +127,7 @@ export const commitAndPush = async (message) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFileUrl = (filePath) => {
|
||||||
|
return `${API_BASE_URL}/files/${filePath}`;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user