Add CHM viewer plugin (WebView2-based)

This commit does not implement the file link
This commit is contained in:
ema
2026-04-28 02:29:02 +08:00
parent a6236884aa
commit 92c2d9ed4c
6 changed files with 301 additions and 7 deletions
@@ -0,0 +1,8 @@
# Ignore HTML files in Resources and subdirectories
Resources/**/*.html linguist-vendored
# Ignore CSS files in Resources and subdirectories
Resources/**/*.css linguist-vendored
# Ignore JS files in Resources and subdirectories
Resources/**/*.js linguist-vendored
@@ -0,0 +1,125 @@
// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using QuickLook.Plugin.HtmlViewer;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace QuickLook.Plugin.ChmViewer;
public class ChmWebpagePanel : WebpagePanel
{
private const string _resourcePrefix = "QuickLook.Plugin.ChmViewer.Resources.";
private static readonly Dictionary<string, byte[]> _resources = [];
private readonly byte[] _homePage;
static ChmWebpagePanel()
{
InitializeResources();
}
public ChmWebpagePanel()
{
_homePage = ReadResource("/chm2html.html") ?? [];
}
private static void InitializeResources()
{
if (_resources.Any())
return;
var assembly = Assembly.GetExecutingAssembly();
foreach (var resourceName in assembly.GetManifestResourceNames())
{
if (!resourceName.StartsWith(_resourcePrefix, StringComparison.Ordinal))
continue;
var relativePath = resourceName.Substring(_resourcePrefix.Length).Replace('\\', '/');
if (string.IsNullOrEmpty(relativePath))
continue;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
continue;
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
_resources[$"/{relativePath}"] = memoryStream.ToArray();
}
}
public void PreviewCompiledHtmlHelp(string path)
{
FallbackPath = Path.GetDirectoryName(path);
var chmFileUrl = Helper.FilePathToFileUrl(path);
var pluginUri = new Uri($"file://quicklook/?plugin=1&chm={Uri.EscapeDataString(chmFileUrl.AbsoluteUri)}");
NavigateToUri(pluginUri);
}
protected override void WebView_WebResourceRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs args)
{
try
{
if (string.IsNullOrWhiteSpace(args.Request.Uri))
{
base.WebView_WebResourceRequested(sender, args);
return;
}
var requestedUri = new Uri(args.Request.Uri);
if (requestedUri.Scheme != "file")
{
base.WebView_WebResourceRequested(sender, args);
return;
}
var requestedPath = Uri.UnescapeDataString(requestedUri.AbsolutePath);
if (requestedPath == "/")
{
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
new MemoryStream(_homePage), 200, "OK", MimeTypes.GetContentType(".html"));
args.Response = response;
return;
}
if (ContainsKey(requestedPath))
{
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
ReadStream(requestedPath), 200, "OK", MimeTypes.GetContentType(Path.GetExtension(requestedPath)));
args.Response = response;
return;
}
base.WebView_WebResourceRequested(sender, args);
}
catch
{
base.WebView_WebResourceRequested(sender, args);
}
}
private static bool ContainsKey(string key) => _resources.ContainsKey(key);
private static Stream ReadStream(string key) => new MemoryStream(_resources[key]);
private static byte[] ReadResource(string key) => _resources.TryGetValue(key, out var value) ? value : null;
}
@@ -0,0 +1,52 @@
// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.IO;
using System.Text;
namespace QuickLook.Plugin.ChmViewer;
internal static class Helper
{
public static Uri FilePathToFileUrl(string filePath)
{
var uri = new StringBuilder();
foreach (var v in filePath)
if (v >= 'a' && v <= 'z' || v >= 'A' && v <= 'Z' || v >= '0' && v <= '9' ||
v == '+' || v == '/' || v == ':' || v == '.' || v == '-' || v == '_' || v == '~' ||
v > '\x80')
uri.Append(v);
else if (v == Path.DirectorySeparatorChar || v == Path.AltDirectorySeparatorChar)
uri.Append('/');
else
uri.Append($"%{(int)v:X2}");
if (uri.Length >= 2 && uri[0] == '/' && uri[1] == '/') // UNC path - Universal Naming Convention
uri.Insert(0, "file:");
else
uri.Insert(0, "file:///");
try
{
return new Uri(uri.ToString());
}
catch
{
return null;
}
}
}
@@ -24,7 +24,13 @@ namespace QuickLook.Plugin.ChmViewer;
public sealed class Plugin : IViewer
{
public int Priority => 0;
private ChmWebpagePanel _panel;
/// <summary>
/// The implementation of this plugin is better than following
/// https://github.com/emako/QuickLook.Plugin.SumatraPDFReader
/// </summary>
public int Priority => 2;
public void Init()
{
@@ -32,26 +38,29 @@ public sealed class Plugin : IViewer
public bool CanHandle(string path)
{
return false;
#pragma warning disable CS0162 // Unreachable code detected
return !Directory.Exists(path)
&& Path.GetExtension(path).Equals(".chm", StringComparison.OrdinalIgnoreCase);
#pragma warning restore CS0162 // Unreachable code detected
}
public void Prepare(string path, ContextObject context)
{
context.Title = Path.GetFileName(path);
context.IsBlocked = true;
context.PreferredSize = new Size { Width = 800, Height = 600 };
context.PreferredSize = new Size(1280, 720);
}
public void View(string path, ContextObject context)
{
_panel = new ChmWebpagePanel();
_panel.PreviewCompiledHtmlHelp(path);
context.ViewerContent = _panel;
context.Title = Path.GetFileName(path);
context.IsBusy = false;
}
public void Cleanup()
{
_panel?.Dispose();
_panel = null;
GC.SuppressFinalize(this);
}
}
@@ -76,6 +76,21 @@
<Name>QuickLook.Common</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\QuickLook.Plugin.HtmlViewer\QuickLook.Plugin.HtmlViewer.csproj">
<Project>{CE22A1F3-7F2C-4EC8-BFDE-B58D0EB625FC}</Project>
<Name>QuickLook.Plugin.HtmlViewer</Name>
<Private>False</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3912.50">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\chm2html.html" />
</ItemGroup>
</Project>
File diff suppressed because one or more lines are too long