plugins: hide the list under the detail overlay

plugins: block activation when the startup check cannot run

ref #3626
This commit is contained in:
bbedward
2026-09-29 16:57:24 -04:00
parent 53b8de2714
commit 765ffe8564
7 changed files with 125 additions and 95 deletions
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1790713744,
"narHash": "sha256-hDpFEPmzXG/7xHnoRFwQMIEjYDJ59dwjUKsqLR9GGvg=",
"lastModified": 1790715373,
"narHash": "sha256-hiykGTIlWoRLntC4BWTZ/HIfTQ1aABmAvDyu9t3Sb+A=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "334c32a814b5129d47ca757c29db3071df5620ea",
"rev": "2d206c20d102fb77edf35a41b098f9cc086d1acf",
"type": "github"
},
"original": {
+4 -18
View File
@@ -846,7 +846,9 @@ RegistryBrowserWindow {
}
]
overlay: Rectangle {
overlayActive: detailPluginId !== ""
overlay: Item {
id: detailPane
property var plugin: ({})
@@ -859,23 +861,7 @@ RegistryBrowserWindow {
plugin = livePlugin;
}
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 0
anchors.bottom: parent.bottom
z: 10
color: Theme.floatingWindowSurface
opacity: root.detailPluginId !== "" ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
anchors.fill: parent
Connections {
target: root
@@ -19,7 +19,8 @@ DankFloatingWindow {
property alias aboveSearch: aboveSearchSlot.data
property alias belowSearch: belowSearchSlot.data
property alias listContent: listSlot.data
property alias overlay: browserContent.data
property alias overlay: overlaySlot.data
property bool overlayActive: false
readonly property alias installConfirm: urlInstallConfirm
readonly property alias listArea: listSlot
@@ -189,58 +190,80 @@ DankFloatingWindow {
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingL
Column {
id: aboveSearchSlot
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Theme.spacingS
}
Item {
id: browserBody
anchors.fill: parent
opacity: 1 - overlaySlot.opacity
visible: opacity > 0
DankSearchField {
id: browserSearchField
anchors.left: parent.left
anchors.right: parent.right
anchors.top: aboveSearchSlot.bottom
anchors.topMargin: aboveSearchSlot.height > 0 ? Theme.spacingM : 0
height: Theme.fieldHeightLarge
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: root.searchPlaceholder
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [browserKeyHandler]
onTextEdited: {
root.searchQuery = text;
root.applySearch();
Column {
id: aboveSearchSlot
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Theme.spacingS
}
DankSearchField {
id: browserSearchField
anchors.left: parent.left
anchors.right: parent.right
anchors.top: aboveSearchSlot.bottom
anchors.topMargin: aboveSearchSlot.height > 0 ? Theme.spacingM : 0
height: Theme.fieldHeightLarge
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: root.searchPlaceholder
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [browserKeyHandler]
onTextEdited: {
root.searchQuery = text;
root.applySearch();
}
}
Column {
id: belowSearchSlot
anchors.left: parent.left
anchors.right: parent.right
anchors.top: browserSearchField.bottom
anchors.topMargin: height > 0 ? Theme.spacingM : 0
spacing: Theme.spacingS
}
Item {
id: listSlot
anchors.left: parent.left
anchors.right: parent.right
anchors.top: belowSearchSlot.bottom
anchors.topMargin: Theme.spacingM
anchors.bottom: parent.bottom
Item {
anchors.fill: parent
visible: root.isLoading
DankSpinner {
anchors.centerIn: parent
running: root.isLoading
}
}
}
}
Column {
id: belowSearchSlot
anchors.left: parent.left
anchors.right: parent.right
anchors.top: browserSearchField.bottom
anchors.topMargin: height > 0 ? Theme.spacingM : 0
spacing: Theme.spacingS
}
Item {
id: listSlot
anchors.left: parent.left
anchors.right: parent.right
anchors.top: belowSearchSlot.bottom
anchors.topMargin: Theme.spacingM
anchors.bottom: parent.bottom
id: overlaySlot
anchors.fill: parent
opacity: root.overlayActive ? 1 : 0
visible: opacity > 0
Item {
anchors.fill: parent
visible: root.isLoading
DankSpinner {
anchors.centerIn: parent
running: root.isLoading
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
+1 -1
View File
@@ -722,7 +722,7 @@ QtObject {
}
```
A synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`.
A synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). A check that fails to load, has no `check` function, or throws blocks activation the same way, with its error as the message. The check also runs again on reload. Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`.
## Plugin Global Variables
+19 -25
View File
@@ -893,10 +893,8 @@ Singleton {
function _makeStartupCheckObject(pluginId, plugin) {
const comp = Qt.createComponent(pluginComponentUrl(pluginId, plugin.startupCheckPath), Component.PreferSynchronous);
if (comp.status === Component.Error) {
log.error("startupCheck component error", pluginId, comp.errorString());
return null;
}
if (comp.status === Component.Error)
throw new Error(comp.errorString());
return comp.createObject(root);
}
@@ -915,8 +913,12 @@ Singleton {
return ok;
}
const probe = _makeStartupCheckObject(pluginId, plugin);
let probe = null;
let settled = false;
const finish = result => {
if (settled)
return;
settled = true;
if (probe)
probe.destroy();
const err = _normalizeStartupError(result);
@@ -937,28 +939,20 @@ Singleton {
onResult(ok);
};
const check = probe ? probe.check : null;
if (typeof check !== "function") {
finish(null);
return true;
}
if (check.length >= 1) {
try {
check(finish);
} catch (e) {
log.warn("startupCheck threw for", pluginId, e.message);
finish(null);
}
return true;
}
let r = null;
try {
r = check();
probe = _makeStartupCheckObject(pluginId, plugin);
const check = probe?.check;
if (typeof check !== "function")
throw new Error("startupCheck has no check function");
if (check.length >= 1)
check(finish);
else
finish(check());
} catch (e) {
log.warn("startupCheck threw for", pluginId, e.message);
r = null;
const message = String(e?.message || e);
log.warn("startupCheck failed for", pluginId, message);
finish(message);
}
finish(r);
return true;
}
@@ -973,7 +967,7 @@ Singleton {
const plugin = availablePlugins[pluginId];
if (plugin)
_loadPluginTranslations(pluginId, plugin.pluginDirectory);
return loadPlugin(pluginId);
return runStartupGate(pluginId);
}
function ensureLauncherInstance(pluginId) {
+27
View File
@@ -176,6 +176,33 @@ test("updated startup checks still prevent activation on failure", () => {
context.forceRescanPlugin("fixture");
pending.shift()();
assert.equal(context.isPluginLoaded("fixture"), true);
files.set("file:///plugins/fixture/Check.qml", {
check: () => "dependency removed",
destroy() {}
});
context.reloadPlugin("fixture");
assert.equal(context.isPluginLoaded("fixture"), false);
assert.equal(failures.at(-1), "dependency removed");
});
test("startup checks that cannot run prevent activation", () => {
const { context, files, pending, manifest, failures } = service();
const createComponent = context.Qt.createComponent;
context.Qt.createComponent = url => url.startsWith("file:///plugins/fixture/Broken.qml") ? { status: 3, errorString: () => "module missing" } : createComponent(url);
const cases = [
["./Broken.qml", null, "module missing"],
["./Check.qml", { destroy() {} }, "startupCheck has no check function"],
["./Check.qml", { check() { throw new Error("sync throw"); }, destroy() {} }, "sync throw"],
["./Check.qml", { check(done) { throw new Error("async throw"); }, destroy() {} }, "async throw"]
];
for (const [path, probe, error] of cases) {
manifest.startupCheck = path;
files.set("file:///plugins/fixture/Check.qml", probe);
context.forceRescanPlugin("fixture");
pending.shift()();
assert.equal(context.isPluginLoaded("fixture"), false, error);
assert.equal(failures.at(-1), error);
}
});
test("composite updates refresh every surface without creating daemons synchronously", () => {