Update to 1.5
V 1.5
This commit is contained in:
@@ -15,9 +15,15 @@ There's an example workflow that illustrates all of them at the end.
|
||||
|
||||
There are also some helper nodes.
|
||||
|
||||
If you prefer trying a workflow to reading docs, use of the nodes is illustrated in this seahorse (drag the image into Comfy):
|
||||
If you prefer trying a workflow to reading docs, use of the nodes is illustrated in this blob (drag the image into Comfy):
|
||||
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/seahorse.png" alt="Seahorse" width="200" height="200">
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/blob.png" alt="Seahorse" width="200" height="200">
|
||||
|
||||
## New in 1.5 ##
|
||||
|
||||
- Floating window for extras and tips
|
||||
- Mask editor node now has option for text extras
|
||||
- Keyboard and mouse navigation in zoomed view
|
||||
|
||||
## New in 1.4 ##
|
||||
|
||||
@@ -34,17 +40,6 @@ Video previews!
|
||||
- pick_list to automatically select images in `Image Filter`
|
||||
- optional initial mask input to `Mask Image Filter`
|
||||
|
||||
## New in 1.2 ##
|
||||
|
||||
- Options have all moved to the main settings
|
||||
- New option to show a miniature window instead of taking over the screen - click on it to go into full screen mode
|
||||
- List of ints output from `Split String by Commas`
|
||||
|
||||
## New in 1.1.6 ##
|
||||
|
||||
- `Pick from List` to allow you to select matching items from any list
|
||||
- `Batch from Image List` and `Image List from Batch` to work with image batches
|
||||
- hover mouse over image and press space to zoom, space again to close the zoom
|
||||
|
||||
## Examples of what you might do with them
|
||||
|
||||
@@ -60,11 +55,13 @@ Video previews!
|
||||
|
||||

|
||||
|
||||
- `AutosendIdentical` In the ImageFilter node, if all images are identical (including if there is just one image) then send
|
||||
- `If all images are identical, autosend one` - in the ImageFilter node, if all images are identical (including if there is just one image) then send
|
||||
an image without user interaction.
|
||||
- `ClickSends` In the ImageFilter node, clicking an image sends it instead of selecting it. Useful if you know you only ever want to send one image at most.
|
||||
- `PlaySound` Play a 'ding' sound when any of the filter nodes becomes active.
|
||||
- `SmallWindow` Instead of taking over the whole screen immediately, display a tiny version of the image in the top left. Click that image to go into the full screen mode.
|
||||
- `Clicking an image sends it` - In the ImageFilter node, clicking an image sends it instead of selecting it. Useful if you know you only ever want to send one image at most.
|
||||
- `Show a small popup instead of covering the screen` - instead of taking over the whole screen immediately, display a tiny version of the image in the top left. Click that image to go into the full screen mode. You can move the tiny image window around to where you want it by dragging the title bar.
|
||||
- `Enter the Image Filter node with an image zoomed` - instead of showing the grid of images, zoom in on one. Options are `first` or `last`
|
||||
- `Play sound when activating` - play a 'ding' sound when any of the filter nodes becomes active. You can change the sound by replacing the file `ding.mp3` in the `js` subfolder.
|
||||
- `Video Frames per Second` - when previewing vidoe(s), try to play them at this speed
|
||||
|
||||
---
|
||||
|
||||
@@ -72,10 +69,7 @@ an image without user interaction.
|
||||
|
||||
In `Image Filter` and `Text Image Filter` (not `Mask Image Filter`)
|
||||
|
||||
- `Space` when hovering over an image enlarges it. While enlarged:
|
||||
- `Space` closes the zoom
|
||||
- `ArrowLeft` and `ArrowRight` select the previous/next image
|
||||
- `ArrowUp` clicks this image (select/deselect or send, depending on the value of `ClickSends`)
|
||||
- `Space` when hovering over an image enlarges it. See `Zoom` in the `Image Filter` section below.
|
||||
- `Escape` to click the cancel button
|
||||
- `Enter`to click the send button
|
||||
- digits `0`, `1`, etc. to click an image (zero-indexed) (select/deselect or send, depending on the value of `ClickSends`)
|
||||
@@ -95,8 +89,8 @@ you want to progress:
|
||||
|
||||

|
||||
|
||||
Click the images that you want to keep (their border will turn green) and then click 'Send' (or press 's') to continue the workflow.
|
||||
If you don't want to keep any of the images, click 'Cancel' (or press 'x') to terminate the workflow.
|
||||
Click the images that you want to keep (their border will turn green) and then click 'Send' to continue the workflow.
|
||||
If you don't want to keep any of the images, click 'Cancel' (or press 'escape') to terminate the workflow.
|
||||
|
||||
The node also has a timeout specified, and a countdown is shown on the left hand side. If you don't Send or Cancel before the timeout,
|
||||
the node will either cancel or send all the images, depending on the option you have selected.
|
||||
@@ -105,6 +99,19 @@ Here's a simple use: generate a batch of images and pick which ones you want to
|
||||
|
||||

