mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
df81d41d95
commit
f0c0b9db94
6 files changed
+189
-4
No files matched your search
@@ -202,6 +202,12 @@
|
|||||||
<div class="about-row"><span>桌面软件</span><a href="https://ofdgo.aite.me/releases/" target="_blank" rel="noreferrer">https://ofdgo.aite.me/releases/</a></div>
|
<div class="about-row"><span>桌面软件</span><a href="https://ofdgo.aite.me/releases/" target="_blank" rel="noreferrer">https://ofdgo.aite.me/releases/</a></div>
|
||||||
<div class="about-row"><span>项目仓库</span><a href="https://github.com/xiaoqidun/ofdgo" target="_blank" rel="noreferrer">https://github.com/xiaoqidun/ofdgo</a></div>
|
<div class="about-row"><span>项目仓库</span><a href="https://github.com/xiaoqidun/ofdgo" target="_blank" rel="noreferrer">https://github.com/xiaoqidun/ofdgo</a></div>
|
||||||
</form>
|
</form>
|
||||||
|
<section id="associationSection" class="about-associations" hidden>
|
||||||
|
<div class="association-heading"><span>文件关联</span><span id="associationPermission" hidden>所有用户需管理员权限</span></div>
|
||||||
|
<div class="association-row" data-scope="user"><span>当前用户</span><span class="association-state"></span><div class="association-actions"><button class="small-button" type="button" data-action="register">注册</button><button class="small-button" type="button" data-action="remove">撤销</button></div></div>
|
||||||
|
<div class="association-row" data-scope="machine"><span>所有用户</span><span class="association-state"></span><div class="association-actions"><button class="small-button" type="button" data-action="register">注册</button><button class="small-button" type="button" data-action="remove">撤销</button></div></div>
|
||||||
|
<div id="associationMessage" class="form-status" hidden></div>
|
||||||
|
</section>
|
||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="closePanel" class="form-dialog confirm-dialog">
|
<dialog id="closePanel" class="form-dialog confirm-dialog">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
|
|||||||
@@ -964,6 +964,54 @@ button:not(:disabled):hover {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.about-associations {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.association-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#associationPermission {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.association-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4em minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.association-row + .association-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.association-state {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.association-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#associationMessage {
|
||||||
|
margin-top: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.form-title {
|
.form-title {
|
||||||
margin: -12px -12px 12px;
|
margin: -12px -12px 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ const el = {
|
|||||||
aboutButton: document.querySelector("#aboutButton"),
|
aboutButton: document.querySelector("#aboutButton"),
|
||||||
aboutPanel: document.querySelector("#aboutPanel"),
|
aboutPanel: document.querySelector("#aboutPanel"),
|
||||||
aboutVersion: document.querySelector("#aboutVersion"),
|
aboutVersion: document.querySelector("#aboutVersion"),
|
||||||
|
associationSection: document.querySelector("#associationSection"),
|
||||||
|
associationPermission: document.querySelector("#associationPermission"),
|
||||||
|
associationMessage: document.querySelector("#associationMessage"),
|
||||||
signButton: document.querySelector("#signButton"),
|
signButton: document.querySelector("#signButton"),
|
||||||
signPanel: document.querySelector("#signPanel"),
|
signPanel: document.querySelector("#signPanel"),
|
||||||
signForm: document.querySelector("#signForm"),
|
signForm: document.querySelector("#signForm"),
|
||||||
@@ -1499,6 +1502,7 @@ el.pageForm.addEventListener("submit", async (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
el.aboutButton.addEventListener("click", openAboutPanel);
|
el.aboutButton.addEventListener("click", openAboutPanel);
|
||||||
|
el.associationSection.addEventListener("click", changeFileAssociation);
|
||||||
let aboutBackdrop = false;
|
let aboutBackdrop = false;
|
||||||
el.aboutPanel.addEventListener("pointerdown", event => { aboutBackdrop = event.target === el.aboutPanel; });
|
el.aboutPanel.addEventListener("pointerdown", event => { aboutBackdrop = event.target === el.aboutPanel; });
|
||||||
el.aboutPanel.addEventListener("click", event => {
|
el.aboutPanel.addEventListener("click", event => {
|
||||||
@@ -2017,7 +2021,9 @@ function formDialogOpen() {
|
|||||||
|
|
||||||
async function openAboutPanel() {
|
async function openAboutPanel() {
|
||||||
el.aboutVersion.textContent = "读取中";
|
el.aboutVersion.textContent = "读取中";
|
||||||
|
el.associationMessage.hidden = true;
|
||||||
documentDialogs.showApplication(el.aboutPanel);
|
documentDialogs.showApplication(el.aboutPanel);
|
||||||
|
if (nativeHost) void refreshFileAssociations();
|
||||||
let raw = "";
|
let raw = "";
|
||||||
try {
|
try {
|
||||||
const response = await fetch("./", { cache: "no-store" });
|
const response = await fetch("./", { cache: "no-store" });
|
||||||
@@ -2041,6 +2047,56 @@ async function openAboutPanel() {
|
|||||||
el.aboutVersion.textContent = `${date.toISOString().slice(0, 10).replaceAll("-", "")}-${match[2].slice(0, 7)}`;
|
el.aboutVersion.textContent = `${date.toISOString().slice(0, 10).replaceAll("-", "")}-${match[2].slice(0, 7)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshFileAssociations() {
|
||||||
|
if (!nativeHost || el.associationSection.dataset.busy) return;
|
||||||
|
el.associationSection.dataset.busy = "true";
|
||||||
|
for (const control of el.associationSection.querySelectorAll("button")) control.disabled = true;
|
||||||
|
for (const label of el.associationSection.querySelectorAll(".association-state")) {
|
||||||
|
label.textContent = "查询中";
|
||||||
|
label.title = "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const states = await nativeHost.call("association.query", {});
|
||||||
|
el.associationSection.hidden = !states.user.supported;
|
||||||
|
el.associationPermission.hidden = !states.machine.supported || states.machine.writable;
|
||||||
|
for (const row of el.associationSection.querySelectorAll(".association-row")) {
|
||||||
|
const state = states[row.dataset.scope];
|
||||||
|
const label = row.querySelector(".association-state");
|
||||||
|
label.textContent = state.error ? "不可用" : state.complete ? "已注册" : state.registered ? "待更新" : "未注册";
|
||||||
|
label.title = state.error || "";
|
||||||
|
row.querySelector('[data-action="register"]').disabled = !state.writable || !!state.error || state.complete;
|
||||||
|
row.querySelector('[data-action="remove"]').disabled = !state.writable || !!state.error || !state.registered;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
el.associationSection.hidden = false;
|
||||||
|
el.associationPermission.hidden = true;
|
||||||
|
for (const label of el.associationSection.querySelectorAll(".association-state")) label.textContent = "不可用";
|
||||||
|
el.associationMessage.textContent = `查询失败:${error.message}`;
|
||||||
|
el.associationMessage.hidden = false;
|
||||||
|
} finally {
|
||||||
|
delete el.associationSection.dataset.busy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeFileAssociation(event) {
|
||||||
|
const button = event.target.closest("button[data-action]");
|
||||||
|
if (!nativeHost || !button || button.disabled || el.associationSection.dataset.busy) return;
|
||||||
|
const row = button.closest(".association-row");
|
||||||
|
el.associationSection.dataset.busy = "true";
|
||||||
|
for (const control of el.associationSection.querySelectorAll("button")) control.disabled = true;
|
||||||
|
el.associationMessage.hidden = true;
|
||||||
|
row.querySelector(".association-state").textContent = button.dataset.action === "register" ? "注册中" : "撤销中";
|
||||||
|
try {
|
||||||
|
await nativeHost.call(`association.${button.dataset.action}`, {scope:row.dataset.scope});
|
||||||
|
} catch (error) {
|
||||||
|
el.associationMessage.textContent = `${row.firstElementChild.textContent}:${error.message}`;
|
||||||
|
el.associationMessage.hidden = false;
|
||||||
|
} finally {
|
||||||
|
delete el.associationSection.dataset.busy;
|
||||||
|
await refreshFileAssociations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setObjectDimension(input, value) {
|
function setObjectDimension(input, value) {
|
||||||
input.value = String(Number(value.toFixed(2)));
|
input.value = String(Number(value.toFixed(2)));
|
||||||
input.defaultValue = input.value;
|
input.defaultValue = input.value;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ require (
|
|||||||
github.com/go-text/typesetting v0.3.5
|
github.com/go-text/typesetting v0.3.5
|
||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
|
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
|
||||||
github.com/xiaoqidun/guigo v0.0.0-20261025062141-103903c8c889
|
github.com/xiaoqidun/guigo v0.0.0-20261025072734-26f8f43c3631
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2
|
github.com/xiaoqidun/j2kgo v1.0.2
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8
|
github.com/xiaoqidun/jbig2 v1.1.8
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261023171626-3473b442eed5
|
github.com/xiaoqidun/pdfgo v0.0.0-20261023171626-3473b442eed5
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
|
|||||||
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/guigo v0.0.0-20261025062141-103903c8c889 h1:0R5ejN3yEatqBUSgavo5V+XBXfaqTNlhuyYBVCyM6iI=
|
github.com/xiaoqidun/guigo v0.0.0-20261025072734-26f8f43c3631 h1:iAIPGYhNvmA/eodRZqChfK4jqXKbABQfDtO3mwCrSWc=
|
||||||
github.com/xiaoqidun/guigo v0.0.0-20261025062141-103903c8c889/go.mod h1:xJF8xE6Bjw+NcOZNvWJEVX/XXIkngs3ltc9F6vNcLls=
|
github.com/xiaoqidun/guigo v0.0.0-20261025072734-26f8f43c3631/go.mod h1:bXz+dPMVntPIVwePTTuMXt8Q0z1YoUGuROQSsXyUEfY=
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw=
|
github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw=
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ type Host struct {
|
|||||||
closeAllowed bool
|
closeAllowed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// associationState 保存单个范围的登记状态和查询错误
|
||||||
|
type associationState struct {
|
||||||
|
guigo.AssociationStatus
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// engineClose 保存并发关闭共享的完成通知及结果
|
// engineClose 保存并发关闭共享的完成通知及结果
|
||||||
type engineClose struct {
|
type engineClose struct {
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
@@ -104,7 +110,7 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
|
|||||||
h.conversionPool = h.resources.ConversionPool()
|
h.conversionPool = h.resources.ConversionPool()
|
||||||
defer h.close()
|
defer h.close()
|
||||||
bindings := make(map[string]guigo.HandlerFunc)
|
bindings := make(map[string]guigo.HandlerFunc)
|
||||||
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close", "update.check", "update.open", "print.capabilities", "print.dialog", "print.printers", "print.query", "print.properties", "print"} {
|
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close", "update.check", "update.open", "association.query", "association.register", "association.remove", "print.capabilities", "print.dialog", "print.printers", "print.query", "print.properties", "print"} {
|
||||||
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
|
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
|
||||||
}
|
}
|
||||||
static := http.FileServerFS(webuiassets.NativeFS)
|
static := http.FileServerFS(webuiassets.NativeFS)
|
||||||
@@ -370,6 +376,10 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
|||||||
return applicationUpdate(ctx, build.Version, applicationVersionURL)
|
return applicationUpdate(ctx, build.Version, applicationVersionURL)
|
||||||
case "update.open":
|
case "update.open":
|
||||||
return nil, guigo.OpenURL(applicationDownloadURL)
|
return nil, guigo.OpenURL(applicationDownloadURL)
|
||||||
|
case "association.query":
|
||||||
|
return queryAssociations()
|
||||||
|
case "association.register", "association.remove":
|
||||||
|
return nil, changeAssociations(text("scope"), name == "association.remove")
|
||||||
case "engine.close":
|
case "engine.close":
|
||||||
return nil, h.closeEngine(text("engine"))
|
return nil, h.closeEngine(text("engine"))
|
||||||
case "engine.finish":
|
case "engine.finish":
|
||||||
@@ -632,6 +642,71 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
|||||||
return nil, fmt.Errorf("未知宿主操作")
|
return nil, fmt.Errorf("未知宿主操作")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// associationOptions 提供Windows文件关联的固定应用配置
|
||||||
|
// 返回: guigo.AssociationOptions 应用配置, error 路径错误
|
||||||
|
func associationOptions() (guigo.AssociationOptions, error) {
|
||||||
|
executable, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return guigo.AssociationOptions{}, err
|
||||||
|
}
|
||||||
|
return guigo.AssociationOptions{
|
||||||
|
AppID: "OFDGo", Name: "OFDGo", Description: "OFD、PDF阅读与编辑工具",
|
||||||
|
Executable: executable, Arguments: []string{"--"},
|
||||||
|
Types: []guigo.FileAssociation{
|
||||||
|
{ID: "OFD", Name: "OFD文档", Extensions: []string{".ofd"}},
|
||||||
|
{ID: "PDF", Name: "PDF文档", Extensions: []string{".pdf"}},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryAssociations 分别查询两个范围,不因其中一个失败而隐藏另一个
|
||||||
|
// 返回: any 两个范围的状态, error 配置错误
|
||||||
|
func queryAssociations() (any, error) {
|
||||||
|
options, err := associationOptions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make(map[string]associationState)
|
||||||
|
for _, scope := range []guigo.AssociationScope{guigo.AssociationUser, guigo.AssociationMachine} {
|
||||||
|
state, err := guigo.QueryFileAssociations(options, scope)
|
||||||
|
row := associationState{AssociationStatus: state}
|
||||||
|
if err != nil {
|
||||||
|
row.Error = associationError(err).Error()
|
||||||
|
}
|
||||||
|
result[string(scope)] = row
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeAssociations 执行固定应用配置的单范围注册或撤销
|
||||||
|
// 入参: scope 作用范围, remove 是否撤销
|
||||||
|
// 返回: error 操作错误
|
||||||
|
func changeAssociations(scope string, remove bool) error {
|
||||||
|
options, err := associationOptions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
err = guigo.UnregisterFileAssociations(options, guigo.AssociationScope(scope))
|
||||||
|
} else {
|
||||||
|
err = guigo.RegisterFileAssociations(options, guigo.AssociationScope(scope))
|
||||||
|
}
|
||||||
|
return associationError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// associationError 区分未提升权限和实际访问失败,提供简短提示
|
||||||
|
// 入参: err 操作错误
|
||||||
|
// 返回: error 用户提示或原始错误
|
||||||
|
func associationError(err error) error {
|
||||||
|
if errors.Is(err, guigo.ErrAssociationElevationRequired) {
|
||||||
|
return errors.New("需管理员权限")
|
||||||
|
}
|
||||||
|
if errors.Is(err, os.ErrPermission) {
|
||||||
|
return errors.New("无权访问")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// applicationUpdate 比较官网和当前程序的完整构建版本
|
// applicationUpdate 比较官网和当前程序的完整构建版本
|
||||||
// 入参: ctx 请求上下文, version 当前版本, endpoint 官网地址
|
// 入参: ctx 请求上下文, version 当前版本, endpoint 官网地址
|
||||||
// 返回: bool 是否有更新, error 检查错误
|
// 返回: bool 是否有更新, error 检查错误
|
||||||
|
|||||||
Reference in new issue
Block a user