Files
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

253 lines
8.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using PCL.Core.Utils;
using PCL.Core.IO.Net.Http;
namespace PCL;
public class MyImage : Image
{
public MyImage()
{
Initialized += (_, _) => Load();
SizeChanged += (_, _) => UpdateClip();
}
/// <summary>
/// 实际被呈现的图片地址。
/// </summary>
public string ActualSource
{
get => field;
set
{
if (string.IsNullOrEmpty(value))
value = null;
if ((field ?? "") == (value ?? ""))
return;
field = value;
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
try
{
ImageSource bitmap = value is null ? null : await Task.Run(() => new MyBitmap(value));
base.Source = bitmap;
}
catch (Exception ex)
{
ModBase.Log(ex, $"加载图片失败({value}");
try
{
if (value.StartsWithF(ModBase.pathTemp) && File.Exists(value)) File.Delete(value);
}
catch
{
}
}
})); // 在这里先触发可能的文件读取,尽量避免在 UI 线程中读取文件
// ignored
}
}
private void Load() // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
{
if (Source is null)
{
ActualSource = null;
return;
}
if (!Source.StartsWithF("http"))
{
ActualSource = Source;
return;
}
var url = Source;
var tempPath = GetTempPath(url);
var tempFile = new FileInfo(tempPath);
var enableCache = this.EnableCache;
if (enableCache && tempFile.Exists)
{
ActualSource = tempPath;
if (DateTime.Now - tempFile.LastWriteTime < fileCacheExpiredTime)
return; // 无需刷新缓存
}
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
try
{
// 下载
ActualSource = LoadingSource;
var resp = await DownloadImageAsync(url);
if (!string.IsNullOrEmpty(resp))
{
ActualSource = resp;
return;
}
resp = await DownloadImageAsync(FallbackSource);
if (!string.IsNullOrEmpty(resp))
{
ActualSource = resp;
return;
}
}
catch (Exception ex)
{
// 更换备用地址
ModBase.Log(ex, $"Online image get failsource = {url}, fallback = {FallbackSource}", ModBase.LogLevel.Developer);
tempPath = GetTempPath(url);
tempFile = new FileInfo(tempPath);
if (enableCache && tempFile.Exists)
{
ActualSource = tempPath;
if (DateTime.Now - tempFile.LastWriteTime < fileCacheExpiredTime)
return;
}
}
}));
}
public static Task<string> DownloadImageAsync(string url)
{
return _downloadTasks.GetOrAdd(url, key =>
{
var t = DownloadImageInternalAsync(key);
t.ContinueWith(_ => _downloadTasks.TryRemove(url, out _));
return t;
});
}
public static string GetTempPath(string url)
{
return Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{ModBase.GetStringMD5(url)}.png");
}
private static readonly ConcurrentDictionary<string, Task<string>> _downloadTasks = new();
private static async Task<string> DownloadImageInternalAsync(string url)
{
var tempPath = GetTempPath(url);
var tempDownloadingPath = tempPath + RandomUtils.NextInt(0, 1000000);
try
{
Directory.CreateDirectory(ModBase.GetPathFromFullPath(tempPath)); // 重新实现下载,以避免携带 Header(#5072
using (var fs = new FileStream(tempDownloadingPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
{
using (var response = await HttpRequest.Create(url)
.SendAsync(addMetedata: false))
{
response.EnsureSuccessStatusCode();
using (var nfs = await response.AsStreamAsync())
{
fs.SetLength(0L);
await nfs.CopyToAsync(fs);
}
}
}
File.Move(tempDownloadingPath, tempPath, true);
return tempPath;
}
catch (Exception ex)
{
if (File.Exists(tempPath)) File.Delete(tempPath);
if (File.Exists(tempDownloadingPath)) File.Delete(tempDownloadingPath);
ModBase.Log(ex, $"Try to get online image fail (url = {url}, dest = {tempPath})");
return string.Empty;
}
}
#region 公开属性
public TimeSpan fileCacheExpiredTime = TimeSpan.FromDays(14d);
public bool EnableCache
{
get => (bool)GetValue(EnableCacheProperty);
set => SetValue(EnableCacheProperty, value);
}
public new static readonly DependencyProperty EnableCacheProperty =
DependencyProperty.Register("EnableCache", typeof(bool), typeof(MyImage), new PropertyMetadata(true));
/// <summary>
/// 与 Image 的 Source 类似。
/// 若输入以 http 开头的字符串,则会尝试下载图片然后显示,图片会保存为本地缓存。
/// 支持 WebP 格式的图片。
/// </summary>
public new string Source // 覆写 Image 的 Source 属性
{
get => field;
set
{
if (string.IsNullOrEmpty(value))
value = null;
if ((field ?? "") == (value ?? ""))
return;
field = value;
if (!IsInitialized)
return; // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
Load();
}
} = "";
public new static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(string),
typeof(MyImage), new PropertyMetadata((sender, e) =>
{
if (sender is not null) ((MyImage)sender).Source = e.NewValue.ToString();
}));
/// <summary>
/// 当 Source 首次下载失败时,会从该备用地址加载图片。
/// </summary>
public string FallbackSource { get; set; }
/// <summary>
/// 正在下载网络图片时显示的本地图片。
/// </summary>
public string LoadingSource { get; set; } = "pack://application:,,,/images/Icons/NoIcon.png";
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
private static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(
"CornerRadius",
typeof(CornerRadius),
typeof(MyImage),
new FrameworkPropertyMetadata(
new CornerRadius(-1),
OnCornerRadiusChanged)
);
private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((MyImage)d).UpdateClip();
}
private void UpdateClip() // Handles Me.SizeChanged will be added separately
{
if (ActualWidth > 0 && ActualHeight > 0 &&
CornerRadius.TopLeft >= 0 && CornerRadius.TopRight >= 0)
{
Clip = new RectangleGeometry(
new Rect(0, 0, ActualWidth, ActualHeight),
CornerRadius.TopLeft,
CornerRadius.TopRight);
}
}
#endregion
}