Skip to content

Add support for 3D/CAD file formats preview #34794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions routers/web/repo/setting/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,6 @@ func LFSFileGet(ctx *context.Context) {
}
ctx.Data["LineNums"] = gotemplate.HTML(output.String())

case st.IsPDF():
ctx.Data["IsPDFFile"] = true
case st.IsVideo():
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
Expand Down
2 changes: 0 additions & 2 deletions routers/web/repo/view_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,6 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["LineEscapeStatus"] = statuses
}

case fInfo.st.IsPDF():
ctx.Data["IsPDFFile"] = true
case fInfo.st.IsVideo():
ctx.Data["IsVideoFile"] = true
case fInfo.st.IsAudio():
Expand Down
4 changes: 1 addition & 3 deletions templates/repo/settings/lfs_file.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@
<audio controls src="{{$.RawFileLink}}">
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
</audio>
{{else if .IsPDFFile}}
<div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
{{else}}
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
{{template "shared/repo/fileviewrender" dict "RawFileLink" $.RawFileLink}}
{{end}}
</div>
{{else if .FileSize}}
Expand Down
4 changes: 1 addition & 3 deletions templates/repo/view_file.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,8 @@
<audio controls src="{{$.RawFileLink}}">
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
</audio>
{{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>
{{template "shared/repo/fileviewrender" dict "RawFileLink" $.RawFileLink}}
{{end}}
</div>
{{else if .FileSize}}
Expand Down
5 changes: 5 additions & 0 deletions templates/shared/repo/fileviewrender.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="file-view-render-container" data-global-init="initFileViewRender" data-raw-file-link="{{$.RawFileLink}}">
<div class="file-view-raw-prompt tw-p-4">
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
</div>
</div>
11 changes: 8 additions & 3 deletions tests/integration/lfs_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-raw-file-link")
assert.True(t, exists, "File view container should have data-raw-file-link attribute")
assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", dataURL, "The data-raw-file-link should use the proper /media link because it's in LFS")
})

// check that a directory with a README file shows its text
Expand Down
3 changes: 3 additions & 0 deletions web_src/css/file-view.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.file-view-render-container :last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius); /* to match the "ui segment" bottom radius */
}
2 changes: 2 additions & 0 deletions web_src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,6 @@

@import "./helpers.css";

@import "./file-view.css";

@tailwind utilities;
3 changes: 1 addition & 2 deletions web_src/css/modules/animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ form.single-button-form.is-loading .button {
}

.markup pre.is-loading,
.editor-loading.is-loading,
.pdf-content.is-loading {
.editor-loading.is-loading {
height: var(--height-loading);
}

Expand Down
23 changes: 0 additions & 23 deletions web_src/css/repo.css
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,6 @@ td .commit-summary {
cursor: default;
}

.view-raw {
display: flex;
justify-content: center;
align-items: center;
}

.view-raw > * {
max-width: 100%;
}
Expand All @@ -206,19 +200,6 @@ td .commit-summary {
max-width: 600px !important;
}

.pdf-content {
width: 100%;
height: 600px;
border: none !important;
display: flex;
align-items: center;
justify-content: center;
}

.pdf-content .pdf-fallback-button {
margin: 50px auto;
}

.repository.file.list .non-diff-file-content .plain-text {
padding: 1em 2em;
}
Expand All @@ -241,10 +222,6 @@ td .commit-summary {
padding: 0 !important;
}

.non-diff-file-content .pdfobject {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}

.repo-editor-header {
width: 100%;
}
Expand Down
52 changes: 52 additions & 0 deletions web_src/js/features/file-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {findFileRenderPlugin} from '../modules/file-render-plugin.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
import {createElementFromHTML} from '../utils/dom.ts';
import {register3DViewerPlugin} from '../render/plugins/3d-viewer.ts';
import {registerPdfViewerPlugin} from '../render/plugins/pdf-viewer.ts';
import {htmlEscape} from 'escape-goat';
import {basename} from '../utils.ts';

export function initFileViewRender(): void {
let pluginRegistered = false;

registerGlobalInitFunc('initFileViewRender', async (container: HTMLElement) => {
if (!pluginRegistered) {
pluginRegistered = true;
register3DViewerPlugin();
registerPdfViewerPlugin();
}

const rawFileLink = container.getAttribute('data-raw-file-link');
const mimeType = container.getAttribute('data-mime-type') || ''; // not used yet
const elViewRawPrompt = container.querySelector('.file-view-raw-prompt');
if (!rawFileLink || !elViewRawPrompt) throw new Error('unexpected file view container');

let rendered = false, errorMsg = '';
try {
const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType);
if (plugin) {
container.classList.add('is-loading');
container.setAttribute('data-render-name', plugin.name); // not used yet
await plugin.render(container, rawFileLink);
rendered = true;
}
} catch (e) {
errorMsg = `${e}`;
} finally {
container.classList.remove('is-loading');
}

if (rendered) {
elViewRawPrompt.remove();
return;
}

// remove all children from the container, and only show the raw file link
container.replaceChildren(elViewRawPrompt);

if (errorMsg) {
const elErrorMessage = createElementFromHTML(htmlEscape`<div class="ui error message">${errorMsg}</div>`);
elViewRawPrompt.insertAdjacentElement('afterbegin', elErrorMessage);
}
});
}
5 changes: 3 additions & 2 deletions web_src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
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 {initFileViewRender} from './features/file-view.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';
Expand Down Expand Up @@ -159,10 +159,11 @@ onDomReady(() => {
initUserAuthWebAuthnRegister,
initUserSettings,
initRepoDiffView,
initPdfViewer,
initColorPickers,

initOAuth2SettingsDisableCheckbox,

initFileViewRender,
]);

// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
Expand Down
26 changes: 26 additions & 0 deletions web_src/js/modules/file-render-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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.
*/
export type FileRenderPlugin = {
// unique plugin name
name: string;

// test if plugin can handle a specified file
canHandle: (filename: string, mimeType: string) => boolean;

// render file content
render: (container: HTMLElement, fileUrl: string, options?: any) => Promise<void>;
}

const plugins: FileRenderPlugin[] = [];

export function registerFileRenderPlugin(plugin: FileRenderPlugin): void {
plugins.push(plugin);
}

export function findFileRenderPlugin(filename: string, mimeType: string): FileRenderPlugin | null {
return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null;
}
17 changes: 0 additions & 17 deletions web_src/js/render/pdf.ts

This file was deleted.

Loading
Loading