mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-09 18:27:00 +00:00
Implement plugin sorting functionality
Add support for sorting plugins by status, name, or source in both ascending and descending order.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Plugins</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
<link rel="stylesheet" href="./plugin-sort.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../include/global.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../css/common.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../css/theme.css" />
|
||||
@@ -14,6 +15,7 @@
|
||||
<script type="text/javascript" src="../js/globalapi.js"></script>
|
||||
<script type="text/javascript" src="../js/common.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<script src="./plugin-sort.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
<div class="app">
|
||||
@@ -21,6 +23,45 @@
|
||||
<!-- <button id="open_terminal" class="ButtonStyleRegular ButtonTypeChoice left-btn">-->
|
||||
<!-- Open Terminal-->
|
||||
<!-- </button>-->
|
||||
<div id="sortDropdown" class="sort-dropdown">
|
||||
<button id="sort_menu_btn" class="ButtonStyleRegular ButtonTypeChoice sort-menu-btn" type="button"
|
||||
aria-haspopup="true" aria-expanded="false" aria-controls="sortMenu" title="Sort plugins">
|
||||
<svg class="sort-order-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2.5 4.5h3.5M2.5 8h5.5M2.5 11.5h7.5" />
|
||||
<path d="M12.5 11.5V4.5M10 7l2.5-2.5L15 7" />
|
||||
</svg>
|
||||
<span id="sort_current_label"></span>
|
||||
<span class="sort-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="sortMenu" class="sort-menu" role="menu" hidden>
|
||||
<div class="sort-group-label" id="sortByLabel">Sort by</div>
|
||||
<div role="group" aria-labelledby="sortByLabel">
|
||||
<button type="button" class="sort-menu-item" role="menuitemradio" data-sort-field="status">Status</button>
|
||||
<button type="button" class="sort-menu-item" role="menuitemradio" data-sort-field="name">Name</button>
|
||||
<button type="button" class="sort-menu-item" role="menuitemradio" data-sort-field="source">Source</button>
|
||||
</div>
|
||||
<div class="sort-group-label" id="sortOrderLabel">Order</div>
|
||||
<div role="group" aria-labelledby="sortOrderLabel">
|
||||
<button type="button" class="sort-menu-item" role="menuitemradio" data-sort-order="asc">
|
||||
<svg class="sort-order-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2.5 4.5h3.5M2.5 8h5.5M2.5 11.5h7.5" />
|
||||
<path d="M12.5 11.5V4.5M10 7l2.5-2.5L15 7" />
|
||||
</svg>
|
||||
Ascending
|
||||
</button>
|
||||
<button type="button" class="sort-menu-item" role="menuitemradio" data-sort-order="desc">
|
||||
<svg class="sort-order-icon desc" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2.5 4.5h3.5M2.5 8h5.5M2.5 11.5h7.5" />
|
||||
<path d="M12.5 11.5V4.5M10 7l2.5-2.5L15 7" />
|
||||
</svg>
|
||||
Descending
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="refresh_btn" class="ButtonStyleRegular ButtonTypeChoice">
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
@@ -118,6 +118,10 @@ function ShowExploreMenu() {
|
||||
if (!exploreMenu || !exploreMenuButton)
|
||||
return;
|
||||
|
||||
// why: our stopPropagation blocks the outside-click handler, so close the sibling sort menu ourselves (mirror of ToggleSortMenu).
|
||||
if (typeof HideSortMenu === "function")
|
||||
HideSortMenu();
|
||||
|
||||
exploreMenu.hidden = false;
|
||||
exploreMenuButton.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
@@ -214,6 +218,10 @@ function HandleStudio(value) {
|
||||
|
||||
if (payload.command === "list_plugins") {
|
||||
SetSelectedInstallAction(payload.install_action, false);
|
||||
if (typeof NormalizePluginSort === "function") {
|
||||
pluginSort = NormalizePluginSort(payload.sort_key, payload.sort_order);
|
||||
RenderSortMenuState();
|
||||
}
|
||||
ApplyPlugins(payload.data || []);
|
||||
} else if (payload.command === "status_message") {
|
||||
ShowStatusMessage(String(payload.message || ""), String(payload.level || "info"));
|
||||
|
||||
89
resources/web/dialog/PluginsDialog/plugin-sort.css
Normal file
89
resources/web/dialog/PluginsDialog/plugin-sort.css
Normal file
@@ -0,0 +1,89 @@
|
||||
/* why: isolate dropdown styling from the existing plugin dialog styles. */
|
||||
|
||||
.sort-dropdown {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.sort-menu-btn {
|
||||
height: 32px !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
/* why: global .ButtonTypeChoice adds margin-left:15px (dialog button rows); as the leftmost
|
||||
toolbar item that stacks on .app's 8px padding - kill it like .explore-dropdown does. */
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* note: asc is bars growing downward + up arrow; a vertical flip yields the desc icon. */
|
||||
.sort-order-icon {
|
||||
display: block;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sort-menu-btn.order-desc .sort-order-icon,
|
||||
.sort-order-icon.desc {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.sort-caret {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: translateY(-2px) rotate(45deg);
|
||||
}
|
||||
|
||||
.sort-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 2px);
|
||||
z-index: 20;
|
||||
min-width: 200px;
|
||||
padding: 4px 0;
|
||||
background: var(--main-color);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.sort-menu[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sort-group-label {
|
||||
padding: 8px 10px 4px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.sort-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 8px 10px 8px 28px; /* why: reserve space for the active-row checkmark. */
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* note: same tint as .explore-menu-item hover/.selected, so the two menus match */
|
||||
.sort-menu-item:hover,
|
||||
.sort-menu-item:focus-visible,
|
||||
.sort-menu-item[aria-checked="true"] {
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.sort-menu-item[aria-checked="true"]::before {
|
||||
content: "\2713"; /* note: checkmark on the active field/order. */
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
90
resources/web/dialog/PluginsDialog/plugin-sort.js
Normal file
90
resources/web/dialog/PluginsDialog/plugin-sort.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// why: C++ owns ordering; this file only sends and reflects sort state.
|
||||
|
||||
const DEFAULT_PLUGIN_SORT = { key: "status", order: "asc" };
|
||||
const SORT_FIELDS = new Set(["status", "name", "source"]);
|
||||
let pluginSort = { ...DEFAULT_PLUGIN_SORT };
|
||||
|
||||
// why: C++ returns canonical sort state; guard stale or malformed values before reflecting them.
|
||||
function NormalizePluginSort(sortKey, sortOrder) {
|
||||
const key = String(sortKey || "");
|
||||
return {
|
||||
key: SORT_FIELDS.has(key) ? key : DEFAULT_PLUGIN_SORT.key,
|
||||
order: sortOrder === "desc" ? "desc" : DEFAULT_PLUGIN_SORT.order,
|
||||
};
|
||||
}
|
||||
|
||||
function RequestPluginSort(sortKey, sortOrder) {
|
||||
pluginSort = NormalizePluginSort(sortKey, sortOrder);
|
||||
RenderSortMenuState();
|
||||
|
||||
if (typeof SendMessage === "function")
|
||||
SendMessage("set_plugin_sort", {
|
||||
sort_key: pluginSort.key,
|
||||
sort_order: pluginSort.order,
|
||||
});
|
||||
}
|
||||
|
||||
let sortMenuEl = null;
|
||||
let sortMenuButton = null;
|
||||
let sortCurrentLabel = null;
|
||||
|
||||
function InitSortDropdown() {
|
||||
sortMenuEl = document.getElementById("sortMenu");
|
||||
sortMenuButton = document.getElementById("sort_menu_btn");
|
||||
sortCurrentLabel = document.getElementById("sort_current_label");
|
||||
if (!sortMenuButton || !sortMenuEl)
|
||||
return;
|
||||
sortMenuButton.addEventListener("click", ToggleSortMenu);
|
||||
sortMenuEl.addEventListener("click", OnSortMenuClick);
|
||||
RenderSortMenuState();
|
||||
}
|
||||
|
||||
function ToggleSortMenu(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!sortMenuEl)
|
||||
return;
|
||||
const willShow = sortMenuEl.hidden;
|
||||
// why: our stopPropagation blocks index.js's outside-click handler, so close the sibling explore menu ourselves.
|
||||
if (willShow && typeof HideExploreMenu === "function")
|
||||
HideExploreMenu();
|
||||
sortMenuEl.hidden = !willShow;
|
||||
sortMenuButton.setAttribute("aria-expanded", willShow ? "true" : "false");
|
||||
}
|
||||
|
||||
function HideSortMenu() {
|
||||
if (!sortMenuEl || sortMenuEl.hidden)
|
||||
return;
|
||||
sortMenuEl.hidden = true;
|
||||
sortMenuButton.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
function OnSortMenuClick(event) {
|
||||
const item = event.target.closest("[data-sort-field],[data-sort-order]");
|
||||
if (!item)
|
||||
return;
|
||||
event.preventDefault();
|
||||
const sortKey = item.dataset.sortField || pluginSort.key;
|
||||
const sortOrder = item.dataset.sortOrder || pluginSort.order;
|
||||
// why: leave the menu open so the user can set field then order in one visit; outside-click/Escape close it.
|
||||
RequestPluginSort(sortKey, sortOrder);
|
||||
}
|
||||
|
||||
function RenderSortMenuState() {
|
||||
const field = sortMenuEl?.querySelector(`[data-sort-field="${pluginSort.key}"]`);
|
||||
if (sortCurrentLabel)
|
||||
sortCurrentLabel.textContent = field?.textContent.trim() || "Status";
|
||||
sortMenuButton?.classList.toggle("order-desc", pluginSort.order === "desc");
|
||||
sortMenuEl?.querySelectorAll("[data-sort-field]").forEach((el) =>
|
||||
el.setAttribute("aria-checked", el.dataset.sortField === pluginSort.key ? "true" : "false"));
|
||||
sortMenuEl?.querySelectorAll("[data-sort-order]").forEach((el) =>
|
||||
el.setAttribute("aria-checked", el.dataset.sortOrder === pluginSort.order ? "true" : "false"));
|
||||
}
|
||||
|
||||
// why: guarded so the module can be loaded in headless syntax checks.
|
||||
// note: owns its own lifecycle + outside-click/Escape, so index.js's OnInit needs no edit.
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", InitSortDropdown);
|
||||
document.addEventListener("click", (event) => { if (!event.target.closest(".sort-dropdown")) HideSortMenu(); });
|
||||
document.addEventListener("keydown", (event) => { if (event.key === "Escape") HideSortMenu(); });
|
||||
}
|
||||
Reference in New Issue
Block a user