Add Plugin Hub "New" filter for recent plugins

- Add recency-based New filter to plugin hub browsing, showing plugins updated in the last 14 days - Add "Newest" sort option and New badge/filter chip styling in browse cards - Auto-switch to newest sorting when New filter is selected

Alessandro committed Apr 21, 2026 at 06:15 UTC 3ded1885422e93962979bf94d99768725d342688
2 files changed +105 -3
plugins/_plugin_installer/webui/install-index.html
+67 -1
@@ -36,6 +36,7 @@
36 <select class="pi-sort-select"
37 x-model="$store.pluginInstallStore.sortBy">
38 <option value="stars">Stars</option>
39 + <option value="updated">Newest</option>
40 <option value="name">Name</option>
41 </select>
42 </label>
@@ -56,7 +57,8 @@
57 class="pi-filter-chip"
58 :class="{
59 active: $store.pluginInstallStore.browseFilter === filter.key,
59 - 'pi-filter-chip-update': filter.key === 'update' && filter.count > 0
60 + 'pi-filter-chip-update': filter.key === 'update' && filter.count > 0,
61 + 'pi-filter-chip-new': filter.key === 'new' && filter.count > 0
62 }"
63 @click="$store.pluginInstallStore.setBrowseFilter(filter.key)">
64 <span x-text="filter.label"></span>
@@ -142,6 +144,12 @@
144 <span class="pi-card-bubble-text">New version</span>
145 </span>
146 </template>
147 + <template x-if="$store.pluginInstallStore.isNewPlugin(plugin)">
148 + <span class="pi-card-bubble pi-card-bubble-new">
149 + <span class="material-symbols-outlined">new_releases</span>
150 + <span class="pi-card-bubble-text">New</span>
151 + </span>
152 + </template>
153 </div>
154
155 </div>
@@ -363,6 +371,23 @@
371 color: #bfdbfe;
372 }
373
374 + .pi-filter-chip-new {
375 + border-color: rgba(34, 197, 94, 0.22);
376 + color: #86efac;
377 + }
378 +
379 + .pi-filter-chip-new:hover,
380 + .pi-filter-chip-new.active {
381 + border-color: rgba(34, 197, 94, 0.4);
382 + background: rgba(34, 197, 94, 0.14);
383 + color: #bbf7d0;
384 + }
385 +
386 + .pi-filter-chip-new .pi-filter-count {
387 + background: rgba(34, 197, 94, 0.16);
388 + color: #bbf7d0;
389 + }
390 +
391 body.light-mode .pi-filter-chip-update {
392 border-color: rgba(37, 99, 235, 0.35);
393 background: rgba(37, 99, 235, 0.08);
@@ -381,6 +406,24 @@
406 color: #1e3a8a;
407 }
408
409 + body.light-mode .pi-filter-chip-new {
410 + border-color: rgba(22, 163, 74, 0.35);
411 + background: rgba(22, 163, 74, 0.08);
412 + color: #166534;
413 + }
414 +
415 + body.light-mode .pi-filter-chip-new:hover,
416 + body.light-mode .pi-filter-chip-new.active {
417 + border-color: rgba(21, 128, 61, 0.5);
418 + background: rgba(22, 163, 74, 0.14);
419 + color: #14532d;
420 + }
421 +
422 + body.light-mode .pi-filter-chip-new .pi-filter-count {
423 + background: rgba(22, 163, 74, 0.16);
424 + color: #14532d;
425 + }
426 +
427 .pi-filter-count {
428 padding: 0.12rem 0.4rem;
429 border-radius: 0.5rem;
@@ -647,6 +690,19 @@
690 border-color: rgba(59, 130, 246, 0.3);
691 }
692
693 + .pi-card-bubble-new .material-symbols-outlined {
694 + color: #4ade80;
695 + }
696 +
697 + .pi-card-bubble-new .pi-card-bubble-text {
698 + color: #4ade80;
699 + }
700 +
701 + .pi-card-bubble-new:hover {
702 + background: rgba(34, 197, 94, 0.1);
703 + border-color: rgba(34, 197, 94, 0.3);
704 + }
705 +
706 .pi-card-bubble-suspended .material-symbols-outlined {
707 color: #f59e0b;
708 }
@@ -680,6 +736,16 @@
736 border-color: rgba(59, 130, 246, 0.35);
737 }
738
739 + body.light-mode .pi-card-bubble-new .material-symbols-outlined,
740 + body.light-mode .pi-card-bubble-new .pi-card-bubble-text {
741 + color: #166534;
742 + }
743 +
744 + body.light-mode .pi-card-bubble-new:hover {
745 + background: rgba(34, 197, 94, 0.12);
746 + border-color: rgba(34, 197, 94, 0.35);
747 + }
748 +
749 body.light-mode .pi-card-bubble-suspended .material-symbols-outlined,
750 body.light-mode .pi-card-bubble-suspended .pi-card-bubble-text {
751 color: #b45309;
plugins/_plugin_installer/webui/pluginInstallStore.js
+38 -2
@@ -12,6 +12,7 @@ import { store as pluginSettingsStore } from "/components/plugins/plugin-setting
12 const PLUGIN_API = "plugins/_plugin_installer/plugin_install";
13 const PER_PAGE = 24;
14 const POPULAR_PLUGIN_MIN_STARS = 3;
15 +const NEW_PLUGIN_WINDOW_DAYS = 14;
16
17 const SECURITY_WARNING = {
18 title: "Security Warning",
@@ -68,7 +69,11 @@ const model = {
69 },
70
71 setBrowseFilter(filter) {
71 - this.browseFilter = filter || "all";
72 + const nextFilter = filter || "all";
73 + this.browseFilter = nextFilter;
74 + if (nextFilter === "new" && this.sortBy === "stars") {
75 + this.sortBy = "updated";
76 + }
77 this.page = 1;
78 },
79
@@ -98,6 +103,21 @@ const model = {
103 return (plugin?.stars || 0) >= POPULAR_PLUGIN_MIN_STARS;
104 },
105
106 + _isNewPlugin(plugin) {
107 + const updatedAt = (plugin?.updated || "").trim();
108 + if (!updatedAt) return false;
109 + const updatedMs = Date.parse(updatedAt);
110 + if (Number.isNaN(updatedMs)) return false;
111 +
112 + const nowMs = Date.now();
113 + const cutoffMs = nowMs - NEW_PLUGIN_WINDOW_DAYS * 24 * 60 * 60 * 1000;
114 + return updatedMs >= cutoffMs;
115 + },
116 +
117 + isNewPlugin(plugin) {
118 + return this._isNewPlugin(plugin);
119 + },
120 +
121 _getSuspensionReason(plugin) {
122 return typeof plugin?.suspended === "string" ? plugin.suspended.trim() : "";
123 },
@@ -111,6 +131,7 @@ const model = {
131 if (filterKey === "installed") return !!plugin?.installed;
132 if (filterKey === "update") return !!plugin?.has_update;
133 if (filterKey === "popular") return this._isPopularPlugin(plugin);
134 + if (filterKey === "new") return this._isNewPlugin(plugin);
135 if (filterKey.startsWith("tag:")) {
136 return this._pluginPrimaryTag(plugin) === filterKey.slice(4);
137 }
@@ -155,6 +176,14 @@ const model = {
176 return (a.title || a.key).localeCompare(b.title || b.key);
177 },
178
179 + _comparePluginsByUpdated(a, b) {
180 + const updatedComparison = this._compareTimestamp(a?.updated, b?.updated);
181 + if (updatedComparison !== 0) {
182 + return updatedComparison > 0 ? -1 : 1;
183 + }
184 + return this._comparePluginsByStars(a, b);
185 + },
186 +
187 // ── ZIP Install ──────────────────────────────
188
189 handleFileUpload(event) {
@@ -400,6 +429,11 @@ const model = {
429 filters.push({ key: "popular", label: "Popular", count: popularCount });
430 }
431
432 + const newCount = plugins.filter((plugin) => this._isNewPlugin(plugin)).length;
433 + if (newCount) {
434 + filters.push({ key: "new", label: "New", count: newCount });
435 + }
436 +
437 const tagCounts = new Map();
438 for (const plugin of plugins) {
439 const tag = this._pluginPrimaryTag(plugin);
@@ -435,7 +469,9 @@ const model = {
469 (p.tags || []).some((t) => t.toLowerCase().includes(q))
470 );
471 }
438 - if (this.sortBy === "stars") {
472 + if (this.sortBy === "updated" || this.browseFilter === "new") {
473 + list.sort((a, b) => this._comparePluginsByUpdated(a, b));
474 + } else if (this.sortBy === "stars") {
475 list.sort((a, b) => this._comparePluginsByStars(a, b));
476 } else {
477 list.sort((a, b) =>