diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js
index 55ff95fbbb..c0239980df 100644
--- a/frontend/config/webpack.entry.js
+++ b/frontend/config/webpack.entry.js
@@ -25,6 +25,7 @@ const entryFiles = {
sharedFileViewAudio: '/shared-file-view-audio.js',
sharedFileViewDocument: '/shared-file-view-document.js',
sharedFileViewSpreadsheet: '/shared-file-view-spreadsheet.js',
+ sharedFileViewExdraw: '/shared-file-view-exdraw.js',
sharedFileViewSdoc: '/shared-file-view-sdoc.js',
sharedFileViewUnknown: '/shared-file-view-unknown.js',
historyTrashFileView: '/history-trash-file-view.js',
diff --git a/frontend/src/pages/excalidraw-viewer/constants.js b/frontend/src/pages/excalidraw-viewer/constants.js
new file mode 100644
index 0000000000..8407f1f02b
--- /dev/null
+++ b/frontend/src/pages/excalidraw-viewer/constants.js
@@ -0,0 +1,15 @@
+export const SAVE_INTERVAL_TIME = 3 * 60 * 1000;
+
+export const langList = {
+ 'zh-cn': 'zh-CN',
+ 'en': 'en',
+ 'zh-tw': 'zh-TW',
+ 'ru': 'ru-RU',
+ 'it': 'it-IT',
+ 'fr': 'fr-FR',
+ 'es-ms': 'en',
+ 'es-ar': 'en',
+ 'es': 'es-ES',
+ 'de': 'de-DE',
+ 'cs': 'cs-CZ',
+};
diff --git a/frontend/src/pages/excalidraw-viewer/editor-api.js b/frontend/src/pages/excalidraw-viewer/editor-api.js
new file mode 100644
index 0000000000..cf433f5f60
--- /dev/null
+++ b/frontend/src/pages/excalidraw-viewer/editor-api.js
@@ -0,0 +1,26 @@
+import { seafileAPI } from '../../utils/seafile-api';
+import { Utils } from '../../utils/utils';
+
+const { repoID, filePath, fileName } = window.shared.pageOptions;
+let dirPath = Utils.getDirName(filePath);
+
+class EditorApi {
+
+ saveContent(content) {
+ return seafileAPI.getUpdateLink(repoID, dirPath).then((res) => {
+ const uploadLink = res.data;
+ return seafileAPI.updateFile(uploadLink, filePath, fileName, content);
+ });
+ }
+
+ getFileContent = () => {
+ return seafileAPI.getFileDownloadLink(repoID, filePath).then(res => {
+ const downLoadUrl = res.data;
+ return seafileAPI.getFileContent(downLoadUrl);
+ });
+ };
+}
+
+const editorApi = new EditorApi();
+
+export default editorApi;
diff --git a/frontend/src/pages/excalidraw-viewer/index.css b/frontend/src/pages/excalidraw-viewer/index.css
new file mode 100644
index 0000000000..c91bf42f0a
--- /dev/null
+++ b/frontend/src/pages/excalidraw-viewer/index.css
@@ -0,0 +1,8 @@
+.dropdown-menu {
+ display: block !important;
+}
+
+.excalidraw .dropdown-menu {
+ --bs-dropdown-padding-x: auto;
+ border: unset;
+}
diff --git a/frontend/src/pages/excalidraw-viewer/index.js b/frontend/src/pages/excalidraw-viewer/index.js
new file mode 100644
index 0000000000..3aafbc7ed3
--- /dev/null
+++ b/frontend/src/pages/excalidraw-viewer/index.js
@@ -0,0 +1,31 @@
+import React, { useState, useEffect } from 'react';
+import SimpleViewer from './simple-viewer';
+import editorApi from './editor-api';
+
+import './index.css';
+
+const ExcaliViewer = () => {
+ const [fileContent, setFileContent] = useState(null);
+ const [isFetching, setIsFetching] = useState(true);
+
+ useEffect(() => {
+ editorApi.getFileContent().then(res => {
+ if (res.data?.appState?.collaborators && !Array.isArray(res.data.appState.collaborators)) {
+ // collaborators.forEach is not a function
+ res.data['appState']['collaborators'] = [];
+ }
+ setFileContent(res.data);
+ setIsFetching(false);
+ });
+ }, []);
+
+
+ return (
+
+ );
+};
+
+export default ExcaliViewer;
diff --git a/frontend/src/pages/excalidraw-viewer/simple-viewer.js b/frontend/src/pages/excalidraw-viewer/simple-viewer.js
new file mode 100644
index 0000000000..9362d74a2f
--- /dev/null
+++ b/frontend/src/pages/excalidraw-viewer/simple-viewer.js
@@ -0,0 +1,50 @@
+import React, { useState } from 'react';
+import { Excalidraw, MainMenu } from '@excalidraw/excalidraw';
+import CodeMirrorLoading from '../../components/code-mirror-loading';
+import { langList } from './constants';
+
+import '@excalidraw/excalidraw/index.css';
+
+const SimpleViewer = ({ sceneContent = null, isFetching }) => {
+ const [excalidrawAPI, setExcalidrawAPI] = useState(null);
+ const UIOptions = {
+ canvasActions: {
+ saveToActiveFile: false,
+ LoadScene: false
+ },
+ tools: { image: false },
+ };
+
+ if (isFetching) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ setExcalidrawAPI(api)}
+ UIOptions={UIOptions}
+ langCode={langList[window.app.config.lang] || 'en'}
+ viewModeEnabled={true}
+ >
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SimpleViewer;
diff --git a/frontend/src/shared-file-view-exdraw.js b/frontend/src/shared-file-view-exdraw.js
new file mode 100644
index 0000000000..a535372eee
--- /dev/null
+++ b/frontend/src/shared-file-view-exdraw.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { Utils } from './utils/utils';
+import ExcaliViewer from './pages/excalidraw-viewer';
+
+const { siteRoot, avatarURL } = window.app.config;
+const { username } = window.app.pageOptions;
+const {
+ repoID,
+ canDownload,
+ canEdit,
+ fileName,
+ assetsUrl,
+ sharedFileDownloadURL,
+} = window.shared.pageOptions;
+
+// share permission of this sdoc
+const sharePermission = { 'can_edit': canEdit, 'can_download': canDownload, 'can_upload': false };
+const sharePermissionStr = Utils.getShareLinkPermissionStr(sharePermission);
+const sharePermissionText = Utils.getShareLinkPermissionObject(sharePermissionStr).text;
+
+window.seafile = {
+ repoID,
+ username,
+ avatarURL,
+ siteRoot,
+ sharePermissionText: sharePermissionText,
+ downloadURL: sharedFileDownloadURL,
+ assetsUrl,
+};
+
+(function () {
+ const fileIcon = Utils.getFileIconUrl(fileName);
+ document.getElementById('favicon').href = fileIcon;
+})();
+
+
+const root = createRoot(document.getElementById('wrapper'));
+root.render();
diff --git a/seahub/templates/shared_file_view_react.html b/seahub/templates/shared_file_view_react.html
index e094d80313..d74be76ffd 100644
--- a/seahub/templates/shared_file_view_react.html
+++ b/seahub/templates/shared_file_view_react.html
@@ -5,18 +5,18 @@
{% block sub_title %}{{file_name}} - {% endblock %}
{% block extra_ogp_tags %}
-
+
-
-
-
-
+
+
+
+
{% endblock %}
{% block extra_style %}
{% if filetype == 'Markdown' %}
-
- {% render_bundle 'sharedFileViewMarkdown' 'css' %}
+
+{% render_bundle 'sharedFileViewMarkdown' 'css' %}
{% elif filetype == 'Text' %}
{% render_bundle 'sharedFileViewText' 'css' %}
{% elif filetype == 'Image' %}
@@ -28,20 +28,22 @@
{% elif filetype == 'Audio' %}
{% render_bundle 'sharedFileViewAudio' 'css' %}
{% elif filetype == 'PDF' %}
-
-
-
- {% render_bundle 'sharedFileViewPDF' 'css' %}
+
+
+
+{% render_bundle 'sharedFileViewPDF' 'css' %}
{% elif filetype == 'Document' %}
-
- {% render_bundle 'sharedFileViewDocument' 'css' %}
+
+{% render_bundle 'sharedFileViewDocument' 'css' %}
{% elif filetype == 'SpreadSheet' %}
{% render_bundle 'sharedFileViewSpreadsheet' 'css' %}
{% elif filetype == 'SDoc' %}
-
- {% render_bundle 'sharedFileViewSdoc' 'css' %}
+
+{% render_bundle 'sharedFileViewSdoc' 'css' %}
{% elif filetype == 'Unknown' %}
{% render_bundle 'sharedFileViewUnknown' 'css' %}
+{% elif filetype == 'Excalidraw' %}
+ {% render_bundle 'sharedFileViewExdraw' 'css' %}
{% endif %}
{% if not permissions.can_download %}