|
||||
|
||||
### Zoom
|
||||
|
||||
If you hover over an image (it gets a red border) and press the space bar, it will zoom to fill the screen.
|
||||
You can also use the `Enter the Image Filter node with an image zoomed` setting to start in this mode.
|
||||
|
||||
While zoomed, you can navigate with keyboard or mouse:
|
||||
|
||||
| |Mouse|Keyboard|Notes|
|
||||
|-|-|-|-|
|
||||
|Select/unselect/send|Click the zoomed image|`Arrow Up`|Will select/unselect the image, or send it if you have `Clicking an image sends it` turned on|
|
||||
|Previous/Next|Click the arrows to the left or right|`Arrow Left` `Arrow Right`|The image number is shown near the top right|
|
||||
|Unzoom| |`Space`|Back to the grid view|
|
||||
|
||||
### Optional inputs
|
||||
|
||||
The Latent and Mask inputs are optional. If used, they should have the same number of latents (or masks) as the image batch,
|
||||
@@ -228,9 +235,9 @@ This workflow:
|
||||
- inpaints
|
||||
- uses 'Image Filter' to choose which, if either, of the two images (before and after inpaint) to save
|
||||
|
||||
The workflow is embedded in the seahorse in a bottle:
|
||||
The workflow is embedded in the blob in a bottle:
|
||||
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/seahorse.png" alt="Seahorse" width="200" height="200">
|
||||
<img src="https://github.com/chrisgoringe/cg-image-filter/raw/main/images/blob.png" alt="Seahorse" width="200" height="200">
|
||||
|
||||
# Bugs, Ideas, and the future
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ from .image_filter import ImageFilter, MaskImageFilter, TextImageFilterWithExtra
|
||||
from .list_utility_nodes import PickFromList, BatchFromImageList, ImageListFromBatch
|
||||
from .string_utility_nodes import SplitByCommas, StringToFloat, StringToInt
|
||||
|
||||
VERSION = "1.4.2"
|
||||
VERSION = "1.5"
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
NODE_CLASS_MAPPINGS= {
|
||||
|
||||
+31
-95
@@ -1,79 +1,11 @@
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
from nodes import PreviewImage, LoadImage
|
||||
from comfy.model_management import InterruptProcessingException, throw_exception_if_processing_interrupted
|
||||
import time, os, random
|
||||
from comfy.model_management import InterruptProcessingException
|
||||
import os
|
||||
import torch
|
||||
|
||||
REQUEST_RESHOW = "-1"
|
||||
REQUEST_TIMER_RESET = "-2"
|
||||
CANCEL = "-3"
|
||||
WAITING_FOR_RESPONSE = "-9"
|
||||
|
||||
SPECIALS = [REQUEST_RESHOW, REQUEST_TIMER_RESET, CANCEL, WAITING_FOR_RESPONSE]
|
||||
|
||||
class CancelledByUser(Exception): pass
|
||||
|
||||
##
|
||||
#
|
||||
# response should be a dict just containing key 'response'
|
||||
# which holds a comma separated list of integers of selected images (0 indexed)
|
||||
# where '' indicates cancel
|
||||
#
|
||||
##
|
||||
|
||||
@PromptServer.instance.routes.post('/cg-image-filter-message')
|
||||
async def cg_image_filter_message(request):
|
||||
post = await request.post()
|
||||
response = post.get("response")
|
||||
if (Message.data is None or (Message.data != CANCEL and response not in SPECIALS)):
|
||||
Message.setdata(response, "response")
|
||||
else:
|
||||
print(f"Ignoring response {response} as current response is {Message.data}")
|
||||
return web.json_response({})
|
||||
|
||||
def wait(secs, uid, unique):
|
||||
Message.setdata(None, "start of wait")
|
||||
end_time = time.monotonic() + secs
|
||||
while(time.monotonic() < end_time and Message.data is None):
|
||||
throw_exception_if_processing_interrupted()
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", {"tick": int(end_time - time.monotonic()), "uid": uid, "unique":unique})
|
||||
time.sleep(0.2)
|
||||
response = Message.data
|
||||
Message.setdata(None, "read response")
|
||||
if response is None:
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", {"timeout": True, "uid": uid, "unique":unique})
|
||||
|
||||
return response
|
||||
|
||||
def send_with_resend(payload, timeout, uid, unique):
|
||||
response = WAITING_FOR_RESPONSE
|
||||
payload['unique'] = unique
|
||||
while response in SPECIALS:
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", payload)
|
||||
response = wait(timeout, uid, unique)
|
||||
if response == CANCEL:
|
||||
raise InterruptProcessingException()
|
||||
if Message.unique != unique:
|
||||
print("Mismatched uniques...")
|
||||
return response
|
||||
|
||||
def mask_to_image(mask:torch.Tensor):
|
||||
return torch.stack([mask, mask, mask, 1.0-mask], -1)
|
||||
|
||||
class Message:
|
||||
data:str = None
|
||||
unique:str = None
|
||||
@classmethod
|
||||
def setdata(cls, v, comment):
|
||||
#print(f"Message.data set to {v} (${comment})")
|
||||
if v:
|
||||
chop = v.split('!unique!')
|
||||
cls.data = chop[0]
|
||||
cls.unique = chop[1] if len(chop)>1 else None
|
||||
else:
|
||||
cls.data = None
|
||||
cls.unique = None
|
||||
from .image_filter_messaging import send_and_wait, Response, TimeoutResponse
|
||||
|
||||
HIDDEN = {
|
||||
"prompt": "PROMPT",
|
||||
@@ -82,8 +14,6 @@ HIDDEN = {
|
||||
"node_identifier": "NID",
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ImageFilter(PreviewImage):
|
||||
RETURN_TYPES = ("IMAGE","LATENT","MASK","STRING","STRING","STRING","STRING")
|
||||
RETURN_NAMES = ("images","latents","masks","extra1","extra2","extra3","indexes")
|
||||
@@ -134,23 +64,23 @@ class ImageFilter(PreviewImage):
|
||||
urls:list[str] = self.save_images(images=images, **kwargs)['ui']['images']
|
||||
payload = {"uid": uid, "urls":urls, "allsame":all_the_same, "extras":[extra1, extra2, extra3], "tip":tip, "video_frames":video_frames}
|
||||
|
||||
response = send_with_resend(payload, timeout, uid, node_identifier)
|
||||
response:Response = send_and_wait(payload, timeout, uid, node_identifier)
|
||||
|
||||
if response:
|
||||
response, e1, e2, e3 = response.split("|||")
|
||||
images_to_return = [int(x) for x in response.split(",") if x]
|
||||
else:
|
||||
if isinstance(response, TimeoutResponse):
|
||||
if ontimeout=='send none': images_to_return = []
|
||||
if ontimeout=='send all': images_to_return = [*range(len(images))]
|
||||
if ontimeout=='send first': images_to_return = [0,]
|
||||
if ontimeout=='send last': images_to_return = [len(images)//video_frames,]
|
||||
else:
|
||||
e1, e2, e3 = response.get_extras([extra1, extra2, extra3])
|
||||
images_to_return = response.selection
|
||||
|
||||
if len(images_to_return) == 0: raise InterruptProcessingException()
|
||||
if images_to_return is None or len(images_to_return) == 0: raise InterruptProcessingException()
|
||||
|
||||
if video_frames>1:
|
||||
images_to_return = [ key*video_frames + frm for key in images_to_return for frm in range(video_frames) ]
|
||||
|
||||
images = torch.stack(list(images[i] for i in images_to_return))
|
||||
images = torch.stack(list(images[int(i)] for i in images_to_return))
|
||||
latents = {"samples": torch.stack(list(latents['samples'][i] for i in images_to_return))} if latents is not None else None
|
||||
masks = torch.stack(list(masks[i] for i in images_to_return)) if masks is not None else None
|
||||
|
||||
@@ -192,15 +122,18 @@ class TextImageFilterWithExtras(PreviewImage):
|
||||
if textareaheight is not None: payload['textareaheight'] = textareaheight
|
||||
if mask is not None: payload['mask_urls'] = self.save_images(images=mask_to_image(mask), **kwargs)['ui']['images']
|
||||
|
||||
response = send_with_resend(payload, timeout, uid, node_identifier)
|
||||
response = response.split("|||") if response else [text, extra1, extra2, extra3]
|
||||
response = send_and_wait(payload, timeout, uid, node_identifier)
|
||||
if isinstance(response, TimeoutResponse):
|
||||
return (image, text, extra1, extra2, extra3)
|
||||
|
||||
return (image, *response)
|
||||
return (image, response.text, *response.get_extras([extra1, extra2, extra3]))
|
||||
|
||||
def mask_to_image(mask:torch.Tensor):
|
||||
return torch.stack([mask, mask, mask, 1.0-mask], -1)
|
||||
|
||||
class MaskImageFilter(PreviewImage, LoadImage):
|
||||
RETURN_TYPES = ("IMAGE","MASK")
|
||||
RETURN_NAMES = ("image","mask")
|
||||
RETURN_TYPES = ("IMAGE","MASK","STRING","STRING","STRING")
|
||||
RETURN_NAMES = ("image","mask","extra1","extra2","extra3")
|
||||
FUNCTION = "func"
|
||||
CATEGORY = "image_filter"
|
||||
OUTPUT_NODE = False
|
||||
@@ -214,7 +147,11 @@ class MaskImageFilter(PreviewImage, LoadImage):
|
||||
"if_no_mask": (["cancel", "send blank"], {}),
|
||||
},
|
||||
"optional": {
|
||||
"mask" : ("MASK", {"tooltip":"optional initial mask"})
|
||||
"mask" : ("MASK", {"tooltip":"optional initial mask"}),
|
||||
"tip" : ("STRING", {"default":"", "tooltip": "Optional - if provided, will be displayed in popup window"}),
|
||||
"extra1" : ("STRING", {"default":""}),
|
||||
"extra2" : ("STRING", {"default":""}),
|
||||
"extra3" : ("STRING", {"default":""}),
|
||||
},
|
||||
"hidden": HIDDEN,
|
||||
}
|
||||
@@ -226,23 +163,22 @@ class MaskImageFilter(PreviewImage, LoadImage):
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs): return True
|
||||
|
||||
def func(self, image, timeout, uid, if_no_mask, node_identifier, mask=None, **kwargs):
|
||||
def func(self, image, timeout, uid, if_no_mask, node_identifier, mask=None, extra1="", extra2="", extra3="", tip="", **kwargs):
|
||||
if mask is not None and mask.shape[:3] == image.shape[:3] and not torch.all(mask==0):
|
||||
saveable = torch.cat((image, mask.unsqueeze(-1)), dim=-1)
|
||||
else:
|
||||
saveable = image
|
||||
|
||||
urls:list[str] = self.save_images(images=saveable, **kwargs)['ui']['images']
|
||||
payload = {"uid": uid, "urls":urls, "maskedit":True}
|
||||
response = send_with_resend(payload, timeout, uid, node_identifier)
|
||||
payload = {"uid": uid, "urls":urls, "maskedit":True, "extras":[extra1, extra2, extra3], "tip":tip}
|
||||
response = send_and_wait(payload, timeout, uid, node_identifier)
|
||||
|
||||
if (response):
|
||||
if (response.masked_image):
|
||||
try:
|
||||
filename = response.split('=')[1].split('&')[0]
|
||||
return self.load_image(os.path.join('clipspace', filename)+" [input]")
|
||||
return ( *(self.load_image(os.path.join('clipspace', response.masked_image)+" [input]")), *response.get_extras([extra1, extra2, extra3]) )
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if if_no_mask == 'cancel':
|
||||
raise InterruptProcessingException()
|
||||
return self.load_image(urls[0]['filename']+" [temp]")
|
||||
return ( *(self.load_image(urls[0]['filename']+" [temp]")), *response.get_extras([extra1, extra2, extra3]) )
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
from comfy.model_management import InterruptProcessingException, throw_exception_if_processing_interrupted
|
||||
import time, json
|
||||
from typing import Optional
|
||||
|
||||
REQUEST_RESHOW = "-1"
|
||||
CANCEL = "-3"
|
||||
WAITING_FOR_RESPONSE = "-9"
|
||||
|
||||
SPECIALS = [REQUEST_RESHOW, CANCEL, WAITING_FOR_RESPONSE]
|
||||
|
||||
class Response:
|
||||
def __init__(self, selection:Optional[list[int]] = None, text:Optional[str] = None,
|
||||
masked_image:Optional[str] = None, extras:Optional[list[str]] = None):
|
||||
self.selection = selection
|
||||
self.text = text
|
||||
self.masked_image = masked_image
|
||||
self.extras = extras
|
||||
|
||||
def get_extras(self,defaults:list[str]) -> list[str]:
|
||||
return self.extras or defaults
|
||||
|
||||
class TimeoutResponse(Response): pass
|
||||
class CancelledResponse(Response): pass
|
||||
class RequestResponse(Response): pass
|
||||
|
||||
class MessageState:
|
||||
latest = None
|
||||
unique_expected = None
|
||||
def __init__(self, data:dict|str={}):
|
||||
if not isinstance(data,dict): data = json.loads(data)
|
||||
self.unique:str = data.pop('unique', None)
|
||||
self.special:Optional[int] = data.pop('special',None)
|
||||
self.response:Response = Response(**data)
|
||||
|
||||
@classmethod
|
||||
def waiting_state(cls): return MessageState(data={'special':WAITING_FOR_RESPONSE})
|
||||
|
||||
@classmethod
|
||||
def request_state(cls): return MessageState(data={'special':REQUEST_RESHOW})
|
||||
|
||||
@classmethod
|
||||
def start_waiting(cls, unique):
|
||||
cls.latest = cls.waiting_state()
|
||||
cls.unique_expected = unique
|
||||
|
||||
@classmethod
|
||||
def get_response(cls) -> Response:
|
||||
if cls.waiting(): return TimeoutResponse()
|
||||
if cls.latest.cancelled: return CancelledResponse()
|
||||
if cls.latest.request: return RequestResponse()
|
||||
return cls.latest.response
|
||||
|
||||
@classmethod
|
||||
def stop_waiting(cls): cls.latest = MessageState()
|
||||
|
||||
@classmethod
|
||||
def waiting(cls): return cls.latest.special == WAITING_FOR_RESPONSE
|
||||
|
||||
@property
|
||||
def cancelled(self): return self.special == CANCEL
|
||||
|
||||
@property
|
||||
def request(self): return self.special == REQUEST_RESHOW
|
||||
|
||||
@property
|
||||
def real(self): return self.special is None
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post('/cg-image-filter-message')
|
||||
async def cg_image_filter_message(request):
|
||||
post = await request.post()
|
||||
response = post.get("response")
|
||||
message = MessageState(response)
|
||||
|
||||
if str(MessageState.unique_expected)==str(message.unique):
|
||||
if (MessageState.waiting()):
|
||||
MessageState.latest = message
|
||||
else:
|
||||
print(f"Ignoring response {response} because not waiting for one")
|
||||
else:
|
||||
print(f"Ignoring mismatched response {response}")
|
||||
|
||||
return web.json_response({})
|
||||
|
||||
def wait_for_response(secs, uid, unique) -> Response:
|
||||
MessageState.start_waiting(unique)
|
||||
try:
|
||||
end_time = time.monotonic() + secs
|
||||
while(time.monotonic() < end_time and MessageState.waiting()):
|
||||
throw_exception_if_processing_interrupted()
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", {"tick": int(end_time - time.monotonic()), "uid": uid, "unique":unique})
|
||||
time.sleep(0.2)
|
||||
if MessageState.waiting():
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", {"timeout": True, "uid": uid, "unique":unique})
|
||||
return MessageState.get_response()
|
||||
finally: MessageState.stop_waiting()
|
||||
|
||||
def send_and_wait(payload, timeout, uid, unique) -> Response:
|
||||
payload['uid'] = uid
|
||||
payload['unique'] = unique
|
||||
|
||||
while True:
|
||||
PromptServer.instance.send_sync("cg-image-filter-images", payload)
|
||||
r = wait_for_response(timeout, uid, unique)
|
||||
if isinstance(r,CancelledResponse): raise InterruptProcessingException()
|
||||
if (not isinstance(r, RequestResponse)): return r
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 878 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
+13
-104
@@ -7,49 +7,12 @@
|
||||
--text_area_height: 150px;
|
||||
}
|
||||
|
||||
.cg_counter {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
left: 70px;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
z-index: 110000;
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tiny_window {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
left: 70px;
|
||||
top: 70px;
|
||||
border: 1px solid white;
|
||||
background-color: #fdfdfd30;
|
||||
z-index: 110000;
|
||||
}
|
||||
|
||||
.tiny_image {
|
||||
padding: 1px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cg_counter .counter_text {}
|
||||
|
||||
.cg_counter .counter_reset {
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.cg_popup .grid {
|
||||
position: absolute;
|
||||
width:80%;
|
||||
left:5%;
|
||||
height:calc(90% - var(--text_area_height));
|
||||
top:5%;
|
||||
width:70%;
|
||||
left:15%;
|
||||
height:calc(100% - 60px - var(--text_area_height));
|
||||
top:55px;
|
||||
display: grid;
|
||||
align-content: space-evenly;
|
||||
justify-content: space-evenly;
|
||||
@@ -62,9 +25,10 @@
|
||||
|
||||
.cg_popup .title {
|
||||
position: absolute;
|
||||
left: 40%;
|
||||
top: 2%;
|
||||
font-size: larger;
|
||||
width:100%;
|
||||
text-align: center;
|
||||
top: 10px;
|
||||
font-size: 250%;
|
||||
}
|
||||
|
||||
.cg_popup .tip {
|
||||
@@ -104,65 +68,10 @@
|
||||
box-shadow: 0 0 4px 4px red;
|
||||
}
|
||||
|
||||
.cg_popup .zoomed {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
display: block;
|
||||
z-index: 120000;
|
||||
background-color: rgba(29, 29, 29, 0.886);
|
||||
}
|
||||
|
||||
.cg_popup .zoomed.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_number {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 30px;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
z-index: 110000;
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
/*text-align: center;*/
|
||||
}
|
||||
|
||||
.cg_popup .zoomed.highlighted .zoomed_image {
|
||||
border: 3px solid green;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.cg_popup .buttons button {
|
||||
margin: 5px 0 5px 0;
|
||||
padding: 4px;
|
||||
display: block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.cg_popup .buttons input {
|
||||
padding: 4px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cg_popup .text_edit {
|
||||
position: absolute;
|
||||
display:block;
|
||||
width: 80%;
|
||||
left: 10%;
|
||||
bottom: 10px;
|
||||
height: calc(var(--text_area_height) - 20px);
|
||||
}
|
||||
|
||||
.cg_popup.hidden {
|
||||
display:none
|
||||
}
|
||||
}
|
||||
|
||||
.cg_popup .hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
.cgfloat {
|
||||
position: absolute;
|
||||
z-index: 999999;
|
||||
border: 1px solid black;
|
||||
background-color: rgba(95, 158, 160, 0.713);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.cgfloat.hidden {
|
||||
display:none
|
||||
}
|
||||
|
||||
.cgfloat_header {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: rgba(28, 51, 52, 0.713);
|
||||
color: whitesmoke;
|
||||
border-bottom: 1px solid black;
|
||||
padding:4px;
|
||||
font-size:larger;
|
||||
}
|
||||
|
||||
.tiny .cgfloat_header {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.cgfloat_body {
|
||||
min-width: fit-content;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cgfloat_body .row {
|
||||
color: white;
|
||||
padding: 0px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* Countdown */
|
||||
.cgfloat_body .counter_text {
|
||||
min-width:50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cgfloat_body .counter_reset {
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.cgfloat_body .row.buttons {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.cgfloat_body button {
|
||||
margin: 4px;
|
||||
padding: 2px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
/* Extras */
|
||||
.cgfloat_body .row.extras {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cgfloat_body .row.extras .extra {
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* Tips */
|
||||
.cgfloat_body .row.tip {
|
||||
margin: 4px;
|
||||
padding: 4px 0px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Text Edit */
|
||||
.cgfloat_body .row.text_edit {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.cgfloat .tiny_image {
|
||||
padding: 1px;
|
||||
width: auto;
|
||||
max-height: 100px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
export class FloatingWindow extends HTMLDivElement {
|
||||
constructor(title, x, y, parent) {
|
||||
super()
|
||||
this.classList.add('cgfloat')
|
||||
this.header = document.createElement('div')
|
||||
this.header.classList.add('cgfloat_header')
|
||||
this.header.innerText = title
|
||||
this.append(this.header)
|
||||
this.body = document.createElement('div')
|
||||
this.body.classList.add('cgfloat_body')
|
||||
this.append(this.body)
|
||||
|
||||
this.header.addEventListener('mousedown',this.header_mousedown.bind(this))
|
||||
document.addEventListener('mouseup',this.header_mouseup.bind(this))
|
||||
document.addEventListener('mousemove',this.header_mousemove.bind(this))
|
||||
document.addEventListener('mouseleave',this.header_mouseup.bind(this))
|
||||
|
||||
this.dragging = false
|
||||
this.move_to(x,y)
|
||||
|
||||
if (parent) parent.append(this)
|
||||
else document.body.append(this)
|
||||
}
|
||||
|
||||
show() { this.style.display = 'block' }
|
||||
hide() { this.style.display = 'none' }
|
||||
set_title(title) { this.header.innerText = title }
|
||||
|
||||
move_to(x,y) {
|
||||
this.position = {x:x,y:y}
|
||||
this.style.left = `${this.position.x}px`
|
||||
this.style.top = `${this.position.y}px`
|
||||
}
|
||||
|
||||
swallow(e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
header_mousedown(e) {
|
||||
this.dragging = true
|
||||
this.swallow(e)
|
||||
}
|
||||
|
||||
header_mouseup(e) {
|
||||
this.dragging = false
|
||||
}
|
||||
|
||||
header_mousemove(e) {
|
||||
if (this.dragging) {
|
||||
this.move_to( this.position.x + e.movementX , this.position.y + e.movementY )
|
||||
this.swallow(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
customElements.define('cg-floater', FloatingWindow, {extends: 'div'})
|
||||
+40
-10
@@ -4,6 +4,7 @@ import { api } from "../../scripts/api.js";
|
||||
import { create } from "./utils.js";
|
||||
import { popup } from "./popup.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import { FloatingWindow } from "./floating_window.js";
|
||||
|
||||
const FILTER_TYPES = ["Image Filter","Text Image Filter","Text Image Filter with Extras","Mask Image Filter"]
|
||||
|
||||
@@ -11,45 +12,73 @@ app.registerExtension({
|
||||
name: "cg.image_filter",
|
||||
settings: [
|
||||
{
|
||||
id: "ImageFilter.PlaySound",
|
||||
id: "Image Filter. Image Filter",
|
||||
name: "Version 1.5",
|
||||
type: () => {
|
||||
const x = document.createElement('span')
|
||||
const a = document.createElement('a')
|
||||
a.innerText = "Report issues or request features"
|
||||
a.href = "https://github.com/chrisgoringe/cg-image-filter/issues"
|
||||
a.target = "_blank"
|
||||
a.style.paddingRight = "12px"
|
||||
x.appendChild(a)
|
||||
return x
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Image Filter.UI.Play Sound",
|
||||
name: "Play sound when activating",
|
||||
type: "boolean",
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: "ImageFilter.ClickSends",
|
||||
name: "In Image Filter clicking an image sends it",
|
||||
id: "Image Filter.Actions.Click Sends",
|
||||
name: "Clicking an image sends it",
|
||||
tooltip: "Use if you always want to send exactly one image.",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: "ImageFilter.AutosendIdentical",
|
||||
name: "In Image Filter, if all images are identical, autosend one",
|
||||
id: "Image Filter.Actions.Autosend Identical",
|
||||
name: "If all images are identical, autosend one",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: "ImageFilter.SmallWindow",
|
||||
name: "Initially show a small popup instead of covering the screen",
|
||||
id: "Image Filter.UI.Start Zoomed",
|
||||
name: "Enter the Image Filter node with an image zoomed",
|
||||
type: "combo",
|
||||
options: [ {value:0, text:"No"}, {value:"1", text:"first"}, {value:"-1", text:"last"} ],
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
id: "Image Filter.UI.Small Window",
|
||||
name: "Show a small popup instead of covering the screen",
|
||||
type: "boolean",
|
||||
tooltip: "Click the small popup to activate it",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: "ImageFilter.DetailedLogging",
|
||||
id: "Image Filter.Z.Detailed Logging",
|
||||
name: "Turn on detailed logging",
|
||||
tooltip: "If you are asked to for debugging!",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: "ImageFilter.FPS",
|
||||
id: "Image Filter.Video.FPS",
|
||||
name: "Video Frames per Second",
|
||||
type: "int",
|
||||
defaultValue: 1,
|
||||
defaultValue: 5,
|
||||
}
|
||||
],
|
||||
setup() {
|
||||
create('link', null, document.getElementsByTagName('HEAD')[0],
|
||||
{'rel':'stylesheet', 'type':'text/css', 'href': new URL("./filter.css", import.meta.url).href } )
|
||||
create('link', null, document.getElementsByTagName('HEAD')[0],
|
||||
{'rel':'stylesheet', 'type':'text/css', 'href': new URL("./floating_window.css", import.meta.url).href } )
|
||||
create('link', null, document.getElementsByTagName('HEAD')[0],
|
||||
{'rel':'stylesheet', 'type':'text/css', 'href': new URL("./zoomed.css", import.meta.url).href } )
|
||||
api.addEventListener("execution_interrupted", popup.send_cancel.bind(popup));
|
||||
api.addEventListener("cg-image-filter-images",popup.handle_message.bind(popup));
|
||||
},
|
||||
@@ -85,4 +114,5 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
})
|
||||
@@ -1,16 +1,18 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
export class Log {
|
||||
static log(s) { if (s) console.log(s) }
|
||||
static error(e) { console.error(e) }
|
||||
static detail(s) {
|
||||
if (app.ui.settings.getSettingValue("ImageFilter.DetailedLogging")) Log.log(s)
|
||||
if (app.ui.settings.getSettingValue("Image Filter.Z.Detailed Logging")) Log.log(s)
|
||||
}
|
||||
static message_in(message, extra) {
|
||||
if (!app.ui.settings.getSettingValue("ImageFilter.DetailedLogging")) return
|
||||
if (!app.ui.settings.getSettingValue("Image Filter.Z.Detailed Logging")) return
|
||||
if (message.detail && !message.detail.tick) Log.log(`--> ${JSON.stringify(message.detail)}` + (extra ? ` ${extra}` : ""))
|
||||
if (message.detail && message.detail.tick) Log.log(`--> tick`)
|
||||
}
|
||||
static message_out(response, extra) {
|
||||
if (!app.ui.settings.getSettingValue("ImageFilter.DetailedLogging")) return
|
||||
if (!app.ui.settings.getSettingValue("Image Filter.Z.Detailed Logging")) return
|
||||
Log.log(`"<-- ${JSON.stringify(response)}` + (extra ? ` ${extra}` : ""))
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,23 @@ function get_mask_editor_cancel_button() {
|
||||
return get_mask_editor_element?.parentElement?.lastChild?.childNodes[2]
|
||||
}
|
||||
|
||||
function get_mask_editor_save_button() {
|
||||
if (document.getElementById("maskEditor_topBarSaveButton")) return document.getElementById("maskEditor_topBarSaveButton")
|
||||
return get_mask_editor_element?.parentElement?.lastChild?.childNodes[2]
|
||||
}
|
||||
|
||||
export function mask_editor_listen_for_cancel(callback) {
|
||||
const cancel_button = get_mask_editor_cancel_button()
|
||||
if (cancel_button && !cancel_button.filter_listener_added) {
|
||||
cancel_button.addEventListener('click', callback )
|
||||
cancel_button.filter_listener_added = true
|
||||
}
|
||||
}
|
||||
|
||||
export function press_maskeditor_save() {
|
||||
get_mask_editor_save_button().click()
|
||||
}
|
||||
|
||||
export function press_maskeditor_cancel() {
|
||||
get_mask_editor_cancel_button().click()
|
||||
}
|
||||
+243
-158
@@ -1,9 +1,10 @@
|
||||
import { app, ComfyApp } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
|
||||
import { mask_editor_listen_for_cancel, mask_editor_showing, hide_mask_editor } from "./mask_utils.js";
|
||||
import { mask_editor_listen_for_cancel, mask_editor_showing, hide_mask_editor, press_maskeditor_cancel, press_maskeditor_save } from "./mask_utils.js";
|
||||
import { Log } from "./log.js";
|
||||
import { create } from "./utils.js";
|
||||
import { FloatingWindow } from "./floating_window.js";
|
||||
|
||||
//const EXTENSION_NODES = ["Image Filter", "Text Image Filter", "Mask Image Filter", "Text Image Filter with Extras",]
|
||||
const POPUP_NODES = ["Image Filter", "Text Image Filter", "Text Image Filter with Extras",]
|
||||
@@ -16,42 +17,14 @@ function get_full_url(url) {
|
||||
return api.apiURL( `/view?filename=${encodeURIComponent(url.filename ?? v)}&type=${url.type ?? "input"}&subfolder=${url.subfolder ?? ""}&r=${Math.random()}`)
|
||||
}
|
||||
|
||||
class State {
|
||||
static INACTIVE = 0
|
||||
static TINY = 1
|
||||
static MASK = 2
|
||||
static FILTER = 3
|
||||
static TEXT = 4
|
||||
static ZOOMED = 5
|
||||
|
||||
static visible(item, value) {
|
||||
if (value) item.classList.remove('hidden')
|
||||
else item.classList.add('hidden')
|
||||
}
|
||||
static highlighted(item, value) {
|
||||
if (value) item.classList.add('highlighted')
|
||||
else item.classList.remove('highlighted')
|
||||
}
|
||||
|
||||
static render(popup) {
|
||||
const state = popup.state
|
||||
State.visible(popup, (state==State.FILTER || state==State.TEXT || state==State.ZOOMED))
|
||||
State.visible(popup.tiny_window, state==State.TINY)
|
||||
State.visible(popup.counter, (state==State.FILTER || state==State.ZOOMED || state==State.TEXT || state==State.MASK))
|
||||
State.visible(popup.zoomed, state==State.ZOOMED)
|
||||
State.visible(popup.text_edit, state==State.TEXT)
|
||||
State.visible(popup.send_button, true /*!app.ui.settings.getSettingValue("ImageFilter.ClickSends")*/)
|
||||
|
||||
if (state==State.ZOOMED) {
|
||||
const img_index = popup.zoomed_image_holder.image_index
|
||||
State.highlighted(popup.zoomed, popup.picked.has(`${img_index}`))
|
||||
popup.zoomed_number.innerHTML = `${img_index+1}/${popup.n_images}`
|
||||
}
|
||||
|
||||
if (state!=State.MASK) hide_mask_editor()
|
||||
}
|
||||
}
|
||||
|
||||
const State = Object.freeze({
|
||||
INACTIVE : 0,
|
||||
TINY : 1,
|
||||
MASK : 2,
|
||||
FILTER : 3,
|
||||
TEXT : 4,
|
||||
ZOOMED : 5,
|
||||
})
|
||||
|
||||
class Popup extends HTMLSpanElement {
|
||||
constructor() {
|
||||
@@ -62,146 +35,220 @@ class Popup extends HTMLSpanElement {
|
||||
|
||||
this.grid = create('span', 'grid', this)
|
||||
this.overlaygrid = create('span', 'grid overlaygrid', this)
|
||||
this.grid.addEventListener('click', this.on_click.bind(this))
|
||||
|
||||
this.zoomed = create('span', 'zoomed', this)
|
||||
this.zoomed_image = create('img', 'zoomed_image', this.zoomed)
|
||||
this.zoomed_number = create('span', 'zoomed_number', this.zoomed)
|
||||
this.text_edit = create('textarea', 'text_edit', this)
|
||||
this.title_bar = create('span', 'title', this)
|
||||
this.buttons = create('span', 'buttons', this)
|
||||
this.checkboxes = create('span', null, this.buttons)
|
||||
this.zoomed_prev = create('span', 'zoomed_prev', this.zoomed)
|
||||
this.zoomed_prev_arrow = create('span', 'zoomed_arrow', this.zoomed_prev, {innerHTML:"⇦"})
|
||||
this.zoomed_image = create('img', 'zoomed_image', this.zoomed)
|
||||
this.zoomed_next = create('span', 'zoomed_next', this.zoomed)
|
||||
this.zoomed_number = create('span', 'zoomed_number', this.zoomed_next)
|
||||
this.zoomed_next_arrow = create('span', 'zoomed_arrow', this.zoomed_next, {innerHTML:"⇨"})
|
||||
|
||||
this.send_button = create('button', 'control', this.buttons, {innerText:"Send"} )
|
||||
this.cancel_button = create('button', 'control', this.buttons, {innerText:"Cancel"} )
|
||||
this.extras = create('span', 'extras', this.buttons)
|
||||
this.tip = create('span', 'tip', this.buttons)
|
||||
this.zoomed_prev_arrow.addEventListener('click', this.zoom_prev.bind(this))
|
||||
this.zoomed_next_arrow.addEventListener('click', this.zoom_next.bind(this))
|
||||
this.zoomed_image.addEventListener('click', this.click_zoomed.bind(this))
|
||||
|
||||
this.tiny_window = create('span', 'tiny_window hidden', document.body)
|
||||
this.tiny_image = create('img', 'tiny_image', this.tiny_window)
|
||||
this.tiny_window = new FloatingWindow('', 100, 100)
|
||||
this.tiny_window.classList.add('tiny')
|
||||
this.tiny_image = create('img', 'tiny_image', this.tiny_window.body)
|
||||
this.tiny_window.addEventListener('click', this.handle_deferred_message.bind(this))
|
||||
|
||||
this.counter = create('span', 'cg_counter hidden', document.body)
|
||||
this.counter_text = create('span', 'counter_text', this.counter)
|
||||
this.counter_reset_button = create('button', 'counter_reset', this.counter, {innerText:"Reset"} )
|
||||
this.floating_window = new FloatingWindow('', 100, 100)
|
||||
|
||||
this.grid.addEventListener('click', this.on_click.bind(this))
|
||||
this.counter_row = create('span', 'counter row', this.floating_window.body)
|
||||
this.counter_reset_button = create('button', 'counter_reset', this.counter_row, {innerText:"Reset"} )
|
||||
this.counter_text = create('span', 'counter_text', this.counter_row)
|
||||
this.counter_reset_button.addEventListener('click', this.request_reset.bind(this) )
|
||||
|
||||
this.extras_row = create('span', 'extras row', this.floating_window.body)
|
||||
|
||||
this.tip_row = create('span', 'tip row', this.floating_window.body)
|
||||
|
||||
this.button_row = create('span', 'buttons row', this.floating_window.body)
|
||||
this.send_button = create('button', 'control', this.button_row, {innerText:"Send"} )
|
||||
this.cancel_button = create('button', 'control', this.button_row, {innerText:"Cancel"} )
|
||||
this.send_button.addEventListener( 'click', this.send_current_state.bind(this) )
|
||||
this.cancel_button.addEventListener('click', this.send_cancel.bind(this) )
|
||||
this.counter_reset_button.addEventListener('click', this.request_reset.bind(this) )
|
||||
|
||||
this.mask_button_row = create('span', 'buttons row', this.floating_window.body)
|
||||
this.mask_send_button = create('button', 'control', this.mask_button_row, {innerText:"Send"} )
|
||||
this.mask_cancel_button = create('button', 'control', this.mask_button_row, {innerText:"Cancel"} )
|
||||
this.mask_send_button.addEventListener( 'click', press_maskeditor_save )
|
||||
this.mask_cancel_button.addEventListener('click', press_maskeditor_cancel )
|
||||
|
||||
this.text_edit = create('textarea', 'text_edit row', this.floating_window.body)
|
||||
|
||||
this.picked = new Set()
|
||||
|
||||
document.addEventListener("keydown", this.on_key_down.bind(this))
|
||||
document.addEventListener("keypress", this.on_key_press.bind(this))
|
||||
|
||||
document.body.appendChild(this)
|
||||
this.last_response_sent = 0
|
||||
this.state = State.INACTIVE
|
||||
State.render(this)
|
||||
this.render()
|
||||
}
|
||||
|
||||
_send_response(msg, special_message, keep_open) {
|
||||
visible(item, value) {
|
||||
if (value) item.classList.remove('hidden')
|
||||
else item.classList.add('hidden')
|
||||
}
|
||||
disabled(item, value) {
|
||||
item.disabled = value
|
||||
}
|
||||
highlighted(item, value) {
|
||||
if (value) item.classList.add('highlighted')
|
||||
else item.classList.remove('highlighted')
|
||||
}
|
||||
|
||||
render() {
|
||||
const state = this.state
|
||||
this.visible(this, (state==State.FILTER || state==State.TEXT || state==State.ZOOMED))
|
||||
|
||||
this.visible(this.tiny_window, state==State.TINY)
|
||||
|
||||
this.visible(this.zoomed, state==State.ZOOMED)
|
||||
|
||||
this.visible(this.floating_window, (state==State.FILTER || state==State.ZOOMED || state==State.TEXT || state==State.MASK))
|
||||
this.visible(this.button_row, state!=State.MASK)
|
||||
this.disabled(this.send_button, (state==State.FILTER || state==State.ZOOMED) && this.picked.size==0)
|
||||
this.visible(this.mask_button_row, state==State.MASK)
|
||||
this.visible(this.extras_row, this.n_extras>0)
|
||||
this.visible(this.tip_row, this.tip_row.innerHTML.length>0)
|
||||
this.visible(this.text_edit, state==State.TEXT)
|
||||
|
||||
if (state==State.ZOOMED) {
|
||||
const img_index = this.zoomed_image_holder.image_index
|
||||
this.highlighted(this.zoomed, this.picked.has(`${img_index}`))
|
||||
this.zoomed_number.innerHTML = `${img_index+1}/${this.n_images}`
|
||||
}
|
||||
|
||||
if (state!=State.MASK) hide_mask_editor()
|
||||
}
|
||||
|
||||
_send_response(msg={}, keep_open=false) {
|
||||
/*
|
||||
msg is a dict. Valid keys are:
|
||||
*selection (list[int])
|
||||
*text (string)
|
||||
special (int)
|
||||
masked_image (string)
|
||||
*extras (list of strings)
|
||||
*unique (string)
|
||||
(*) are added
|
||||
*/
|
||||
if (Date.now()-this.last_response_sent < 1000) {
|
||||
Log.message_out(msg, "(throttled)")
|
||||
return
|
||||
}
|
||||
|
||||
if (!special_message) {
|
||||
Array.from(this.extras.children).forEach((e)=>{ msg = msg + "|||" + e.value })
|
||||
msg.unique = `${this.node._ni_widget?.value}`
|
||||
|
||||
if (!msg.special) {
|
||||
if (this.n_extras>0) {
|
||||
msg.extras = []
|
||||
Array.from(this.extras_row.children).forEach((e)=>{ msg.extras.push(e.value) })
|
||||
}
|
||||
if (this.state==State.FILTER || this.state==State.ZOOMED) msg.selection = Array.from(this.picked)
|
||||
if (this.state==State.TEXT) msg.text = this.text_edit.value
|
||||
|
||||
this.last_response_sent = Date.now()
|
||||
}
|
||||
|
||||
try {
|
||||
const body = new FormData();
|
||||
msg = `${msg}!unique!${this.unique}`
|
||||
body.append('response', msg);
|
||||
body.append('response', JSON.stringify(msg));
|
||||
api.fetchApi("/cg-image-filter-message", { method: "POST", body, });
|
||||
Log.message_out(msg)
|
||||
} catch (e) { Log.error(e) }
|
||||
finally { if (!keep_open) this.close() }
|
||||
} catch (e) {
|
||||
Log.error(e)
|
||||
} finally {
|
||||
if (!keep_open) this.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
send_current_state() { this._send_response( (this.state == State.TEXT) ? this.text_edit.value : Array.from(this.picked).join() ) }
|
||||
send_current_state() {
|
||||
if (this.state == State.TEXT) {
|
||||
this._send_response()
|
||||
} else {
|
||||
this._send_response()
|
||||
}
|
||||
}
|
||||
|
||||
send_cancel() { this._send_response(CANCEL, true) }
|
||||
send_cancel() { this._send_response({special:CANCEL}) }
|
||||
|
||||
request_reset() { this._send_response(REQUEST_RESHOW, true, true) }
|
||||
request_reset() { this._send_response({special:REQUEST_RESHOW}, true) }
|
||||
|
||||
close() {
|
||||
this.state = State.INACTIVE
|
||||
State.render(this)
|
||||
this.render()
|
||||
}
|
||||
|
||||
maybe_play_sound() { if (app.ui.settings.getSettingValue("ImageFilter.PlaySound")) this.audio.play(); }
|
||||
maybe_play_sound() { if (app.ui.settings.getSettingValue("Image Filter.UI.Play Sound")) this.audio.play(); }
|
||||
|
||||
handle_message(message) {
|
||||
Log.message_in(message)
|
||||
Log.log( this._handle_message(message, false) )
|
||||
State.render(this)
|
||||
this.render()
|
||||
}
|
||||
|
||||
handle_deferred_message(e) {
|
||||
Log.message_in(this.saved_message, "(deferred)")
|
||||
Log.log( this._handle_message(this.saved_message, true) )
|
||||
State.render(this)
|
||||
this.render()
|
||||
}
|
||||
|
||||
autosend() {
|
||||
return (app.ui.settings.getSettingValue("ImageFilter.AutosendIdentical") && this.allsame)
|
||||
return (app.ui.settings.getSettingValue("ImageFilter.Actions.AutosendIdentical") && this.allsame)
|
||||
}
|
||||
|
||||
_handle_message(message, use_saved) {
|
||||
_handle_message(message, using_saved) {
|
||||
const detail = message.detail
|
||||
const uid = detail.uid
|
||||
this.node = app.graph._nodes_by_id[uid]
|
||||
|
||||
const uid = message.detail.uid
|
||||
const unique = message.detail.unique
|
||||
const node = app.graph._nodes_by_id[uid]
|
||||
if (!this.node) return console.log(`Message was for ${uid} which doesn't exist`)
|
||||
if (this.node._ni_widget?.value != message.detail.unique) return console.log(`Message unique id wasn't mine`)
|
||||
|
||||
if (!node) {
|
||||
console.log(`Message was for ${uid} which doesn't exist`)
|
||||
if (detail.tick) {
|
||||
this.counter_text.innerText = `${detail.tick}s`
|
||||
if (this.state==State.INACTIVE) this.request_reset()
|
||||
return
|
||||
}
|
||||
|
||||
if (node._ni_widget?.value != message.detail.unique) {
|
||||
console.log(`Message unique id wasn't mine`)
|
||||
return
|
||||
if (detail.timeout) {
|
||||
this.close()
|
||||
return `Timeout`
|
||||
}
|
||||
|
||||
this.node = node
|
||||
if (this.handling_message) return `Ignoring message because we're already handling a message`
|
||||
|
||||
this.allsame = message.detail.allsame || false
|
||||
this.set_title(this.node.title ?? "Image Filter")
|
||||
this.allsame = detail.allsame || false
|
||||
if (detail.tip) this.tip_row.innerHTML = detail.tip.replace(/(?:\r\n|\r|\n)/g, '<br/>')
|
||||
else this.tip_row.innerHTML = ""
|
||||
|
||||
if (this.state==State.INACTIVE && message.detail.urls && app.ui.settings.getSettingValue("ImageFilter.SmallWindow") && !use_saved && !this.autosend()) {
|
||||
if (this.state==State.INACTIVE && app.ui.settings.getSettingValue("Image Filter.UI.Small Window") && !using_saved && !this.autosend()) {
|
||||
this.state = State.TINY
|
||||
this.saved_message = message
|
||||
this.tiny_image.src = get_full_url(message.detail.urls[message.detail.urls.length-1])
|
||||
this.maybe_play_sound()
|
||||
return `Deferring message and showing small window`
|
||||
}
|
||||
|
||||
//if ((Date.now()-this.last_response_sent < 500)) return `Ignoring message because we just responded`
|
||||
if (this.handling_message && !detail.timeout) return `Ignoring message because we're already handling a message`
|
||||
this.handling_message = true
|
||||
|
||||
try {
|
||||
const detail = message.detail
|
||||
|
||||
if (detail.tick) {
|
||||
this.counter_text.innerText = `${detail.tick}s`
|
||||
if (this.state==State.INACTIVE) this.request_reset()
|
||||
return
|
||||
}
|
||||
|
||||
if (detail.timeout) {
|
||||
this.close()
|
||||
return `Timeout`
|
||||
}
|
||||
|
||||
this.handling_message = true
|
||||
this.n_extras = detail.extras ? message.detail.extras.length : 0
|
||||
this.extras_row.innerHTML = ''
|
||||
for (let i=0; i<this.n_extras; i++) { create('input', 'extra', this.extras_row, {value:detail.extras[i]}) }
|
||||
|
||||
if (!use_saved && !this.autosend()) this.maybe_play_sound()
|
||||
if (!using_saved && !this.autosend()) this.maybe_play_sound()
|
||||
|
||||
if (detail.maskedit) this.handle_maskedit(detail)
|
||||
else if (detail.urls) this.handle_urls(detail)
|
||||
|
||||
if (detail.tip) this.tip.innerHTML = detail.tip.replace(/(?:\r\n|\r|\n)/g, '<br/>')
|
||||
else this.tip.innerHTML = ""
|
||||
|
||||
} finally { this.handling_message = false }
|
||||
}
|
||||
|
||||
@@ -215,6 +262,11 @@ class Popup extends HTMLSpanElement {
|
||||
)
|
||||
}
|
||||
|
||||
set_title(title) {
|
||||
this.floating_window.set_title(title)
|
||||
this.tiny_window.set_title(title)
|
||||
}
|
||||
|
||||
handle_maskedit(detail) {
|
||||
this.state = State.MASK
|
||||
|
||||
@@ -234,26 +286,28 @@ class Popup extends HTMLSpanElement {
|
||||
wait_while_mask_editing() {
|
||||
if (!this.seen_editor && mask_editor_showing()) {
|
||||
mask_editor_listen_for_cancel( this.send_cancel.bind(this) )
|
||||
this.render()
|
||||
this.seen_editor = true
|
||||
}
|
||||
|
||||
if (mask_editor_showing()) {
|
||||
setTimeout(this.wait_while_mask_editing.bind(this), 100)
|
||||
} else {
|
||||
this._send_response(this.node.imgs[0].src)
|
||||
this._send_response({masked_image:this.extract_filename(this.node.imgs[0].src)})
|
||||
}
|
||||
}
|
||||
|
||||
handle_urls(detail) {
|
||||
this.n_extras = detail.extras ? detail.extras.length : 0
|
||||
this.video_frames = detail.video_frames || 1
|
||||
extract_filename(url_string) {
|
||||
return (new URL(url_string)).searchParams.get('filename')
|
||||
}
|
||||
|
||||
this.extras.innerHTML = ''
|
||||
for (let i=0; i<this.n_extras; i++) { create('input', 'extra', this.extras, {value:detail.extras[i]}) }
|
||||
handle_urls(detail) {
|
||||
this.video_frames = detail.video_frames || 1
|
||||
|
||||
// do this after the extras are set up so that we send the right extras
|
||||
if (this.autosend()) {
|
||||
this._send_response("0")
|
||||
this.picked.add(0)
|
||||
this._send_response()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -261,16 +315,18 @@ class Popup extends HTMLSpanElement {
|
||||
this.state = State.TEXT
|
||||
//this.text_edit.innerHTML = detail.text
|
||||
this.text_edit.value = detail.text
|
||||
if (detail.textareaheight) document.getElementsByClassName('cg_popup')[0].style.setProperty('--text_area_height', `${detail.textareaheight}px`)
|
||||
if (detail.textareaheight) this.text_edit.style.height = `${detail.textareaheight}px`
|
||||
} else {
|
||||
if (this.state != State.FILTER && this.state != State.ZOOMED && app.ui.settings.getSettingValue("Image Filter.UI.Start Zoomed")!=0) {
|
||||
setTimeout(this.zoom_auto.bind(this), 50)
|
||||
}
|
||||
this.state = State.FILTER
|
||||
}
|
||||
|
||||
this.n_images = detail.urls?.length
|
||||
|
||||
this.laidOut = false
|
||||
this.title_bar.innerText = app.graph._nodes_by_id[detail.uid]?.title ?? "Image Filter"
|
||||
|
||||
this.laidOut = -1
|
||||
|
||||
this.picked = new Set()
|
||||
if (this.n_images==1) this.picked.add('0')
|
||||
|
||||
@@ -310,7 +366,7 @@ class Popup extends HTMLSpanElement {
|
||||
this.frame = (this.frame+1)%this.video_frames
|
||||
Array.from(this.grid.children).forEach((img)=>{img.src = img.frames[this.frame]})
|
||||
|
||||
const fps = app.ui.settings.getSettingValue("ImageFilter.FPS")
|
||||
const fps = app.ui.settings.getSettingValue("Image Filter.Video.FPS")
|
||||
const delay = (fps>0) ? 1000/fps : 1000
|
||||
setTimeout(this.advance_videos.bind(this), delay)
|
||||
}
|
||||
@@ -325,65 +381,92 @@ class Popup extends HTMLSpanElement {
|
||||
this.redraw()
|
||||
}
|
||||
|
||||
on_key_down(e) {
|
||||
var used_keypress = false;
|
||||
zoom_auto() {
|
||||
this.state = State.ZOOMED
|
||||
this.zoomed_image_holder = (app.ui.settings.getSettingValue("Image Filter.UI.Start Zoomed")==1) ? this.grid.firstChild : this.grid.lastChild
|
||||
return this.show_zoomed()
|
||||
}
|
||||
zoom_next() {
|
||||
this.zoomed_image_holder = this.zoomed_image_holder.nextSibling || this.zoomed_image_holder.parentNode.firstChild
|
||||
this.show_zoomed()
|
||||
}
|
||||
zoom_prev() {
|
||||
this.zoomed_image_holder = this.zoomed_image_holder.previousSibling || this.zoomed_image_holder.parentNode.lastChild
|
||||
this.show_zoomed()
|
||||
}
|
||||
click_zoomed() {
|
||||
const fake_event = { target:this.zoomed_image_holder}
|
||||
this.on_click(fake_event)
|
||||
this.show_zoomed()
|
||||
}
|
||||
show_zoomed() {
|
||||
this.zoomed_image.src = this.zoomed_image_holder.src
|
||||
return this.render()
|
||||
}
|
||||
eat_event(e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
on_key_press(e) {
|
||||
if (document.activeElement?.type=='text' || document.activeElement?.type=='textarea') {
|
||||
if (this.floating_window.contains(document.activeElement) || this.contains(document.activeElement)) return
|
||||
}
|
||||
if (this.state!=State.INACTIVE && this.state!=State.TINY) {
|
||||
this.eat_event(e)
|
||||
}
|
||||
}
|
||||
|
||||
on_key_down(e) {
|
||||
if (document.activeElement?.type=='text' || document.activeElement?.type=='textarea') {
|
||||
if (this.floating_window.contains(document.activeElement) || this.contains(document.activeElement)) return
|
||||
if (this.state==State.INACTIVE && this.state==State.TINY) return
|
||||
}
|
||||
if (this.state==State.FILTER || this.state==State.TEXT) {
|
||||
if (document.activeElement?.type=='text' || document.activeElement?.type=='textarea') {
|
||||
return
|
||||
// don't capture keys when editing text
|
||||
} else {
|
||||
if (e.key=='Enter') {
|
||||
this.send_current_state()
|
||||
used_keypress = true
|
||||
}
|
||||
if (e.key=='Escape') {
|
||||
this.send_cancel()
|
||||
used_keypress = true
|
||||
}
|
||||
if (`${parseInt(e.key)}`==e.key) {
|
||||
this.select_unselect(parseInt(e.key))
|
||||
used_keypress = true
|
||||
}
|
||||
if (e.key=='Enter') {
|
||||
this.send_current_state()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (e.key=='Escape') {
|
||||
this.send_cancel()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
if (`${parseInt(e.key)}`==e.key) {
|
||||
this.select_unselect(parseInt(e.key))
|
||||
this.render()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state==State.FILTER && !used_keypress) {
|
||||
if (this.state==State.FILTER) {
|
||||
if (e.key==' ' && this.mouse_is_over) {
|
||||
this.state = State.ZOOMED
|
||||
this.zoomed_image_holder = this.mouse_is_over
|
||||
this.on_mouse_out(this.mouse_is_over)
|
||||
used_keypress = true
|
||||
//this.on_mouse_out(this.mouse_is_over)
|
||||
this.eat_event(e)
|
||||
return this.show_zoomed()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state==State.ZOOMED && !used_keypress) {
|
||||
if (this.state==State.ZOOMED) {
|
||||
if (e.key==' ') {
|
||||
this.state = State.FILTER
|
||||
this.zoomed_image_holder = null
|
||||
used_keypress = true
|
||||
this.eat_event(e)
|
||||
return this.render()
|
||||
} else if (e.key=='ArrowUp') {
|
||||
const fake_event = { target:this.zoomed_image_holder}
|
||||
this.on_click(fake_event)
|
||||
this.click_zoomed()
|
||||
return this.eat_event(e)
|
||||
} else if (e.key=='ArrowDown') {
|
||||
// select or unselect
|
||||
} else if (e.key=='ArrowRight') {
|
||||
this.zoomed_image_holder = this.zoomed_image_holder.nextSibling || this.zoomed_image_holder.parentNode.firstChild
|
||||
used_keypress = true
|
||||
this.zoom_next()
|
||||
return this.eat_event(e)
|
||||
} else if (e.key=='ArrowLeft') {
|
||||
this.zoomed_image_holder = this.zoomed_image_holder.previousSibling || this.zoomed_image_holder.parentNode.lastChild
|
||||
used_keypress = true
|
||||
this.zoom_prev()
|
||||
return this.eat_event(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (used_keypress) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (this.zoomed_image_holder) this.zoomed_image.src = this.zoomed_image_holder.src
|
||||
State.render(this)
|
||||
}
|
||||
}
|
||||
|
||||
select_unselect(n) {
|
||||
@@ -391,8 +474,9 @@ class Popup extends HTMLSpanElement {
|
||||
return
|
||||
}
|
||||
const s = `${n}`
|
||||
if (app.ui.settings.getSettingValue("ImageFilter.ClickSends")) {
|
||||
this._send_response(s)
|
||||
if (app.ui.settings.getSettingValue("Image Filter.Actions.Click Sends")) {
|
||||
this.picked.add(s)
|
||||
this._send_response()
|
||||
} else {
|
||||
if (this.picked.has(s)) this.picked.delete(s)
|
||||
else this.picked.add(s)
|
||||
@@ -407,11 +491,12 @@ class Popup extends HTMLSpanElement {
|
||||
}
|
||||
|
||||
layout() {
|
||||
if (this.laidOut) return
|
||||
const box = this.grid.getBoundingClientRect()
|
||||
if (this.laidOut==box.width) return
|
||||
|
||||
const im_w = this.grid.firstChild.naturalWidth
|
||||
const im_h = this.grid.firstChild.naturalHeight
|
||||
const box = this.grid.getBoundingClientRect()
|
||||
|
||||
var per_row
|
||||
if (!im_w || !im_h || !box.width || !box.height) {
|
||||
setTimeout(this.layout.bind(this), 100)
|
||||
@@ -428,7 +513,7 @@ class Popup extends HTMLSpanElement {
|
||||
}
|
||||
}
|
||||
per_row = best_pick
|
||||
this.laidOut = true
|
||||
this.laidOut = box.width
|
||||
}
|
||||
|
||||
const rows = Math.ceil(this.n_images/per_row)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
.cg_popup .zoomed {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
z-index: 120000;
|
||||
background-color: rgba(29, 29, 29, 0.886);
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_prev, .zoomed_next {
|
||||
flex-grow: 1;
|
||||
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_prev {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_arrow {
|
||||
font-size: 48px;
|
||||
border-radius: 40px;
|
||||
padding: 0px 8px 4px 8px;
|
||||
margin: 6px;
|
||||
border: 1px solid rgb(128, 128, 128, 1.0);
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_arrow:hover {
|
||||
border: 1px solid rgb(255, 128, 128, 1.0);
|
||||
}
|
||||
|
||||
|
||||
.cg_popup .zoomed_image {
|
||||
/*max-width: 90%;*/
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border: 1px solid white;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.cg_popup .zoomed_number {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
margin-left: 12px;
|
||||
color: white;
|
||||
padding: 10px 8px 8px 8px;
|
||||
border-radius: 4px;
|
||||
z-index: 110000;
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
border: 1px solid rgba(128, 128, 128, 1.0);
|
||||
}
|
||||
|
||||
.cg_popup .zoomed.highlighted .zoomed_image {
|
||||
border: 3px solid green;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "cg-image-filter"
|
||||
description = "A set of custom nodes that pause a workflow while you select images, add masks, or edit text."
|
||||
version = "1.4.2"
|
||||
version = "1.5"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
[project.urls]
|
||||
|
||||
Reference in New Issue
Block a user