diff --git a/CHANGES.md b/CHANGES.md index 6163da1e..2b5ae762 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - A `What's New?` tab in the UI ### Detailed changelog +* 2.4.13 - 21 Nov 2022 - Change the modifier weight via mouse wheel, drag to reorder selected modifiers, and some more modifier-related fixes. Thanks @patriceac * 2.4.12 - 21 Nov 2022 - Another fix for improving how long images take to generate. Reduces the time taken for an enqueued task to start processing. * 2.4.11 - 21 Nov 2022 - Installer improvements: avoid crashing if the username contains a space or special characters, allow moving/renaming the folder after installation on Windows, whitespace fix on git apply * 2.4.11 - 21 Nov 2022 - Validate inputs before submitting the Image request diff --git a/NSIS/README.md b/NSIS/README.md new file mode 100644 index 00000000..d1cf0e19 --- /dev/null +++ b/NSIS/README.md @@ -0,0 +1 @@ +Scripts to be used with the Nullsoft Scriptable Installation System diff --git a/NSIS/astro.bmp b/NSIS/astro.bmp new file mode 100644 index 00000000..4c10400e Binary files /dev/null and b/NSIS/astro.bmp differ diff --git a/NSIS/sd.ico b/NSIS/sd.ico new file mode 100644 index 00000000..0f8915e4 Binary files /dev/null and b/NSIS/sd.ico differ diff --git a/NSIS/sdui.nsi b/NSIS/sdui.nsi new file mode 100644 index 00000000..501ea32b --- /dev/null +++ b/NSIS/sdui.nsi @@ -0,0 +1,265 @@ +; Script generated by the HM NIS Edit Script Wizard. + +Target x86-unicode +Unicode True +!AddPluginDir /x86-unicode "." +; HM NIS Edit Wizard helper defines +!define PRODUCT_NAME "Stable Diffusion UI" +!define PRODUCT_VERSION "Installer 2.35" +!define PRODUCT_PUBLISHER "cmdr2 and contributors" +!define PRODUCT_WEB_SITE "https://stable-diffusion-ui.github.io" +!define PRODUCT_DIR_REGKEY "Software\Microsoft\Cmdr2\App Paths\installer.exe" + +; MUI 1.67 compatible ------ +!include "MUI.nsh" +!include "LogicLib.nsh" +!include "nsDialogs.nsh" + +Var Dialog +Var Label +Var Button + +Var InstDirLen +Var LongPathsEnabled +Var AccountType + +;--------------------------------------------------------------------------------------------------------- +; This function returns the number of spaces in a string. +; The string is passed on the stack (using Push $STRING) +; The result is also returned on the stack and can be consumed with Pop $var +; https://nsis.sourceforge.io/Check_for_spaces_in_a_directory_path +Function CheckForSpaces + Exch $R0 + Push $R1 + Push $R2 + Push $R3 + StrCpy $R1 -1 + StrCpy $R3 $R0 + StrCpy $R0 0 + loop: + StrCpy $R2 $R3 1 $R1 + IntOp $R1 $R1 - 1 + StrCmp $R2 "" done + StrCmp $R2 " " 0 loop + IntOp $R0 $R0 + 1 + Goto loop + done: + Pop $R3 + Pop $R2 + Pop $R1 + Exch $R0 +FunctionEnd + +;--------------------------------------------------------------------------------------------------------- +; The function DirectoryLeave is called after the user chose the installation directory. +; If it calls "abort", the user is sent back to choose a different directory. +Function DirectoryLeave + ; check whether the installation directory path is longer than 30 characters. + ; If yes, we suggest to the user to enable long filename support + ;---------------------------------------------------------------------------- + StrLen $InstDirLen "$INSTDIR" + + ; Check whether the registry key that allows for >260 characters in a path name is set + ReadRegStr $LongPathsEnabled HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" + + ${If} $InstDirLen > 30 + ${AndIf} $LongPathsEnabled == "0" + ; Check whether we're in the Admin group + UserInfo::GetAccountType + Pop $AccountType + + ${If} $AccountType == "Admin" + ${AndIf} ${Cmd} `MessageBox MB_YESNO|MB_ICONQUESTION 'The path name is too long. $\n$\nYou can either enable long file name support in Windows,$\nor you can go back and choose a different path.$\n$\nFor details see: shorturl.at/auBD1$\n$\nEnable long path name support in Windows?' IDYES` + ; Enable long path names + WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1 + ${Else} + MessageBox MB_OK|MB_ICONEXCLAMATION "Installation path name too long. The installation path must not have more than 30 characters." + abort + ${EndIf} + ${EndIf} + + ; Check for spaces in the installation directory path. + ; ---------------------------------------------------- + + ; $R0 = CheckForSpaces( $INSTDIR ) + Push $INSTDIR # Input string (install path). + Call CheckForSpaces + Pop $R0 # The function returns the number of spaces found in the input string. + + ; Check if any spaces exist in $INSTDIR. + ${If} $R0 != 0 + ; Plural if more than 1 space in $INSTDIR. + ; If $R0 == 1: $R1 = ""; else: $R1 = "s" + StrCmp $R0 1 0 +3 + StrCpy $R1 "" + Goto +2 + StrCpy $R1 "s" + + ; Show message box then take the user back to the Directory page. + MessageBox MB_OK|MB_ICONEXCLAMATION "Error: The Installaton directory \ + has $R0 space character$R1.$\nPlease choose an installation directory without space characters." + Abort + ${EndIf} + + ; Check for NTFS filesystem. Installations on FAT fail. + ; ----------------------------------------------------- + StrCpy $5 $INSTDIR 3 + System::Call 'Kernel32::GetVolumeInformation(t "$5",t,i ${NSIS_MAX_STRLEN},*i,*i,*i,t.r1,i ${NSIS_MAX_STRLEN})i.r0' + ${If} $0 <> 0 + ${AndIf} $1 == "NTFS" + MessageBox mb_ok "$5 has filesystem type '$1'.$\nOnly NTFS filesystems are supported.$\nPlease choose a different drive." + Abort + ${EndIf} + +FunctionEnd + + +;--------------------------------------------------------------------------------------------------------- +; Open the MS download page in a browser and enable the [Next] button +Function MSMediaFeaturepack + ExecShell "open" "https://www.microsoft.com/en-us/software-download/mediafeaturepack" + + GetDlgItem $0 $HWNDPARENT 1 + EnableWindow $0 1 +FunctionEnd + +;--------------------------------------------------------------------------------------------------------- +; Install the MS Media Feature Pack, if it is missing (e.g. on Windows 10 N) +Function MediaPackDialog + !insertmacro MUI_HEADER_TEXT "Windows Media Feature Pack" "Required software module is missing" + + ; Skip this dialog if mf.dll is installed + ${If} ${FileExists} "$WINDIR\system32\mf.dll" + Abort + ${EndIf} + + nsDialogs::Create 1018 + Pop $Dialog + + ${If} $Dialog == error + Abort + ${EndIf} + + ${NSD_CreateLabel} 0 0 100% 48u "The Windows Media Feature Pack is missing on this computer. It is required for the Stable Diffusion UI.$\nYou can continue the installation after installing the Windows Media Feature Pack." + Pop $Label + + ${NSD_CreateButton} 10% 49u 80% 12u "Download Meda Feature Pack from Microsoft" + Pop $Button + + GetFunctionAddress $0 MSMediaFeaturePack + nsDialogs::OnClick $Button $0 + GetDlgItem $0 $HWNDPARENT 1 + EnableWindow $0 0 + nsDialogs::Show +FunctionEnd + +;--------------------------------------------------------------------------------------------------------- +; MUI Settings +;--------------------------------------------------------------------------------------------------------- +!define MUI_ABORTWARNING +!define MUI_ICON "sd.ico" + +!define MUI_WELCOMEFINISHPAGE_BITMAP "astro.bmp" + +; Welcome page +!define MUI_WELCOMEPAGE_TEXT "This installer will guide you through the installation of Stable Diffusion UI.$\n$\n\ +Click Next to continue." +!insertmacro MUI_PAGE_WELCOME +Page custom MediaPackDialog + +; License page +!insertmacro MUI_PAGE_LICENSE "..\LICENSE" +!insertmacro MUI_PAGE_LICENSE "..\CreativeML Open RAIL-M License" +; Directory page +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE "DirectoryLeave" +!insertmacro MUI_PAGE_DIRECTORY + +; Instfiles page +!insertmacro MUI_PAGE_INSTFILES + +; Finish page +!define MUI_FINISHPAGE_RUN "$INSTDIR\Start Stable Diffusion UI.cmd" +!insertmacro MUI_PAGE_FINISH + +; Language files +!insertmacro MUI_LANGUAGE "English" +;--------------------------------------------------------------------------------------------------------- +; MUI end +;--------------------------------------------------------------------------------------------------------- + +Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" +OutFile "Install Stable Diffusion UI.exe" +InstallDir "C:\Stable-Diffusion-UI\" +InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" "" +ShowInstDetails show + +;--------------------------------------------------------------------------------------------------------- +; List of files to be installed +Section "MainSection" SEC01 + SetOutPath "$INSTDIR" + File "..\CreativeML Open RAIL-M License" + File "..\How to install and run.txt" + File "..\LICENSE" + File "..\Start Stable Diffusion UI.cmd" + SetOutPath "$INSTDIR\scripts" + File "..\scripts\bootstrap.bat" + File "..\scripts\install_status.txt" + File "..\scripts\on_env_start.bat" + File "C:\windows\system32\curl.exe" + CreateDirectory "$INSTDIR\profile" + CreateDirectory "$SMPROGRAMS\Stable Diffusion UI" + CreateShortCut "$SMPROGRAMS\Stable Diffusion UI\Start Stable Diffusion UI.lnk" "$INSTDIR\Start Stable Diffusion UI.cmd" +SectionEnd + +;--------------------------------------------------------------------------------------------------------- +; Our installer only needs 25 KB, but once it has run, we need 25 GB +; So we need to overwrite the automatically detected space requirements. +; https://nsis.sourceforge.io/Docs/Chapter4.html#4.9.13.7 +; The example in section 4.9.13.7 seems to be wrong: the number +; needs to be provided in Kilobytes. +Function .onInit + ; Set required size of section 'SEC01' to 25 Gigabytes + SectionSetSize ${SEC01} 26214400 + + + ; Check system meory size. We need at least 8GB + ; ---------------------------------------------------- + + ; allocate a few bytes of memory + System::Alloc 64 + Pop $1 + + ; Retrieve HW info from the Windows Kernel + System::Call "*$1(i64)" + System::Call "Kernel32::GlobalMemoryStatusEx(i r1)" + ; unpack the data into $R2 - $R10 + System::Call "*$1(i.r2, i.r3, l.r4, l.r5, l.r6, l.r7, l.r8, l.r9, l.r10)" + + # free up the memory + System::Free $1 + + ; Result mapping: + ; "Structure size: $2 bytes" + ; "Memory load: $3%" + ; "Total physical memory: $4 bytes" + ; "Free physical memory: $5 bytes" + ; "Total page file: $6 bytes" + ; "Free page file: $7 bytes" + ; "Total virtual: $8 bytes" + ; "Free virtual: $9 bytes" + + ; Mem size in MB + System::Int64Op $4 / 1048576 + Pop $4 + + ${If} $4 < "8000" + MessageBox MB_OK|MB_ICONEXCLAMATION "Warning!$\n$\nYour system has less than 8GB of memory (RAM).$\n$\n\ +You can still try to install Stable Diffusion UI,$\nbut it might have problems to start, or run$\nvery slowly." + ${EndIf} + +FunctionEnd + + +;Section -Post +; WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\installer.exe" +;SectionEnd diff --git a/ui/index.html b/ui/index.html index d287aca2..25a94a13 100644 --- a/ui/index.html +++ b/ui/index.html @@ -20,7 +20,7 @@
diff --git a/ui/media/js/dnd.js b/ui/media/js/dnd.js index 9cbbe8af..4c14333c 100644 --- a/ui/media/js/dnd.js +++ b/ui/media/js/dnd.js @@ -161,18 +161,7 @@ const TASK_MAPPING = { setUI: (use_stable_diffusion_model) => { const oldVal = stableDiffusionModelField.value - let pathIdx = use_stable_diffusion_model.lastIndexOf('/') // Linux, Mac paths - if (pathIdx < 0) { - pathIdx = use_stable_diffusion_model.lastIndexOf('\\') // Windows paths. - } - if (pathIdx >= 0) { - use_stable_diffusion_model = use_stable_diffusion_model.slice(pathIdx + 1) - } - const modelExt = '.ckpt' - if (use_stable_diffusion_model.endsWith(modelExt)) { - use_stable_diffusion_model = use_stable_diffusion_model.slice(0, use_stable_diffusion_model.length - modelExt.length) - } - + use_stable_diffusion_model = getModelPath(use_stable_diffusion_model, ['.ckpt']) stableDiffusionModelField.value = use_stable_diffusion_model if (!stableDiffusionModelField.value) { @@ -182,6 +171,19 @@ const TASK_MAPPING = { readUI: () => stableDiffusionModelField.value, parse: (val) => val }, + use_vae_model: { name: 'VAE model', + setUI: (use_vae_model) => { + const oldVal = vaeModelField.value + + if (use_vae_model !== '') { + use_vae_model = getModelPath(use_vae_model, ['.vae.pt', '.ckpt']) + use_vae_model = use_vae_model !== '' ? use_vae_model : oldVal + } + vaeModelField.value = use_vae_model + }, + readUI: () => vaeModelField.value, + parse: (val) => val + }, numOutputsParallel: { name: 'Parallel Images', setUI: (numOutputsParallel) => { @@ -310,6 +312,21 @@ function readUI() { 'reqBody': reqBody } } +function getModelPath(filename, extensions) +{ + let pathIdx = filename.lastIndexOf('/') // Linux, Mac paths + if (pathIdx < 0) { + pathIdx = filename.lastIndexOf('\\') // Windows paths. + } + if (pathIdx >= 0) { + filename = filename.slice(pathIdx + 1) + } + extensions.forEach(ext => { + if (filename.endsWith(ext)) { + filename = filename.slice(0, filename.length - ext.length) + } + }) +} const TASK_TEXT_MAPPING = { width: 'Width', @@ -365,29 +382,35 @@ function parseTaskFromText(str) { return task } -async function readFile(file, i) { - const fileContent = (await file.text()).trim() - - // JSON File. - if (fileContent.startsWith('{') && fileContent.endsWith('}')) { +async function parseContent(text) { + text = text.trim(); + if (text.startsWith('{') && text.endsWith('}')) { try { - const task = JSON.parse(fileContent) + const task = JSON.parse(text) restoreTaskToUI(task) + return true } catch (e) { - console.warn(`file[${i}]:${file.name} - File couldn't be parsed.`, e) + console.warn(`JSON text content couldn't be parsed.`, e) } - return + return false } - // Normal txt file. - const task = parseTaskFromText(fileContent) + const task = parseTaskFromText(text) if (task) { restoreTaskToUI(task) + return true } else { - console.warn(`file[${i}]:${file.name} - File couldn't be parsed.`) + console.warn(`Raw text content couldn't be parsed.`) + return false } } +async function readFile(file, i) { + console.log(`Event %o reading file[${i}]:${file.name}...`, e) + const fileContent = (await file.text()).trim() + return await parseContent(fileContent) +} + function dropHandler(ev) { console.log('Content dropped...') let items = [] @@ -434,72 +457,73 @@ const TASK_REQ_NO_EXPORT = [ "use_full_precision", "save_to_disk_path" ] +const resetSettings = document.getElementById('reset-image-settings') -// Retrieve clipboard content and try to parse it -async function pasteFromClipboard() { - //const text = await navigator.clipboard.readText() - let text = await navigator.clipboard.readText(); - text=text.trim(); - if (text.startsWith('{') && text.endsWith('}')) { - try { - const task = JSON.parse(text) - restoreTaskToUI(task) - } catch (e) { - console.warn(`Clipboard JSON couldn't be parsed.`, e) - } +function checkReadTextClipboardPermission (result) { + if (result.state != "granted" && result.state != "prompt") { return } - // Normal txt file. - const task = parseTaskFromText(text) - if (task) { - restoreTaskToUI(task) - } else { - console.warn(`Clipboard content - File couldn't be parsed.`) - } + // PASTE ICON + const pasteIcon = document.createElement('i') + pasteIcon.className = 'fa-solid fa-paste section-button' + pasteIcon.innerHTML = `Paste Image Settings` + pasteIcon.addEventListener('click', async (event) => { + event.stopPropagation() + // Add css class 'active' + pasteIcon.classList.add('active') + // In 350 ms remove the 'active' class + asyncDelay(350).then(() => pasteIcon.classList.remove('active')) + + // Retrieve clipboard content and try to parse it + const text = await navigator.clipboard.readText(); + await parseContent(text) + }) + resetSettings.parentNode.insertBefore(pasteIcon, resetSettings) } +navigator.permissions.query({ name: "clipboard-read" }).then(checkReadTextClipboardPermission, (reason) => console.log('clipboard-read is not available. %o', reason)) + +document.addEventListener('paste', async (event) => { + if (event.target) { + const targetTag = event.target.tagName.toLowerCase() + // Disable when targeting input elements. + if (targetTag === 'input' || targetTag === 'textarea') { + return + } + } + const paste = (event.clipboardData || window.clipboardData).getData('text') + const selection = window.getSelection() + if (selection.toString().trim().length <= 0 && await parseContent(paste)) { + event.preventDefault() + return + } +}) // Adds a copy and a paste icon if the browser grants permission to write to clipboard. function checkWriteToClipboardPermission (result) { - if (result.state == "granted" || result.state == "prompt") { - const resetSettings = document.getElementById('reset-image-settings') - - // COPY ICON - const copyIcon = document.createElement('i') - copyIcon.className = 'fa-solid fa-clipboard section-button' - copyIcon.innerHTML = `Copy Image Settings` - copyIcon.addEventListener('click', (event) => { - event.stopPropagation() - // Add css class 'active' - copyIcon.classList.add('active') - // In 350 ms remove the 'active' class - asyncDelay(350).then(() => copyIcon.classList.remove('active')) - const uiState = readUI() - TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key]) - if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) { - delete uiState.reqBody.init_image - delete uiState.reqBody.prompt_strength - } - navigator.clipboard.writeText(JSON.stringify(uiState, undefined, 4)) - }) - resetSettings.parentNode.insertBefore(copyIcon, resetSettings) - - // PASTE ICON - const pasteIcon = document.createElement('i') - pasteIcon.className = 'fa-solid fa-paste section-button' - pasteIcon.innerHTML = `Paste Image Settings` - pasteIcon.addEventListener('click', (event) => { - event.stopPropagation() - // Add css class 'active' - pasteIcon.classList.add('active') - // In 350 ms remove the 'active' class - asyncDelay(350).then(() => pasteIcon.classList.remove('active')) - pasteFromClipboard() - }) - resetSettings.parentNode.insertBefore(pasteIcon, resetSettings) + if (result.state != "granted" && result.state != "prompt") { + return } + // COPY ICON + const copyIcon = document.createElement('i') + copyIcon.className = 'fa-solid fa-clipboard section-button' + copyIcon.innerHTML = `Copy Image Settings` + copyIcon.addEventListener('click', (event) => { + event.stopPropagation() + // Add css class 'active' + copyIcon.classList.add('active') + // In 350 ms remove the 'active' class + asyncDelay(350).then(() => copyIcon.classList.remove('active')) + const uiState = readUI() + TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key]) + if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) { + delete uiState.reqBody.init_image + delete uiState.reqBody.prompt_strength + } + navigator.clipboard.writeText(JSON.stringify(uiState, undefined, 4)) + }) + resetSettings.parentNode.insertBefore(copyIcon, resetSettings) } - -// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS. +// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS. navigator.permissions.query({ name: "clipboard-write" }).then(checkWriteToClipboardPermission, (e) => { if (e instanceof TypeError && typeof navigator?.clipboard?.writeText === 'function') { // Fix for firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1560373 diff --git a/ui/media/js/image-modifiers.js b/ui/media/js/image-modifiers.js index 8cf26a49..24347fc4 100644 --- a/ui/media/js/image-modifiers.js +++ b/ui/media/js/image-modifiers.js @@ -166,11 +166,13 @@ function refreshModifiersState(newTags) { const modifierName = modifierCard.querySelector('.modifier-card-label').innerText if (tag == modifierName) { // add modifier to active array - activeTags.push({ - 'name': modifierName, - 'element': modifierCard.cloneNode(true), - 'originElement': modifierCard - }) + if (!activeTags.map(x => x.name).includes(tag)) { // only add each tag once even if several custom modifier cards share the same tag + activeTags.push({ + 'name': modifierName, + 'element': modifierCard.cloneNode(true), + 'originElement': modifierCard + }) + } modifierCard.classList.add(activeCardClass) modifierCard.querySelector('.modifier-card-image-overlay').innerText = '-' found = true diff --git a/ui/plugins/ui/Modifiers-dnd.plugin.js b/ui/plugins/ui/Modifiers-dnd.plugin.js new file mode 100644 index 00000000..f4feb4b6 --- /dev/null +++ b/ui/plugins/ui/Modifiers-dnd.plugin.js @@ -0,0 +1,91 @@ +(function () { + "use strict" + + var styleSheet = document.createElement("style"); + styleSheet.textContent = ` + .modifier-card-tiny.drag-sort-active { + background: transparent; + border: 2px dashed white; + opacity:0.2; + } + `; + document.head.appendChild(styleSheet); + + // observe for changes in tag list + var observer = new MutationObserver(function (mutations) { + // mutations.forEach(function (mutation) { + if (editorModifierTagsList.childNodes.length > 0) { + ModifierDragAndDrop(editorModifierTagsList) + } + // }) + }) + + observer.observe(editorModifierTagsList, { + childList: true + }) + + let current + function ModifierDragAndDrop(target) { + let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay') + overlays.forEach (i => { + i.parentElement.draggable = true; + + i.parentElement.ondragstart = (e) => { + current = i + i.parentElement.getElementsByClassName('modifier-card-image-overlay')[0].innerText = '' + i.parentElement.draggable = true + i.parentElement.classList.add('drag-sort-active') + for(let item of document.querySelector('#editor-inputs-tags-list').getElementsByClassName('modifier-card-image-overlay')) { + if (item.parentElement.parentElement.getElementsByClassName('modifier-card-overlay')[0] != current) { + item.parentElement.parentElement.getElementsByClassName('modifier-card-image-overlay')[0].style.opacity = 0 + if(item.parentElement.getElementsByClassName('modifier-card-image').length > 0) { + item.parentElement.getElementsByClassName('modifier-card-image')[0].style.filter = 'none' + } + item.parentElement.parentElement.style.transform = 'none' + item.parentElement.parentElement.style.boxShadow = 'none' + } + item.innerText = '' + } + } + + i.ondragenter = (e) => { + e.preventDefault() + if (i != current) { + let currentPos = 0, droppedPos = 0; + for (let it = 0; it < overlays.length; it++) { + if (current == overlays[it]) { currentPos = it; } + if (i == overlays[it]) { droppedPos = it; } + } + + if (i.parentElement != current.parentElement) { + let currentPos = 0, droppedPos = 0 + for (let it = 0; it < overlays.length; it++) { + if (current == overlays[it]) { currentPos = it } + if (i == overlays[it]) { droppedPos = it } + } + if (currentPos < droppedPos) { + current = i.parentElement.parentNode.insertBefore(current.parentElement, i.parentElement.nextSibling).getElementsByClassName('modifier-card-overlay')[0] + } else { + current = i.parentElement.parentNode.insertBefore(current.parentElement, i.parentElement).getElementsByClassName('modifier-card-overlay')[0] + } + // update activeTags + const tag = activeTags.splice(currentPos, 1) + activeTags.splice(droppedPos, 0, tag[0]) + } + } + }; + + i.ondragover = (e) => { + e.preventDefault() + } + + i.parentElement.ondragend = (e) => { + i.parentElement.classList.remove('drag-sort-active') + for(let item of document.querySelector('#editor-inputs-tags-list').getElementsByClassName('modifier-card-image-overlay')) { + item.style.opacity = '' + item.innerText = '-' + } + } + }) + } +})() diff --git a/ui/plugins/ui/Modifiers-wheel.plugin.js b/ui/plugins/ui/Modifiers-wheel.plugin.js new file mode 100644 index 00000000..0f75c2e1 --- /dev/null +++ b/ui/plugins/ui/Modifiers-wheel.plugin.js @@ -0,0 +1,60 @@ +(function () { + "use strict" + + // observe for changes in tag list + var observer = new MutationObserver(function (mutations) { + // mutations.forEach(function (mutation) { + if (editorModifierTagsList.childNodes.length > 0) { + ModifierMouseWheel(editorModifierTagsList) + } + // }) + }) + + observer.observe(editorModifierTagsList, { + childList: true + }) + + function ModifierMouseWheel(target) { + let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay') + overlays.forEach (i => { + i.onwheel = (e) => { + e.preventDefault() + + const delta = Math.sign(event.deltaY) + let s = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText + if (delta < 0) { + // wheel scrolling up + if (s.substring(0, 1) == '[' && s.substring(s.length-1) == ']') { + s = s.substring(1, s.length - 1) + } + else + { + if (s.substring(0, 10) !== '('.repeat(10) && s.substring(s.length-10) !== ')'.repeat(10)) { + s = '(' + s + ')' + } + } + } + else{ + // wheel scrolling down + if (s.substring(0, 1) == '(' && s.substring(s.length-1) == ')') { + s = s.substring(1, s.length - 1) + } + else + { + if (s.substring(0, 10) !== '['.repeat(10) && s.substring(s.length-10) !== ']'.repeat(10)) { + s = '[' + s + ']' + } + } + } + i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText = s + // update activeTags + for (let it = 0; it < overlays.length; it++) { + if (i == overlays[it]) { + activeTags[it].name = s + break + } + } + } + }) + } +})()