@@ -113787,6 +113787,8 @@ define("nls/root/strings", {
113787113787 "EXTENSION_VERIFIED_SORT": "Verified",
113788113788 "EXTENSION_STAR": "Star",
113789113789 "EXTENSION_STAR_SORT": "Star Rating",
113790+ "ERROR_UNINSTALLING_EXTENSION_TITLE": "Error Uninstalling Extension",
113791+ "ERROR_UNINSTALLING_EXTENSION_MESSAGE": "Failed to uninstall extension {0}",
113790113792 // For NOT_FOUND_ERR, see generic strings above
113791113793 "EXTENSION_MANAGER_TITLE": "Extension Manager",
113792113794 "EXTENSION_MANAGER_ERROR_LOAD": "Unable to access the extension registry. Please try again later.",
@@ -114154,10 +114156,12 @@ define("nls/root/strings", {
114154114156 "DOWNLOAD_ERROR": "Error occurred while downloading.",
114155114157 "NETWORK_SLOW_OR_DISCONNECTED": "Network is disconnected or too slow.",
114156114158 "RESTART_BUTTON": "Restart",
114159+ "RESTART_APP_BUTTON": "Restart {APP_NAME}",
114157114160 "LATER_BUTTON": "Later",
114158114161 "DESCRIPTION_AUTO_UPDATE": "Enable/disable {APP_NAME} Auto-update",
114159114162 "AUTOUPDATE_ERROR": "Error!",
114160114163 "AUTOUPDATE_IN_PROGRESS": "An update is already in progress.",
114164+ "REMOVING": "Removing\u2026",
114161114165
114162114166 "NUMBER_WITH_PERCENTAGE": "{0}%",
114163114167 // Strings for Related Files
@@ -114706,6 +114710,10 @@ define("nls/root/strings", {
114706114710 "LICENSE_ACTIVATE_FAIL_APPLY": "'Failed to apply license to device'",
114707114711 "LICENSE_ENTER_KEY": "Please enter a license key",
114708114712 "LICENSE_REAPPLY_TO_DEVICE": "Already activated? Reapply system-wide",
114713+ // Deprecated Extensions Dialog
114714+ "DEPRECATED_EXTENSIONS_TITLE": "Deprecated Extensions Detected",
114715+ "DEPRECATED_EXTENSIONS_MESSAGE": "The following installed extensions are now natively supported by {APP_NAME} and can be safely uninstalled from the Extension Manager:",
114716+ "DEPRECATED_EXTENSIONS_LEARN_MORE": "Now built-in — learn more",
114709114717 // AI CONTROL
114710114718 "AI_LOGIN_DIALOG_TITLE": "Sign In to Use AI Edits",
114711114719 "AI_LOGIN_DIALOG_MESSAGE": "Please log in to use AI-powered edits",
@@ -176575,11 +176583,57 @@ define("utils/ExtensionLoader", function (require, exports, module) {
176575176583 UrlParams = require("utils/UrlParams").UrlParams,
176576176584 NodeUtils = require("utils/NodeUtils"),
176577176585 PathUtils = require("thirdparty/path-utils/path-utils"),
176578- DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json"));
176586+ DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json")),
176587+ Dialogs = require("widgets/Dialogs"),
176588+ PreferencesManager = require("preferences/PreferencesManager"),
176589+ Mustache = require("thirdparty/mustache/mustache"),
176590+ Strings = require("strings"),
176591+ StringUtils = require("utils/StringUtils"),
176592+ Metrics = require("utils/Metrics"),
176593+ DeprecatedExtensionsTemplate = `<div class="deprecated-extensions-dialog modal">
176594+ <div class="modal-header">
176595+ <h1 class="dialog-title">{{Strings.DEPRECATED_EXTENSIONS_TITLE}}</h1>
176596+ </div>
176597+ <div class="modal-body">
176598+ <p class="dialog-message">{{Strings.DEPRECATED_EXTENSIONS_MESSAGE}}</p>
176599+
176600+ <div class="deprecated-extensions-list" style="margin-top: 16px;">
176601+ {{#extensions}}
176602+ <div class="deprecated-extension-item" style="margin-bottom: 12px; padding: 12px; display: flex; justify-content: space-between; align-items: flex-start;">
176603+ <div style="flex: 1;">
176604+ <div class="extension-info" style="margin-bottom: 6px;">
176605+ <i class="fa fa-exclamation-triangle" style="color: #f5a623; margin-right: 8px;"></i>
176606+ <strong>{{name}}</strong>
176607+ </div>
176608+ <div class="extension-alternative" style="margin-left: 24px;">
176609+ <a href="{{docUrl}}" target="_blank" rel="noopener">
176610+ {{Strings.DEPRECATED_EXTENSIONS_LEARN_MORE}}
176611+ <i class="fa fa-external-link" style="margin-left: 4px; font-size: 11px;"></i>
176612+ </a>
176613+ </div>
176614+ </div>
176615+ <div style="margin-left: 12px;">
176616+ <button class="btn btn-mini uninstall-extension-btn" data-extension-id="{{id}}">
176617+ {{Strings.REMOVE}}
176618+ </button>
176619+ </div>
176620+ </div>
176621+ {{/extensions}}
176622+ </div>
176623+ </div>
176624+ <div class="modal-footer">
176625+ <button class="dialog-button btn primary" data-button-id="ok">{{Strings.OK}}</button>
176626+ </div>
176627+ </div>
176628+ `,
176629+ CommandManager = require("command/CommandManager");
176579176630
176580176631 // takedown/dont load extensions that are compromised at app start - start
176581176632 const EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY = "PH_EXTENSION_TAKEDOWN_LIST";
176582176633
176634+ // deprecated extensions dialog state key
176635+ const STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN = "deprecatedExtensionsDialogShown";
176636+
176583176637 function _getTakedownListLS() {
176584176638 try{
176585176639 let list = localStorage.getItem(EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY);
@@ -176595,7 +176649,7 @@ define("utils/ExtensionLoader", function (require, exports, module) {
176595176649 return [];
176596176650 }
176597176651
176598- const loadedExtensionIDs = new Set ();
176652+ const loadedExtensionIDs = new Map ();
176599176653 let takedownExtensionList = new Set(_getTakedownListLS());
176600176654
176601176655 const EXTENSION_TAKEDOWN_URL = brackets.config.extensionTakedownURL;
@@ -176609,16 +176663,16 @@ define("utils/ExtensionLoader", function (require, exports, module) {
176609176663
176610176664 if (takedownExtensionList.size < loadedExtensionIDs.size) {
176611176665 smaller = takedownExtensionList;
176612- larger = loadedExtensionIDs;
176666+ larger = Array.from( loadedExtensionIDs.keys()) ;
176613176667 } else {
176614- smaller = loadedExtensionIDs;
176668+ smaller = Array.from( loadedExtensionIDs.keys()) ;
176615176669 larger = takedownExtensionList;
176616176670 }
176617176671
176618176672 const matches = [];
176619176673
176620176674 for (const id of smaller) {
176621- if (larger.has(id)) {
176675+ if (larger.has ? larger.has(id) : larger.includes (id)) {
176622176676 matches.push(id);
176623176677 }
176624176678 }
@@ -177034,7 +177088,11 @@ define("utils/ExtensionLoader", function (require, exports, module) {
177034177088 }
177035177089
177036177090 if(metadata.name) {
177037- loadedExtensionIDs.add(metadata.name);
177091+ loadedExtensionIDs.set(metadata.name, {
177092+ loadedFromDisc: !!config.nativeDir,
177093+ extensionPath: config.nativeDir || config.baseUrl,
177094+ extensionName: metadata.title || metadata.name
177095+ });
177038177096 }
177039177097
177040177098 // No special handling for themes... Let the promise propagate into the ExtensionManager
@@ -177539,6 +177597,8 @@ define("utils/ExtensionLoader", function (require, exports, module) {
177539177597
177540177598 promise.always(function () {
177541177599 _init = true;
177600+ // Check for deprecated extensions after all extensions have loaded
177601+ _checkAndShowDeprecatedExtensionsDialog();
177542177602 });
177543177603
177544177604 return promise;
@@ -177553,6 +177613,128 @@ define("utils/ExtensionLoader", function (require, exports, module) {
177553177613 return takedownExtensionList.has(extensionID);
177554177614 }
177555177615
177616+ /**
177617+ * Uninstall a deprecated extension
177618+ * @param {string} extensionID - The ID of the extension to uninstall
177619+ * @return {Promise} A promise that resolves when the extension is uninstalled successfully
177620+ */
177621+ async function uninstallExtension(extensionID) {
177622+ const extensionInfo = loadedExtensionIDs.get(extensionID);
177623+
177624+ if (!extensionInfo) {
177625+ throw new Error(`Extension ${extensionID} not found in loaded extensions`);
177626+ }
177627+
177628+ if (!extensionInfo.loadedFromDisc) {
177629+ throw new Error(`Cannot uninstall built-in extension: ${extensionID}`);
177630+ }
177631+
177632+ const extensionDir = FileSystem.getDirectoryForPath(extensionInfo.extensionPath);
177633+ await extensionDir.unlinkAsync();
177634+ }
177635+
177636+ /**
177637+ * Check if any deprecated extensions are installed and show a dialog once per extension
177638+ * @private
177639+ */
177640+ function _checkAndShowDeprecatedExtensionsDialog() {
177641+ // Get deprecated extensions config
177642+ let needsRestart = false;
177643+ const deprecatedExtensionsConfig = DefaultExtensions.deprecatedExtensions;
177644+ if (!deprecatedExtensionsConfig || !deprecatedExtensionsConfig.extensionIDsAndDocs) {
177645+ return;
177646+ }
177647+
177648+ const deprecatedExtensionIDs = deprecatedExtensionsConfig.extensionIDsAndDocs;
177649+
177650+ // Get the state object that tracks which deprecated extensions we've already shown
177651+ let shownDeprecatedExtensions = PreferencesManager.stateManager.get(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN);
177652+ if (!shownDeprecatedExtensions || typeof shownDeprecatedExtensions !== 'object') {
177653+ shownDeprecatedExtensions = {};
177654+ }
177655+
177656+ // Check which deprecated extensions are loaded and not yet shown
177657+ const deprecatedExtensionsFound = [];
177658+ for (const extensionID of loadedExtensionIDs.keys()) {
177659+ if (deprecatedExtensionIDs[extensionID] && !shownDeprecatedExtensions[extensionID]) {
177660+ const extensionInfo = loadedExtensionIDs.get(extensionID);
177661+ const extensionName = extensionInfo && extensionInfo.extensionName;
177662+ deprecatedExtensionsFound.push({
177663+ id: extensionID,
177664+ name: extensionName || extensionID,
177665+ docUrl: deprecatedExtensionIDs[extensionID]
177666+ });
177667+ }
177668+ }
177669+
177670+ // If no new deprecated extensions found, return
177671+ if (deprecatedExtensionsFound.length === 0) {
177672+ return;
177673+ }
177674+
177675+ // Show the dialog
177676+ const templateVars = {
177677+ extensions: deprecatedExtensionsFound,
177678+ Strings: Strings
177679+ };
177680+
177681+ const $template = $(Mustache.render(DeprecatedExtensionsTemplate, templateVars));
177682+ const dialog = Dialogs.showModalDialogUsingTemplate($template, false); // autoDismiss = false
177683+
177684+ // Wire up uninstall button click handlers
177685+ $template.on('click', '.uninstall-extension-btn', async function() {
177686+ const $button = $(this);
177687+ const extensionID = $button.data('extension-id');
177688+ const $extensionItem = $button.closest('.deprecated-extension-item');
177689+
177690+ // Disable button during uninstall
177691+ $button.prop('disabled', true);
177692+ $button.text(Strings.REMOVING);
177693+
177694+ try {
177695+ Metrics.countEvent(Metrics.EVENT_TYPE.EXTENSIONS, "removeDep", extensionID);
177696+ await uninstallExtension(extensionID);
177697+
177698+ // Update the OK button to "Restart App"
177699+ const $okButton = $template.find('[data-button-id="ok"]');
177700+ $okButton.text(Strings.RESTART_APP_BUTTON);
177701+
177702+ // Strike through the extension name and disable/strike the uninstall button
177703+ $extensionItem.find('.extension-info strong').addClass('striked');
177704+ $button.remove();
177705+ needsRestart = true;
177706+ } catch (err) {
177707+ Metrics.countEvent(Metrics.EVENT_TYPE.EXTENSIONS, "removeDep", "fail");
177708+ logger.reportError(err, 'Failed to uninstall deprecated extension:' + extensionID);
177709+
177710+ // Show error dialog
177711+ const message = StringUtils.format(Strings.ERROR_UNINSTALLING_EXTENSION_MESSAGE, extensionID);
177712+ Dialogs.showErrorDialog(Strings.ERROR_UNINSTALLING_EXTENSION_TITLE, message);
177713+
177714+ // Re-enable button
177715+ $button.prop('disabled', false);
177716+ $button.text(Strings.REMOVE);
177717+ }
177718+ });
177719+
177720+ // Handle OK button click
177721+ $template.on('click', '[data-button-id="ok"]', function() {
177722+ if (needsRestart) {
177723+ // Reload the app to complete uninstallation
177724+ CommandManager.execute("debug.refreshWindow");
177725+ } else {
177726+ // Just close the dialog
177727+ dialog.close();
177728+ }
177729+ });
177730+
177731+ // Mark each extension as shown
177732+ for (const ext of deprecatedExtensionsFound) {
177733+ shownDeprecatedExtensions[ext.id] = true;
177734+ }
177735+ PreferencesManager.stateManager.set(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN, shownDeprecatedExtensions);
177736+ }
177737+
177556177738
177557177739 EventDispatcher.makeEventDispatcher(exports);
177558177740
@@ -177577,6 +177759,7 @@ define("utils/ExtensionLoader", function (require, exports, module) {
177577177759 exports.loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory;
177578177760 exports.loadExtensionFromNativeDirectory = loadExtensionFromNativeDirectory;
177579177761 exports.isExtensionTakenDown = isExtensionTakenDown;
177762+ exports.uninstallExtension = uninstallExtension;
177580177763 exports.testAllExtensionsInNativeDirectory = testAllExtensionsInNativeDirectory;
177581177764 exports.testAllDefaultExtensions = testAllDefaultExtensions;
177582177765 exports.EVENT_EXTENSION_LOADED = EVENT_EXTENSION_LOADED;
0 commit comments