mirror of https://github.com/go-gitea/gitea.git
Merge 2e80917e25 into 29b28002aa
This commit is contained in:
commit
c65390d764
|
|
@ -40,6 +40,7 @@
|
|||
"minimatch": "10.0.2",
|
||||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"online-3d-viewer": "0.16.0",
|
||||
"pdfobject": "2.3.1",
|
||||
"perfect-debounce": "1.0.0",
|
||||
"postcss": "8.5.5",
|
||||
|
|
@ -2026,6 +2027,16 @@
|
|||
"vue": "^3.2.29"
|
||||
}
|
||||
},
|
||||
"node_modules/@simonwep/pickr": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.9.0.tgz",
|
||||
"integrity": "sha512-oEYvv15PyfZzjoAzvXYt3UyNGwzsrpFxLaZKzkOSd0WYBVwLd19iJerePDONxC1iF6+DpcswPdLIM2KzCJuYFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "3.32.2",
|
||||
"nanopop": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stoplight/better-ajv-errors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz",
|
||||
|
|
@ -5337,6 +5348,17 @@
|
|||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.32.2",
|
||||
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.32.2.tgz",
|
||||
"integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.43.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
|
||||
|
|
@ -7721,6 +7743,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
|
|
@ -10285,6 +10313,12 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanopop": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.3.0.tgz",
|
||||
"integrity": "sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz",
|
||||
|
|
@ -10525,6 +10559,17 @@
|
|||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/online-3d-viewer": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmmirror.com/online-3d-viewer/-/online-3d-viewer-0.16.0.tgz",
|
||||
"integrity": "sha512-Mcmo41TM3K+svlMDRH8ySKSY2e8s7Sssdb5U9LV3gkFKVWGGuS304Vk5gqxopAJbE72DpsC67Ve3YNtcAuROwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@simonwep/pickr": "1.9.0",
|
||||
"fflate": "0.8.2",
|
||||
"three": "0.176.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
|
|
@ -13193,6 +13238,12 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.176.0",
|
||||
"resolved": "https://registry.npmmirror.com/three/-/three-0.176.0.tgz",
|
||||
"integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"minimatch": "10.0.2",
|
||||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"online-3d-viewer": "0.16.0",
|
||||
"pdfobject": "2.3.1",
|
||||
"perfect-debounce": "1.0.0",
|
||||
"postcss": "8.5.5",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
<div class="file-view-container" data-global-init="initFileView" data-filename="{{.TreePath}}" data-url="{{$.RawFileLink}}" data-fallback-text="{{ctx.Locale.Tr "repo.file_view_raw"}}"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .FileSize}}
|
||||
|
|
|
|||
|
|
@ -73,9 +73,14 @@ func TestLFSRender(t *testing.T) {
|
|||
fileInfo := doc.Find("div.file-info-entry").First().Text()
|
||||
assert.Contains(t, fileInfo, "LFS")
|
||||
|
||||
rawLink, exists := doc.Find("div.file-view > div.view-raw > a").Attr("href")
|
||||
assert.True(t, exists, "Download link should render instead of content because this is a binary file")
|
||||
assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", rawLink, "The download link should use the proper /media link because it's in LFS")
|
||||
// find new file view container
|
||||
fileViewContainer := doc.Find("div.file-view-container")
|
||||
assert.Positive(t, fileViewContainer.Length(), "File view container should exist")
|
||||
|
||||
// check data attribute instead of link href
|
||||
dataURL, exists := fileViewContainer.Attr("data-url")
|
||||
assert.True(t, exists, "File view container should have data-url attribute")
|
||||
assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", dataURL, "The data-url should use the proper /media link because it's in LFS")
|
||||
})
|
||||
|
||||
// check that a directory with a README file shows its text
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* File View & Render Plugin Styles
|
||||
*/
|
||||
|
||||
/* file view container */
|
||||
.file-view-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-view-container.is-loading {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-view-container.is-loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
border: 5px solid var(--color-secondary);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.view-raw-fallback {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 3D model viewer */
|
||||
.model3d-content {
|
||||
width: 100% !important;
|
||||
min-height: 400px !important;
|
||||
border: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* error message */
|
||||
.file-view-container .ui.error.message {
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-view-container .ui.error.message pre {
|
||||
margin-top: 0.5em;
|
||||
font-size: 12px;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
|
@ -85,4 +85,6 @@
|
|||
|
||||
@import "./helpers.css";
|
||||
|
||||
@import "./file-view.css";
|
||||
|
||||
@tailwind utilities;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import {applyRenderPlugin} from '../modules/file-render-plugin.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
/**
|
||||
* init file view renderer
|
||||
*
|
||||
* detect renderable files and apply appropriate plugins
|
||||
*/
|
||||
export function initFileView(): void {
|
||||
// register file view renderer init function
|
||||
registerGlobalInitFunc('initFileView', async (container: HTMLElement) => {
|
||||
// get file info
|
||||
const filename = container.getAttribute('data-filename');
|
||||
const fileUrl = container.getAttribute('data-url');
|
||||
|
||||
// mark loading state
|
||||
container.classList.add('is-loading');
|
||||
|
||||
try {
|
||||
// check if filename and url exist
|
||||
if (!filename || !fileUrl) {
|
||||
console.error(`missing filename(${filename}) or file url(${fileUrl}) for rendering`);
|
||||
throw new Error('missing necessary file info');
|
||||
}
|
||||
|
||||
// try to apply render plugin
|
||||
const success = await applyRenderPlugin(container);
|
||||
|
||||
// if no suitable plugin is found, show default view
|
||||
if (!success) {
|
||||
// show default view raw file link
|
||||
const fallbackText = container.getAttribute('data-fallback-text') || 'View Raw File';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="view-raw-fallback">
|
||||
<a href="${fileUrl}" class="ui basic button" target="_blank">${fallbackText}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('file view init error:', error);
|
||||
|
||||
// show error message
|
||||
const fallbackText = container.getAttribute('data-fallback-text') || 'View Raw File';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="ui error message">
|
||||
<div class="header">Failed to render file</div>
|
||||
<p>Error: ${String(error)}</p>
|
||||
<pre>${JSON.stringify({filename, fileUrl}, null, 2)}</pre>
|
||||
<a class="ui basic button" href="${fileUrl || '#'}" target="_blank">${fallbackText}</a>
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
// remove loading state
|
||||
container.classList.remove('is-loading');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ import {initStopwatch} from './features/stopwatch.ts';
|
|||
import {initFindFileInRepo} from './features/repo-findfile.ts';
|
||||
import {initMarkupContent} from './markup/content.ts';
|
||||
import {initPdfViewer} from './render/pdf.ts';
|
||||
import {initFileView} from './features/file-view.ts';
|
||||
import {register3DViewerPlugin} from './render/plugins/3d-viewer.ts';
|
||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||
import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
|
||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||
|
|
@ -163,6 +165,9 @@ onDomReady(() => {
|
|||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
|
||||
initFileView,
|
||||
register3DViewerPlugin,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* File Render Plugin System
|
||||
*
|
||||
* This module provides a plugin architecture for rendering different file types
|
||||
* in the browser without requiring backend support for identifying file types.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for file render plugins
|
||||
*/
|
||||
export type FileRenderPlugin = {
|
||||
// unique plugin name
|
||||
name: string;
|
||||
|
||||
// test if plugin can handle specified file
|
||||
canHandle: (filename: string, mimeType: string) => boolean;
|
||||
|
||||
// render file content
|
||||
render: (container: HTMLElement, fileUrl: string, options?: any) => Promise<void>;
|
||||
}
|
||||
|
||||
// store registered render plugins
|
||||
const plugins: FileRenderPlugin[] = [];
|
||||
|
||||
/**
|
||||
* register a file render plugin
|
||||
*/
|
||||
export function registerFileRenderPlugin(plugin: FileRenderPlugin): void {
|
||||
plugins.push(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* find suitable render plugin by filename and mime type
|
||||
*/
|
||||
function findPlugin(filename: string, mimeType: string): FileRenderPlugin | null {
|
||||
return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* apply render plugin to specified container
|
||||
*/
|
||||
export async function applyRenderPlugin(container: HTMLElement): Promise<boolean> {
|
||||
try {
|
||||
// get file info from container element
|
||||
const filename = container.getAttribute('data-filename') || '';
|
||||
const fileUrl = container.getAttribute('data-url') || '';
|
||||
|
||||
if (!filename || !fileUrl) {
|
||||
console.warn('Missing filename or file URL for renderer');
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mime type (optional)
|
||||
const mimeType = container.getAttribute('data-mime-type') || '';
|
||||
|
||||
// find plugin that can handle this file
|
||||
const plugin = findPlugin(filename, mimeType);
|
||||
if (!plugin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// apply plugin to render file
|
||||
await plugin.render(container, fileUrl);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error applying render plugin:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import type {FileRenderPlugin} from '../../modules/file-render-plugin.ts';
|
||||
import {registerFileRenderPlugin} from '../../modules/file-render-plugin.ts';
|
||||
|
||||
/**
|
||||
* 3D model file render plugin
|
||||
*
|
||||
* support common 3D model file formats, use online-3d-viewer library for rendering
|
||||
*/
|
||||
export function register3DViewerPlugin(): void {
|
||||
// supported 3D file extensions
|
||||
const SUPPORTED_EXTENSIONS = [
|
||||
'.3dm', '.3ds', '.3mf', '.amf', '.bim', '.brep',
|
||||
'.dae', '.fbx', '.fcstd', '.glb', '.gltf',
|
||||
'.ifc', '.igs', '.iges', '.stp', '.step',
|
||||
'.stl', '.obj', '.off', '.ply', '.wrl',
|
||||
];
|
||||
|
||||
// create and register plugin
|
||||
const plugin: FileRenderPlugin = {
|
||||
name: '3d-model-viewer',
|
||||
|
||||
// check if file extension is supported 3D file
|
||||
canHandle(filename: string, _mimeType: string): boolean {
|
||||
const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
||||
const canHandle = SUPPORTED_EXTENSIONS.includes(ext);
|
||||
return canHandle;
|
||||
},
|
||||
|
||||
// render 3D model
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
// add loading indicator
|
||||
container.classList.add('is-loading');
|
||||
|
||||
try {
|
||||
// dynamically load 3D rendering library
|
||||
const OV = await import(/* webpackChunkName: "online-3d-viewer" */'online-3d-viewer');
|
||||
|
||||
// configure container style
|
||||
container.classList.add('model3d-content');
|
||||
|
||||
// initialize 3D viewer
|
||||
const viewer = new OV.EmbeddedViewer(container, {
|
||||
backgroundColor: new OV.RGBAColor(59, 68, 76, 0), // transparent
|
||||
defaultColor: new OV.RGBColor(65, 131, 196),
|
||||
edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1),
|
||||
});
|
||||
|
||||
// load model from url
|
||||
viewer.LoadModelFromUrlList([fileUrl]);
|
||||
} catch (error) {
|
||||
// handle render error
|
||||
console.error('error rendering 3D model:', error);
|
||||
|
||||
// add error message and download button
|
||||
const fallbackText = container.getAttribute('data-fallback-text') || 'View Raw File';
|
||||
container.innerHTML = `
|
||||
<div class="ui error message">
|
||||
<div class="header">Failed to render 3D model</div>
|
||||
<p>The 3D model could not be displayed in the browser.</p>
|
||||
<a class="ui basic button" href="${fileUrl}" target="_blank">${fallbackText}</a>
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
// remove loading state
|
||||
container.classList.remove('is-loading');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// register plugin
|
||||
registerFileRenderPlugin(plugin);
|
||||
}
|
||||
Loading…
Reference in New Issue