Improve extension type indication
| @@ -90,7 +90,7 @@ label[for="extensions_autoconnect"] { | |||
| 90 | border-radius: 10px; | 90 | border-radius: 10px; |
| 91 | align-items: center; | 91 | align-items: center; |
| 92 | justify-content: space-between; | 92 | justify-content: space-between; |
| 93 | gap: 10px; | 93 | gap: 5px; |
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | .extensions_info .extension_name { | 96 | .extensions_info .extension_name { |
| @@ -21,7 +21,11 @@ export { | |||
| 21 | 21 | ||
| 22 | /** @type {string[]} */ | 22 | /** @type {string[]} */ |
| 23 | export let extensionNames = []; | 23 | export let extensionNames = []; |
| 24 | /** @type {Record<string, string>} */ | 24 | /** |
| 25 | * Holds the type of each extension. | ||
| 26 | * Don't use this directly, use getExtensionType instead! | ||
| 27 | * @type {Record<string, string>} | ||
| 28 | */ | ||
| 25 | export let extensionTypes = {}; | 29 | export let extensionTypes = {}; |
| 26 | 30 | ||
| 27 | let manifests = {}; | 31 | let manifests = {}; |
| @@ -198,6 +202,16 @@ function showHideExtensionsMenu() { | |||
| 198 | // Periodically check for new extensions | 202 | // Periodically check for new extensions |
| 199 | const menuInterval = setInterval(showHideExtensionsMenu, 1000); | 203 | const menuInterval = setInterval(showHideExtensionsMenu, 1000); |
| 200 | 204 | ||
| 205 | /** | ||
| 206 | * Gets the type of an extension based on its external ID. | ||
| 207 | * @param {string} externalId External ID of the extension (excluding or including the leading 'third-party/') | ||
| 208 | * @returns {string} Type of the extension (global, local, system, or empty string if not found) | ||
| 209 | */ | ||
| 210 | function getExtensionType(externalId) { | ||
| 211 | const id = Object.keys(extensionTypes).find(id => id === externalId || (id.startsWith('third-party') && id.endsWith(externalId))); | ||
| 212 | return id ? extensionTypes[id] : ''; | ||
| 213 | } | ||
| 214 | |||
| 201 | async function doExtrasFetch(endpoint, args) { | 215 | async function doExtrasFetch(endpoint, args) { |
| 202 | if (!args) { | 216 | if (!args) { |
| 203 | args = {}; | 217 | args = {}; |
| @@ -457,63 +471,72 @@ function updateStatus(success) { | |||
| 457 | $('#extensions_status').attr('class', _class); | 471 | $('#extensions_status').attr('class', _class); |
| 458 | } | 472 | } |
| 459 | 473 | ||
| 474 | /** | ||
| 475 | * Adds a CSS file for an extension. | ||
| 476 | * @param {string} name Extension name | ||
| 477 | * @param {object} manifest Extension manifest | ||
| 478 | * @returns {Promise<void>} When the CSS is loaded | ||
| 479 | */ | ||
| 460 | function addExtensionStyle(name, manifest) { | 480 | function addExtensionStyle(name, manifest) { |
| 461 | if (manifest.css) { | 481 | if (!manifest.css) { |
| 462 | return new Promise((resolve, reject) => { | 482 | return Promise.resolve(); |
| 463 | const url = `/scripts/extensions/${name}/${manifest.css}`; | ||
| 464 | |||
| 465 | if ($(`link[id="${name}"]`).length === 0) { | ||
| 466 | const link = document.createElement('link'); | ||
| 467 | link.id = name; | ||
| 468 | link.rel = 'stylesheet'; | ||
| 469 | link.type = 'text/css'; | ||
| 470 | link.href = url; | ||
| 471 | link.onload = function () { | ||
| 472 | resolve(); | ||
| 473 | }; | ||
| 474 | link.onerror = function (e) { | ||
| 475 | reject(e); | ||
| 476 | }; | ||
| 477 | document.head.appendChild(link); | ||
| 478 | } | ||
| 479 | }); | ||
| 480 | } | 483 | } |
| 481 | 484 | ||
| 482 | return Promise.resolve(); | 485 | return new Promise((resolve, reject) => { |
| 486 | const url = `/scripts/extensions/${name}/${manifest.css}`; | ||
| 487 | |||
| 488 | if ($(`link[id="${name}"]`).length === 0) { | ||
| 489 | const link = document.createElement('link'); | ||
| 490 | link.id = name; | ||
| 491 | link.rel = 'stylesheet'; | ||
| 492 | link.type = 'text/css'; | ||
| 493 | link.href = url; | ||
| 494 | link.onload = function () { | ||
| 495 | resolve(); | ||
| 496 | }; | ||
| 497 | link.onerror = function (e) { | ||
| 498 | reject(e); | ||
| 499 | }; | ||
| 500 | document.head.appendChild(link); | ||
| 501 | } | ||
| 502 | }); | ||
| 483 | } | 503 | } |
| 484 | 504 | ||
| 505 | /** | ||
| 506 | * Loads a JS file for an extension. | ||
| 507 | * @param {string} name Extension name | ||
| 508 | * @param {object} manifest Extension manifest | ||
| 509 | * @returns {Promise<void>} When the script is loaded | ||
| 510 | */ | ||
| 485 | function addExtensionScript(name, manifest) { | 511 | function addExtensionScript(name, manifest) { |
| 486 | if (manifest.js) { | 512 | if (!manifest.js) { |
| 487 | return new Promise((resolve, reject) => { | 513 | return Promise.resolve(); |
| 488 | const url = `/scripts/extensions/${name}/${manifest.js}`; | ||
| 489 | let ready = false; | ||
| 490 | |||
| 491 | if ($(`script[id="${name}"]`).length === 0) { | ||
| 492 | const script = document.createElement('script'); | ||
| 493 | script.id = name; | ||
| 494 | script.type = 'module'; | ||
| 495 | script.src = url; | ||
| 496 | script.async = true; | ||
| 497 | script.onerror = function (err) { | ||
| 498 | reject(err, script); | ||
| 499 | }; | ||
| 500 | script.onload = script.onreadystatechange = function () { | ||
| 501 | // console.log(this.readyState); // uncomment this line to see which ready states are called. | ||
| 502 | if (!ready && (!this.readyState || this.readyState == 'complete')) { | ||
| 503 | ready = true; | ||
| 504 | resolve(); | ||
| 505 | } | ||
| 506 | }; | ||
| 507 | document.body.appendChild(script); | ||
| 508 | } | ||
| 509 | }); | ||
| 510 | } | 514 | } |
| 511 | 515 | ||
| 512 | return Promise.resolve(); | 516 | return new Promise((resolve, reject) => { |
| 517 | const url = `/scripts/extensions/${name}/${manifest.js}`; | ||
| 518 | let ready = false; | ||
| 519 | |||
| 520 | if ($(`script[id="${name}"]`).length === 0) { | ||
| 521 | const script = document.createElement('script'); | ||
| 522 | script.id = name; | ||
| 523 | script.type = 'module'; | ||
| 524 | script.src = url; | ||
| 525 | script.async = true; | ||
| 526 | script.onerror = function (err) { | ||
| 527 | reject(err); | ||
| 528 | }; | ||
| 529 | script.onload = function () { | ||
| 530 | if (!ready) { | ||
| 531 | ready = true; | ||
| 532 | resolve(); | ||
| 533 | } | ||
| 534 | }; | ||
| 535 | document.body.appendChild(script); | ||
| 536 | } | ||
| 537 | }); | ||
| 513 | } | 538 | } |
| 514 | 539 | ||
| 515 | |||
| 516 | |||
| 517 | /** | 540 | /** |
| 518 | * Generates HTML string for displaying an extension in the UI. | 541 | * Generates HTML string for displaying an extension in the UI. |
| 519 | * | 542 | * |
| @@ -526,6 +549,22 @@ function addExtensionScript(name, manifest) { | |||
| 526 | * @return {string} - The HTML string that represents the extension. | 549 | * @return {string} - The HTML string that represents the extension. |
| 527 | */ | 550 | */ |
| 528 | function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { | 551 | function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { |
| 552 | function getExtensionIcon() { | ||
| 553 | const type = getExtensionType(name); | ||
| 554 | switch (type) { | ||
| 555 | case 'global': | ||
| 556 | return '<i class="fa-fw fa-solid fa-server" data-i18n="[title]ext_type_global" title="This is a global extension, available for all users."></i>'; | ||
| 557 | case 'local': | ||
| 558 | return '<i class="fa-fw fa-solid fa-user" data-i18n="[title]ext_type_local" title="This is a local extension, available only for you."></i>'; | ||
| 559 | case 'system': | ||
| 560 | return '<i class="fa-fw fa-solid fa-cog" data-i18n="[title]ext_type_system" title="This is a built-in extension. It cannot be deleted and updates with the app."></i>'; | ||
| 561 | default: | ||
| 562 | return '<i class="fa-fw fa-solid fa-question" title="Unknown extension type."></i>'; | ||
| 563 | } | ||
| 564 | } | ||
| 565 | |||
| 566 | const isUserAdmin = isAdmin(); | ||
| 567 | const extensionIcon = getExtensionIcon(); | ||
| 529 | const displayName = manifest.display_name; | 568 | const displayName = manifest.display_name; |
| 530 | let displayVersion = manifest.version ? ` v${manifest.version}` : ''; | 569 | let displayVersion = manifest.version ? ` v${manifest.version}` : ''; |
| 531 | const externalId = name.replace('third-party', ''); | 570 | const externalId = name.replace('third-party', ''); |
| @@ -540,6 +579,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, | |||
| 540 | 579 | ||
| 541 | let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : ''; | 580 | let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : ''; |
| 542 | let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : ''; | 581 | let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : ''; |
| 582 | let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : ''; | ||
| 543 | let modulesInfo = ''; | 583 | let modulesInfo = ''; |
| 544 | 584 | ||
| 545 | if (isActive && Array.isArray(manifest.optional)) { | 585 | if (isActive && Array.isArray(manifest.optional)) { |
| @@ -565,6 +605,9 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, | |||
| 565 | <div class="extension_toggle"> | 605 | <div class="extension_toggle"> |
| 566 | ${toggleElement} | 606 | ${toggleElement} |
| 567 | </div> | 607 | </div> |
| 608 | <div class="extension_icon"> | ||
| 609 | ${extensionIcon} | ||
| 610 | </div> | ||
| 568 | <div class="flexGrow"> | 611 | <div class="flexGrow"> |
| 569 | ${originHtml} | 612 | ${originHtml} |
| 570 | <span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}"> | 613 | <span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}"> |
| @@ -577,6 +620,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, | |||
| 577 | 620 | ||
| 578 | <div class="extension_actions flex-container alignItemsCenter"> | 621 | <div class="extension_actions flex-container alignItemsCenter"> |
| 579 | ${updateButton} | 622 | ${updateButton} |
| 623 | ${moveButton} | ||
| 580 | ${deleteButton} | 624 | ${deleteButton} |
| 581 | </div> | 625 | </div> |
| 582 | </div>`; | 626 | </div>`; |
| @@ -622,6 +666,7 @@ function getModuleInformation() { | |||
| 622 | * Generates the HTML strings for all extensions and displays them in a popup. | 666 | * Generates the HTML strings for all extensions and displays them in a popup. |
| 623 | */ | 667 | */ |
| 624 | async function showExtensionsDetails() { | 668 | async function showExtensionsDetails() { |
| 669 | const abortController = new AbortController(); | ||
| 625 | let popupPromise; | 670 | let popupPromise; |
| 626 | try { | 671 | try { |
| 627 | const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>'); | 672 | const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>'); |
| @@ -688,13 +733,14 @@ async function showExtensionsDetails() { | |||
| 688 | }, | 733 | }, |
| 689 | }); | 734 | }); |
| 690 | popupPromise = popup.show(); | 735 | popupPromise = popup.show(); |
| 691 | checkForUpdatesManual().finally(() => htmlLoading.remove()); | 736 | checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); |
| 692 | } catch (error) { | 737 | } catch (error) { |
| 693 | toastr.error('Error loading extensions. See browser console for details.'); | 738 | toastr.error('Error loading extensions. See browser console for details.'); |
| 694 | console.error(error); | 739 | console.error(error); |
| 695 | } | 740 | } |
| 696 | if (popupPromise) { | 741 | if (popupPromise) { |
| 697 | await popupPromise; | 742 | await popupPromise; |
| 743 | abortController.abort(); | ||
| 698 | } | 744 | } |
| 699 | if (requiresReload) { | 745 | if (requiresReload) { |
| 700 | showLoader(); | 746 | showLoader(); |
| @@ -702,7 +748,6 @@ async function showExtensionsDetails() { | |||
| 702 | } | 748 | } |
| 703 | } | 749 | } |
| 704 | 750 | ||
| 705 | |||
| 706 | /** | 751 | /** |
| 707 | * Handles the click event for the update button of an extension. | 752 | * Handles the click event for the update button of an extension. |
| 708 | * This function makes a POST request to '/update_extension' with the extension's name. | 753 | * This function makes a POST request to '/update_extension' with the extension's name. |
| @@ -712,7 +757,7 @@ async function showExtensionsDetails() { | |||
| 712 | async function onUpdateClick() { | 757 | async function onUpdateClick() { |
| 713 | const isCurrentUserAdmin = isAdmin(); | 758 | const isCurrentUserAdmin = isAdmin(); |
| 714 | const extensionName = $(this).data('name'); | 759 | const extensionName = $(this).data('name'); |
| 715 | const isGlobal = extensionTypes[extensionName] === 'global'; | 760 | const isGlobal = getExtensionType(extensionName) === 'global'; |
| 716 | if (isGlobal && !isCurrentUserAdmin) { | 761 | if (isGlobal && !isCurrentUserAdmin) { |
| 717 | toastr.error(t`You don't have permission to update global extensions.`); | 762 | toastr.error(t`You don't have permission to update global extensions.`); |
| 718 | return; | 763 | return; |
| @@ -734,7 +779,7 @@ async function updateExtension(extensionName, quiet) { | |||
| 734 | headers: getRequestHeaders(), | 779 | headers: getRequestHeaders(), |
| 735 | body: JSON.stringify({ | 780 | body: JSON.stringify({ |
| 736 | extensionName, | 781 | extensionName, |
| 737 | global: extensionTypes[extensionName] === 'global', | 782 | global: getExtensionType(extensionName) === 'global', |
| 738 | }), | 783 | }), |
| 739 | }); | 784 | }); |
| 740 | 785 | ||
| @@ -765,7 +810,7 @@ async function updateExtension(extensionName, quiet) { | |||
| 765 | async function onDeleteClick() { | 810 | async function onDeleteClick() { |
| 766 | const extensionName = $(this).data('name'); | 811 | const extensionName = $(this).data('name'); |
| 767 | const isCurrentUserAdmin = isAdmin(); | 812 | const isCurrentUserAdmin = isAdmin(); |
| 768 | const isGlobal = extensionTypes[extensionName] === 'global'; | 813 | const isGlobal = getExtensionType(extensionName) === 'global'; |
| 769 | if (isGlobal && !isCurrentUserAdmin) { | 814 | if (isGlobal && !isCurrentUserAdmin) { |
| 770 | toastr.error(t`You don't have permission to delete global extensions.`); | 815 | toastr.error(t`You don't have permission to delete global extensions.`); |
| 771 | return; | 816 | return; |
| @@ -778,6 +823,18 @@ async function onDeleteClick() { | |||
| 778 | } | 823 | } |
| 779 | } | 824 | } |
| 780 | 825 | ||
| 826 | async function onMoveClick() { | ||
| 827 | const extensionName = $(this).data('name'); | ||
| 828 | const isCurrentUserAdmin = isAdmin(); | ||
| 829 | const isGlobal = getExtensionType(extensionName) === 'global'; | ||
| 830 | if (isGlobal && !isCurrentUserAdmin) { | ||
| 831 | toastr.error(t`You don't have permission to move extensions.`); | ||
| 832 | return; | ||
| 833 | } | ||
| 834 | |||
| 835 | toastr.info('Not implemented yet'); | ||
| 836 | } | ||
| 837 | |||
| 781 | /** | 838 | /** |
| 782 | * Deletes an extension via the API. | 839 | * Deletes an extension via the API. |
| 783 | * @param {string} extensionName Extension name to delete | 840 | * @param {string} extensionName Extension name to delete |
| @@ -789,7 +846,7 @@ export async function deleteExtension(extensionName) { | |||
| 789 | headers: getRequestHeaders(), | 846 | headers: getRequestHeaders(), |
| 790 | body: JSON.stringify({ | 847 | body: JSON.stringify({ |
| 791 | extensionName, | 848 | extensionName, |
| 792 | global: extensionTypes[extensionName] === 'global', | 849 | global: getExtensionType(extensionName) === 'global', |
| 793 | }), | 850 | }), |
| 794 | }); | 851 | }); |
| 795 | } catch (error) { | 852 | } catch (error) { |
| @@ -806,16 +863,21 @@ export async function deleteExtension(extensionName) { | |||
| 806 | * Fetches the version details of a specific extension. | 863 | * Fetches the version details of a specific extension. |
| 807 | * | 864 | * |
| 808 | * @param {string} extensionName - The name of the extension. | 865 | * @param {string} extensionName - The name of the extension. |
| 866 | * @param {AbortSignal} [abortSignal] - The signal to abort the operation. | ||
| 809 | * @return {Promise<object>} - An object containing the extension's version details. | 867 | * @return {Promise<object>} - An object containing the extension's version details. |
| 810 | * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl. | 868 | * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl. |
| 811 | * @throws {error} - If there is an error during the fetch operation, it logs the error to the console. | 869 | * @throws {error} - If there is an error during the fetch operation, it logs the error to the console. |
| 812 | */ | 870 | */ |
| 813 | async function getExtensionVersion(extensionName) { | 871 | async function getExtensionVersion(extensionName, abortSignal) { |
| 814 | try { | 872 | try { |
| 815 | const response = await fetch('/api/extensions/version', { | 873 | const response = await fetch('/api/extensions/version', { |
| 816 | method: 'POST', | 874 | method: 'POST', |
| 817 | headers: getRequestHeaders(), | 875 | headers: getRequestHeaders(), |
| 818 | body: JSON.stringify({ extensionName }), | 876 | body: JSON.stringify({ |
| 877 | extensionName, | ||
| 878 | global: getExtensionType(extensionName) === 'global', | ||
| 879 | }), | ||
| 880 | signal: abortSignal, | ||
| 819 | }); | 881 | }); |
| 820 | 882 | ||
| 821 | const data = await response.json(); | 883 | const data = await response.json(); |
| @@ -900,13 +962,18 @@ export function doDailyExtensionUpdatesCheck() { | |||
| 900 | }, 1); | 962 | }, 1); |
| 901 | } | 963 | } |
| 902 | 964 | ||
| 903 | async function checkForUpdatesManual() { | 965 | /** |
| 966 | * Performs a manual check for updates on all 3rd-party extensions. | ||
| 967 | * @param {AbortSignal} abortSignal Signal to abort the operation | ||
| 968 | * @returns {Promise<any[]>} | ||
| 969 | */ | ||
| 970 | async function checkForUpdatesManual(abortSignal) { | ||
| 904 | const promises = []; | 971 | const promises = []; |
| 905 | for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { | 972 | for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { |
| 906 | const externalId = id.replace('third-party', ''); | 973 | const externalId = id.replace('third-party', ''); |
| 907 | const promise = new Promise(async (resolve, reject) => { | 974 | const promise = new Promise(async (resolve, reject) => { |
| 908 | try { | 975 | try { |
| 909 | const data = await getExtensionVersion(externalId); | 976 | const data = await getExtensionVersion(externalId, abortSignal); |
| 910 | const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); | 977 | const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); |
| 911 | if (extensionBlock) { | 978 | if (extensionBlock) { |
| 912 | if (data.isUpToDate === false) { | 979 | if (data.isUpToDate === false) { |
| @@ -969,7 +1036,7 @@ async function checkForExtensionUpdates(force) { | |||
| 969 | const promises = []; | 1036 | const promises = []; |
| 970 | 1037 | ||
| 971 | for (const [id, manifest] of Object.entries(manifests)) { | 1038 | for (const [id, manifest] of Object.entries(manifests)) { |
| 972 | const isGlobal = extensionTypes[id] === 'global'; | 1039 | const isGlobal = getExtensionType(id) === 'global'; |
| 973 | if (isGlobal && !isCurrentUserAdmin) { | 1040 | if (isGlobal && !isCurrentUserAdmin) { |
| 974 | console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); | 1041 | console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); |
| 975 | continue; | 1042 | continue; |
| @@ -1012,7 +1079,7 @@ async function autoUpdateExtensions(forceAll) { | |||
| 1012 | const isCurrentUserAdmin = isAdmin(); | 1079 | const isCurrentUserAdmin = isAdmin(); |
| 1013 | const promises = []; | 1080 | const promises = []; |
| 1014 | for (const [id, manifest] of Object.entries(manifests)) { | 1081 | for (const [id, manifest] of Object.entries(manifests)) { |
| 1015 | const isGlobal = extensionTypes[id] === 'global'; | 1082 | const isGlobal = getExtensionType(id) === 'global'; |
| 1016 | if (isGlobal && !isCurrentUserAdmin) { | 1083 | if (isGlobal && !isCurrentUserAdmin) { |
| 1017 | console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); | 1084 | console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); |
| 1018 | continue; | 1085 | continue; |
| @@ -1043,9 +1110,9 @@ async function runGenerationInterceptors(chat, contextSize) { | |||
| 1043 | 1110 | ||
| 1044 | for (const manifest of Object.values(manifests).sort((a, b) => a.loading_order - b.loading_order)) { | 1111 | for (const manifest of Object.values(manifests).sort((a, b) => a.loading_order - b.loading_order)) { |
| 1045 | const interceptorKey = manifest.generate_interceptor; | 1112 | const interceptorKey = manifest.generate_interceptor; |
| 1046 | if (typeof window[interceptorKey] === 'function') { | 1113 | if (typeof globalThis[interceptorKey] === 'function') { |
| 1047 | try { | 1114 | try { |
| 1048 | await window[interceptorKey](chat, contextSize, abort); | 1115 | await globalThis[interceptorKey](chat, contextSize, abort); |
| 1049 | } catch (e) { | 1116 | } catch (e) { |
| 1050 | console.error(`Failed running interceptor for ${manifest.display_name}`, e); | 1117 | console.error(`Failed running interceptor for ${manifest.display_name}`, e); |
| 1051 | } | 1118 | } |
| @@ -1124,7 +1191,7 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { | |||
| 1124 | 1191 | ||
| 1125 | let global = false; | 1192 | let global = false; |
| 1126 | const installForAllButton = { | 1193 | const installForAllButton = { |
| 1127 | text: t`Install for all`, | 1194 | text: t`Install for all users`, |
| 1128 | appendAtEnd: false, | 1195 | appendAtEnd: false, |
| 1129 | action: async () => { | 1196 | action: async () => { |
| 1130 | global = true; | 1197 | global = true; |
| @@ -1153,10 +1220,11 @@ export async function initExtensions() { | |||
| 1153 | $('#extensions_autoconnect').on('input', autoConnectInputHandler); | 1220 | $('#extensions_autoconnect').on('input', autoConnectInputHandler); |
| 1154 | $('#extensions_details').on('click', showExtensionsDetails); | 1221 | $('#extensions_details').on('click', showExtensionsDetails); |
| 1155 | $('#extensions_notify_updates').on('input', notifyUpdatesInputHandler); | 1222 | $('#extensions_notify_updates').on('input', notifyUpdatesInputHandler); |
| 1156 | $(document).on('click', '.toggle_disable', onDisableExtensionClick); | 1223 | $(document).on('click', '.extensions_info .extension_block .toggle_disable', onDisableExtensionClick); |
| 1157 | $(document).on('click', '.toggle_enable', onEnableExtensionClick); | 1224 | $(document).on('click', '.extensions_info .extension_block .toggle_enable', onEnableExtensionClick); |
| 1158 | $(document).on('click', '.btn_update', onUpdateClick); | 1225 | $(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick); |
| 1159 | $(document).on('click', '.btn_delete', onDeleteClick); | 1226 | $(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick); |
| 1227 | $(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick); | ||
| 1160 | 1228 | ||
| 1161 | /** | 1229 | /** |
| 1162 | * Handles the click event for the third-party extension import button. | 1230 | * Handles the click event for the third-party extension import button. |
| @@ -246,26 +246,28 @@ router.get('/discover', jsonParser, function (request, response) { | |||
| 246 | } | 246 | } |
| 247 | 247 | ||
| 248 | // Get all folders in system extensions folder, excluding third-party | 248 | // Get all folders in system extensions folder, excluding third-party |
| 249 | const buildInExtensions = fs | 249 | const builtInExtensions = fs |
| 250 | .readdirSync(PUBLIC_DIRECTORIES.extensions) | 250 | .readdirSync(PUBLIC_DIRECTORIES.extensions) |
| 251 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) | 251 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) |
| 252 | .filter(f => f !== 'third-party') | 252 | .filter(f => f !== 'third-party') |
| 253 | .map(f => ({ type: 'system', name: f })); | 253 | .map(f => ({ type: 'system', name: f })); |
| 254 | 254 | ||
| 255 | // Get all folders in global extensions folder | ||
| 256 | const globalExtensions = fs | ||
| 257 | .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) | ||
| 258 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) | ||
| 259 | .map(f => ({ type: 'global', name: `third-party/${f}` })); | ||
| 260 | |||
| 261 | // Get all folders in local extensions folder | 255 | // Get all folders in local extensions folder |
| 262 | const userExtensions = fs | 256 | const userExtensions = fs |
| 263 | .readdirSync(path.join(request.user.directories.extensions)) | 257 | .readdirSync(path.join(request.user.directories.extensions)) |
| 264 | .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) | 258 | .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) |
| 265 | .map(f => ({ type: 'local', name: `third-party/${f}` })); | 259 | .map(f => ({ type: 'local', name: `third-party/${f}` })); |
| 266 | 260 | ||
| 261 | // Get all folders in global extensions folder | ||
| 262 | // In case of a conflict, the extension will be loaded from the user folder | ||
| 263 | const globalExtensions = fs | ||
| 264 | .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) | ||
| 265 | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) | ||
| 266 | .map(f => ({ type: 'global', name: `third-party/${f}` })) | ||
| 267 | .filter(f => !userExtensions.some(e => e.name === f.name)); | ||
| 268 | |||
| 267 | // Combine all extensions | 269 | // Combine all extensions |
| 268 | const allExtensions = Array.from(new Set([...buildInExtensions, ...globalExtensions, ...userExtensions])); | 270 | const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions]; |
| 269 | console.log(allExtensions); | 271 | console.log(allExtensions); |
| 270 | 272 | ||
| 271 | return response.send(allExtensions); | 273 | return response.send(allExtensions); |