Skip to content
Merged
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
573 changes: 297 additions & 276 deletions src/App.tsx

Large diffs are not rendered by default.

45 changes: 40 additions & 5 deletions src/components/CommitGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
line: tr('-L Zeilenbereich', '-L line range'),
}), [tr]);
const logContainerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [containerHeight, setContainerHeight] = useState(800);
const {
layout,
workingTreeStatus,
Expand All @@ -170,6 +172,17 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
},
});

useEffect(() => {
if (!layout) return;
const container = logContainerRef.current?.parentElement;
if (!container) return;
const onScroll = () => setScrollTop(container.scrollTop);
setScrollTop(container.scrollTop);
setContainerHeight(container.clientHeight);
container.addEventListener('scroll', onScroll, { passive: true });
return () => container.removeEventListener('scroll', onScroll);
}, [layout]);

useEffect(() => {
try {
const raw = localStorage.getItem(FORENSIC_PATH_HISTORY_STORAGE_KEY);
Expand Down Expand Up @@ -774,7 +787,17 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
return <div style={{ color: 'var(--text-secondary)', padding: '2rem', textAlign: 'center' }}>{tr('Bitte waehle ein Repository aus, um den Graphen zu sehen.', 'Please select a repository to view the graph.')}</div>;
}
if (loading) {
return <EmptyState title={tr('Lade Commit-Historie...', 'Loading commit history...')} />;
return (
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: '6px' }}>
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', opacity: 1 - i * 0.09 }}>
<div className="skeleton-circle" style={{ width: 12, height: 12, borderRadius: '50%', flexShrink: 0 }} />
<div className="skeleton-line" style={{ height: 10, width: `${45 + (i % 3) * 15}%`, borderRadius: 4 }} />
<div className="skeleton-line" style={{ height: 10, width: 70, borderRadius: 4, marginLeft: 'auto', flexShrink: 0 }} />
</div>
))}
</div>
);
}
if (!layout || layout.nodes.length === 0) {
return (
Expand All @@ -793,6 +816,16 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
const graphWidth = Math.max((layout.maxLane + 1) * LANE_WIDTH + GRAPH_PADDING * 2, 60);
const totalHeight = (layout.nodes.length + workingTreeRowOffset) * ROW_HEIGHT;
const laneX = (lane: number) => GRAPH_PADDING + lane * LANE_WIDTH + LANE_WIDTH / 2;

const OVERSCAN = 8;
const visibleStartIdx = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT - workingTreeRowOffset) - OVERSCAN);
const visibleEndIdx = Math.min(layout.nodes.length, Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT - workingTreeRowOffset) + OVERSCAN);
const visibleNodes = layout.nodes.slice(visibleStartIdx, visibleEndIdx);
const topSpacerHeight = visibleStartIdx * ROW_HEIGHT;
const bottomSpacerHeight = Math.max(0, (layout.nodes.length - visibleEndIdx) * ROW_HEIGHT);
const visibleEdges = layout.edges.filter(
(e) => Math.min(e.fromRow, e.toRow) <= visibleEndIdx && Math.max(e.fromRow, e.toRow) >= visibleStartIdx
);
const nodeByHash = new Map(layout.nodes.map(node => [node.commit.hash, node]));
const headNode = layout.nodes.find(node => (
node.commit.refs.some(ref => ref.startsWith('HEAD ->') || ref === 'HEAD')
Expand Down Expand Up @@ -1040,7 +1073,7 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
/>
</>
)}
{layout.edges.map((edge, i) => (
{visibleEdges.map((edge, i) => (
<path
key={`eg${i}`}
d={buildEdgePath(edge)}
Expand All @@ -1052,7 +1085,7 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
strokeDasharray={edge.kind === 'merge' ? '4 4' : undefined}
/>
))}
{layout.edges.map((edge, i) => (
{visibleEdges.map((edge, i) => (
<path
key={`em${i}`}
d={buildEdgePath(edge)}
Expand All @@ -1064,7 +1097,7 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
strokeDasharray={edge.kind === 'merge' ? '4 4' : undefined}
/>
))}
{layout.nodes.map((node) => {
{visibleNodes.map((node) => {
const cx = laneX(node.lane);
const cy = (node.row + workingTreeRowOffset) * ROW_HEIGHT + ROW_HEIGHT / 2;
const isSelected = selectedHash === node.commit.hash;
Expand Down Expand Up @@ -1154,7 +1187,8 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
</div>
)}

{layout.nodes.map((node) => {
{topSpacerHeight > 0 && <div style={{ height: topSpacerHeight }} aria-hidden="true" />}
{visibleNodes.map((node) => {
const isSelected = selectedHash === node.commit.hash;
const isSecondary = isSecondaryCommit(node.commit.hash);
const isSearchMatch = normalizedSearch ? matchedHashSet.has(node.commit.hash) : false;
Expand Down Expand Up @@ -1195,6 +1229,7 @@ export const CommitGraph: React.FC<CommitGraphProps> = ({
</div>
);
})}
{bottomSpacerHeight > 0 && <div style={{ height: bottomSpacerHeight }} aria-hidden="true" />}
{(loadingMore || hasMoreCommits) && (
<div style={{ display: 'flex', justifyContent: 'center', padding: '12px 0 18px', paddingLeft: graphWidth }}>
<button
Expand Down
37 changes: 35 additions & 2 deletions src/components/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,28 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ repoPath, request, onClo
</div>
</div>

{isLoading && <div className="diff-empty-state">{tr('Diff wird geladen...', 'Loading diff...')}</div>}
{isLoading && (
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: '5px' }}>
{Array.from({ length: 10 }).map((_, i) => {
const isAdd = i % 5 === 1;
const isDel = i % 5 === 3;
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '8px', opacity: 1 - i * 0.07 }}>
<div className="skeleton-line" style={{ width: 28, height: 9, borderRadius: 3, flexShrink: 0, opacity: 0.5 }} />
<div
className="skeleton-line"
style={{
height: 9,
width: `${30 + (i * 7) % 55}%`,
borderRadius: 3,
background: isAdd ? 'rgba(79,174,148,0.2)' : isDel ? 'rgba(211,93,105,0.2)' : undefined,
}}
/>
</div>
);
})}
</div>
)}
{error && !isLoading && <div className="diff-empty-state error">{error}</div>}
{!isLoading && !error && !diffText.trim() && <div className="diff-empty-state">{tr('Keine Unterschiede vorhanden.', 'No differences found.')}</div>}

Expand All @@ -437,7 +458,19 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ repoPath, request, onClo
<div className="diff-content-scroll">
{isTooLarge && (
<div className="diff-large-warning">
{tr('Großer Diff erkannt. Anzeige wurde aus Performance-Gründen gekürzt.', 'Large diff detected. Output was truncated for performance.')}
<span>
{tr(
`Großer Diff: ${diffText.split('\n').length.toLocaleString()} Zeilen – Anzeige auf ${MAX_RENDER_LINES.toLocaleString()} Zeilen gekürzt.`,
`Large diff: ${diffText.split('\n').length.toLocaleString()} lines – display truncated to ${MAX_RENDER_LINES.toLocaleString()} lines.`
)}
</span>
<button
className="diff-large-warning-copy"
onClick={() => navigator.clipboard.writeText(diffText)}
title={tr('Vollständigen Diff in Zwischenablage kopieren', 'Copy full diff to clipboard')}
>
{tr('Vollständig kopieren', 'Copy full diff')}
</button>
</div>
)}

Expand Down
Loading
Loading