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 | outline-offset: 0; | 136 | outline-offset: 0; |
| 137 | } | 137 | } |
| 138 | 138 | ||
| 139 | .bg_example.locked-background::after { | 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 | font-family: 'Font Awesome 6 Free'; | 142 | font-family: 'Font Awesome 6 Free'; |
| 142 | font-weight: 900; | 143 | font-weight: 900; |
| 143 | |||
| 144 | position: absolute; | 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 | bottom: 5px; | 160 | bottom: 5px; |
| 146 | right: 5px; | 161 | right: 5px; |
| 147 | z-index: 4; | ||
| 148 | color: var(--golden); | 162 | color: var(--golden); |
| 149 | filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8)); | 163 | filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8)); |
| 150 | font-size: calc(var(--mainFontSize) * 0.8); | 164 | font-size: calc(var(--mainFontSize) * 0.8); |
| 151 | pointer-events: none; | ||
| 152 | } | 165 | } |
| 153 | 166 | ||
| 154 | .bg_example:not(.locked-background) .jg-unlock, | 167 | .bg_example:not(.locked-background) .jg-unlock, |
| @@ -162,17 +175,28 @@ | |||
| 162 | } | 175 | } |
| 163 | 176 | ||
| 164 | .bg_example.selected-background::before { | 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 | left: 5px; | 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 | font-size: calc(var(--mainFontSize) * 0.9); | 182 | color: var(--golden); |
| 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 | .bg_example .jg-menu { | 202 | .bg_example .jg-menu { |
| @@ -153,10 +153,30 @@ | |||
| 153 | 153 | ||
| 154 | #add_background_button_top>span, | 154 | #add_background_button_top>span, |
| 155 | #auto_background>span, | 155 | #auto_background>span, |
| 156 | #bg_add_folder_button>span { | 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 | display: none; | 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 | #extensions_settings, | 180 | #extensions_settings, |
| 161 | #extensions_settings2 { | 181 | #extensions_settings2 { |
| 162 | width: 100% !important; | 182 | width: 100% !important; |
| @@ -5635,6 +5635,14 @@ | |||
| 5635 | <a href="#bg_chat_tab" data-i18n="Chat">Chat</a> | 5635 | <a href="#bg_chat_tab" data-i18n="Chat">Chat</a> |
| 5636 | </li> | 5636 | </li> |
| 5637 | <div class="heading-controls"> | 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 | <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> | 5646 | <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> |
| 5639 | <i class="fa-solid fa-minus"></i> | 5647 | <i class="fa-solid fa-minus"></i> |
| 5640 | </button> | 5648 | </button> |
| @@ -5650,6 +5658,11 @@ | |||
| 5650 | <span data-i18n="Back">Back</span> | 5658 | <span data-i18n="Back">Back</span> |
| 5651 | </button> | 5659 | </button> |
| 5652 | <span id="bg_current_folder_name" class="bg_current_folder_name"></span> | 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 | </div> | 5666 | </div> |
| 5654 | <div id="bg_folder_grid" class="bg_list bg_folder_grid"></div> | 5667 | <div id="bg_folder_grid" class="bg_list bg_folder_grid"></div> |
| 5655 | <div id="bg_menu_content" class="bg_list"></div> | 5668 | <div id="bg_menu_content" class="bg_list"></div> |
| @@ -20,6 +20,10 @@ let folderList = []; | |||
| 20 | let imageFolderMap = {}; | 20 | let imageFolderMap = {}; |
| 21 | /** @type {string|null} Currently active folder drill-in, or null for root */ | 21 | /** @type {string|null} Currently active folder drill-in, or null for root */ |
| 22 | let activeFolderId = null; | 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 | // A single transparent PNG pixel used as a placeholder for errored backgrounds | 28 | // A single transparent PNG pixel used as a placeholder for errored backgrounds |
| 25 | const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; | 29 | const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; |
| @@ -365,6 +369,11 @@ function removeBackgroundMetadata() { | |||
| 365 | function onSelectBackgroundClick(e) { | 369 | function onSelectBackgroundClick(e) { |
| 366 | const bgFile = $(this).attr('bgfile'); | 370 | const bgFile = $(this).attr('bgfile'); |
| 367 | const isCustom = $(this).attr('custom') === 'true'; | 371 | const isCustom = $(this).attr('custom') === 'true'; |
| 372 | if (isBackgroundSelectionMode && !isCustom) { | ||
| 373 | toggleBackgroundGroupSelection(bgFile); | ||
| 374 | return; | ||
| 375 | } | ||
| 376 | |||
| 368 | const backgroundCssUrl = getUrlParameter(this); | 377 | const backgroundCssUrl = getUrlParameter(this); |
| 369 | const bypassGlobalLock = !isCustom && e.shiftKey; | 378 | const bypassGlobalLock = !isCustom && e.shiftKey; |
| 370 | 379 | ||
| @@ -576,6 +585,7 @@ async function onDeleteBackgroundClick(e) { | |||
| 576 | if (deletedBg) { | 585 | if (deletedBg) { |
| 577 | const cachedIdx = cachedSystemBackgrounds.findIndex(img => img.filename === deletedBg); | 586 | const cachedIdx = cachedSystemBackgrounds.findIndex(img => img.filename === deletedBg); |
| 578 | if (cachedIdx !== -1) cachedSystemBackgrounds.splice(cachedIdx, 1); | 587 | if (cachedIdx !== -1) cachedSystemBackgrounds.splice(cachedIdx, 1); |
| 588 | selectedSystemBackgroundFiles.delete(deletedBg); | ||
| 579 | 589 | ||
| 580 | // Update folder map and clear folder thumbnail if it referenced this image | 590 | // Update folder map and clear folder thumbnail if it referenced this image |
| 581 | if (imageFolderMap[deletedBg]) { | 591 | if (imageFolderMap[deletedBg]) { |
| @@ -605,6 +615,7 @@ async function onDeleteBackgroundClick(e) { | |||
| 605 | 615 | ||
| 606 | highlightLockedBackground(); | 616 | highlightLockedBackground(); |
| 607 | highlightSelectedBackground(); | 617 | highlightSelectedBackground(); |
| 618 | syncGroupSelectionUi(); | ||
| 608 | } | 619 | } |
| 609 | } | 620 | } |
| 610 | 621 | ||
| @@ -652,7 +663,10 @@ function renderSystemBackgrounds(backgrounds) { | |||
| 652 | const container = $('#bg_menu_content'); | 663 | const container = $('#bg_menu_content'); |
| 653 | container.empty(); | 664 | container.empty(); |
| 654 | 665 | ||
| 655 | if (sourceList.length === 0) return; | 666 | if (sourceList.length === 0) { |
| 667 | syncGroupSelectionUi(); | ||
| 668 | return; | ||
| 669 | } | ||
| 656 | 670 | ||
| 657 | const sortedList = sortBackgrounds(sourceList.map(bg => bg.filename), false); | 671 | const sortedList = sortBackgrounds(sourceList.map(bg => bg.filename), false); |
| 658 | const metadataByFilename = new Map(sourceList.map(bg => [bg.filename, bg])); | 672 | const metadataByFilename = new Map(sourceList.map(bg => [bg.filename, bg])); |
| @@ -663,6 +677,7 @@ function renderSystemBackgrounds(backgrounds) { | |||
| 663 | container.append(thumbnail); | 677 | container.append(thumbnail); |
| 664 | }); | 678 | }); |
| 665 | 679 | ||
| 680 | syncGroupSelectionUi(); | ||
| 666 | activateLazyLoader(); | 681 | activateLazyLoader(); |
| 667 | } | 682 | } |
| 668 | 683 | ||
| @@ -700,6 +715,12 @@ export async function getBackgrounds() { | |||
| 700 | const { images, config } = await response.json(); | 715 | const { images, config } = await response.json(); |
| 701 | Object.assign(THUMBNAIL_CONFIG, config); | 716 | Object.assign(THUMBNAIL_CONFIG, config); |
| 702 | cachedSystemBackgrounds = images; | 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 | // Load folders first so getFilteredImages() works correctly in folder view | 725 | // Load folders first so getFilteredImages() works correctly in folder view |
| 705 | await loadFolders(); | 726 | await loadFolders(); |
| @@ -859,6 +880,7 @@ function onFolderDrillIn(folderId) { | |||
| 859 | const folder = folderList.find(f => f.id === folderId); | 880 | const folder = folderList.find(f => f.id === folderId); |
| 860 | if (!folder) return; | 881 | if (!folder) return; |
| 861 | 882 | ||
| 883 | clearBackgroundGroupSelection(); | ||
| 862 | activeFolderId = folderId; | 884 | activeFolderId = folderId; |
| 863 | $('#Backgrounds').addClass('in-folder-view'); | 885 | $('#Backgrounds').addClass('in-folder-view'); |
| 864 | 886 | ||
| @@ -876,6 +898,7 @@ function onFolderDrillIn(folderId) { | |||
| 876 | * Returns to the root folder overview. | 898 | * Returns to the root folder overview. |
| 877 | */ | 899 | */ |
| 878 | function onBackToFolders() { | 900 | function onBackToFolders() { |
| 901 | clearBackgroundGroupSelection(); | ||
| 879 | activeFolderId = null; | 902 | activeFolderId = null; |
| 880 | $('#Backgrounds').removeClass('in-folder-view'); | 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 | * Creates a new folder via API. | 1155 | * Creates a new folder via API. |
| 894 | */ | 1156 | */ |
| 895 | async function onCreateFolder() { | 1157 | async function onCreateFolder() { |
| @@ -1037,40 +1299,12 @@ async function onAssignToFolder(bgFile) { | |||
| 1037 | if (!isChecked && wasChecked) toUnassign.push(fid); | 1299 | if (!isChecked && wasChecked) toUnassign.push(fid); |
| 1038 | }); | 1300 | }); |
| 1039 | 1301 | ||
| 1040 | const relativePath = getBackgroundRelativePath(bgFile); | ||
| 1041 | |||
| 1042 | try { | 1302 | try { |
| 1043 | for (const fid of toAssign) { | 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 | for (const fid of toUnassign) { | 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 | renderFolderGrid(); | 1310 | renderFolderGrid(); |
| @@ -1449,7 +1683,11 @@ const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debou | |||
| 1449 | * @returns {BG_SOURCES} Active background tab source | 1683 | * @returns {BG_SOURCES} Active background tab source |
| 1450 | */ | 1684 | */ |
| 1451 | export function getActiveBackgroundTab() { | 1685 | export function getActiveBackgroundTab() { |
| 1452 | return $('#bg_tabs').tabs('option', 'active'); | 1686 | const tabs = $('#bg_tabs'); |
| 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 | export function initBackgrounds() { | 1693 | export function initBackgrounds() { |
| @@ -1511,6 +1749,9 @@ export function initBackgrounds() { | |||
| 1511 | }) | 1749 | }) |
| 1512 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { | 1750 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 1513 | e.stopPropagation(); | 1751 | e.stopPropagation(); |
| 1752 | if (isBackgroundSelectionMode && $(this).closest('#bg_menu_content').length) { | ||
| 1753 | return; | ||
| 1754 | } | ||
| 1514 | const action = $(this).data('action'); | 1755 | const action = $(this).data('action'); |
| 1515 | 1756 | ||
| 1516 | switch (action) { | 1757 | switch (action) { |
| @@ -1553,6 +1794,9 @@ export function initBackgrounds() { | |||
| 1553 | applyThumbnailColumns(background_settings.thumbnailColumns + 1); | 1794 | applyThumbnailColumns(background_settings.thumbnailColumns + 1); |
| 1554 | }); | 1795 | }); |
| 1555 | $('#auto_background').on('click', autoBackgroundCommand); | 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 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); | 1800 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); |
| 1557 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); | 1801 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 1558 | $('#bg-sort').on('change', function () { | 1802 | $('#bg-sort').on('change', function () { |
| @@ -1615,4 +1859,7 @@ export function initBackgrounds() { | |||
| 1615 | }); | 1859 | }); |
| 1616 | 1860 | ||
| 1617 | $('#bg_tabs').tabs(); | 1861 | $('#bg_tabs').tabs(); |
| 1862 | $('#bg_tabs').on('tabsactivate', () => updateGroupFolderControlsVisibility()); | ||
| 1863 | updateGroupFolderControlsVisibility(); | ||
| 1864 | syncGroupSelectionUi(); | ||
| 1618 | } | 1865 | } |