Group add to background folder (#5237) * group add * make button not wide * make single and batch add to folder use the same style popup * prevent stuck UI, allow scrolling, prevent ::after conflicts * extremely large commit, beware * clear everything after performing bulk actions * Unify bulk selection design language --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -136,19 +136,32 @@ | ||
| 136 | 136 | outline-offset: 0; |
| 137 | 137 | } |
| 138 | 138 | |
| 139 | 139 | .bg_example.locked-background::after {, |
| 140 | - content: '\f023'; | |
| 140 | +.bg_example.selected-background::before, | |
| 141 | +#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected::before { | |
| 141 | 142 | font-family: 'Font Awesome 6 Free'; |
| 142 | 143 | font-weight: 900; |
| 143 | - | |
| 144 | 144 | position: absolute; |
| 145 | + z-index: 4; | |
| 146 | + filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.8)); | |
| 147 | + pointer-events: none; | |
| 148 | +} | |
| 149 | + | |
| 150 | +.bg_example.selected-background::before, | |
| 151 | +#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected::before { | |
| 152 | + content: '\f00c'; | |
| 153 | + top: 5px; | |
| 154 | + color: var(--white100); | |
| 155 | + font-size: calc(var(--mainFontSize) * 0.9); | |
| 156 | +} | |
| 157 | + | |
| 158 | +.bg_example.locked-background::after { | |
| 159 | + content: '\f023'; | |
| 145 | 160 | bottom: 5px; |
| 146 | 161 | right: 5px; |
| 147 | - z-index: 4; | |
| 148 | 162 | color: var(--golden); |
| 149 | 163 | filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8)); |
| 150 | 164 | font-size: calc(var(--mainFontSize) * 0.8); |
| 151 | - pointer-events: none; | |
| 152 | 165 | } |
| 153 | 166 | |
| 154 | 167 | .bg_example:not(.locked-background) .jg-unlock, |
| @@ -162,17 +175,28 @@ | ||
| 162 | 175 | } |
| 163 | 176 | |
| 164 | 177 | .bg_example.selected-background::before { |
| 165 | - content: '\f00c'; | |
| 166 | - font-family: 'Font Awesome 6 Free'; | |
| 167 | - font-weight: 900; | |
| 168 | - position: absolute; | |
| 169 | - top: 5px; | |
| 170 | 178 | left: 5px; |
| 171 | - z-index: 4; | |
| 179 | +} | |
| 172 | - color: var(--white100); | |
| 180 | + | |
| 173 | - filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.8)); | |
| 181 | +#bg_selection_mode_button.active { | |
| 174 | 182 | font-sizecolor: calc(var(--mainFontSize) * 0.9golden); |
| 175 | - pointer-events: none; | |
| 183 | +} | |
| 184 | + | |
| 185 | +#bg_group_select_count { | |
| 186 | + font-weight: 700; | |
| 187 | +} | |
| 188 | + | |
| 189 | +#Backgrounds.bg-selection-mode #bg_menu_content .bg_example .jg-menu { | |
| 190 | + display: none; | |
| 191 | +} | |
| 192 | + | |
| 193 | +#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected { | |
| 194 | + outline: 2px solid var(--interactable-outline-color); | |
| 195 | + outline-offset: 0; | |
| 196 | +} | |
| 197 | + | |
| 198 | +#Backgrounds.bg-selection-mode #bg_menu_content .bg_example.folder-group-selected::before { | |
| 199 | + right: 5px; | |
| 176 | 200 | } |
| 177 | 201 | |
| 178 | 202 | .bg_example .jg-menu { |
| @@ -153,10 +153,30 @@ | ||
| 153 | 153 | |
| 154 | 154 | #add_background_button_top>span, |
| 155 | 155 | #auto_background>span, |
| 156 | 156 | #bg_add_folder_button>span {, |
| 157 | + #bg_selection_mode_button>span:not(#bg_group_select_count), | |
| 158 | + #bg_group_add_to_folder_button>span, | |
| 159 | + #bg_folder_remove_selected_button>span { | |
| 157 | 160 | display: none; |
| 158 | 161 | } |
| 159 | 162 | |
| 163 | + #bg_group_select_count:not(:empty) { | |
| 164 | + display: inline !important; | |
| 165 | + } | |
| 166 | + | |
| 167 | + .bg_folder_breadcrumb { | |
| 168 | + gap: 5px; | |
| 169 | + } | |
| 170 | + | |
| 171 | + .bg_folder_breadcrumb .expander { | |
| 172 | + min-width: 0; | |
| 173 | + } | |
| 174 | + | |
| 175 | + #bg_current_folder_name { | |
| 176 | + min-width: 0; | |
| 177 | + flex: 1 1 auto; | |
| 178 | + } | |
| 179 | + | |
| 160 | 180 | #extensions_settings, |
| 161 | 181 | #extensions_settings2 { |
| 162 | 182 | width: 100% !important; |
| @@ -5635,6 +5635,14 @@ | ||
| 5635 | 5635 | <a href="#bg_chat_tab" data-i18n="Chat">Chat</a> |
| 5636 | 5636 | </li> |
| 5637 | 5637 | <div class="heading-controls"> |
| 5638 | + <button id="bg_group_add_to_folder_button" class="menu_button menu_button_icon" style="display:none;" data-i18n="[title]Add selected backgrounds to folder" title="Add selected backgrounds to folder"> | |
| 5639 | + <i class="fa-solid fa-folder-tree"></i> | |
| 5640 | + <span data-i18n="Add to Folder">Add to Folder</span> | |
| 5641 | + </button> | |
| 5642 | + <button id="bg_selection_mode_button" class="menu_button menu_button_icon" data-i18n="[title]Select backgrounds for bulk folder actions" title="Select backgrounds for bulk folder actions"> | |
| 5643 | + <i class="fa-solid fa-edit"></i> | |
| 5644 | + <span id="bg_group_select_count" style="display:none;"></span> | |
| 5645 | + </button> | |
| 5638 | 5646 | <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> |
| 5639 | 5647 | <i class="fa-solid fa-minus"></i> |
| 5640 | 5648 | </button> |
| @@ -5650,6 +5658,11 @@ | ||
| 5650 | 5658 | <span data-i18n="Back">Back</span> |
| 5651 | 5659 | </button> |
| 5652 | 5660 | <span id="bg_current_folder_name" class="bg_current_folder_name"></span> |
| 5661 | + <span class="expander"></span> | |
| 5662 | + <button id="bg_folder_remove_selected_button" class="menu_button menu_button_icon" style="display:none;" data-i18n="[title]Remove selected backgrounds from this folder" title="Remove selected backgrounds from this folder"> | |
| 5663 | + <i class="fa-solid fa-folder-minus"></i> | |
| 5664 | + <span data-i18n="Remove from Folder">Remove from Folder</span> | |
| 5665 | + </button> | |
| 5653 | 5666 | </div> |
| 5654 | 5667 | <div id="bg_folder_grid" class="bg_list bg_folder_grid"></div> |
| 5655 | 5668 | <div id="bg_menu_content" class="bg_list"></div> |
| @@ -20,6 +20,10 @@ let folderList = []; | ||
| 20 | 20 | let imageFolderMap = {}; |
| 21 | 21 | /** @type {string|null} Currently active folder drill-in, or null for root */ |
| 22 | 22 | let activeFolderId = null; |
| 23 | +/** @type {Set<string>} Selected system backgrounds for group folder actions */ | |
| 24 | +const selectedSystemBackgroundFiles = new Set(); | |
| 25 | +/** @type {boolean} Whether click-to-select mode is active for system backgrounds */ | |
| 26 | +let isBackgroundSelectionMode = false; | |
| 23 | 27 | |
| 24 | 28 | // A single transparent PNG pixel used as a placeholder for errored backgrounds |
| 25 | 29 | const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; |
| @@ -365,6 +369,11 @@ function removeBackgroundMetadata() { | ||
| 365 | 369 | function onSelectBackgroundClick(e) { |
| 366 | 370 | const bgFile = $(this).attr('bgfile'); |
| 367 | 371 | const isCustom = $(this).attr('custom') === 'true'; |
| 372 | + if (isBackgroundSelectionMode && !isCustom) { | |
| 373 | + toggleBackgroundGroupSelection(bgFile); | |
| 374 | + return; | |
| 375 | + } | |
| 376 | + | |
| 368 | 377 | const backgroundCssUrl = getUrlParameter(this); |
| 369 | 378 | const bypassGlobalLock = !isCustom && e.shiftKey; |
| 370 | 379 | |
| @@ -576,6 +585,7 @@ async function onDeleteBackgroundClick(e) { | ||
| 576 | 585 | if (deletedBg) { |
| 577 | 586 | const cachedIdx = cachedSystemBackgrounds.findIndex(img => img.filename === deletedBg); |
| 578 | 587 | if (cachedIdx !== -1) cachedSystemBackgrounds.splice(cachedIdx, 1); |
| 588 | + selectedSystemBackgroundFiles.delete(deletedBg); | |
| 579 | 589 | |
| 580 | 590 | // Update folder map and clear folder thumbnail if it referenced this image |
| 581 | 591 | if (imageFolderMap[deletedBg]) { |
| @@ -605,6 +615,7 @@ async function onDeleteBackgroundClick(e) { | ||
| 605 | 615 | |
| 606 | 616 | highlightLockedBackground(); |
| 607 | 617 | highlightSelectedBackground(); |
| 618 | + syncGroupSelectionUi(); | |
| 608 | 619 | } |
| 609 | 620 | } |
| 610 | 621 | |
| @@ -652,7 +663,10 @@ function renderSystemBackgrounds(backgrounds) { | ||
| 652 | 663 | const container = $('#bg_menu_content'); |
| 653 | 664 | container.empty(); |
| 654 | 665 | |
| 655 | 666 | if (sourceList.length === 0) return;{ |
| 667 | + syncGroupSelectionUi(); | |
| 668 | + return; | |
| 669 | + } | |
| 656 | 670 | |
| 657 | 671 | const sortedList = sortBackgrounds(sourceList.map(bg => bg.filename), false); |
| 658 | 672 | const metadataByFilename = new Map(sourceList.map(bg => [bg.filename, bg])); |
| @@ -663,6 +677,7 @@ function renderSystemBackgrounds(backgrounds) { | ||
| 663 | 677 | container.append(thumbnail); |
| 664 | 678 | }); |
| 665 | 679 | |
| 680 | + syncGroupSelectionUi(); | |
| 666 | 681 | activateLazyLoader(); |
| 667 | 682 | } |
| 668 | 683 | |
| @@ -700,6 +715,12 @@ export async function getBackgrounds() { | ||
| 700 | 715 | const { images, config } = await response.json(); |
| 701 | 716 | Object.assign(THUMBNAIL_CONFIG, config); |
| 702 | 717 | cachedSystemBackgrounds = images; |
| 718 | + const existingFiles = new Set(images.map(x => x.filename)); | |
| 719 | + for (const selectedFile of selectedSystemBackgroundFiles) { | |
| 720 | + if (!existingFiles.has(selectedFile)) { | |
| 721 | + selectedSystemBackgroundFiles.delete(selectedFile); | |
| 722 | + } | |
| 723 | + } | |
| 703 | 724 | |
| 704 | 725 | // Load folders first so getFilteredImages() works correctly in folder view |
| 705 | 726 | await loadFolders(); |
| @@ -859,6 +880,7 @@ function onFolderDrillIn(folderId) { | ||
| 859 | 880 | const folder = folderList.find(f => f.id === folderId); |
| 860 | 881 | if (!folder) return; |
| 861 | 882 | |
| 883 | + clearBackgroundGroupSelection(); | |
| 862 | 884 | activeFolderId = folderId; |
| 863 | 885 | $('#Backgrounds').addClass('in-folder-view'); |
| 864 | 886 | |
| @@ -876,6 +898,7 @@ function onFolderDrillIn(folderId) { | ||
| 876 | 898 | * Returns to the root folder overview. |
| 877 | 899 | */ |
| 878 | 900 | function onBackToFolders() { |
| 901 | + clearBackgroundGroupSelection(); | |
| 879 | 902 | activeFolderId = null; |
| 880 | 903 | $('#Backgrounds').removeClass('in-folder-view'); |
| 881 | 904 | |
| @@ -890,6 +913,245 @@ function onBackToFolders() { | ||
| 890 | 913 | } |
| 891 | 914 | |
| 892 | 915 | /** |
| 916 | + * Refreshes click-to-select and group action UI state. | |
| 917 | + */ | |
| 918 | +function syncGroupSelectionUi() { | |
| 919 | + const selectedCount = selectedSystemBackgroundFiles.size; | |
| 920 | + const isGlobalTab = getActiveBackgroundTab() === BG_SOURCES.GLOBAL; | |
| 921 | + const showAddButton = isGlobalTab && isBackgroundSelectionMode && selectedCount > 0; | |
| 922 | + const showRemoveFromCurrentFolderButton = isGlobalTab && Boolean(activeFolderId) && isBackgroundSelectionMode && selectedCount > 0; | |
| 923 | + | |
| 924 | + $('#Backgrounds').toggleClass('bg-selection-mode', isBackgroundSelectionMode); | |
| 925 | + $('#bg_selection_mode_button').toggleClass('active', isBackgroundSelectionMode); | |
| 926 | + $('#bg_group_select_count').text(selectedCount > 0 ? ` (${selectedCount})` : '').toggle(selectedCount > 0); | |
| 927 | + | |
| 928 | + $('#bg_group_add_to_folder_button').toggle(showAddButton); | |
| 929 | + $('#bg_folder_remove_selected_button').toggle(showRemoveFromCurrentFolderButton); | |
| 930 | + | |
| 931 | + $('#bg_menu_content .bg_example').each(function () { | |
| 932 | + const bgFile = String($(this).attr('bgfile') || ''); | |
| 933 | + $(this).toggleClass('folder-group-selected', selectedSystemBackgroundFiles.has(bgFile)); | |
| 934 | + }); | |
| 935 | +} | |
| 936 | + | |
| 937 | +/** | |
| 938 | + * Enables/disables click-to-select mode for system backgrounds. | |
| 939 | + * @param {boolean} enabled | |
| 940 | + */ | |
| 941 | +function setBackgroundSelectionMode(enabled) { | |
| 942 | + isBackgroundSelectionMode = enabled; | |
| 943 | + if (!enabled) { | |
| 944 | + selectedSystemBackgroundFiles.clear(); | |
| 945 | + } | |
| 946 | + // Clear any open mobile menus | |
| 947 | + $('#bg_menu_content .bg_example.mobile-menu-open').removeClass('mobile-menu-open'); | |
| 948 | + syncGroupSelectionUi(); | |
| 949 | +} | |
| 950 | + | |
| 951 | +/** | |
| 952 | + * Toggles selected state of a system background for group folder actions. | |
| 953 | + * @param {string} bgFile | |
| 954 | + */ | |
| 955 | +function toggleBackgroundGroupSelection(bgFile) { | |
| 956 | + if (!bgFile) return; | |
| 957 | + if (selectedSystemBackgroundFiles.has(bgFile)) { | |
| 958 | + selectedSystemBackgroundFiles.delete(bgFile); | |
| 959 | + } else { | |
| 960 | + selectedSystemBackgroundFiles.add(bgFile); | |
| 961 | + } | |
| 962 | + syncGroupSelectionUi(); | |
| 963 | +} | |
| 964 | + | |
| 965 | +/** | |
| 966 | + * Clears all selected system backgrounds for group folder actions. | |
| 967 | + */ | |
| 968 | +function clearBackgroundGroupSelection() { | |
| 969 | + selectedSystemBackgroundFiles.clear(); | |
| 970 | + syncGroupSelectionUi(); | |
| 971 | +} | |
| 972 | + | |
| 973 | +/** | |
| 974 | + * Updates selection/folder action control visibility for the active tab. | |
| 975 | + */ | |
| 976 | +function updateGroupFolderControlsVisibility() { | |
| 977 | + const isGlobalTab = getActiveBackgroundTab() === BG_SOURCES.GLOBAL; | |
| 978 | + $('#bg_selection_mode_button').toggle(isGlobalTab); | |
| 979 | + | |
| 980 | + if (!isGlobalTab && isBackgroundSelectionMode) { | |
| 981 | + setBackgroundSelectionMode(false); | |
| 982 | + return; | |
| 983 | + } | |
| 984 | + syncGroupSelectionUi(); | |
| 985 | +} | |
| 986 | + | |
| 987 | +/** | |
| 988 | + * Shows a folder selection popup and returns the selected folder id. | |
| 989 | + * @param {string} headingText | |
| 990 | + * @returns {Promise<string[]|null>} Array of selected folder IDs, or null if cancelled | |
| 991 | + */ | |
| 992 | +async function selectFoldersForGroupAction(headingText) { | |
| 993 | + if (folderList.length === 0) { | |
| 994 | + toastr.info(t`Create a folder first`); | |
| 995 | + return null; | |
| 996 | + } | |
| 997 | + | |
| 998 | + const contentEl = document.createElement('div'); | |
| 999 | + const heading = document.createElement('h3'); | |
| 1000 | + heading.textContent = headingText; | |
| 1001 | + contentEl.appendChild(heading); | |
| 1002 | + | |
| 1003 | + for (const folder of folderList) { | |
| 1004 | + const label = document.createElement('label'); | |
| 1005 | + label.className = 'checkbox_label flexGap5'; | |
| 1006 | + label.style.margin = '4px 0'; | |
| 1007 | + | |
| 1008 | + const checkbox = document.createElement('input'); | |
| 1009 | + checkbox.type = 'checkbox'; | |
| 1010 | + checkbox.dataset.folderId = folder.id; | |
| 1011 | + | |
| 1012 | + const span = document.createElement('span'); | |
| 1013 | + span.textContent = folder.name; | |
| 1014 | + | |
| 1015 | + label.appendChild(checkbox); | |
| 1016 | + label.appendChild(span); | |
| 1017 | + contentEl.appendChild(label); | |
| 1018 | + } | |
| 1019 | + | |
| 1020 | + const content = $(contentEl); | |
| 1021 | + const result = await callGenericPopup(content, POPUP_TYPE.CONFIRM, '', { | |
| 1022 | + okButton: t`Apply`, | |
| 1023 | + cancelButton: t`Cancel`, | |
| 1024 | + allowVerticalScrolling: true, | |
| 1025 | + leftAlign: true, | |
| 1026 | + }); | |
| 1027 | + if (!result) return null; | |
| 1028 | + | |
| 1029 | + const selectedIds = []; | |
| 1030 | + content.find('input[type="checkbox"]:checked').each(function () { | |
| 1031 | + selectedIds.push($(this).data('folderId')); | |
| 1032 | + }); | |
| 1033 | + return selectedIds.length > 0 ? selectedIds : null; | |
| 1034 | +} | |
| 1035 | + | |
| 1036 | +/** | |
| 1037 | + * Sends a folder assign/unassign request and updates local imageFolderMap state. | |
| 1038 | + * @param {string[]} bgFiles - Background filenames to update | |
| 1039 | + * @param {string} folderId - Target folder ID | |
| 1040 | + * @param {boolean} isRemove - Whether to remove (unassign) or add (assign) | |
| 1041 | + */ | |
| 1042 | +async function updateFolderAssignments(bgFiles, folderId, isRemove) { | |
| 1043 | + const paths = bgFiles.map(getBackgroundRelativePath); | |
| 1044 | + const endpoint = isRemove ? '/api/image-metadata/folders/unassign' : '/api/image-metadata/folders/assign'; | |
| 1045 | + | |
| 1046 | + const response = await fetch(endpoint, { | |
| 1047 | + method: 'POST', | |
| 1048 | + headers: getRequestHeaders(), | |
| 1049 | + body: JSON.stringify({ id: folderId, paths }), | |
| 1050 | + }); | |
| 1051 | + | |
| 1052 | + if (!response.ok) { | |
| 1053 | + throw new Error(`Folder ${isRemove ? 'unassign' : 'assign'} failed: ${response.status}`); | |
| 1054 | + } | |
| 1055 | + | |
| 1056 | + for (const bgFile of bgFiles) { | |
| 1057 | + const currentFolderIds = imageFolderMap[bgFile] || []; | |
| 1058 | + if (isRemove) { | |
| 1059 | + const nextFolderIds = currentFolderIds.filter(id => id !== folderId); | |
| 1060 | + if (nextFolderIds.length > 0) { | |
| 1061 | + imageFolderMap[bgFile] = nextFolderIds; | |
| 1062 | + } else { | |
| 1063 | + delete imageFolderMap[bgFile]; | |
| 1064 | + } | |
| 1065 | + } else if (!currentFolderIds.includes(folderId)) { | |
| 1066 | + imageFolderMap[bgFile] = [...currentFolderIds, folderId]; | |
| 1067 | + } | |
| 1068 | + } | |
| 1069 | +} | |
| 1070 | + | |
| 1071 | +/** | |
| 1072 | + * Adds selected system backgrounds to a chosen folder. | |
| 1073 | + */ | |
| 1074 | +async function onAddSelectedToFolder() { | |
| 1075 | + if (getActiveBackgroundTab() !== BG_SOURCES.GLOBAL) { | |
| 1076 | + toastr.warning(t`Folder actions are only available in the Global tab`); | |
| 1077 | + return; | |
| 1078 | + } | |
| 1079 | + | |
| 1080 | + const bgFiles = Array.from(selectedSystemBackgroundFiles); | |
| 1081 | + if (bgFiles.length === 0) { | |
| 1082 | + toastr.info(t`Select one or more backgrounds first`); | |
| 1083 | + return; | |
| 1084 | + } | |
| 1085 | + | |
| 1086 | + const folderIds = await selectFoldersForGroupAction(t`Add selected backgrounds to folders`); | |
| 1087 | + if (!folderIds) return; | |
| 1088 | + | |
| 1089 | + try { | |
| 1090 | + let totalAdded = 0; | |
| 1091 | + for (const folderId of folderIds) { | |
| 1092 | + const actionableBgFiles = bgFiles.filter(bgFile => { | |
| 1093 | + const currentFolderIds = imageFolderMap[bgFile] || []; | |
| 1094 | + return !currentFolderIds.includes(folderId); | |
| 1095 | + }); | |
| 1096 | + if (actionableBgFiles.length > 0) { | |
| 1097 | + await updateFolderAssignments(actionableBgFiles, folderId, false); | |
| 1098 | + totalAdded += actionableBgFiles.length; | |
| 1099 | + } | |
| 1100 | + } | |
| 1101 | + | |
| 1102 | + renderFolderGrid(); | |
| 1103 | + | |
| 1104 | + if (activeFolderId) { | |
| 1105 | + renderSystemBackgrounds(getFilteredImages()); | |
| 1106 | + highlightSelectedBackground(); | |
| 1107 | + } | |
| 1108 | + | |
| 1109 | + setBackgroundSelectionMode(false); | |
| 1110 | + if (totalAdded > 0) { | |
| 1111 | + toastr.success(t`Added backgrounds to ${folderIds.length} folder(s)`); | |
| 1112 | + } else { | |
| 1113 | + toastr.info(t`Selected backgrounds are already in the chosen folders`); | |
| 1114 | + } | |
| 1115 | + } catch (error) { | |
| 1116 | + console.error('Error adding selected backgrounds to folder:', error); | |
| 1117 | + toastr.error(t`Failed to update folder assignment`); | |
| 1118 | + } | |
| 1119 | +} | |
| 1120 | + | |
| 1121 | +/** | |
| 1122 | + * Removes selected system backgrounds from the currently drilled-in folder. | |
| 1123 | + */ | |
| 1124 | +async function onRemoveSelectedFromCurrentFolder() { | |
| 1125 | + if (getActiveBackgroundTab() !== BG_SOURCES.GLOBAL) { | |
| 1126 | + toastr.warning(t`Folder actions are only available in the Global tab`); | |
| 1127 | + return; | |
| 1128 | + } | |
| 1129 | + | |
| 1130 | + if (!activeFolderId) { | |
| 1131 | + toastr.info(t`Open a folder first`); | |
| 1132 | + return; | |
| 1133 | + } | |
| 1134 | + | |
| 1135 | + const bgFiles = Array.from(selectedSystemBackgroundFiles); | |
| 1136 | + if (bgFiles.length === 0) { | |
| 1137 | + toastr.info(t`Select one or more backgrounds first`); | |
| 1138 | + return; | |
| 1139 | + } | |
| 1140 | + | |
| 1141 | + try { | |
| 1142 | + await updateFolderAssignments(bgFiles, activeFolderId, true); | |
| 1143 | + renderFolderGrid(); | |
| 1144 | + renderSystemBackgrounds(getFilteredImages()); | |
| 1145 | + highlightSelectedBackground(); | |
| 1146 | + setBackgroundSelectionMode(false); | |
| 1147 | + toastr.success(t`Removed ${bgFiles.length} background(s) from folder`); | |
| 1148 | + } catch (error) { | |
| 1149 | + console.error('Error removing selected backgrounds from current folder:', error); | |
| 1150 | + toastr.error(t`Failed to update folder assignment`); | |
| 1151 | + } | |
| 1152 | +} | |
| 1153 | + | |
| 1154 | +/** | |
| 893 | 1155 | * Creates a new folder via API. |
| 894 | 1156 | */ |
| 895 | 1157 | async function onCreateFolder() { |
| @@ -1037,40 +1299,12 @@ async function onAssignToFolder(bgFile) { | ||
| 1037 | 1299 | if (!isChecked && wasChecked) toUnassign.push(fid); |
| 1038 | 1300 | }); |
| 1039 | 1301 | |
| 1040 | - const relativePath = getBackgroundRelativePath(bgFile); | |
| 1041 | - | |
| 1042 | 1302 | try { |
| 1043 | 1303 | for (const fid of toAssign) { |
| 1044 | - const resp = await fetch('/api/image-metadata/folders/assign', { | |
| 1304 | + await updateFolderAssignments([bgFile], fid, false); | |
| 1045 | - method: 'POST', | |
| 1046 | - headers: getRequestHeaders(), | |
| 1047 | - body: JSON.stringify({ id: fid, paths: [relativePath] }), | |
| 1048 | - }); | |
| 1049 | - if (!resp.ok) throw new Error(`Assign to folder ${fid} failed: ${resp.status}`); | |
| 1050 | 1305 | } |
| 1051 | 1306 | for (const fid of toUnassign) { |
| 1052 | - const resp = await fetch('/api/image-metadata/folders/unassign', { | |
| 1307 | + await updateFolderAssignments([bgFile], fid, true); | |
| 1053 | - method: 'POST', | |
| 1054 | - headers: getRequestHeaders(), | |
| 1055 | - body: JSON.stringify({ id: fid, paths: [relativePath] }), | |
| 1056 | - }); | |
| 1057 | - if (!resp.ok) throw new Error(`Unassign from folder ${fid} failed: ${resp.status}`); | |
| 1058 | - } | |
| 1059 | - | |
| 1060 | - // Update local state | |
| 1061 | - const newFolderIds = folderList | |
| 1062 | - .filter(f => { | |
| 1063 | - const wasIn = currentFolderIds.includes(f.id); | |
| 1064 | - if (toAssign.includes(f.id)) return true; | |
| 1065 | - if (toUnassign.includes(f.id)) return false; | |
| 1066 | - return wasIn; | |
| 1067 | - }) | |
| 1068 | - .map(f => f.id); | |
| 1069 | - | |
| 1070 | - if (newFolderIds.length > 0) { | |
| 1071 | - imageFolderMap[bgFile] = newFolderIds; | |
| 1072 | - } else { | |
| 1073 | - delete imageFolderMap[bgFile]; | |
| 1074 | 1308 | } |
| 1075 | 1309 | |
| 1076 | 1310 | renderFolderGrid(); |
| @@ -1449,7 +1683,11 @@ const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debou | ||
| 1449 | 1683 | * @returns {BG_SOURCES} Active background tab source |
| 1450 | 1684 | */ |
| 1451 | 1685 | export function getActiveBackgroundTab() { |
| 1452 | 1686 | returnconst tabs = $('#bg_tabs').tabs('option', 'active'); |
| 1687 | + if (!tabs.length || !tabs.data('ui-tabs')) { | |
| 1688 | + return BG_SOURCES.GLOBAL; | |
| 1689 | + } | |
| 1690 | + return tabs.tabs('option', 'active'); | |
| 1453 | 1691 | } |
| 1454 | 1692 | |
| 1455 | 1693 | export function initBackgrounds() { |
| @@ -1511,6 +1749,9 @@ export function initBackgrounds() { | ||
| 1511 | 1749 | }) |
| 1512 | 1750 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 1513 | 1751 | e.stopPropagation(); |
| 1752 | + if (isBackgroundSelectionMode && $(this).closest('#bg_menu_content').length) { | |
| 1753 | + return; | |
| 1754 | + } | |
| 1514 | 1755 | const action = $(this).data('action'); |
| 1515 | 1756 | |
| 1516 | 1757 | switch (action) { |
| @@ -1553,6 +1794,9 @@ export function initBackgrounds() { | ||
| 1553 | 1794 | applyThumbnailColumns(background_settings.thumbnailColumns + 1); |
| 1554 | 1795 | }); |
| 1555 | 1796 | $('#auto_background').on('click', autoBackgroundCommand); |
| 1797 | + $('#bg_selection_mode_button').on('click', () => setBackgroundSelectionMode(!isBackgroundSelectionMode)); | |
| 1798 | + $('#bg_group_add_to_folder_button').on('click', onAddSelectedToFolder); | |
| 1799 | + $('#bg_folder_remove_selected_button').on('click', onRemoveSelectedFromCurrentFolder); | |
| 1556 | 1800 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); |
| 1557 | 1801 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 1558 | 1802 | $('#bg-sort').on('change', function () { |
| @@ -1615,4 +1859,7 @@ export function initBackgrounds() { | ||
| 1615 | 1859 | }); |
| 1616 | 1860 | |
| 1617 | 1861 | $('#bg_tabs').tabs(); |
| 1862 | + $('#bg_tabs').on('tabsactivate', () => updateGroupFolderControlsVisibility()); | |
| 1863 | + updateGroupFolderControlsVisibility(); | |
| 1864 | + syncGroupSelectionUi(); | |
| 1618 | 1865 | } |