From 0a8454ca2ee0984ccf6b8a317f443a79a9e5d2d2 Mon Sep 17 00:00:00 2001 From: Yume <2839681263@qq.com> Date: Fri, 6 Jun 2025 10:48:26 +0800 Subject: [PATCH] feat(UI): enhance CodeContent with line copy functionality and toolbar --- .../CodeView/BlobView/CodeContent.module.css | 39 +++-- .../CodeView/BlobView/CodeContent.tsx | 162 ++++++++++++++++-- .../CodeView/TreeView/BreadCrumb.tsx | 4 +- .../web/pages/[org]/code/blob/[...path].tsx | 20 +-- 4 files changed, 185 insertions(+), 40 deletions(-) diff --git a/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css b/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css index 2bc476089..d4d5382a9 100644 --- a/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css +++ b/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css @@ -1,28 +1,37 @@ .codeLineNumber { - margin-left: 15px; + margin-left: 4px; margin-right: 25px; + text-align: center; + user-select: none; } -.viewChangeTab { - background-color: rgba(53, 53, 53, 0.103); +.toolbar { + background-color: #f6f8fa; display: flex; - position: absolute; + align-items: center; + text-align: center; height: 50px; - border-top-left-radius: 1rem; - border-top-right-radius: 1rem; + border-bottom: 1px solid #d1d9e0; } -.viewChangeTabButton { - padding-left: 20px; - padding-right: 20px; +.toolbarLeftButton { + padding:0 20px; height: 100%; - border-radius: 1rem; - border: none; - background-color: transparent; + border-radius: 0.5rem; } -.viewChangeTabButton:hover, -.viewChangeTabButton:checked { - background-color: rgba(0, 0, 0, 0.121); +.toolbarLeftButton.active { + background-color: #ffffff; + border: solid 1px #818b98; +} + +.toolbarRightButton { + padding:0 20px; + height: 100%; +} + +.toolbarRightButton:hover { + background-color: #e0e5ea; + transition: all 0.5s; } \ No newline at end of file diff --git a/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx b/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx index 87f0bdeef..d6e486553 100644 --- a/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx +++ b/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx @@ -1,12 +1,26 @@ +'use client' import 'github-markdown-css/github-markdown-light.css' + import { useEffect, useRef, useState } from 'react' +import { ListIcon } from '@gitmono/ui' +import { Button, Dropdown, MenuProps, message} from 'antd' import { Highlight, themes } from 'prism-react-renderer' import styles from './CodeContent.module.css' // @ts-ignore -const CodeContent = ({ fileContent }) => { +const CodeContent = ({ fileContent, path }: { fileContent: string, path?: string[] }) => { const [lfs, setLfs] = useState(false) + const [selectedLine, setSelectedLine] = useState(null) + + const menuItems: MenuProps = { + items: [ + { + label: 'Copy line', + key: '1' + } + ] + } useEffect(() => { if (isLfsContent(fileContent)) { @@ -16,6 +30,86 @@ const CodeContent = ({ fileContent }) => { const lineRef = useRef([]) + const handleLineClick = (lineNumber: number) => { + setSelectedLine(lineNumber === selectedLine ? null : lineNumber) + } + + const handleCopyLine = (line: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(line) + .then(() => message.success('Copied to clipboard')) + .catch(() => message.error('Copied failed')) + } else { + const textarea = document.createElement('textarea'); + + textarea.value = line; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + message.success('Copied to clipboard'); + } catch { + message.error('Copied failed'); + } + } + } + + const handleCopy = () => { + handleCopyLine(fileContent) + } + + let filename; + + if (!path || path.length === 0) { + message.error('Path information is missing'); + filename = ""; + } + else { + filename = path[path.length - 1] + } + const handleRawView = () => { + // Create a new window/tab with the raw content + const newWindow = window.open(); + + if (newWindow) { + newWindow.document.write(` + + + + Raw Content + + + ${fileContent.replace(//g, '>')} + + `); + newWindow.document.close(); + } else { + message.error('Unable to open new window. Please check your browser settings.'); + } + } + const handleDownload = () => { + const blob = new Blob([fileContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + + a.href = url; + a.download = filename; + + // Append to the document, click it, and remove it + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Release the URL object + URL.revokeObjectURL(url); + } + function isLfsContent(content: string): boolean { const lines = content.split('\n') let foundVersion = false @@ -39,15 +133,36 @@ const CodeContent = ({ fileContent }) => { return (
+
+
+ + +
+ {`${fileContent.split('\n').length} lines . 2.79 KB`} +
+
+ + + +
+
+ +
+
+ {/*todo: Dynamic support for language types*/} {({ style, tokens, getLineProps, getTokenProps }) => (
             {!lfs &&
               tokens.map((line, i) => (
@@ -56,13 +171,40 @@ const CodeContent = ({ fileContent }) => {
                   key={i}
                   {...getLineProps({ line })}
                   // @ts-ignore
-                  ref={(el) => lineRef.current[i] = el as HTMLDivElement}
+                  ref={(el) => (lineRef.current[i] = el as HTMLDivElement)}
+                  style={{
+                    backgroundColor: selectedLine === i ? '#f0f7ff' : 'transparent'
+                  }}
                 >
-                  {i + 1}
-                  {line.map((token, key) => (
-                    // eslint-disable-next-line react/no-array-index-key
-                    
-                  ))}
+                  
+ + {selectedLine === i ? + { + if(props.key === '1') { + handleCopyLine(line.map(i=>i.content).join('')) + } + } + }} + className='bg-gray-100 border rounded border-gray-200 justify-center' + + > +
))} {lfs && (Sorry about that, but we can’t show files that are this big right now.)} @@ -73,4 +215,4 @@ const CodeContent = ({ fileContent }) => { ) } -export default CodeContent; \ No newline at end of file +export default CodeContent; diff --git a/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx b/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx index ec8ff5b69..15236ac15 100644 --- a/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx +++ b/moon/apps/web/components/CodeView/TreeView/BreadCrumb.tsx @@ -20,13 +20,13 @@ const Bread = ({ path }:any) => { }); return ( -
+
{breadCrumbItems?.map((item: { isLast: any; title: string; href: string | UrlObject; }, index: number) => ( {/* displayed after the home item and before non-last items */} {index > 0 && ( - / + / )} {/* Current breadcrumb item */} {item.isLast ? ( diff --git a/moon/apps/web/pages/[org]/code/blob/[...path].tsx b/moon/apps/web/pages/[org]/code/blob/[...path].tsx index bb04f396b..df00ebcbb 100644 --- a/moon/apps/web/pages/[org]/code/blob/[...path].tsx +++ b/moon/apps/web/pages/[org]/code/blob/[...path].tsx @@ -11,15 +11,9 @@ import { CommentSection } from '@/components/CodeView/BlobView/CommentSection' const codeStyle = { borderRadius: 8, width: 'calc(85% - 8px)', - background: '#fff' -} - -const breadStyle = { - minHeight: 30, - borderRadius: 8, - overflow: 'hidden', - width: 'calc(100% - 8px)', - background: '#fff' + background: '#fff', + border: '1px solid #d1d9e0', + margin: '0 10px' } interface Comment { @@ -85,11 +79,11 @@ function BlobPage() { return (
- + - + {/* @ts-ignore */} @@ -105,7 +99,7 @@ BlobPage.getProviders = ( | string | number | boolean - | React.ReactElement> + | React.ReactElement | Iterable | React.ReactPortal | Promise @@ -120,4 +114,4 @@ BlobPage.getProviders = ( ) } -export default BlobPage \ No newline at end of file +export default BlobPage