diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index 277334ac9f230db0570c3943435ec70a9060e834..b15b403e8e30826afb1ca09d3b7dcfcdb3e497c5 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -1357,20 +1357,14 @@ class Pref_Prefs extends Handler_Protected {
 		}
 	}
 
-	private function _get_available_plugins(array $installed = []) {
+	private function _get_available_plugins() {
 		if ($_SESSION["access_level"] >= 10 && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
-			$obj = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
-
-			// TODO: filter installed, we'll need class names in the plugins.json
-
-			return $obj;
+			return json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
 		}
 	}
 	function getAvailablePlugins() {
-		$installed = $_REQUEST['installed'];
-
 		if ($_SESSION["access_level"] >= 10) {
-			print json_encode($this->_get_available_plugins($installed));
+			print json_encode($this->_get_available_plugins());
 		}
 	}
 
diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js
index 6012d2d028f2baad7aaedcf3ed7d203ba639fcc5..5ee84f25803bc5d53cad909d87d9cc97844653c5 100644
--- a/js/PrefHelpers.js
+++ b/js/PrefHelpers.js
@@ -374,6 +374,7 @@ const	Helpers = {
 				need_refresh: false,
 				entries: false,
 				search_query: "",
+				installed_plugins: [],
 				onHide: function() {
 					if (this.need_refresh) {
 						Helpers.Prefs.refresh();
@@ -449,6 +450,9 @@ const	Helpers = {
 							.filter((stoken) => (stoken.length > 0 ? stoken : null));
 
 						dialog.entries.forEach((plugin) => {
+							const is_installed = (dialog.installed_plugins
+								.filter((p) => plugin.topics.map((t) => t.replace(/-/, "_")).includes(p))).length > 0;
+
 							if (search_tokens.length == 0 ||
 									Object.values(plugin).filter((pval) =>
 										search_tokens.filter((stoken) =>
@@ -458,8 +462,9 @@ const	Helpers = {
 								++results_rendered;
 
 								container.innerHTML += `
-									<li data-row-value="${App.escapeHtml(plugin.name)}">
-										${App.FormFields.button_tag(__('Install'), "", {class: 'alt-primary pull-right',
+									<li data-row-value="${App.escapeHtml(plugin.name)}" class="${is_installed ? "plugin-installed" : ""}">
+										${App.FormFields.button_tag(is_installed ? __("Already installed") : __('Install'), "", {class: 'alt-primary pull-right',
+											disabled: is_installed,
 											onclick: `App.dialogOf(this).performInstall("${App.escapeHtml(plugin.name)}")`})}
 
 										<h3 style="margin-top: 0">${plugin.name}
@@ -487,9 +492,7 @@ const	Helpers = {
 					const container = dialog.domNode.querySelector(".contents");
 					container.innerHTML = `<li class='text-center'>${__("Looking for plugins...")}</li>`;
 
-					const installed = [...document.querySelectorAll('*[data-plugin-name]')].map((p) => p.getAttribute('data-plugin-name'));
-
-					xhr.json("backend.php", {op: "pref-prefs", method: "getAvailablePlugins", 'installed[]': installed}, (reply) => {
+					xhr.json("backend.php", {op: "pref-prefs", method: "getAvailablePlugins"}, (reply) => {
 						dialog.entries = reply;
 						dialog.render_contents();
 					});
@@ -502,7 +505,7 @@ const	Helpers = {
 						<div style='height : 16px'>&nbsp;</div> <!-- disgusting -->
 					</div>
 
-					<ul style='clear : both' class="panel panel-scrollable-400px contents"> </ul>
+					<ul style='clear : both' class="panel panel-scrollable-400px contents plugin-installer-list"> </ul>
 
 					<footer>
 						${App.FormFields.button_tag(__("Refresh"), "", {class: 'alt-primary', onclick: 'App.dialogOf(this).reload()'})}
@@ -513,6 +516,9 @@ const	Helpers = {
 
 			const tmph = dojo.connect(dialog, 'onShow', function () {
 				dojo.disconnect(tmph);
+
+				dialog.installed_plugins = [...document.querySelectorAll('*[data-plugin-name]')].map((p) => p.getAttribute('data-plugin-name'));
+
 				dialog.reload();
 			});
 
diff --git a/themes/compact.css b/themes/compact.css
index 9656cd383d4a99a64dff936814865ae7f2c81737..12a01bedd771d2b865794a2dee720103df1948e7 100644
--- a/themes/compact.css
+++ b/themes/compact.css
@@ -1516,6 +1516,9 @@ body.ttrss_prefs fieldset.plugin label.description .dijitCheckBox {
 body.ttrss_prefs .users-list td {
   cursor: pointer;
 }
+body.ttrss_prefs .plugin-installer-list .plugin-installed {
+  opacity: 0.5;
+}
 body.ttrss_prefs .event-log tr td {
   font-size: 10px;
   padding: 8px;
diff --git a/themes/compact_night.css b/themes/compact_night.css
index 987f98b1bcac29cfe5863abad1686b9ceccbcac6..0ca19d6efeb15917171118940542f1a13e1d30eb 100644
--- a/themes/compact_night.css
+++ b/themes/compact_night.css
@@ -1516,6 +1516,9 @@ body.ttrss_prefs fieldset.plugin label.description .dijitCheckBox {
 body.ttrss_prefs .users-list td {
   cursor: pointer;
 }
+body.ttrss_prefs .plugin-installer-list .plugin-installed {
+  opacity: 0.5;
+}
 body.ttrss_prefs .event-log tr td {
   font-size: 10px;
   padding: 8px;
diff --git a/themes/light.css b/themes/light.css
index 12ef7efd22697e64713c4daf4306dc2f2f77bc8a..f3ad4813da69403ad0ae083c8a39922539839f9b 100644
--- a/themes/light.css
+++ b/themes/light.css
@@ -1516,6 +1516,9 @@ body.ttrss_prefs fieldset.plugin label.description .dijitCheckBox {
 body.ttrss_prefs .users-list td {
   cursor: pointer;
 }
+body.ttrss_prefs .plugin-installer-list .plugin-installed {
+  opacity: 0.5;
+}
 body.ttrss_prefs .event-log tr td {
   font-size: 10px;
   padding: 8px;
diff --git a/themes/light/prefs.less b/themes/light/prefs.less
index 3fe328ec555b6d8817a9ade975634f0294295a3e..1c09271a69853cc233980c73470b1303e801adde 100644
--- a/themes/light/prefs.less
+++ b/themes/light/prefs.less
@@ -112,6 +112,12 @@ body.ttrss_prefs {
 		}
 	}
 
+	.plugin-installer-list {
+		.plugin-installed {
+			opacity : 0.5;
+		}
+	}
+
 	.event-log {
 		tr {
 			td {
diff --git a/themes/night.css b/themes/night.css
index 9a2ee53fa6524163d5013045ef8ffc9c80477c2b..1d536a84c408ee323b1278093064d6ebb72a3663 100644
--- a/themes/night.css
+++ b/themes/night.css
@@ -1517,6 +1517,9 @@ body.ttrss_prefs fieldset.plugin label.description .dijitCheckBox {
 body.ttrss_prefs .users-list td {
   cursor: pointer;
 }
+body.ttrss_prefs .plugin-installer-list .plugin-installed {
+  opacity: 0.5;
+}
 body.ttrss_prefs .event-log tr td {
   font-size: 10px;
   padding: 8px;
diff --git a/themes/night_blue.css b/themes/night_blue.css
index 80f173c7e1e2a1b84669a25041fd18c6d8a0bda3..69c454763fcce1bae29bf0624e6769ee60c34750 100644
--- a/themes/night_blue.css
+++ b/themes/night_blue.css
@@ -1517,6 +1517,9 @@ body.ttrss_prefs fieldset.plugin label.description .dijitCheckBox {
 body.ttrss_prefs .users-list td {
   cursor: pointer;
 }
+body.ttrss_prefs .plugin-installer-list .plugin-installed {
+  opacity: 0.5;
+}
 body.ttrss_prefs .event-log tr td {
   font-size: 10px;
   padding: 8px;