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) =>