Skip to content

Commit 5f55e54

Browse files
committed
feature: add a built-in hex-viewer to preview content of a binary file (#2593)
Signed-off-by: leo <longshuang@msn.cn>
1 parent 5f611d9 commit 5f55e54

10 files changed

Lines changed: 470 additions & 9 deletions

File tree

src/Models/RevisionFile.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ namespace SourceGit.Models
66
{
77
public class RevisionBinaryFile
88
{
9+
public string Repository { get; set; } = string.Empty;
10+
public string File { get; set; } = string.Empty;
11+
public string Revision { get; set; } = string.Empty;
912
public long Size { get; set; } = 0;
13+
14+
public RevisionBinaryFile(string repo, string file, string revision, long size)
15+
{
16+
Repository = repo;
17+
File = file;
18+
Revision = revision;
19+
Size = size;
20+
}
1021
}
1122

1223
public class RevisionImageFile

src/Resources/Locales/en_US.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@
420420
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">Fetch without tags</x:String>
421421
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">Remote:</x:String>
422422
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">Fetch Remote Changes</x:String>
423+
<x:String x:Key="Text.File.Binary" xml:space="preserve">BINARY FILE</x:String>
424+
<x:String x:Key="Text.File.HexViewer" xml:space="preserve">HEX VIEWER</x:String>
423425
<x:String x:Key="Text.FileCM.AssumeUnchanged" xml:space="preserve">Assume unchanged</x:String>
424426
<x:String x:Key="Text.FileCM.CustomAction" xml:space="preserve">Custom Action</x:String>
425427
<x:String x:Key="Text.FileCM.Discard" xml:space="preserve">Discard...</x:String>

src/Resources/Locales/zh_CN.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@
424424
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">不拉取远程标签</x:String>
425425
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">远程仓库 :</x:String>
426426
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">拉取远程仓库内容</x:String>
427+
<x:String x:Key="Text.File.Binary" xml:space="preserve">二进制文件</x:String>
428+
<x:String x:Key="Text.File.HexViewer" xml:space="preserve">查看内容</x:String>
427429
<x:String x:Key="Text.FileCM.AssumeUnchanged" xml:space="preserve">不跟踪此文件的更改</x:String>
428430
<x:String x:Key="Text.FileCM.CustomAction" xml:space="preserve">自定义操作</x:String>
429431
<x:String x:Key="Text.FileCM.Discard" xml:space="preserve">放弃更改...</x:String>

src/Resources/Locales/zh_TW.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@
424424
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">不提取遠端標籤</x:String>
425425
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">遠端存放庫:</x:String>
426426
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">提取遠端存放庫內容</x:String>
427+
<x:String x:Key="Text.File.Binary" xml:space="preserve">二進位檔案</x:String>
428+
<x:String x:Key="Text.File.HexViewer" xml:space="preserve">檢視內容</x:String>
427429
<x:String x:Key="Text.FileCM.AssumeUnchanged" xml:space="preserve">不追蹤此檔案的變更</x:String>
428430
<x:String x:Key="Text.FileCM.CustomAction" xml:space="preserve">自訂動作</x:String>
429431
<x:String x:Key="Text.FileCM.Discard" xml:space="preserve">捨棄變更...</x:String>

src/ViewModels/BinaryFile.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using CommunityToolkit.Mvvm.ComponentModel;
5+
6+
namespace SourceGit.ViewModels
7+
{
8+
public class BinaryFile : ObservableObject, IDisposable
9+
{
10+
public long FileSize
11+
{
12+
get => _fileSize;
13+
private set => SetProperty(ref _fileSize, value);
14+
}
15+
16+
public static async Task<BinaryFile> LoadAsync(string repo, string path, string revision = "HEAD")
17+
{
18+
string saveTo = Path.GetTempFileName();
19+
await Commands.SaveRevisionFile.RunAsync(repo, revision, path, saveTo).ConfigureAwait(false);
20+
return new BinaryFile(saveTo);
21+
}
22+
23+
public BinaryFile(string file)
24+
{
25+
_filePath = file;
26+
27+
if (File.Exists(_filePath))
28+
{
29+
_fileSize = new FileInfo(_filePath).Length;
30+
_reader = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BUFFER_SIZE, FileOptions.RandomAccess);
31+
_readedStart = 0;
32+
_readedEnd = Math.Min(_fileSize, BUFFER_SIZE);
33+
34+
if (_fileSize > 0)
35+
{
36+
_reader.Seek(_readedStart, SeekOrigin.Begin);
37+
_reader.ReadExactly(_buffer, 0, (int)_readedEnd);
38+
}
39+
}
40+
}
41+
42+
public void Dispose()
43+
{
44+
_reader?.Dispose();
45+
_reader = null;
46+
47+
if (File.Exists(_filePath))
48+
File.Delete(_filePath);
49+
}
50+
51+
public ArraySegment<byte> Read(long offset, long length)
52+
{
53+
if (_reader == null || _fileSize == 0 || offset >= _fileSize)
54+
return Array.Empty<byte>();
55+
56+
if (length > 8192)
57+
length = 8192;
58+
59+
if (offset + length > _fileSize)
60+
length = _fileSize - offset;
61+
62+
if (_readedStart <= offset && _readedEnd >= offset + length)
63+
return new ArraySegment<byte>(_buffer, (int)(offset - _readedStart), (int)length);
64+
65+
_readedStart = (Math.Max(0, offset - 2048) / 1024) * 1024;
66+
_readedEnd = Math.Min(_readedStart + BUFFER_SIZE, _fileSize);
67+
68+
_reader.Seek(_readedStart, SeekOrigin.Begin);
69+
_reader.ReadExactly(_buffer, 0, (int)(_readedEnd - _readedStart));
70+
71+
return new ArraySegment<byte>(_buffer, (int)(offset - _readedStart), (int)length);
72+
}
73+
74+
private const int BUFFER_SIZE = 16384;
75+
76+
private string _filePath = string.Empty;
77+
private FileStream _reader = null;
78+
private long _fileSize = 0;
79+
private long _readedStart = 0;
80+
private long _readedEnd = 0;
81+
private byte[] _buffer = new byte[BUFFER_SIZE];
82+
}
83+
}

src/ViewModels/CommitDetail.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ private async Task SetViewingBlobAsync(Models.Object file)
688688
else
689689
{
690690
var size = await new Commands.QueryFileSize(_repo.FullPath, file.Path, _commit.SHA).GetResultAsync();
691-
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
691+
ViewRevisionFileContent = new Models.RevisionBinaryFile(_repo.FullPath, file.Path, _commit.SHA, size);
692692
}
693693

694694
return;

src/ViewModels/FileHistories.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private async Task<object> GetRevisionFileContentAsync(Models.Object obj)
127127
}
128128

