From f557ea6d946da3c52d3d8ad486073c95f1922eb1 Mon Sep 17 00:00:00 2001 From: Erik Bigler Date: Sat, 11 Jul 2026 14:03:27 -0600 Subject: [PATCH 1/4] feat(plugins): support categories, sorting, and pagination in community plugin browser --- src/backend/plugins/plugin-manager.ts | 57 ++- .../plugins/community-plugin-browser-modal.js | 349 ++++++++++++++++++ .../plugins/install-community-plugin-modal.js | 247 ------------- .../settings/categories/plugin-settings.js | 2 +- src/gui/app/services/plugins.service.js | 4 +- src/gui/scss/core/_scripts.scss | 58 +++ src/shared/plugin-constants.js | 33 ++ src/types/plugins.ts | 25 ++ 8 files changed, 514 insertions(+), 261 deletions(-) create mode 100644 src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js delete mode 100644 src/gui/app/directives/modals/plugins/install-community-plugin-modal.js create mode 100644 src/shared/plugin-constants.js diff --git a/src/backend/plugins/plugin-manager.ts b/src/backend/plugins/plugin-manager.ts index 437220b76..deb728935 100644 --- a/src/backend/plugins/plugin-manager.ts +++ b/src/backend/plugins/plugin-manager.ts @@ -5,10 +5,13 @@ import { randomUUID, createHash } from "crypto"; import { app } from "electron"; import type { + CommunityPluginSearchCriteria, + CommunityPluginSearchResult, InstalledPlugin, InstalledPluginConfig, LegacyCustomScript, ManagedPlugin, + ManagedPluginBase, ManagedPluginExtended, ManagedPluginUpdateRequest, Manifest, @@ -23,6 +26,7 @@ import type { PluginApiContext, PluginApiContextSource } from "./plugin-api"; import type { DisposeBag } from "./plugin-api/internal/dispose-bag"; import { PluginConfigManager } from "./plugin-config-manager"; +import { AccountAccess } from "../common/account-access"; import { ProfileManager } from "../common/profile-manager"; import { SettingsManager } from "../common/settings-manager"; import webhookConfigManager from "../webhooks/webhook-config-manager"; @@ -174,8 +178,8 @@ class PluginManager { ); frontendCommunicator.onAsync("plugin-manager:search-community-plugins", - async (query: string) => { - return await this.searchForCommunityPlugins(query); + async (criteria: CommunityPluginSearchCriteria) => { + return await this.searchForCommunityPlugins(criteria); } ); @@ -1071,15 +1075,15 @@ class PluginManager { // #region Managed (Community) Plugins - private async searchForCommunityPlugins(query: string): Promise { + private async searchForCommunityPlugins(criteria: CommunityPluginSearchCriteria): Promise { const plugins: ManagedPluginExtended[] = []; + let total = 0; try { const firebotVersionString = app.getVersion(); - const firebotVersion = parseVersion(firebotVersionString); const body = { - query, - firebotVersion + ...criteria, + firebotVersion: parseVersion(firebotVersionString) }; const response = await fetch(`${COMMUNITY_PLUGIN_SERVICE_ROOT_URL}search`, { @@ -1092,9 +1096,10 @@ class PluginManager { }); if (response.ok) { - const pluginSearchResults = await response.json() as ManagedPlugin[]; + const searchResult = await response.json() as { items: ManagedPlugin[], total: number }; + total = searchResult.total; - for (const result of pluginSearchResults) { + for (const result of searchResult.items) { const installedPlugin = PluginConfigManager.getAllItems().find(c => c.managedPluginDetails?.author === result.author && c.managedPluginDetails?.name === result.name @@ -1109,14 +1114,14 @@ class PluginManager { } else { const responseBody = await response.text(); - this._logger.error(`Failed to search community plugins for "${query}". Response: ${responseBody}`); + this._logger.error(`Failed to search community plugins. Response: ${responseBody}`); frontendCommunicator.send("showToast", { content: "Failed to search for community plugins. Check the log for more details.", className: "warning" }); } } catch (error) { - this._logger.error(`Failed to search community plugins for "${query}"`, error); + this._logger.error("Failed to search community plugins", error); frontendCommunicator.send("showToast", { content: "Failed to search for community plugins. Check the log for more details.", className: "warning" @@ -1127,7 +1132,33 @@ class PluginManager { plugin.manifest = resolvePluginManifestLinks(plugin.manifest); } - return plugins; + return { items: plugins, total }; + } + + private trackCommunityPluginDownload(plugin: ManagedPluginBase): void { + void (async () => { + try { + const streamer = AccountAccess.getAccounts().streamer; + if (streamer?.loggedIn !== true) { + return; + } + + await fetch(`${COMMUNITY_PLUGIN_SERVICE_ROOT_URL}track-download`, { + method: "POST", + body: JSON.stringify({ + author: plugin.author, + name: plugin.name + }), + headers: { + "User-Agent": `Firebot/${app.getVersion()}`, + "Content-Type": "application/json", + "Authorization": `Bearer ${streamer.auth.access_token}` + } + }); + } catch (error) { + this._logger.debug("Failed to track community plugin download", error); + } + })(); } private async downloadAndSaveCommunityPlugin( @@ -1264,6 +1295,8 @@ class PluginManager { void this.triggerUiRefresh(); + this.trackCommunityPluginDownload(plugin); + return { success: true, installedPlugin: { @@ -1358,6 +1391,8 @@ class PluginManager { void this.triggerUiRefresh(); + this.trackCommunityPluginDownload(pluginUpdate); + return { success: true, installedPlugin: { diff --git a/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js b/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js new file mode 100644 index 000000000..21338e21b --- /dev/null +++ b/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js @@ -0,0 +1,349 @@ +"use strict"; + +(function() { + const { DateTime } = require("luxon"); + const { PluginCategory, PluginCategoryLabels } = require("../../shared/plugin-constants"); + + const PAGE_SIZE = 20; + const SCROLL_THRESHOLD_PX = 300; + + angular + .module("firebotApp") + .component("communityPluginBrowserModal", { + template: ` + + + `, + bindings: { + resolve: "<", + close: "&", + dismiss: "&" + }, + controller: function( + $rootScope, + $scope, + $timeout, + pluginsService, + modalFactory, + modalService, + ngToast + ) { + const $ctrl = this; + + $ctrl.openLink = $rootScope.openLinkExternally; + + $ctrl.categories = Object.values(PluginCategory); + $ctrl.categoryLabels = PluginCategoryLabels; + $ctrl.activeCategory = null; + $ctrl.searchQuery = ""; + $ctrl.sortBy = "name"; + $ctrl.sortOptions = [ + { name: "Name (A-Z)", value: "name", icon: "fa-sort-alpha-down" }, + { name: "Popular", value: "popular", icon: "fa-fire" }, + { name: "Recently Updated", value: "recently-updated", icon: "fa-clock" } + ]; + + $ctrl.plugins = []; + $ctrl.total = 0; + $ctrl.isLoading = false; + $ctrl.isLoadingMore = false; + $ctrl.isInstallingPlugin = false; + + let currentPage = 1; + let requestId = 0; + + $ctrl.hasMore = () => $ctrl.plugins.length < $ctrl.total; + + const fetchPage = async (page) => { + const thisRequest = ++requestId; + + const result = await pluginsService.searchCommunityPlugins({ + query: $ctrl.searchQuery?.trim() || undefined, + category: $ctrl.activeCategory ?? undefined, + sortBy: $ctrl.sortBy, + page, + pageSize: PAGE_SIZE + }); + + // Discard stale responses from superseded requests + if (thisRequest !== requestId) { + return null; + } + + return result ?? { items: [], total: 0 }; + }; + + $ctrl.reload = async () => { + $ctrl.isLoading = true; + + const result = await fetchPage(1); + if (result == null) { + return; + } + + currentPage = 1; + $ctrl.plugins = result.items; + $ctrl.total = result.total; + $ctrl.isLoading = false; + + const listEl = document.getElementById("pluginBrowserList"); + if (listEl) { + listEl.scrollTop = 0; + } + }; + + $ctrl.loadMore = async () => { + if ($ctrl.isLoading || $ctrl.isLoadingMore || !$ctrl.hasMore()) { + return; + } + + $ctrl.isLoadingMore = true; + + const result = await fetchPage(currentPage + 1); + if (result != null) { + currentPage += 1; + $ctrl.plugins = $ctrl.plugins.concat(result.items); + $ctrl.total = result.total; + } + + $ctrl.isLoadingMore = false; + }; + + $ctrl.selectCategory = (category) => { + if ($ctrl.activeCategory !== category) { + $ctrl.activeCategory = category; + $ctrl.reload(); + } + }; + + let lastSort = $ctrl.sortBy; + $ctrl.onSortChanged = () => { + if ($ctrl.sortBy !== lastSort) { + lastSort = $ctrl.sortBy; + $ctrl.reload(); + } + }; + + $scope.$watch("$ctrl.searchQuery", (newValue, oldValue) => { + if (newValue !== oldValue) { + $ctrl.reload(); + } + }); + + $ctrl.$onInit = () => { + $ctrl.reload(); + + $timeout(() => { + const listEl = document.getElementById("pluginBrowserList"); + listEl?.addEventListener("scroll", () => { + if (listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight - SCROLL_THRESHOLD_PX) { + $scope.$applyAsync(() => $ctrl.loadMore()); + } + }); + + angular.element("#pluginBrowserSearch").trigger("focus"); + }, 100); + }; + + $ctrl.getPluginReleaseDate = (plugin) => { + if (plugin?.manifest?.releaseDate) { + const releaseDate = DateTime.fromISO(plugin.manifest.releaseDate); + return releaseDate.toFormat("MMMM d, yyyy"); + } + + return "Unknown"; + }; + + $ctrl.hasPluginLinks = function(plugin) { + const manifest = (plugin && plugin.manifest) || {}; + return !!(manifest.repo || manifest.website || manifest.support); + }; + + $ctrl.installCommunityPlugin = async (pluginDetails) => { + $ctrl.isInstallingPlugin = true; + pluginDetails.installing = true; + + const result = await pluginsService.installCommunityPlugin(pluginDetails); + + if (result.success === true) { + pluginDetails.installed = true; + pluginDetails.installedVersion = result.installedPlugin.config.managedPluginDetails.version; + + ngToast.create({ + className: "success", + content: `${result.installedPlugin.details.manifest.name} plugin installed!` + }); + + if (!!result.installedPlugin.details.parametersSchema?.length) { + modalService.showModal({ + component: "configurePluginModal", + size: "md", + resolveObj: { + plugin: () => result.installedPlugin + } + }); + } + } else { + modalFactory.showErrorModal(result.error); + } + + pluginDetails.installing = false; + $ctrl.isInstallingPlugin = false; + }; + } + }); +}()); diff --git a/src/gui/app/directives/modals/plugins/install-community-plugin-modal.js b/src/gui/app/directives/modals/plugins/install-community-plugin-modal.js deleted file mode 100644 index d8fde0a17..000000000 --- a/src/gui/app/directives/modals/plugins/install-community-plugin-modal.js +++ /dev/null @@ -1,247 +0,0 @@ -"use strict"; - -(function() { - const { DateTime } = require("luxon"); - - angular - .module("firebotApp") - .component("installCommunityPluginModal", { - template: ` -
- -
- -
-
- - `, - bindings: { - resolve: "<", - close: "&", - dismiss: "&" - }, - controller: function( - $rootScope, - $scope, - pluginsService, - modalFactory, - modalService, - ngToast - ) { - const $ctrl = this; - - $ctrl.openLink = $rootScope.openLinkExternally; - - $ctrl.isSearching = false; - $ctrl.searchPerformed = false; - $ctrl.searchQuery = ""; - $ctrl.searchResults = []; - $ctrl.isInstallingPlugin = false; - - $ctrl.performSearch = async () => { - if (!$ctrl.searchQuery?.length) { - return; - } - - $ctrl.isSearching = true; - - const results = await pluginsService.searchCommunityPlugins($ctrl.searchQuery); - $ctrl.searchResults = results ?? []; - - $ctrl.searchPerformed = true; - $ctrl.isSearching = false; - }; - - $scope.$watch("$ctrl.searchQuery", (newValue, oldValue) => { - if (newValue !== oldValue) { - if (!!newValue?.length) { - $ctrl.performSearch(); - } else if (newValue === "") { - $ctrl.searchPerformed = false; - $ctrl.searchResults = []; - } - } - }); - - $ctrl.getPluginReleaseDate = (plugin) => { - if (plugin?.manifest?.releaseDate) { - const releaseDate = DateTime.fromISO(plugin.manifest.releaseDate); - return releaseDate.toFormat("MMMM d, yyyy"); - } - - return "Unknown"; - }; - - $ctrl.hasPluginLinks = function(plugin) { - const manifest = (plugin && plugin.manifest) || {}; - return !!(manifest.repo || manifest.website || manifest.support); - }; - - $ctrl.installCommunityPlugin = async (pluginDetails) => { - $ctrl.isInstallingPlugin = true; - pluginDetails.installing = true; - - const result = await pluginsService.installCommunityPlugin(pluginDetails); - - if (result.success === true) { - pluginDetails.installed = true; - pluginDetails.installedVersion = result.installedPlugin.config.managedPluginDetails.version; - - ngToast.create({ - className: "success", - content: `${result.installedPlugin.details.manifest.name} plugin installed!` - }); - - if (!!result.installedPlugin.details.parametersSchema?.length) { - modalService.showModal({ - component: "configurePluginModal", - size: "md", - resolveObj: { - plugin: () => result.installedPlugin - } - }); - } - } else { - modalFactory.showErrorModal(result.error); - } - - pluginDetails.installing = false; - $ctrl.isInstallingPlugin = false; - }; - } - }); -}()); \ No newline at end of file diff --git a/src/gui/app/directives/settings/categories/plugin-settings.js b/src/gui/app/directives/settings/categories/plugin-settings.js index a9ac3f86b..459b266b9 100644 --- a/src/gui/app/directives/settings/categories/plugin-settings.js +++ b/src/gui/app/directives/settings/categories/plugin-settings.js @@ -406,7 +406,7 @@ $scope.showCommunityPluginModal = () => { modalService.showModal({ - component: "installCommunityPluginModal", + component: "communityPluginBrowserModal", size: "lg" }); }; diff --git a/src/gui/app/services/plugins.service.js b/src/gui/app/services/plugins.service.js index b78b3eeaa..2ba92d1a5 100644 --- a/src/gui/app/services/plugins.service.js +++ b/src/gui/app/services/plugins.service.js @@ -83,8 +83,8 @@ ); }; - service.searchCommunityPlugins = async (searchQuery) => { - return await backendCommunicator.fireEventAsync("plugin-manager:search-community-plugins", searchQuery); + service.searchCommunityPlugins = async (criteria) => { + return await backendCommunicator.fireEventAsync("plugin-manager:search-community-plugins", criteria); }; service.installCommunityPlugin = async (pluginDetails) => { diff --git a/src/gui/scss/core/_scripts.scss b/src/gui/scss/core/_scripts.scss index 6a8cd2956..5d3e40ad2 100644 --- a/src/gui/scss/core/_scripts.scss +++ b/src/gui/scss/core/_scripts.scss @@ -27,3 +27,61 @@ font-size: 12px; } } + +.plugin-browser-header { + background: $light-background-color; +} + +.plugin-browser-toolbar { + display: flex; + align-items: center; + gap: 10px; + background: $light-background-color; + border-bottom: 2px solid $light-background-border; + padding: 8px 13px; +} + +.plugin-browser-categories { + background: $effect-category-bg; + display: flex; + flex-direction: column; + flex-shrink: 0; + height: 100%; + width: 170px; + padding: 5px; + overflow-y: auto; +} + +.plugin-browser-category-header { + font-weight: 700; + padding: 5px 0 5px 15px; + font-size: 13px; +} + +.plugin-browser-category { + font-weight: 300; + display: flex; + flex-direction: row; + align-items: center; + padding: 7px 10px; + border-radius: 10px; + cursor: pointer; + + &:hover { + background: $effect-category-hover-bg; + text-decoration: none; + } + + &.selected { + font-weight: 400; + background: $effect-category-selected-bg; + } +} + +.plugin-browser-list { + width: 100%; + height: 100%; + overflow-y: scroll; + padding: 15px; + background: $effect-list-bg-color; +} diff --git a/src/shared/plugin-constants.js b/src/shared/plugin-constants.js new file mode 100644 index 000000000..0364b8630 --- /dev/null +++ b/src/shared/plugin-constants.js @@ -0,0 +1,33 @@ +"use strict"; + +/** + * Enum for community plugin categories. + * @readonly + * @enum {string} + */ +const PluginCategory = Object.freeze({ + CHAT_AND_COMMANDS: "chat-commands", + ALERTS_AND_EVENTS: "alerts-events", + OVERLAY_WIDGETS: "overlay-widgets", + INTEGRATIONS: "integrations", + GAMES_AND_FUN: "games-fun", + EFFECTS_AND_VARIABLES: "effects-variables", + UTILITIES: "utilities" +}); + +/** + * Display labels for community plugin categories. + * @readonly + */ +const PluginCategoryLabels = Object.freeze({ + [PluginCategory.CHAT_AND_COMMANDS]: "Chat & Commands", + [PluginCategory.ALERTS_AND_EVENTS]: "Alerts & Events", + [PluginCategory.OVERLAY_WIDGETS]: "Overlay Widgets", + [PluginCategory.INTEGRATIONS]: "Integrations", + [PluginCategory.GAMES_AND_FUN]: "Games & Fun", + [PluginCategory.EFFECTS_AND_VARIABLES]: "Effects & Variables", + [PluginCategory.UTILITIES]: "Utilities" +}); + +exports.PluginCategory = PluginCategory; +exports.PluginCategoryLabels = PluginCategoryLabels; diff --git a/src/types/plugins.ts b/src/types/plugins.ts index 3dabe1c7f..bf0988fe6 100644 --- a/src/types/plugins.ts +++ b/src/types/plugins.ts @@ -43,6 +43,15 @@ type CustomIcon = { export type PluginIcon = FontAwesomeIcon | CustomIcon; +export type PluginCategory = + | "chat-commands" + | "alerts-events" + | "overlay-widgets" + | "integrations" + | "games-fun" + | "effects-variables" + | "utilities"; + export type ManagedPluginManifest = { name: string; author: string; @@ -56,6 +65,7 @@ export type ManagedPluginManifest = { type: "single-file" | "zip"; icon?: PluginIcon; + categories?: PluginCategory[]; tags?: string[]; repo?: string; website?: string; @@ -84,6 +94,21 @@ export type ManagedPluginUpdateRequest = { firebotVersion: ManifestFirebotVersion; }; +export type CommunityPluginSearchSortMode = "popular" | "recently-updated" | "name"; + +export type CommunityPluginSearchCriteria = { + query?: string; + category?: PluginCategory; + sortBy: CommunityPluginSearchSortMode; + page: number; + pageSize: number; +}; + +export type CommunityPluginSearchResult = { + items: ManagedPluginExtended[]; + total: number; +}; + export type InstalledPluginConfig = { id: string; fileName: string; From 5a32045653eb98d7a62cddb9126def3ea5fbf141 Mon Sep 17 00:00:00 2001 From: Erik Bigler Date: Sat, 11 Jul 2026 14:24:22 -0600 Subject: [PATCH 2/4] feat(plugins): pass plugin version to download track endpoint --- src/backend/plugins/plugin-manager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/plugins/plugin-manager.ts b/src/backend/plugins/plugin-manager.ts index deb728935..156dcd390 100644 --- a/src/backend/plugins/plugin-manager.ts +++ b/src/backend/plugins/plugin-manager.ts @@ -1147,7 +1147,8 @@ class PluginManager { method: "POST", body: JSON.stringify({ author: plugin.author, - name: plugin.name + name: plugin.name, + version: plugin.version }), headers: { "User-Agent": `Firebot/${app.getVersion()}`, From f357b2c29033d8b4e7eadf4c954b1347231e88cd Mon Sep 17 00:00:00 2001 From: Erik Bigler Date: Sat, 11 Jul 2026 21:48:45 -0600 Subject: [PATCH 3/4] feat(plugins): support ability to filter plugins by features --- package.json | 2 +- .../plugins/community-plugin-browser-modal.js | 90 ++++++++++++++++--- src/gui/scss/core/_scripts.scss | 51 +++++++++++ src/shared/plugin-constants.js | 59 +++++++++--- src/types/plugins.ts | 25 ++++-- 5 files changed, 192 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 3c6b0d99a..13bbfadbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebotv5", - "version": "5.67.0-alpha28", + "version": "5.67.0-alpha29", "description": "Powerful all-in-one bot for Twitch streamers.", "main": "build/main.js", "scripts": { diff --git a/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js b/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js index 21338e21b..7e4ddec53 100644 --- a/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js +++ b/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js @@ -2,7 +2,7 @@ (function() { const { DateTime } = require("luxon"); - const { PluginCategory, PluginCategoryLabels } = require("../../shared/plugin-constants"); + const { PluginCategory, PluginCategoryLabels, PluginFeature, PluginFeatureLabels } = require("../../shared/plugin-constants"); const PAGE_SIZE = 20; const SCROLL_THRESHOLD_PX = 300; @@ -25,14 +25,57 @@ debounce="500" /> - +
+ + {{$ctrl.selectedFeatureCount()}} +
+
@@ -197,6 +240,11 @@ { name: "Recently Updated", value: "recently-updated", icon: "fa-clock" } ]; + $ctrl.features = Object.values(PluginFeature); + $ctrl.featureLabels = PluginFeatureLabels; + $ctrl.selectedFeatures = {}; + $ctrl.filtersPopoverOpen = false; + $ctrl.plugins = []; $ctrl.total = 0; $ctrl.isLoading = false; @@ -208,12 +256,19 @@ $ctrl.hasMore = () => $ctrl.plugins.length < $ctrl.total; + const getSelectedFeatures = () => + Object.keys($ctrl.selectedFeatures).filter(f => $ctrl.selectedFeatures[f] === true); + + $ctrl.selectedFeatureCount = () => getSelectedFeatures().length; + const fetchPage = async (page) => { const thisRequest = ++requestId; + const features = getSelectedFeatures(); const result = await pluginsService.searchCommunityPlugins({ query: $ctrl.searchQuery?.trim() || undefined, category: $ctrl.activeCategory ?? undefined, + features: features.length ? features : undefined, sortBy: $ctrl.sortBy, page, pageSize: PAGE_SIZE @@ -270,14 +325,23 @@ } }; - let lastSort = $ctrl.sortBy; - $ctrl.onSortChanged = () => { - if ($ctrl.sortBy !== lastSort) { - lastSort = $ctrl.sortBy; + $ctrl.setSortBy = (sortBy) => { + if ($ctrl.sortBy !== sortBy) { + $ctrl.sortBy = sortBy; $ctrl.reload(); } }; + $ctrl.onFeatureToggled = (feature, isSelected) => { + $ctrl.selectedFeatures[feature] = isSelected === true; + $ctrl.reload(); + }; + + $ctrl.clearFeatures = () => { + $ctrl.selectedFeatures = {}; + $ctrl.reload(); + }; + $scope.$watch("$ctrl.searchQuery", (newValue, oldValue) => { if (newValue !== oldValue) { $ctrl.reload(); diff --git a/src/gui/scss/core/_scripts.scss b/src/gui/scss/core/_scripts.scss index 5d3e40ad2..585e67cb9 100644 --- a/src/gui/scss/core/_scripts.scss +++ b/src/gui/scss/core/_scripts.scss @@ -85,3 +85,54 @@ padding: 15px; background: $effect-list-bg-color; } + +.plugin-filters-badge { + position: absolute; + top: -6px; + right: -6px; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 9px; + background: $primary-button-bg; + color: #fff; + font-size: 11px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.plugin-filters-popover { + min-width: 215px; + + .popover-content { + padding: 10px 12px; + } +} + +.plugin-filters-section-header { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 6px; +} + +.plugin-filters-sort-option { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + + &:hover { + background: $effect-category-hover-bg; + } + + &.selected { + background: $effect-category-selected-bg; + } +} diff --git a/src/shared/plugin-constants.js b/src/shared/plugin-constants.js index 0364b8630..7031947c9 100644 --- a/src/shared/plugin-constants.js +++ b/src/shared/plugin-constants.js @@ -6,13 +6,12 @@ * @enum {string} */ const PluginCategory = Object.freeze({ - CHAT_AND_COMMANDS: "chat-commands", - ALERTS_AND_EVENTS: "alerts-events", - OVERLAY_WIDGETS: "overlay-widgets", - INTEGRATIONS: "integrations", - GAMES_AND_FUN: "games-fun", - EFFECTS_AND_VARIABLES: "effects-variables", - UTILITIES: "utilities" + STREAM_SERVICES: "stream-services", + SOCIAL: "social", + MUSIC_AND_MEDIA: "music-media", + GAMES: "games", + OVERLAYS: "overlays", + TOOLS_AND_UTILITIES: "tools-utilities" }); /** @@ -20,14 +19,46 @@ const PluginCategory = Object.freeze({ * @readonly */ const PluginCategoryLabels = Object.freeze({ - [PluginCategory.CHAT_AND_COMMANDS]: "Chat & Commands", - [PluginCategory.ALERTS_AND_EVENTS]: "Alerts & Events", - [PluginCategory.OVERLAY_WIDGETS]: "Overlay Widgets", - [PluginCategory.INTEGRATIONS]: "Integrations", - [PluginCategory.GAMES_AND_FUN]: "Games & Fun", - [PluginCategory.EFFECTS_AND_VARIABLES]: "Effects & Variables", - [PluginCategory.UTILITIES]: "Utilities" + [PluginCategory.STREAM_SERVICES]: "Stream Services", + [PluginCategory.SOCIAL]: "Social", + [PluginCategory.MUSIC_AND_MEDIA]: "Music & Media", + [PluginCategory.GAMES]: "Games/Gaming", + [PluginCategory.OVERLAYS]: "Overlays", + [PluginCategory.TOOLS_AND_UTILITIES]: "Tools & Utilities" +}); + +/** + * Enum for Firebot features a community plugin can provide. + * @readonly + * @enum {string} + */ +const PluginFeature = Object.freeze({ + EFFECTS: "effects", + EVENTS: "events", + VARIABLES: "variables", + INTEGRATIONS: "integrations", + OVERLAY_WIDGETS: "overlay-widgets", + GAMES: "games", + COMMANDS: "commands", + UI_EXTENSIONS: "ui-extensions" +}); + +/** + * Display labels for community plugin features. + * @readonly + */ +const PluginFeatureLabels = Object.freeze({ + [PluginFeature.EFFECTS]: "Effects", + [PluginFeature.EVENTS]: "Events", + [PluginFeature.VARIABLES]: "Variables", + [PluginFeature.INTEGRATIONS]: "Integrations", + [PluginFeature.OVERLAY_WIDGETS]: "Overlay Widgets", + [PluginFeature.GAMES]: "Games", + [PluginFeature.COMMANDS]: "Commands", + [PluginFeature.UI_EXTENSIONS]: "UI Extensions" }); exports.PluginCategory = PluginCategory; exports.PluginCategoryLabels = PluginCategoryLabels; +exports.PluginFeature = PluginFeature; +exports.PluginFeatureLabels = PluginFeatureLabels; diff --git a/src/types/plugins.ts b/src/types/plugins.ts index bf0988fe6..d4b184ae5 100644 --- a/src/types/plugins.ts +++ b/src/types/plugins.ts @@ -44,13 +44,22 @@ type CustomIcon = { export type PluginIcon = FontAwesomeIcon | CustomIcon; export type PluginCategory = - | "chat-commands" - | "alerts-events" - | "overlay-widgets" + | "stream-services" + | "social" + | "music-media" + | "games" + | "overlays" + | "tools-utilities"; + +export type PluginFeature = + | "effects" + | "events" + | "variables" | "integrations" - | "games-fun" - | "effects-variables" - | "utilities"; + | "overlay-widgets" + | "games" + | "commands" + | "ui-extensions"; export type ManagedPluginManifest = { name: string; @@ -65,7 +74,8 @@ export type ManagedPluginManifest = { type: "single-file" | "zip"; icon?: PluginIcon; - categories?: PluginCategory[]; + category?: PluginCategory; + features?: PluginFeature[]; tags?: string[]; repo?: string; website?: string; @@ -99,6 +109,7 @@ export type CommunityPluginSearchSortMode = "popular" | "recently-updated" | "na export type CommunityPluginSearchCriteria = { query?: string; category?: PluginCategory; + features?: PluginFeature[]; sortBy: CommunityPluginSearchSortMode; page: number; pageSize: number; From d7baaef9c549bdaba2a24a11265c0f6ec63e6919 Mon Sep 17 00:00:00 2001 From: Erik Bigler Date: Sun, 12 Jul 2026 09:58:30 -0600 Subject: [PATCH 4/4] feat(plugins): add support for "Official" (first-party) plugins --- .../modals/plugins/community-plugin-browser-modal.js | 11 ++++++++++- src/types/plugins.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js b/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js index 7e4ddec53..459f42436 100644 --- a/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js +++ b/src/gui/app/directives/modals/plugins/community-plugin-browser-modal.js @@ -85,6 +85,11 @@ ng-class="{'selected': $ctrl.activeCategory == null}" ng-click="$ctrl.selectCategory(null)" >All
+
Official