129129
var size = await new Commands.QueryFileSize(_repo, _file, _revision.SHA).GetResultAsync().ConfigureAwait(false);
130-
var binaryFile = new Models.RevisionBinaryFile() { Size = size };
130+
var binaryFile = new Models.RevisionBinaryFile(_repo, _file, _revision.SHA, size);
131131
return new FileHistoriesRevisionFile(_file, binaryFile, true);
132132
}
133133

src/Views/BinaryFileViewer.axaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:m="using:SourceGit.Models"
6+
xmlns:vm="using:SourceGit.ViewModels"
7+
xmlns:v="using:SourceGit.Views"
8+
xmlns:c="using:SourceGit.Converters"
9+
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
10+
x:Class="SourceGit.Views.BinaryFileViewer">
11+
<UserControl.DataTemplates>
12+
<DataTemplate DataType="m:RevisionBinaryFile">
13+
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
14+
<Path Width="64" Height="64" Data="{StaticResource Icons.Binary}" Fill="{DynamicResource Brush.FG2}"/>
15+
<TextBlock Margin="0,16,0,0" Text="{DynamicResource Text.File.Binary}" FontSize="18" FontWeight="Bold" HorizontalAlignment="Center" Foreground="{DynamicResource Brush.FG2}"/>
16+
<StackPanel Margin="0,8,0,0" Orientation="Horizontal" HorizontalAlignment="Center">
17+
<TextBlock Text="{Binding Size, Converter={x:Static c:LongConverters.ToFileSize}}" Foreground="{DynamicResource Brush.FG2}"/>
18+
</StackPanel>
19+
<Button Classes="flat"
20+
Margin="0,16,0,0"
21+
HorizontalAlignment="Center"
22+
Content="{DynamicResource Text.File.HexViewer}"
23+
Click="OnOpenHexViewer"/>
24+
</StackPanel>
25+
</DataTemplate>
26+
27+
<DataTemplate DataType="m:Null">
28+
<v:LoadingIcon Width="64" Height="64"
29+
HorizontalAlignment="Center" VerticalAlignment="Center"/>
30+
</DataTemplate>
31+
32+
<DataTemplate DataType="vm:BinaryFile">
33+
<Grid ColumnDefinitions="*,Auto" Background="Transparent">
34+
<v:HexViewer Grid.Column="0"
35+
Foreground="{DynamicResource Brush.FG1}"
36+
HeaderForeground="{DynamicResource Brush.Accent}"
37+
ClipToBounds="True"/>
38+
39+
<ScrollBar Grid.Column="1"
40+
Margin="0,24,0,0"
41+
Orientation="Vertical"
42+
AllowAutoHide="False"
43+
SmallChange="18"
44+
ValueChanged="OnScrollBarValueChanged"
45+
SizeChanged="OnScrollBarSizeChanged"/>
46+
</Grid>
47+
</DataTemplate>
48+
</UserControl.DataTemplates>
49+
</UserControl>

0 commit comments

Comments
 (0)