Compare commits

...
148 Commits
Author SHA1 Message Date
Hu Xiuhan a00828fe81 Add user-friendly solution link to README
Added a note about neta.art as a user-friendly solution.
2026-08-09 12:03:58 +08:00
Hu Xiuhan 95fd55d663 Merge pull request #66 from willybcode/main
Addition of both menu buttons to the new ui
2024-11-11 09:42:29 +08:00
willy 989c21205f feat: added buttons to new ui
- added "Browser" button as 📚
- added save button as 💾
2024-11-07 12:06:34 +00:00
南辰燏炚 dbf7e780ad Merge pull request #65 from talesofai/update/load_img_from_url
update LoadImageByUrl
2024-11-04 15:55:37 +08:00
LingXuanYin 79055476bd Revert "update exception if img file not found"
This reverts commit 7153ef7b01.
2024-11-04 15:52:27 +08:00
LingXuanYin 7153ef7b01 update exception if img file not found 2024-11-04 15:45:46 +08:00
LingXuanYin 18fabdb7b2 update code 2024-11-04 15:41:53 +08:00
Samwise Wang fc003f4945 Merge pull request #40 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-07-01 20:41:20 +08:00
Samwise Wang 1363aff363 Merge pull request #41 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-07-01 20:41:03 +08:00
haohaocreates ccf090be74 Update pyproject.toml desc 2024-05-21 16:51:16 -04:00
haohaocreates 1514341e97 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-21 20:50:37 +00:00
haohaocreates a42f979ead chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-21 20:50:36 +00:00
Samwise Wang a27e603bf2 Merge pull request #39 from talesofai/feat/add_request_retry
add retry to http_client
2024-05-21 16:57:31 +08:00
tzwm 8ff9a64888 add retry to http_client 2024-05-21 16:10:47 +08:00
tzwm 23f77cdd1d add more error logs to nodes 2024-05-05 11:45:59 +08:00
tzwm fe649c94f8 bump new mirror version 2024-04-27 15:37:19 +08:00
tzwm b9414c0a32 Fix a bug about outputs_path 2024-04-23 14:21:31 +08:00
Samwise Wang 22b536cd06 Merge pull request #35 from LoganBarnett/fix-missed-configurable-paths
fix missed configurable paths
2024-04-23 14:09:57 +08:00
Logan Barnett 506cf662e3 fix missed configurable paths
I missed some paths using function calls from #34.  Apologies!  I've
done further searching into the repository and I think this covers
everything now.
2024-04-22 20:08:58 -07:00
Samwise Wang 18836122ff Merge pull request #34 from LoganBarnett/add-configurable-paths
read paths from configuration file
2024-04-23 10:08:01 +08:00
Logan Barnett e406d40ad6 read paths from configuration file
This updates the automatic path detection to use a configuration file
_optionally_ as a means of overriding the paths used by
`comfyui-browser`.  This change is motivated by recent work I have done
with ComfyUI and Nix, wherein I provided [an easy means to install
`comfyui-browser` via
Nix](https://github.com/loganbarnett/dotfiles/blob/ccbfe12e755ab3d6d24b3f0f5a3386208b23357f/nix/hacks/comfyui/custom-nodes.nix#L50-L74).
The big reason this was necessary for my Nix work is because Nix's
installations are locked down into a purely read-only store.  I needed a
place to put things such as the `collections` path into a location that
`comfyui-browser` could write to.

I don't expect `comfyui-browser` to directly support Nix. My hope is
that allowing the directories to be configurable, with the defaults
being exactly what was used to before, to be something desirable for the
project in a general sense, and not just for Nix.

I've tested this under a Nix configuration with `config.json` values
being used.  Unfortunately I don't have a means to test
`comfyui-browser` outside of Nix, as the `comfyui` and CUDA setup was
hard enough with Nix itself.  So to be clear - this isn't tested under
the standard configuration that most `comfyui-browser` users are using.
I suggest giving these changes a quick spin to make sure I didn't miss
anything.  I did my best to make the code changes branchless - in that
no new code paths are added - just a default means of determining what
the configuration values should be.  Memoization is used to ensure that
the order in which these functions are called is irrelevant.

Unfortunately I am not familiar at all with Chinese/Mandarin(?) to write
the documentation for it - perhaps someone else can pick that up?

Earmarked as part of this:  I've changed the `mkdir` calls to
`makedirs`, which adds all directories in the chain that are missing,
and the `exist_ok` argument means the call will not fail if the
directory already exists, which allows us to clean up a little code.

Thanks for your work on maintaining this useful custom nodes project!
2024-04-22 16:09:34 -07:00
tzwm f6960d29c2 [xyz_plot] Fix wrong workflow format 2024-04-20 15:09:38 +08:00
tzwm 283c84d24a Fix #21 about load more method in the FilesList 2024-04-20 14:46:04 +08:00
tzwm 0a39d125cb [upload_to_remote] fix cannot write mode RGBA as JPEG 2024-04-07 15:38:44 +08:00
tzwm f1f0975caa [select_inputs] load widgets_values from workflow 2024-04-04 11:07:23 +08:00
tzwm 5d501642d5 bump new vervion for xyz plot styles 2024-03-31 13:15:51 +08:00
tzwm 195b3f102b update some styles for xyz plot again 2024-03-31 11:13:20 +08:00
tzwm fe0c5390b2 add more logs to upload_to_remote node 2024-03-29 17:07:11 +08:00
tzwm 5940fcfaa9 update load_image_by_url node 2024-03-26 17:34:22 +08:00
Samwise Wang e17f683e18 Update README.md 2024-03-24 17:41:02 +08:00
tzwm d5f769042d update some styles for xyz plot again 2024-03-23 23:41:23 +08:00
tzwm 80254b9126 update some styles for xyz plot again 2024-03-23 23:35:16 +08:00
tzwm cf65c09ea4 bump to v1.8.2 to update some styles for xyz 2024-03-23 22:50:39 +08:00
tzwm a8c663e083 update some styles for xyz plot 2024-03-23 22:50:23 +08:00
Samwise Wang 48f443714b Merge pull request #29 from talesofai/feat-like
like and dislike
2024-03-22 17:11:44 +08:00
tzwm 7689cf23c2 bump to v1.8.1 to support xyz_plot score 2024-03-22 17:07:20 +08:00
miaobuao 64e2553c83 feat: refetch likes 2024-03-22 16:37:50 +08:00
tzwm fdff294b8c fix nodes/xyz_plot 2024-03-22 16:06:12 +08:00
tzwm 7bd14ec4df move exist result to another directory for xyz_plot 2024-03-22 14:50:02 +08:00
tzwm 3d584d9e68 fix the path parse in the xyz_plot#api_get_score_statistic 2024-03-22 14:12:00 +08:00
miaobuao 8e1e254a8b fix: auto request 2024-03-22 14:09:01 +08:00
miaobuao 5d8379dc50 fix: data format 2024-03-22 13:46:58 +08:00
miaobuao c084687f0e update api 2024-03-22 13:46:58 +08:00
miaobuao 6617ed5244 feat: display score 2024-03-22 13:46:58 +08:00
tzwm 583f16358e add xyz_plot#statistic API 2024-03-22 11:28:03 +08:00
tzwm a1872b9d96 add update_score api for xyz_plot 2024-03-21 16:14:04 +08:00
miaobuao b2ed89bf23 update default width 2024-03-21 16:03:46 +08:00
miaobuao 27be9e53a0 update status position 2024-03-21 15:43:25 +08:00
miaobuao fcb9e14b10 feat: switch mode 2024-03-21 13:42:03 +08:00
miaobuao 3a13898314 max width 30% 2024-03-20 21:58:21 +08:00
miaobuao 5c72c9d3e8 like and dislike 2024-03-20 18:26:14 +08:00
Samwise Wang 42da8a1ee9 Merge pull request #27 from talesofai/feat-result-analyze
feat: result analyze
2024-03-19 16:54:36 +08:00
tzwm 716bebfa57 bump to v1.8.0 to support Z axis for xyz_plot 2024-03-19 16:52:32 +08:00
tzwm a823325559 rename xyz_plots to xyz_plot 2024-03-19 16:47:04 +08:00
miaobuao b25965ce33 fix: img size 2024-03-19 16:43:19 +08:00
tzwm 3f16d825b2 update xyz_plot node 2024-03-19 16:40:13 +08:00
miaobuao 008d3b0649 rename AnalyzeTable -> MultiDimTable 2024-03-19 15:43:15 +08:00
miaobuao 8d74ed0cca feat: remove sidebar 2024-03-19 14:34:05 +08:00
miaobuao 90ada1a3f8 refactor: mv analyze -> xyz-plots 2024-03-19 13:22:25 +08:00
miaobuao ad5b73e81d feat: result analyze 2024-03-19 00:38:27 +08:00
tzwm 610dd6be61 add ZHO-ZHO-ZHO/ComfyUI-Workflows-ZHO to sources 2024-03-17 20:56:23 +08:00
tzwm 7f41ac743e fix the bug about encoding for xyz plot 2024-03-12 22:22:52 +08:00
tzwm b1e3650d38 Fix #26 to support IPv6 2024-01-26 18:03:22 +08:00
tzwm 7043ca2f2f add extra field to upload_to_remote node 2024-01-22 18:30:47 +08:00
tzwm 79223fe124 add headers to requests for upload_to_remote node 2024-01-21 22:40:38 +08:00
Samwise Wang 245c5bdb7f Merge pull request #22 from talesofai/feat/add-dify-nodes
add dify_text_generator node
2024-01-20 20:28:23 +08:00
Samwise Wang dc51306504 Merge branch 'main' into feat/add-dify-nodes 2024-01-20 20:24:34 +08:00
Hu Xiuhan 9dca10cb23 Merge pull request #24 from talesofai/feat/add-remote-callback
add nodes/remote_callback
2024-01-20 16:21:29 +08:00
tzwm 02aa0bbc90 rename remote_callback to upload_to_remote node 2024-01-20 16:20:32 +08:00
tzwm 30983261ca add nodes/remote_callback 2024-01-20 15:52:44 +08:00
tzwm df215cd27d [sources] add if-ai/IF-Animation-Workflows 2024-01-20 00:06:04 +08:00
Samwise Wang ed1322e5fd Update README.md 2024-01-17 23:07:05 +08:00
tzwm a97c497ff5 add inputs_json_str to dify_text_generator node 2024-01-17 18:01:56 +08:00
tzwm 8d440db432 add dify_text_generator node 2024-01-17 17:36:40 +08:00
tzwm 51048427ac try to fix #1 and #20 2024-01-15 00:54:28 +08:00
tzwm 58b9e0e87c add discord link to Navbar 2024-01-14 18:37:06 +08:00
tzwm 910bdde2ca [xyz_plot] fix a tiny bug 2024-01-14 17:54:45 +08:00
tzwm a09f0533ab use no_proxy to request for some special enviroments 2024-01-14 17:33:36 +08:00
tzwm ee29a55478 fix pandas missing error to update requiremtents 2024-01-14 16:53:26 +08:00
tzwm b03bd2574c fix wrong location.origin for the result page of xyz_polt 2024-01-14 01:12:45 +08:00
tzwm 9cfc9085d6 bump to v1.7.0 to add xyz_plot and select_inputs nodes 2024-01-14 00:57:40 +08:00
tzwm 6247ea9c0b clean up debuggers 2024-01-14 00:55:43 +08:00
tzwm 8a1bbe9c2a add open result button to xyz plot 2024-01-14 00:54:41 +08:00
tzwm e5cad2d407 support to browse HTML files 2024-01-13 23:40:00 +08:00
Samwise Wang ec367d2fbf Merge pull request #19 from talesofai/fix/some-bugs
fix node small bugs
2024-01-13 22:48:42 +08:00
Xiuhan Hu 90d706296c fix node small bugs 2024-01-13 22:45:51 +08:00
Hu Xiuhan a36c74714c Merge pull request #18 from talesofai/xyz
Add xyz_plot node
2024-01-13 21:02:27 +08:00
tzwm 1aa9a8335a clean unused code for xyz_plot 2024-01-13 20:40:16 +08:00
tzwm da98222269 update generated HTML for xyz plot 2024-01-13 20:25:21 +08:00
tzwm 9ff9a37a39 generate gird HTML for xyz plot result 2024-01-13 19:37:19 +08:00
tzwm 8c98bb95e1 finish basic xyz_plot generates 2024-01-13 17:27:58 +08:00
Samwise Wang bf7a45a46c Merge pull request #17 from xinlijia/main
Fix README typos and add more guidance
2024-01-12 15:30:19 +08:00
Xinli Jia faf00d91e2 Fix README typos and add more guidance 2024-01-11 22:58:14 -05:00
tzwm 5604063aaf fix missing argument for select_inputs node 2024-01-11 22:51:48 +08:00
Samwise Wang b31209b310 Merge pull request #15 from talesofai/feat/add-select-inputs
add select inputs node
2024-01-11 10:26:19 +08:00
tzwm f9466e029a Merge branch 'main' into xyz 2024-01-11 10:20:17 +08:00
tzwm 71e18925e9 rename select_inputs node 2024-01-11 10:18:45 +08:00
tzwm a83d902507 remove unfinished node 2024-01-11 10:17:20 +08:00
Samwise Wang 8a2ec63a3f Merge pull request #14 from talesofai/load_image_by_url
add basic load_image_by_url node
2024-01-10 20:02:19 +08:00
tzwm b8a6708c09 add basic load_image_by_url node 2024-01-10 18:52:15 +08:00
tzwm 5abbc50890 add preview to select_inputs 2024-01-10 11:45:59 +08:00
tzwm 0545ceb556 finish basic select_inputs node 2024-01-10 00:38:45 +08:00
tzwm 92bc0ce868 add UI of select_inputs and xyz_plot nodes 2024-01-09 22:24:31 +08:00
Hu Xiuhan 19d6c2855c Update sources.json 2024-01-06 22:42:17 +08:00
Hu Xiuhan 22b422fde5 Update README.md 2024-01-06 22:30:32 +08:00
tzwm ccd46f59a4 [sources] add wyrde/wyrde-comfyui-workflows and atlasunified/Templates-ComfyUI- 2024-01-03 13:55:52 +08:00
tzwm 22dc6b179e fix #13: try to fix wrong encoding in the Windows 2024-01-02 21:27:37 +08:00
tzwm 9e5aa99ffc bump to v1.6.1 to fix some bugs in CollectionsTab 2023-12-31 09:26:49 +08:00
tzwm c461313a75 fix: invalid filename updates for Collections 2023-12-31 09:24:39 +08:00
tzwm 8f04466805 fix: no refresh after some actions 2023-12-31 09:03:14 +08:00
tzwm 8d7b8ce130 bump to v1.6.0 to support i18n 2023-12-30 19:55:17 +08:00
Samwise Wang 27be96667a Merge pull request #12 from talesofai/feat/add-i18n
Add i18n support
2023-12-30 19:52:08 +08:00
tzwm 14b902d453 Merge branch 'main' into feat/add-i18n 2023-12-30 19:50:54 +08:00
tzwm 555ec75533 refresh list after Browser modal opening 2023-12-30 17:01:13 +08:00
tzwm 278f0497d5 [sources] add C0nsumption/Consume-ComfyUI-Workflows 2023-12-30 15:06:08 +08:00
tzwm bd6c1ef9e1 [i18n] finish more tranlations in SourcesTab 2023-12-30 14:50:39 +08:00
tzwm f705d01fd6 [i18n] finish more tranlations 2023-12-30 14:46:39 +08:00
tzwm 4a98283f6e update package-lock 2023-12-30 00:26:13 +08:00
tzwm 9eeb848067 [i18n] finish CollectionsTab translation 2023-12-30 00:19:06 +08:00
tzwm 747bc3408d [i18n] add i18n/index to init 2023-12-29 23:38:36 +08:00
tzwm dac3867c83 [i18n] add select to Navbar about i18n 2023-12-29 22:46:02 +08:00
tzwm 25fa7ae047 [sources] add BoosterCore/ChaosFlow 2023-12-27 20:54:14 +08:00
tzwm 5450a92aaa Merge branch 'main' into feat/add-i18n 2023-12-27 18:02:11 +08:00
Samwise Wang c6e7b12b63 Merge pull request #11 from talesofai/feat/delete-gototop
Move delete button to right and add go to top
2023-12-27 13:32:55 +08:00
Xiuhan Hu a277691935 Move delete button to right and add go to top 2023-12-27 12:13:08 +08:00
Hu Xiuhan 89b3dbd1b1 Merge pull request #10 from talesofai/feat/downloader
Add basic model downloader
2023-12-26 14:07:49 +08:00
tzwm ea32af33bf Add a shortcut to toggle Browser dialog 2023-12-24 23:28:42 +08:00
tzwm 688f88b056 update DownloadHistory component 2023-12-24 22:01:07 +08:00
tzwm 597e91d618 fix downloads#create by starting as a new thread 2023-12-24 19:39:34 +08:00
tzwm 58f10f064f add NewDownload component 2023-12-24 16:57:38 +08:00
tzwm 366553e582 add DownloadHistory component 2023-12-24 15:48:16 +08:00
tzwm 83901022da update downloads#index API to add more information 2023-12-24 14:48:14 +08:00
tzwm bbbaf6d365 add downloads@index and downloads#show API 2023-12-24 13:32:56 +08:00
tzwm 45125145af update file download 2023-12-24 12:09:58 +08:00
tzwm 374b32b796 add basic downloads#create API 2023-12-24 01:13:56 +08:00
tzwm 8fa8c3e159 fix some styles in the SourcesTab 2023-12-23 23:08:57 +08:00
tzwm edaad9014c [sources] add fictions-ai/sharing-is-caring 2023-12-23 23:00:04 +08:00
Samwise Wang c81a9daf83 Merge pull request #9 from talesofai/feat/split_pys
Split __init__.py file
2023-12-23 21:27:20 +08:00
tzwm e86b725464 fix some bugs for python scripts 2023-12-23 20:21:19 +08:00
tzwm 0b262956c7 split python files 2023-12-23 20:03:41 +08:00
tzwm 894fbabf8c bump to v1.5.3 to merge the same filename files 2023-12-23 13:58:31 +08:00
tzwm f4d8abe73a support the same name of JSON and media in the same directory 2023-12-23 13:54:11 +08:00
limbo.wang 66204953ae Merge branch 'main' into feat/add-i18n 2023-12-23 01:55:08 +08:00
tzwm fb11bc1240 add shadowcz007/awesome-comfyui-workflow to Sources data 2023-12-22 17:27:20 +08:00
limbo.wang ab2b3a4af4 feat: add svelte-i18n lib and make basic translation and make the prettier file 2023-12-21 01:04:01 +08:00
tzwm 65cbb9bdd2 Add Development to README 2023-12-20 15:56:26 +08:00
Samwise Wang 0babfa37d6 Merge pull request #7 from talesofai/feat/sources-stars
add example sources stars
2023-12-20 13:42:13 +08:00
Xiuhan Hu e38122ab7f Add Bilibili intro video 2023-12-20 12:51:36 +08:00
97 changed files with 8797 additions and 3275 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+2
View File
@@ -2,3 +2,5 @@ config.json
__pycache__/
collections/
sources/
.DS_Store
download_logs/
+24
View File
@@ -1,5 +1,29 @@
# ChangeLog
## v1.8.1 - 2024-03-22
- Update xyz_plot to support score mode
## v1.8.0 - 2024-03-19
- Update xyz_plot to support Z axis
## v1.7.0 - 2024-01-14
- Add xyz_plot and select_inputs nodes
## v1.6.1 - 2023-12-31
- Fix some bugs about CollectionsTab
## v1.6.0 - 2023-12-30
- Add i18n support and zh-CN language
## v1.5.3 - 2023-12-23
- Merge the same name JSON and media file in the same directory
## v1.5.2 - 2023-12-20
- Support resize the modal
+110 -7
View File
@@ -1,5 +1,7 @@
# ComfyUI Browser
### For user-friendly agentic solution, please try [neta.art](https://neta.art) from the team :)
[中文说明](README_CN.md)
This is an image/video/workflow browser and manager for ComfyUI.
@@ -16,19 +18,81 @@ https://github.com/talesofai/comfyui-browser/assets/828837/803ce57a-1cf2-4e1c-be
- Sync your 'Saves' anywhere by Git.
- Subscribe workflow sources by Git and load them more easily.
- Search your workflow by keywords.
- Some useful custom nodes like xyz_plot, inputs_select.
## Custom Nodes
#### Select Inputs
- Select any inputs of the current graph.
<img width="256" alt="image" src="https://github.com/talesofai/comfyui-browser/assets/828837/8e505f62-f709-426a-8a0a-fca291784b08">
#### XYZ Plot
- Simple XYZ Plot by selecting inputs and filling in the values.
<img width="512" alt="image" src="https://github.com/talesofai/comfyui-browser/assets/828837/23ce6a4d-3311-4058-9b46-ddb50d07e22a">
<img width="768" alt="image" src="https://github.com/talesofai/comfyui-browser/assets/828837/d46461ac-3fb3-4037-b1be-048a7ae5e89a">
## Preview
![b359de5f6556649512e7ed8f812ba67d444be9914173e2467018450ce1a3ce1d](https://github.com/talesofai/comfyui-browser/assets/828837/4b0b0f4c-28a8-49ef-98c2-d293df5b7747)
![c91157bf819ef5b9a129976d9e45588106dd6c7ea39ecb0a22519acd72afc7ce](https://github.com/talesofai/comfyui-browser/assets/828837/ee3df970-017c-4825-ab5d-9465cdb77ed6)
![53053f43847da9597efebab207140eed703b8c7bbe8eb1e63ce5630b5d8c9a3f](https://github.com/talesofai/comfyui-browser/assets/828837/4acb522a-f21c-47ad-9a23-56b08c6e73a5)
![c7b93b2ec0891eb7cac1385505e855fb28934ec958f7b21cac53c9bf18e6136c](https://github.com/talesofai/comfyui-browser/assets/828837/ef0d5cd2-9238-4e80-9f65-0f7db05ffbf3)
#### Outputs
<img width="1212" alt="Outputs" src="https://github.com/talesofai/comfyui-browser/assets/1184998/49936e2f-9682-4df8-af8e-6e1653e78ca1">
#### Saves
<img width="1207" alt="Saves" src="https://github.com/talesofai/comfyui-browser/assets/1184998/de3327e3-643e-4ae4-9e31-86df6a0353e0">
#### Sources
<img width="1504" alt="Sources" src="https://github.com/talesofai/comfyui-browser/assets/1184998/07671822-c4d1-4327-bd87-6b1c7e85a354">
<img width="1212" alt="Recommended Sources" src="https://github.com/talesofai/comfyui-browser/assets/1184998/f37852b9-1030-4044-abf3-12bd8158c446">
#### Models
<img width="1510" alt="Models" src="https://github.com/talesofai/comfyui-browser/assets/1184998/4f36378e-05e0-49dc-a5b2-07d24a8b96bc">
#### Side Bar View
<img width="1506" alt="SideBar" src="https://github.com/talesofai/comfyui-browser/assets/1184998/746a031e-88b0-4ccf-8e2d-e448c001f319">
## Installation
### Comfy Manager
### ComfyUI Manager
Search for `comfyui-browser` and install it.
Install [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager), search `comfyui-browser` in Install Custom Node and install it.
#### Configuring
In your `comfyui-browser` directory, you can add a `config.json` to override
the directories that `comfyui-browser` uses. Ex:
``` json
{
"collections": "/var/lib/comfyui/comfyui-browser-collections",
"download_logs": "/var/lib/comfyui/comfyui-browser-download-logs",
"outputs": "/var/lib/comfyui/outputs",
"sources": "/var/lib/comfyui/comfyui-browser-sources"
}
```
The default configuration values are:
``` json
{
"collections": "[comfyui-browser]/collections",
"download_logs": "[comfyui-browser]/download-logs",
"outputs": "[comfyui]/outputs",
"sources": "[comfyui-browser]/sources"
}
```
Where `[comfyui-browser]` is the automatically determined path of your
`comfyui-browser` installation, and `[comfyui]` is the automatically determined
path of your `comfyui` server. Notably, the `outputs` directory defaults to the
`--output-directory` argument to `comfyui` itself, or the default path that
`comfyui` wishes to use for the `--output-directory` argument.
### Manually
@@ -40,7 +104,46 @@ cd custom_nodes && git clone https://github.com/tzwm/comfyui-browser.git
## Notes
- Your 'Saves' are stored in the `ComfyUI/custom_nodes/comfyui-browser/collections`.
- Your 'Saves' are stored in the `collections` configuration value. See <a
href="#Configuring">Configuring</a> for its default, and how to set the path
to something different.
- Press 'B' to toggle the Browser dialog in the ComfyUI.
## Development
- Prerequisite
- Install [Node](https://nodejs.org/en/download/current)
- Framework
- Frontend: [Svelte](https://kit.svelte.dev/)
- Backend: [aiohttp](https://docs.aiohttp.org/)(the same as ComfyUI)
- Project Structure
```
├── __init__.py (Backend Server)
├── web (Frontend code loaded by ComfyUI)
├── build (Built in Svelte)
└── index.js (Frontend that interact with ComfyUI)
├── svelte (Frontend in the Modal as a iframe, written in Svelte)
```
- Build and Run
- Copy or link `comfyui-browser` to `ComfyUI/custom_nodes/`
- Start backend by `cd ComfyUI && python main.py --enable-cors-header`
- Start frontend by `cd ComfyUI/custom_nodes/comfyui-browser/svelte && npm i && npm run dev`
- Open and debug by `http://localhost:5173/?comfyUrl=http://localhost:8188`
- It will use `localhost:8188` as ComfyUI server
- `localhost:5173` is a Vite dev server
- Notes
- Please try to test on Windows, because I only have Linux/macOS
## TODO
+40 -3
View File
@@ -8,6 +8,8 @@
https://github.com/talesofai/comfyui-browser/assets/828837/803ce57a-1cf2-4e1c-be17-0efab401ef54
也可以参考B站的中文解说:
https://www.bilibili.com/video/BV1qc411m7Gp/
## 功能
@@ -27,9 +29,8 @@ https://github.com/talesofai/comfyui-browser/assets/828837/803ce57a-1cf2-4e1c-be
## 安装方式
### Comfy Manager
搜索 `comfyui-browser` 来安装。
### ComfyUI Manager
安装[ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager), 在 Install Custom Node 中搜索 `comfyui-browser` 来安装。
### 手动
@@ -39,6 +40,42 @@ https://github.com/talesofai/comfyui-browser/assets/828837/803ce57a-1cf2-4e1c-be
cd custom_nodes && git clone https://github.com/tzwm/comfyui-browser.git
```
## 开发
- 前置需求
- 安装[Node](https://nodejs.org/en/download/current)
- 使用的框架
- 前端: [Svelte](https://kit.svelte.dev/)
- 后端: [aiohttp](https://docs.aiohttp.org/)(和 ComfyUI 一样)
- 目录介绍
```
├── __init__.py (后端服务)
├── web (ComfyUI 加载的前端路径)
├── build (Svelte 的生成文件)
└── index.js (和 ComfyUI 交互的前端代码)
├── svelte (前端主体部分)
```
- 开发和调试
- 复制或者链接 `comfyui-browser` 到 `ComfyUI/custom_nodes/`
- 启动服务端: `cd ComfyUI && python main.py --enable-cors-header`
- 启动前端: `cd ComfyUI/custom_nodes/comfyui-browser/svelte && npm i && npm run dev`
- 调试地址 `http://localhost:5173/?comfyUrl=http://localhost:8188`
- `localhost:8188` 是 ComfyUI server 地址
- `localhost:5173` 是 Vite dev server
- 备注
- 请尽量在 Windows 上测试, 因为我只有 Linux 和 macOS
- 在 ComfyUI 中可以按 'B' 键来打开/关闭 Browser
## 更新记录
详见:[ChangeLog](CHANGELOG.md)
+46 -550
View File
@@ -1,564 +1,60 @@
import json
import os, stat, errno
from os import path
from aiohttp import web, ClientSession, ClientTimeout
from typing import TypedDict, List
import shutil
import time
import subprocess
import re
import os
from aiohttp import web
import server
import folder_paths
class FileInfoDict(TypedDict):
type: str
name: str
bytes: int
created_at: float
folder_path: str
notes: str
from .utils import collections_path, browser_path, sources_path, download_logs_path, outputs_path
from .routes import sources, collections, config, files, downloads, xyz_plot as xyz_plot_routes
from .nodes import select_inputs, load_image_by_url, xyz_plot, dify_text_generator, upload_to_remote
routes = server.PromptServer.instance.routes
browser_path = path.dirname(__file__)
collections_path = path.join(browser_path, 'collections')
config_path = path.join(browser_path, 'config.json')
sources_path = path.join(browser_path, 'sources')
browser_app = web.Application()
browser_app.add_routes([
web.get("/files", files.api_get_files),
web.delete("/files", files.api_delete_file),
web.put("/files", files.api_update_file),
web.get("/files/view", files.api_view_file),
git_remote_name = 'origin'
web.post("/collections", collections.api_add_to_collections),
web.post("/collections/workflows", collections.api_create_new_workflow),
web.post("/collections/sync", collections.api_sync_my_collections),
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv']
web.get("/sources", sources.api_get_sources),
web.post("/sources", sources.api_create_source),
web.delete("/sources/{name}", sources.api_delete_source),
web.post("/sources/sync/{name}", sources.api_sync_source),
web.get("/sources/all", sources.api_get_all_sources),
info_file_suffix = '.info'
web.get("/config", config.api_get_browser_config),
web.put("/config", config.api_update_browser_config),
web.post("/downloads", downloads.api_create_new_download),
web.get("/downloads", downloads.api_list_downloads),
web.get("/downloads/{uuid}", downloads.api_show_download),
def init_path():
if not path.exists(collections_path):
os.mkdir(collections_path)
web.put("/xyz_plot/score", xyz_plot_routes.api_update_score),
web.get("/xyz_plot/statistic", xyz_plot_routes.api_get_score_statistic),
if not path.exists(sources_path):
os.mkdir(sources_path)
web.static("/web", os.path.join(browser_path, 'web/build')),
# folder_type = 'outputs', 'collections', 'sources'
def get_parent_path(folder_type: str):
if folder_type == 'collections':
return collections_path
if folder_type == 'sources':
return sources_path
# outputs
return folder_paths.output_directory
# folder_type = 'outputs', 'collections', 'sources'
def get_target_folder_files(folder_path: str, folder_type: str = 'outputs'):
if '..' in folder_path:
return None
parent_path = get_parent_path(folder_type)
files: List[FileInfoDict] = []
target_path = path.join(parent_path, folder_path)
if not path.exists(target_path):
return []
folder_listing = os.scandir(target_path)
folder_listing = sorted(folder_listing, key=lambda f: (f.is_file(), -f.stat().st_ctime))
for item in folder_listing:
if not path.exists(item.path):
continue
name = path.basename(item.path)
ext = path.splitext(name)[1].lower()
if name == '' or name[0] == '.':
continue
if item.is_file():
if not (ext in (image_extensions + video_extensions + ['.json'])):
continue
created_at = item.stat().st_ctime
info_file_path = get_info_filename(item.path)
info_data = {}
if path.exists(info_file_path):
with open(info_file_path, 'r') as f:
info_data = json.load(f)
if item.is_file():
bytes = item.stat().st_size
files.append({
"type": "file",
"name": name,
"bytes": bytes,
"created_at": created_at,
"folder_path": folder_path,
"notes": info_data.get("notes", "")
})
elif item.is_dir():
files.append({
"type": "dir",
"name": name,
"bytes": 0,
"created_at": created_at,
"folder_path": folder_path,
"notes": info_data.get("notes", "")
})
return files
def get_info_filename(filename):
return path.splitext(filename)[0] + info_file_suffix
def run_cmd(cmd, run_path, log_cmd=True, log_code=True, log_message=True):
if log_cmd:
log(f'running: {cmd}')
ret = subprocess.run(
f'cd {run_path} && {cmd}',
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="UTF-8"
)
if log_code:
if ret.returncode == 0:
log('successed')
else:
log('failed')
if log_message:
if (len(ret.stdout) > 0 or len(ret.stderr) > 0):
log(ret.stdout + ret.stderr)
return ret
def get_config():
if not path.exists(config_path):
return {}
with open(config_path, 'r') as f:
return json.load(f)
def set_config(config):
with open(config_path, 'w') as f:
json.dump(config, f)
def git_set_remote_url(remote_url, run_path = collections_path):
ret = run_cmd('git remote', run_path)
if git_remote_name in ret.stdout.split('\n'):
return run_cmd(f'git remote set-url {git_remote_name} {remote_url}', run_path)
else:
return run_cmd(f'git remote add {git_remote_name} {remote_url}', run_path)
def git_init(run_path = collections_path):
if not path.exists(path.join(run_path, '.git')):
run_cmd('git init', collections_path)
ret = run_cmd('git config user.name', collections_path,
log_cmd=False, log_code=False, log_message=False)
if len(ret.stdout) == 0:
ret = run_cmd('whoami', collections_path,
log_cmd=False, log_code=False, log_message=False)
username = ret.stdout.rstrip("\n")
run_cmd(f'git config user.name "{username}"', collections_path)
ret = run_cmd('git config user.email', collections_path,
log_cmd=False, log_code=False, log_message=False)
if len(ret.stdout) == 0:
ret = run_cmd('hostname', collections_path,
log_cmd=False, log_code=False, log_message=False)
hostname = ret.stdout.rstrip("\n")
run_cmd(f'git config user.email "{hostname}"', collections_path)
def log(message):
print('[comfyui-browser] ' + message)
def add_uuid_to_filename(filename):
name, ext = path.splitext(filename)
return f'{name}_{int(time.time())}{ext}'
def handle_remove_readonly(func, path, exc):
excvalue = exc[1]
if excvalue.errno == errno.EACCES:
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
func(path)
else:
raise
# folder_path, folder_type
@routes.get("/browser/files")
async def api_get_files(request):
folder_path = request.query.get('folder_path', '')
folder_type = request.query.get('folder_type', 'outputs')
files = get_target_folder_files(folder_path, folder_type=folder_type)
if files == None:
return web.Response(status=404)
return web.json_response({
'files': files
})
# filename, folder_path, folder_type
@routes.delete("/browser/files")
async def api_delete_file(request):
json_data = await request.json()
filename = json_data['filename']
folder_path = json_data.get('folder_path', '')
folder_type = json_data.get('folder_type', 'outputs')
parent_path = get_parent_path(folder_type)
target_path = path.join(parent_path, folder_path, filename)
if not path.exists(target_path):
return web.json_response(status=404)
if path.isdir(target_path):
shutil.rmtree(target_path)
else:
os.remove(target_path)
info_file_path = get_info_filename(target_path)
if path.exists(info_file_path):
os.remove(info_file_path)
return web.Response(status=201)
# filename, folder_path, folder_type, new_data: {}
@routes.put("/browser/files")
async def api_update_file(request):
json_data = await request.json()
filename = json_data['filename']
folder_path = json_data.get('folder_path', '')
folder_type = json_data.get('folder_type', 'outputs')
parent_path = get_parent_path(folder_type)
new_data = json_data.get('new_data', None)
if not new_data:
return web.Response(status=400)
new_filename = new_data['filename']
notes = new_data['notes']
old_file_path = path.join(parent_path, folder_path, filename)
new_file_path = path.join(parent_path, folder_path, new_filename)
if not path.exists(old_file_path):
return web.Response(status=404)
if new_filename and filename != new_filename:
shutil.move(
old_file_path,
new_file_path
)
old_info_file_path = get_info_filename(old_file_path)
if path.exists(old_info_file_path):
new_info_file_path = get_info_filename(new_file_path)
shutil.move(
old_info_file_path,
new_info_file_path
)
if notes:
extra = {
"notes": notes
}
info_file_path = get_info_filename(new_file_path)
with open(info_file_path, "w") as outfile:
json.dump(extra, outfile)
return web.Response(status=201)
# filename, folder_path, folder_type
@routes.get("/browser/files/view")
async def api_view_file(request):
folder_type = request.query.get("folder_type", "outputs")
folder_path = request.query.get("folder_path", "")
filename = request.query.get("filename", None)
if not filename:
return web.Response(status=404)
parent_path = get_parent_path(folder_type)
file_path = path.join(parent_path, folder_path, filename)
if not path.exists(file_path):
return web.Response(status=404)
with open(file_path, 'rb') as f:
media_file = f.read()
content_type = 'application/json'
file_extension = path.splitext(filename)[1].lower()
if file_extension in image_extensions:
content_type = f'image/{file_extension[1:]}'
if file_extension in video_extensions:
content_type = f'video/{file_extension[1:]}'
return web.Response(
body=media_file,
content_type=content_type,
headers={"Content-Disposition": f"filename=\"{filename}\""}
)
# filename, folder_path, folder_type = 'outputs' | 'sources'
@routes.post("/browser/collections")
async def api_add_to_collections(request):
json_data = await request.json()
filename = json_data.get('filename')
if not filename:
return web.Response(status=404)
folder_path = json_data.get('folder_path', '')
folder_type = json_data.get("folder_type", "outputs")
parent_path = get_parent_path(folder_type)
if not path.exists(collections_path):
os.mkdir(collections_path)
source_file_path = path.join(parent_path, folder_path, filename)
if not path.exists(source_file_path):
return web.Response(status=404)
new_filepath = path.join(
collections_path,
add_uuid_to_filename(filename)
)
if path.isdir(source_file_path):
shutil.copytree(source_file_path, new_filepath)
else:
shutil.copy(source_file_path, new_filepath)
return web.Response(status=201)
# filename, content
@routes.post("/browser/collections/workflows")
async def api_create_new_workflow(request):
json_data = await request.json()
filename = json_data.get('filename')
content = json_data.get('content')
if not (filename and content):
return web.Response(status=404)
new_filepath = path.join(
collections_path,
add_uuid_to_filename(filename)
)
with open(new_filepath, 'w') as f:
f.write(content)
return web.Response(status=201)
@routes.get("/browser/config")
async def api_get_browser_config(_):
config = get_config()
return web.json_response(config)
# git_repo
@routes.put("/browser/config")
async def api_update_browser_config(request):
json_data = await request.json()
config = get_config()
git_repo = json_data.get('git_repo', config.get('git_repo'))
git_init()
if git_repo == '':
ret = run_cmd(f'git remote remove {git_remote_name}', collections_path)
if not ret.returncode == 0:
return web.json_response(
{ 'message': ret.stderr },
status=500,
)
set_config({ 'git_repo': git_repo })
return web.Response(status=200)
ret = git_set_remote_url(git_repo)
if not ret.returncode == 0:
return web.json_response(
{ 'message': ret.stderr },
status=500,
)
set_config({ 'git_repo': git_repo })
return web.Response(status=200)
@routes.post("/browser/collections/sync")
async def api_sync_my_collections(_):
if not path.exists(config_path):
return web.Response(status=404)
config = get_config()
git_repo = config.get('git_repo')
if not git_repo:
return web.Response(status=404)
git_init()
cmd = 'git status -s'
ret = run_cmd(cmd, collections_path)
if len(ret.stdout) > 0:
cmd = f'git add . && git commit -m "sync by comfyui-browser at {int(time.time())}"'
ret = run_cmd(cmd, collections_path)
if not ret.returncode == 0:
return web.json_response(
{ 'message': "\n".join([ret.stdout, ret.stderr]) },
status=500,
)
cmd = f'git fetch {git_remote_name} -v'
ret = run_cmd(cmd, collections_path)
if not ret.returncode == 0:
return web.json_response(
{ 'message': "\n".join([ret.stdout, ret.stderr]) },
status=500,
)
cmd = 'git branch --show-current'
ret = run_cmd(cmd, collections_path)
branch = ret.stdout.replace('\n', '')
cmd = f'git merge {git_remote_name}/{branch}'
ret = run_cmd(cmd, collections_path, log_code=False)
cmd = f'git push {git_remote_name} {branch}'
ret = run_cmd(cmd, collections_path)
if not ret.returncode == 0:
return web.json_response(
{ 'message': "\n".join([ret.stdout, ret.stderr]) },
status=500,
)
return web.Response(status=200)
@routes.get("/browser/sources")
async def api_get_sources(_):
if not path.exists(sources_path):
return web.json_response({
'sources': []
})
sources = []
source_list = os.scandir(sources_path)
source_list = sorted(source_list, key=lambda f: (-f.stat().st_ctime))
for item in source_list:
if not path.exists(item.path):
continue
if item.is_file():
continue
cmd = f'git remote get-url {git_remote_name}'
ret = run_cmd(cmd, path.join(item.path), log_cmd=False, log_code=False, log_message=False)
if not (ret.returncode == 0 and len(ret.stdout) > 0):
continue
url = ret.stdout.split('\n')[0]
name = path.basename(item.path)
created_at = item.stat().st_ctime
sources.append({
"name": name,
"created_at": created_at,
"url": url
})
return web.json_response({
'sources': sources
})
# repo_url
@routes.post("/browser/sources")
async def api_create_source(request):
json_data = await request.json()
repo_url = json_data['repo_url']
if not repo_url:
return web.Response(status=400)
if not path.exists(sources_path):
os.mkdir(sources_path)
pattern = r'[\:\/]([a-zA-Z0-9-_]+)\/([a-zA-Z0-9-_]+)(\.git)?'
ret = re.search(pattern, repo_url)
author = ret.group(1)
name = ret.group(2)
if not (author and name):
return web.Response(status=400, text='wrong url')
cmd = f'git clone --depth 1 {repo_url} {author}-{name}'
ret = run_cmd(cmd, sources_path)
if ret.returncode != 0:
return web.Response(status=400, text=ret.stdout + ret.stderr)
return web.Response(status=201)
# name
@routes.delete("/browser/sources/{name}")
async def api_delete_source(request):
name = request.match_info.get('name', None)
if not name:
return web.Response(status=401)
target_path = path.join(sources_path, name)
if not path.exists(target_path):
return web.Response(status=404)
shutil.rmtree(target_path, onerror=handle_remove_readonly)
return web.Response(status=200)
# name
@routes.post("/browser/sources/sync/{name}")
async def api_sync_source(request):
name = request.match_info.get('name', None)
if not name:
return web.Response(status=401)
if not path.exists(path.join(sources_path, name)):
return web.Response(status=404)
cmd = f'git pull'
ret = run_cmd(cmd, path.join(sources_path, name))
if ret.returncode == 0:
return web.Response(status=200)
else:
return web.Response(status=400, text=ret.stdout + ret.stderr)
@routes.get("/browser/sources/all")
async def api_get_all_sources(_):
source_url = 'https://github.com/talesofai/comfyui-browser/raw/main/data/sources.json'
file_path = path.join(browser_path, 'data/sources.json')
timeout = ClientTimeout(connect=2, total=4)
sources = {
"sources": []
}
try:
async with ClientSession(timeout=timeout) as session:
async with session.get(source_url) as resp:
if resp.ok:
ret = await resp.text()
sources = json.loads(ret)
except:
with open(file_path, 'r') as f:
sources = json.load(f)
return web.json_response(sources)
routes.static(
'/browser/web',
path.join(browser_path, 'web/build')
)
web.static("/s/outputs", outputs_path()),
web.static("/s/collections", collections_path()),
web.static("/s/sources", sources_path()),
])
server.PromptServer.instance.app.add_subapp("/browser/", browser_app)
WEB_DIRECTORY = "web"
NODE_CLASS_MAPPINGS = {}
init_path()
NODE_CLASS_MAPPINGS = {
"LoadImageByUrl //Browser": load_image_by_url.LoadImageByUrl,
"SelectInputs //Browser": select_inputs.SelectInputs,
"XyzPlot //Browser": xyz_plot.XyzPlot,
"DifyTextGenerator //Browser": dify_text_generator.DifyTextGenerator,
"UploadToRemote //Browser": upload_to_remote.UploadToRemote,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageByUrl //Browser": "Load Image By URL",
"SelectInputs //Browser": "Select Node Inputs",
"XyzPlot //Browser": "XYZ Plot",
"DifyTextGenerator //Browser": "Dify Text Generator",
"UploadToRemote //Browser": "Upload To Remote",
}
+56
View File
@@ -7,12 +7,68 @@
"type": "git",
"description": "Examples of ComfyUI workflows"
},
{
"title": "ComfyUI-Workflows-ZHO",
"author": "ZHO-ZHO-ZHO",
"url": "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Workflows-ZHO",
"type": "git",
"description": "我的 ComfyUI 工作流合集 | My ComfyUI workflows collection"
},
{
"title": "comfyui-workflow",
"author": "hylarucoder",
"url": "https://github.com/hylarucoder/comfyui-workflow",
"type": "git",
"description": "Some workflows that I have authored myself as well as some workflows that I have modified from online sources."
},
{
"title": "sharing-is-caring",
"author": "fictions-ai",
"url": "https://github.com/fictions-ai/sharing-is-caring",
"type": "git",
"description": "This repository is maintained by the fictions.ai team. We believe in the power of collaboration and the magic that happens when we share knowledge. Here, we'll be sharing our workflow, useful scripts, and tools related to A.I. generation."
},
{
"title": "Consume-ComfyUI-Workflows",
"author": "C0nsumption",
"url": "https://github.com/C0nsumption/Consume-ComfyUI-Workflows",
"type": "git",
"description": "A collection of some of my basic ComfyUI workflows. These are meant to act as building block to construct larger workflows of your own."
},
{
"title": "ChaosFlow",
"author": "BoosterCore",
"url": "https://github.com/BoosterCore/ChaosFlow",
"type": "git",
"description": "ChaosFlow is a code repository used to showcase some of my own ComfyUI workflow creations. The purpose is to share my exploration and practice with ComfyUI, and at the same time, I hope to receive feedback and suggestions from interested friends. ChaosFlow welcomes visual design enthusiasts of any level to browse and comment. You can also message me on GitHub, and I am very happy to communicate and learn with you."
},
{
"title": "awesome-comfyui-workflow",
"author": "shadowcz007",
"url": "https://github.com/shadowcz007/awesome-comfyui-workflow",
"type": "git",
"description": "Some workflows includes CUTE YOU, Real-time design, Extended image, AI Dialogue Game..."
},
{
"title": "IF-Animation-Workflows",
"author": "if-ai",
"url": "https://github.com/if-ai/IF-Animation-Workflows",
"type": "git",
"description": "This are a series of ComfyUI workflows that work together to create and repurpose animation"
},
{
"title": "wyrde-comfyui-workflows",
"author": "wyrde",
"url": "https://github.com/wyrde/wyrde-comfyui-workflows",
"type": "git",
"description": "some wyrde workflows for comfyUI"
},
{
"title": "Templates-ComfyUI-",
"author": "atlasunified",
"url": "https://github.com/atlasunified/Templates-ComfyUI-",
"type": "git",
"description": "Templates to view the variety of a prompt based on the samplers available in ComfyUI. Variety of sizes and singlular seed and random seed templates."
}
]
}
+53
View File
@@ -0,0 +1,53 @@
import json
from ..utils import http_client
class DifyTextGenerator:
CATEGORY = "Browser"
RETURN_TYPES = ("STRING", )
FUNCTION = "run"
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"dify_api_endpoint": ["STRING", {}],
"api_key": ["STRING", {}],
},
"optional": {
"query": ["STRING", {"multiline": True, "placeholder": "Input as the Query field."}],
"inputs_json_str": ["STRING", {"multiline": True, "placeholder": "JSON format. It will overwrite the query field above."}],
},
}
def run(self, dify_api_endpoint, api_key, query, inputs_json_str=None):
# for some special network environments like AutoDL
proxies = {"http": "", "https": ""}
header = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = {
"user": "comfyui-browser",
"response_mode": "blocking",
"inputs" : { "query": query },
}
if inputs_json_str and len(inputs_json_str.strip()) > 0:
# something weird, I have to add '{' and '}' manually
data["inputs"] = json.loads("{" + inputs_json_str + "}")
r = http_client().post(
dify_api_endpoint,
headers=header,
data=json.dumps(data),
proxies=proxies
)
if not r.ok:
raise Exception(f"Request Dify Error: {r.text}")
content = json.loads(r.content)
return (content["answer"], )
+70
View File
@@ -0,0 +1,70 @@
import hashlib
import os
import io
from PIL import Image, ImageSequence, ImageOps
import numpy as np
import torch
from ..utils import http_client
import folder_paths
class LoadImageByUrl:
CATEGORY = "Browser"
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("IMAGE", )
FUNCTION = "run"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"url": ["STRING", {}],
},
"optional": {
"cache": ["BOOLEAN", {"default": True}],
},
}
def __init__(self):
self.url = ""
def filename(self):
return hashlib.md5(self.url.encode()).hexdigest()[:48] + '.jpg'
def download_by_url(self):
input_dir = folder_paths.get_input_directory()
res = http_client().get(self.url)
if res.status_code == 200:
download_path = os.path.join(input_dir, self.filename())
with open(download_path, 'wb') as file:
file.write(res.content)
return res.content
else:
raise ValueError(f"Failed to load image from {self.url}: {res.status_code} {res.text}")
def run(self, url, cache=True):
self.url = url
input_dir = folder_paths.get_input_directory()
image_path = os.path.join(input_dir, self.filename())
if cache == False or not os.path.isfile(image_path):
img = Image.open(io.BytesIO(self.download_by_url()))
else:
img = Image.open(image_path)
output_images = []
for i in ImageSequence.Iterator(img):
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
output_images.append(image)
if len(output_images) > 1:
output_image = torch.cat(output_images, dim=0)
else:
output_image = output_images[0]
return (output_image, )
+46
View File
@@ -0,0 +1,46 @@
class SelectInputs:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_1": [["none"], {}],
"input_2": [["none"], {}],
"input_3": [["none"], {}],
"input_4": [["none"], {}],
"preview": ["STRING", {"multiline": True}],
}
}
@classmethod
def VALIDATE_INPUTS(s, input_1, input_2, input_3, input_4, preview):
return True
# {
# "node_id": 4,
# "node_title": "CheckpointLoaderSimple",
# "widget_name": "ckpt_name",
# }
RETURN_TYPES = ("INPUT", "INPUT", "INPUT", "INPUT",)
RETURN_NAMES = ("input_1", "input_2", "input_3", "input_4",)
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "Browser"
def run(self, input_1, input_2, input_3, input_4, preview):
ret = ()
for input in [input_1, input_2, input_3, input_4]:
node_id, node_title, widget_name = input.split("::")
ret = ret + ({
"node_id": node_id[1:],
"node_title": node_title,
"widget_name": widget_name,
},)
return ret
+101
View File
@@ -0,0 +1,101 @@
import threading
import asyncio
import json
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import numpy as np
import io
import base64
from ..utils import http_client, log
class UploadToRemote:
CATEGORY = "Browser"
RETURN_TYPES = ()
RETURN_NAMES = ()
FUNCTION = "run"
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"remote_url": ["STRING", {}],
"extension": (['jpeg', 'webp', 'png', 'jpg', 'gif'], ),
"quality": ("INT", {"default": 85, "min": 1, "max": 100, "step": 1}),
"embed_workflow": (["false", "true"],),
},
"optional": {
"images": ["IMAGE", {}],
"extra": ["STRING", {"forceInput": True}],
"track_id": ["STRING", {"placeholder": "Optional. Post it as the track_id field."}],
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
}
}
def run(self, remote_url, extension='jpeg', quality=85, images=[], extra='', embed_workflow='false', track_id=None, unique_id=None, prompt=None):
def process_images(images, extension='jpeg', quality=85, embed_workflow='false', prompt=None):
results = list()
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
# Delegate metadata/pnginfo
if extension == 'webp':
img_exif = img.getexif()
if embed_workflow == 'true' and prompt is not None:
img_exif[0x010f] = "Prompt:" + json.dumps(prompt)
exif_data = img_exif.tobytes()
else:
metadata = PngInfo()
if embed_workflow == 'true' and prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
exif_data = metadata
output = io.BytesIO()
if extension in ["jpg", "jpeg"]:
img.convert('RGB').save(output, quality=quality, optimize=True, format="JPEG")
elif extension == 'webp':
img.convert('RGB').save(output, quality=quality, exif=exif_data, format="WEBP")
else:
img.save(output, pnginfo=exif_data, optimize=True, format="PNG")
image_bytes = output.getvalue()
image_base64 = base64.b64encode(image_bytes)
image_base64_str = image_base64.decode('utf-8')
results.append(image_base64_str)
return results
async def callback(images, extra, remote_url, extension='jpeg', quality=85, embed_workflow='false', track_id=None, unique_id=None, prompt=None):
data = {
"images": process_images(images, extension, quality, embed_workflow, prompt),
"extra": extra,
}
if track_id:
data['track_id'] = track_id
if unique_id:
data['unique_id'] = unique_id
headers = {
"Content-Type": "application/json",
}
data = json.dumps(data).encode('utf-8')
log(f"uploading {track_id} to {remote_url}")
res = http_client().post(remote_url, data=data, headers=headers)
log(f"uploaded {track_id}: {res.status_code} {res.text}")
# TODO: check the response
threading.Thread(
target=asyncio.run,
args=(callback(images, extra, remote_url, extension, quality, embed_workflow, track_id, unique_id, prompt),),
).start()
return ()
+217
View File
@@ -0,0 +1,217 @@
import shutil
import time
import json
from PIL import Image
import numpy as np
import os
import copy
import folder_paths
from ..utils import SERVER_BASE_URL, http_client
class XyzPlot:
CATEGORY = "Browser"
RETURN_TYPES = ()
RETURN_NAMES = ()
FUNCTION = "run"
OUTPUT_NODE = True
# xyz_data = {
# "source_unique_id": "",
# "output_folder_name": "",
# "x_index": 0,
# "y_index": 0,
# }
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ["IMAGE", {}],
"input_x": ["INPUT", {}],
"input_y": ["INPUT", {}],
"value_x": ["STRING", {"multiline": True, "placeholder": 'X values split by semicolon such as "1girl; 1boy"'}],
"value_y": ["STRING", {"multiline": True, "placeholder": 'Y values split by semicolon such as "1girl; 1boy"'}],
"value_z": ["STRING", {"multiline": True, "placeholder": 'Z values split by semicolon such as "1girl; 1boy"'}],
"output_folder_name": ["STRING", {"default": "xyz_plot"}],
},
"optional": {
"input_z": ["INPUT", {}],
},
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
# "xyz_data": "XYZ",
},
}
@classmethod
def IS_CHANGED(s, *args):
return True
def __init__(self) -> None:
self.output_folder_name = os.path.join(folder_paths.get_output_directory(), "xyz_plot")
self.x_index = 0
self.y_index = 0
@staticmethod
def get_filename(ix, iy, iz, i):
if (iz >= 0):
return f"x{ix}_y{iy}_z{iz}_{i}.jpeg"
else:
return f"x{ix}_y{iy}_{i}.jpeg"
@staticmethod
def get_preview_url(folder_name, filename):
return f"/browser/files/view?folder_type=outputs&filename={filename}&folder_path={folder_name}"
def save_images(self, images):
os.makedirs(self.output_folder_name, exist_ok=True)
for index, image in enumerate(images):
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
img = img.convert('RGB')
filename = self.get_filename(self.x_index, self.y_index, self.z_index, index)
target_path = os.path.join(self.output_folder_name, filename)
img.save(target_path, 'JPEG', quality=90)
def run(self, images, input_x, input_y, value_x, value_y, output_folder_name, prompt, unique_id, input_z=None, value_z="", extra_pnginfo=None):
self.output_folder_name = os.path.join(
folder_paths.get_output_directory(),
output_folder_name,
)
if 'xyz_data' in prompt[unique_id]['inputs']:
self.x_index = prompt[unique_id]['inputs']['xyz_data']['x_index']
self.y_index = prompt[unique_id]['inputs']['xyz_data']['y_index']
self.z_index = prompt[unique_id]['inputs']['xyz_data']['z_index']
self.save_images(images)
return ()
if os.path.exists(self.output_folder_name):
shutil.move(self.output_folder_name, self.output_folder_name + f'_old_{int(time.time())}')
def filter_values(value):
return list(filter(lambda x: x != '', value.split(";")))
def queue_new_prompt(prompt):
data = json.dumps({
'prompt': prompt
}).encode('utf-8')
# for some special network environments like AutoDL
proxies = {"http": "", "https": ""}
return http_client().post(SERVER_BASE_URL + '/prompt', data=data, proxies=proxies)
batch_size = len(images)
values_x = filter_values(value_x)
values_y = filter_values(value_y)
values_z = filter_values(value_z)
new_prompt = copy.deepcopy(prompt)
ret = []
for ix, vx in enumerate(values_x):
vx = vx.strip()
row = []
for iy, vy in enumerate(values_y):
vy = vy.strip()
new_prompt[input_x["node_id"]]["inputs"][input_x["widget_name"]] = vx
new_prompt[input_y["node_id"]]["inputs"][input_y["widget_name"]] = vy
new_prompt[unique_id]['inputs']['xyz_data'] = {
"source_unique_id": unique_id,
"output_folder_name": output_folder_name,
"x_index": ix,
"y_index": iy,
"z_index": -1,
}
ceil = []
if (input_z and len(values_z) > 0):
for iz, vz in enumerate(values_z):
vz = vz.strip()
new_prompt[input_z["node_id"]]["inputs"][input_z["widget_name"]] = vz
new_prompt[unique_id]['inputs']['xyz_data']['z_index'] = iz
queue_new_prompt(new_prompt)
zCeil = []
for i in range(batch_size):
filename = self.get_filename(ix, iy, iz, i)
preview_url = self.get_preview_url(output_folder_name, filename)
zCeil.append({
"uuid": ":".join([output_folder_name, str(ix), str(iy), str(iz), str(i)]),
"type": "img",
"src": preview_url,
})
ceil.append({
"type": "axis",
"value": vz,
"children": zCeil,
})
else:
queue_new_prompt(new_prompt)
for i in range(batch_size):
filename = self.get_filename(ix, iy, -1, i)
preview_url = self.get_preview_url(output_folder_name, filename)
ceil.append({
"uuid": ":".join([output_folder_name, str(ix), str(iy), "-1", str(i)]),
"type": "img",
"src": preview_url,
})
row.append({
"type": "axis",
"value": vy,
"children": ceil,
})
ret.append({
"type": "axis",
"value": vx,
"children": row,
})
# Check if the directory exists
try:
os.makedirs(self.output_folder_name, exist_ok=True)
except Exception as e:
raise Exception(f"Failed to create directory: {e}")
browser_base_url = f"/browser/s/outputs/{output_folder_name}"
retData = {
"result": ret,
}
if extra_pnginfo:
workflow_filename = "workflow.json"
with open(f"{self.output_folder_name}/{workflow_filename}", "w", encoding="utf-8") as f:
json.dump(extra_pnginfo['workflow'], f)
retData["workflow"] = {
"url": f'{browser_base_url}/{workflow_filename}',
}
axisConst = ["X", "Y", "Z"]
retData["annotations"] = []
for index, ele in enumerate([input_x, input_y, input_z]):
if not ele:
continue
retData["annotations"].append({
"axis": axisConst[index],
"key": f"#{ele['node_id']} {ele['node_title']}",
"type": ele['widget_name'],
})
target_path = f"{self.output_folder_name}/result.json"
with open(target_path, "w", encoding="utf-8") as f:
json.dump(retData, f)
return {
"ui": {
"result_path": [f"/browser/web/xyz_plot.html?path={browser_base_url}/result.json"],
},
"result": (),
}
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-browser"
description = "This is an image/video/workflow browser and manager for ComfyUI. You could add image/video/workflow to collections and load it to ComfyUI. You will be able to use your collections everywhere."
version = "1.0.0"
license = "LICENSE"
dependencies = ["tqdm", "pandas", "numpy", "requests", "urllib3"]
[project.urls]
Repository = "https://github.com/talesofai/comfyui-browser"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = ""
DisplayName = "comfyui-browser"
Icon = ""
+5
View File
@@ -0,0 +1,5 @@
tqdm
pandas
numpy
requests
urllib3
+103
View File
@@ -0,0 +1,103 @@
from os import path, makedirs
from aiohttp import web
import shutil
import time
from ..utils import collections_path, get_parent_path, add_uuid_to_filename, \
config_path, get_config, git_init, run_cmd, git_remote_name
# filename, folder_path, folder_type = 'outputs' | 'sources'
async def api_add_to_collections(request):
json_data = await request.json()
filename = json_data.get('filename')
if not filename:
return web.Response(status=404)
folder_path = json_data.get('folder_path', '')
folder_type = json_data.get("folder_type", "outputs")
parent_path = get_parent_path(folder_type)
makedirs(collections_path(), exist_ok=True)
source_file_path = path.join(parent_path, folder_path, filename)
if not path.exists(source_file_path):
return web.Response(status=404)
new_filepath = path.join(
collections_path(),
add_uuid_to_filename(filename)
)
if path.isdir(source_file_path):
shutil.copytree(source_file_path, new_filepath)
else:
shutil.copy(source_file_path, new_filepath)
return web.Response(status=201)
# filename, content
async def api_create_new_workflow(request):
json_data = await request.json()
filename = json_data.get('filename')
content = json_data.get('content')
if not (filename and content):
return web.Response(status=404)
new_filepath = path.join(
collections_path(),
add_uuid_to_filename(filename)
)
with open(new_filepath, 'w', encoding='utf-8') as f:
f.write(content)
return web.Response(status=201)
async def api_sync_my_collections(_):
if not path.exists(config_path):
return web.Response(status=404)
config = get_config()
git_repo = config.get('git_repo')
if not git_repo:
return web.Response(status=404)
git_init()
cmd = 'git status -s'
ret = run_cmd(cmd, collections_path())
if len(ret.stdout) > 0:
cmd = f'git add . && git commit -m "sync by comfyui-browser at {int(time.time())}"'
ret = run_cmd(cmd, collections_path())
if not ret.returncode == 0:
return web.json_response(
{ 'message': "\n".join([ret.stdout, ret.stderr]) },
status=500,
)
cmd = f'git fetch {git_remote_name} -v'
ret = run_cmd(cmd, collections_path())
if not ret.returncode == 0:
return web.json_response(
{ 'message': "\n".join([ret.stdout, ret.stderr]) },
status=500,
)
cmd = 'git branch --show-current'
ret = run_cmd(cmd, collections_path())
branch = ret.stdout.replace('\n', '')
cmd = f'git merge {git_remote_name}/{branch}'
ret = run_cmd(cmd, collections_path(), log_code=False)
cmd = f'git push {git_remote_name} {branch}'
ret = run_cmd(cmd, collections_path())
if not ret.returncode == 0:
return web.json_response(
{ 'message': "\n".join([ret.stdout, ret.stderr]) },
status=500,
)
return web.Response(status=200)
+51
View File
@@ -0,0 +1,51 @@
from aiohttp import web
import json
from ..utils import get_config, git_remote_name, run_cmd, git_init, \
collections_path, config_path
async def api_get_browser_config(_):
config = get_config()
return web.json_response(config)
# git_repo
async def api_update_browser_config(request):
json_data = await request.json()
config = get_config()
git_repo = json_data.get('git_repo', config.get('git_repo'))
git_init()
if git_repo == '':
ret = run_cmd(f'git remote remove {git_remote_name}', collections_path())
if not ret.returncode == 0:
return web.json_response(
{ 'message': ret.stderr },
status=500,
)
set_config({ 'git_repo': git_repo })
return web.Response(status=200)
ret = git_set_remote_url(git_repo)
if not ret.returncode == 0:
return web.json_response(
{ 'message': ret.stderr },
status=500,
)
set_config({ 'git_repo': git_repo })
return web.Response(status=200)
def set_config(config):
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config, f)
def git_set_remote_url(remote_url, run_path = collections_path()):
ret = run_cmd('git remote', run_path)
if git_remote_name in ret.stdout.split('\n'):
return run_cmd(f'git remote set-url {git_remote_name} {remote_url}', run_path)
else:
return run_cmd(f'git remote add {git_remote_name} {remote_url}', run_path)
+172
View File
@@ -0,0 +1,172 @@
from os import path
import os
import shutil
import time
import asyncio
import json
import threading
from aiohttp import web
from tqdm import tqdm
import folder_paths
from ..utils import download_logs_path, log, http_client
def parse_options_header(content_disposition):
param, options = '', {}
split_header = content_disposition.split(';')
# Extract the first parameter
if len(split_header) > 0:
param = split_header[0].strip()
# Extract the options
for option in split_header[1:]:
option_split = option.split('=')
if len(option_split) == 2:
key = option_split[0].strip()
value = option_split[1].strip().strip('"')
options[key] = value
return param, options
# credit: https://gist.github.com/phineas-pta/d73f9a035b05f8e923af8c01df057175
async def download_by_requests(uuid:str, download_url:str, save_in:str, filename:str="", overwrite:bool=False, chunk_size:int=1):
log_file_path = path.join(download_logs_path(), uuid + '.json')
base_info = {
'uuid': uuid,
'download_url': download_url,
'save_in': save_in,
'filename': filename,
'overwrite': overwrite,
'method': 'requests',
'result': 'connecting',
'total_size': 0,
'downloaded_size': 0,
'created_at': int(time.time()),
'updated_at': int(time.time()),
}
with open(log_file_path, 'w', encoding='utf-8') as log_file:
json.dump(base_info, log_file)
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"}
with http_client().get(download_url, headers=HEADERS, stream=True) as resp:
MISSING_FILENAME = f"unkwown_{uuid}"
# get file name
if filename == "":
if content_disposition := resp.headers.get("Content-Disposition"):
param, options = parse_options_header(content_disposition)
if param == "attachment":
filename = options.get("filename", MISSING_FILENAME)
else:
fileext = path.splitext(filename)[-1]
if fileext != "":
filename = path.basename(download_url)
if filename == "":
filename = MISSING_FILENAME
base_info['filename'] = filename
with open(log_file_path, 'w', encoding='utf-8') as log_file:
json.dump(base_info, log_file)
target_path = path.join(folder_paths.models_dir, save_in, filename)
base_info['save_in'] = target_path
if not overwrite and path.exists(target_path):
result = f'Already exists: {target_path}'
base_info['result'] = result
with open(log_file_path, 'w', encoding='utf-8') as log_file:
json.dump(base_info, log_file)
log(result)
return
# download file
tmp_target_path = target_path + '.downloading'
TOTAL_SIZE = int(resp.headers.get("Content-Length", 0))
CHUNK_SIZE = chunk_size * 10**6
base_info['total_size'] = TOTAL_SIZE
base_info['result'] = resp.reason
log('Download to ' + target_path)
with (
open(tmp_target_path, mode="wb") as file,
open(log_file_path, 'w', encoding='utf-8') as log_file,
tqdm(total=TOTAL_SIZE, desc=f"download {filename}", unit="B", unit_scale=True) as bar,
):
for data in resp.iter_content(chunk_size=CHUNK_SIZE):
size = file.write(data)
bar.update(size)
base_info['downloaded_size'] += size
base_info['updated_at'] = int(time.time())
log_file.seek(0)
json.dump(base_info, log_file)
log_file.write('\n' * 2)
shutil.move(tmp_target_path, target_path)
# download_url, filename, save_in, overwrite
async def api_create_new_download(request):
json_data = await request.json()
download_url = json_data.get('download_url', None)
save_in = json_data.get('save_in', None)
filename = json_data.get('filename', '')
overwrite = json_data.get('overwrite', False)
if not (download_url and save_in):
return web.Response(status=400)
if '..' in save_in:
return web.Response(status=400)
threading.Thread(
target=asyncio.run,
args=(download_by_requests(str(int(time.time())), download_url, save_in, filename, overwrite),)
).start()
return web.json_response(status=201)
async def api_list_downloads(_):
download_logs = []
folder_listing = os.scandir(download_logs_path())
folder_listing = sorted(folder_listing, key=lambda f: (f.is_file(), -f.stat().st_ctime))
for item in folder_listing:
if not path.exists(item.path):
continue
name = path.basename(item.path)
ext = path.splitext(name)[1].lower()
if name == '' or name[0] == '.':
continue
if item.is_file() and not ext in ['.json']:
continue
info = {}
with open(item.path, 'r') as f:
try:
info = json.load(f)
except:
pass
if 'uuid' in info:
download_logs.append(info)
return web.json_response({
'download_logs': download_logs,
})
# uuid
async def api_show_download(request):
uuid = request.match_info.get('uuid', '')
if uuid == '':
return web.Response(status=400)
target_path = path.join(download_logs_path(), uuid + '.json')
if not path.exists(target_path):
return web.Response(status=404)
download_log = {}
with open(target_path, 'r') as file:
download_log = json.load(file)
return web.json_response(download_log)
+119
View File
@@ -0,0 +1,119 @@
from aiohttp import web
import json
from os import path
import os
import shutil
from ..utils import get_target_folder_files, get_parent_path, get_info_filename, \
image_extensions, video_extensions
# folder_path, folder_type
async def api_get_files(request):
folder_path = request.query.get('folder_path', '')
folder_type = request.query.get('folder_type', 'outputs')
files = get_target_folder_files(folder_path, folder_type=folder_type)
if files == None:
return web.Response(status=404)
return web.json_response({
'files': files
})
# filename, folder_path, folder_type
async def api_delete_file(request):
json_data = await request.json()
filename = json_data['filename']
folder_path = json_data.get('folder_path', '')
folder_type = json_data.get('folder_type', 'outputs')
parent_path = get_parent_path(folder_type)
target_path = path.join(parent_path, folder_path, filename)
if not path.exists(target_path):
return web.json_response(status=404)
if path.isdir(target_path):
shutil.rmtree(target_path)
else:
os.remove(target_path)
info_file_path = get_info_filename(target_path)
if path.exists(info_file_path):
os.remove(info_file_path)
return web.Response(status=201)
# filename, folder_path, folder_type, new_data: {}
async def api_update_file(request):
json_data = await request.json()
filename = json_data['filename']
folder_path = json_data.get('folder_path', '')
folder_type = json_data.get('folder_type', 'outputs')
parent_path = get_parent_path(folder_type)
new_data = json_data.get('new_data', None)
if not new_data:
return web.Response(status=400)
new_filename = new_data['filename']
notes = new_data['notes']
old_file_path = path.join(parent_path, folder_path, filename)
new_file_path = path.join(parent_path, folder_path, new_filename)
if not path.exists(old_file_path):
return web.Response(status=404)
if new_filename and filename != new_filename:
shutil.move(
old_file_path,
new_file_path
)
old_info_file_path = get_info_filename(old_file_path)
if path.exists(old_info_file_path):
new_info_file_path = get_info_filename(new_file_path)
shutil.move(
old_info_file_path,
new_info_file_path
)
if notes:
extra = {
"notes": notes
}
info_file_path = get_info_filename(new_file_path)
with open(info_file_path, "w", encoding="utf-8") as outfile:
json.dump(extra, outfile)
return web.Response(status=201)
# filename, folder_path, folder_type
async def api_view_file(request):
folder_type = request.query.get("folder_type", "outputs")
folder_path = request.query.get("folder_path", "")
filename = request.query.get("filename", None)
if not filename:
return web.Response(status=404)
parent_path = get_parent_path(folder_type)
file_path = path.join(parent_path, folder_path, filename)
if not path.exists(file_path):
return web.Response(status=404)
with open(file_path, 'rb') as f:
media_file = f.read()
content_type = 'application/json'
file_extension = path.splitext(filename)[1].lower()
if file_extension in image_extensions:
content_type = f'image/{file_extension[1:]}'
if file_extension in video_extensions:
content_type = f'video/{file_extension[1:]}'
return web.Response(
body=media_file,
content_type=content_type,
headers={"Content-Disposition": f"filename=\"{filename}\""}
)
+127
View File
@@ -0,0 +1,127 @@
from os import path, scandir, makedirs
import json
from aiohttp import web, ClientSession, ClientTimeout
import re
import shutil
import os, stat, errno
from ..utils import sources_path, run_cmd, browser_path, git_remote_name
def handle_remove_readonly(func, path, exc):
excvalue = exc[1]
if excvalue.errno == errno.EACCES:
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
func(path)
else:
raise
async def api_get_sources(_):
if not path.exists(sources_path()):
return web.json_response({
'sources': []
})
sources = []
source_list = scandir(sources_path())
source_list = sorted(source_list, key=lambda f: (-f.stat().st_ctime))
for item in source_list:
if not path.exists(item.path):
continue
if item.is_file():
continue
cmd = f'git remote get-url {git_remote_name}'
ret = run_cmd(cmd, path.join(item.path), log_cmd=False, log_code=False, log_message=False)
if not (ret.returncode == 0 and len(ret.stdout) > 0):
continue
url = ret.stdout.split('\n')[0]
name = path.basename(item.path)
created_at = item.stat().st_ctime
sources.append({
"name": name,
"created_at": created_at,
"url": url
})
return web.json_response({
'sources': sources
})
# repo_url
async def api_create_source(request):
json_data = await request.json()
repo_url = json_data['repo_url']
if not repo_url:
return web.Response(status=400)
makedirs(sources_path(), exist_ok=True)
pattern = r'[\:\/]([a-zA-Z0-9-_]+)\/([a-zA-Z0-9-_]+)(\.git)?'
ret = re.search(pattern, repo_url)
author = ret.group(1)
name = ret.group(2)
if not (author and name):
return web.Response(status=400, text='wrong url')
cmd = f'git clone --depth 1 {repo_url} {author}-{name}'
ret = run_cmd(cmd, sources_path())
if ret.returncode != 0:
return web.Response(status=400, text=ret.stdout + ret.stderr)
return web.Response(status=201)
# name
async def api_delete_source(request):
name = request.match_info.get('name', None)
if not name:
return web.Response(status=401)
target_path = path.join(sources_path(), name)
if not path.exists(target_path):
return web.Response(status=404)
shutil.rmtree(target_path, onerror=handle_remove_readonly)
return web.Response(status=200)
# name
async def api_sync_source(request):
name = request.match_info.get('name', None)
if not name:
return web.Response(status=401)
if not path.exists(path.join(sources_path(), name)):
return web.Response(status=404)
cmd = f'git pull'
ret = run_cmd(cmd, path.join(sources_path(), name))
if ret.returncode == 0:
return web.Response(status=200)
else:
return web.Response(status=400, text=ret.stdout + ret.stderr)
async def api_get_all_sources(_):
source_url = 'https://github.com/talesofai/comfyui-browser/raw/main/data/sources.json'
file_path = path.join(browser_path, 'data/sources.json')
timeout = ClientTimeout(connect=2, total=4)
sources = {
"sources": []
}
try:
async with ClientSession(timeout=timeout) as session:
async with session.get(source_url) as resp:
if resp.ok:
ret = await resp.text()
sources = json.loads(ret)
except:
with open(file_path, 'r', encoding="utf-8") as f:
sources = json.load(f)
return web.json_response(sources)
+124
View File
@@ -0,0 +1,124 @@
import os
from aiohttp import web
import datetime
import csv
import json
import folder_paths
LOG_FILE_NAME = 'score_log.csv'
async def api_update_score(request):
body = await request.json()
user_uuid = body.get("user", "anonymous")
source = body.get("source")
score = body.get("score")
source_arr = source.split(":")
if len(source_arr) != 5:
return web.Response(status=404)
log_path = os.path.join(
folder_paths.get_output_directory(),
source_arr[0],
LOG_FILE_NAME,
)
dtstr = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S%Z")
# created_at, user_uuid, score, ix, iy, iz, index
new_line = ",".join([dtstr, user_uuid, str(score), source_arr[1], source_arr[2], source_arr[3], source_arr[4]]) + '\n'
if not os.path.exists(log_path):
with open(log_path, "w", encoding="utf-8") as f:
f.write(new_line)
else:
lines = []
with open(log_path, "r", encoding="utf-8") as f:
lines = f.readlines()
pattern = ",".join([source_arr[1], source_arr[2], source_arr[3], source_arr[4]])
lines = [line for line in lines if pattern not in line]
lines.append(new_line)
with open(log_path, "w", encoding="utf-8") as f:
f.writelines(lines)
return web.Response(status=200)
async def api_get_score_statistic(request):
target_path = request.query.get('path')
if not target_path:
return web.Response(status=404)
#/browser/s/outputs/xyz_plot_4/result.json
if len(target_path.split('/')) < 5:
return web.Response(status=400)
target_path = target_path.split('/')[4]
log_path = os.path.join(
folder_paths.get_output_directory(),
target_path,
LOG_FILE_NAME,
)
if not os.path.exists(log_path):
return web.Response(status=404)
data = []
with open(log_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
data.append(row)
statistic = {}
for row in data:
if not row[3] in statistic:
statistic[row[3]] = { 'total': 0 }
if not row[4] in statistic[row[3]]:
statistic[row[3]][row[4]] = { 'total': 0 }
if not row[5] in statistic[row[3]][row[4]]:
statistic[row[3]][row[4]][row[5]] = 0
statistic[row[3]][row[4]][row[5]] += int(row[2])
statistic[row[3]][row[4]]['total'] += int(row[2])
statistic[row[3]]['total'] += int(row[2])
result_path = os.path.join(
folder_paths.get_output_directory(),
target_path,
'result.json',
)
xyz_result = []
with open(result_path, 'r', encoding='utf-8') as f:
xyz_result = json.load(f)['result']
stat_ret = []
for ix, _ in enumerate(xyz_result):
vy = xyz_result[ix]['children']
rx = {
'type': 'axis',
'score': statistic.get(str(ix), {}).get('total', 0),
'children': [],
}
for iy, _ in enumerate(vy):
vz = vy[iy]['children']
ry = {
'type': 'axis',
'score': statistic.get(str(ix), {}).get(str(iy), {}).get('total', 0),
'children': [],
}
if vz[0]['type'] == 'img':
ry['children'].append({
'type': 'axis',
'score': statistic.get(str(ix), {}).get(str(iy), {}).get('-1', 0),
})
else:
for iz, _ in enumerate(vz):
ry['children'].append({
'type': 'axis',
'score': statistic.get(str(ix), {}).get(str(iy), {}).get(str(iz), 0),
})
rx['children'].append(ry)
stat_ret.append(rx)
return web.json_response({
'result': stat_ret,
})
+5
View File
@@ -0,0 +1,5 @@
{
"tabWidth": 2,
"singleQuote": true,
"semi": true
}
+6
View File
@@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": [
"src/i18n"
],
"i18n-ally.keystyle": "nested"
}
+3305 -2436
View File
File diff suppressed because it is too large Load Diff
+40 -29
View File
@@ -1,30 +1,41 @@
{
"name": "comfyui-browser",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.27.4",
"autoprefixer": "^10.4.16",
"daisyui": "^4.4.19",
"postcss": "^8.4.32",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.3.5",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": ">=4.5.1"
},
"type": "module",
"dependencies": {
"dayjs": "^1.11.10"
}
}
"name": "comfyui-browser",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"extract": "svelte-i18n extract './src/**/*.svelte' ./src/i18n/zh-CN.json && svelte-i18n extract './src/**/*.svelte' ./src/i18n/en-US.json"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@sveltejs/adapter-auto": "^3.0.1",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@types/node": "^20.11.28",
"@types/pubsub-js": "^1.8.6",
"@types/uuid": "^9.0.8",
"autoprefixer": "^10.4.16",
"daisyui": "^4.4.22",
"dexie": "4.0.1-beta.14",
"postcss": "^8.4.32",
"pubsub-js": "^1.9.4",
"sass": "^1.72.0",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"tailwindcss": "^3.4.0",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"uuid": "^9.0.1",
"vite": "^5.0.10"
},
"type": "module",
"dependencies": {
"dayjs": "^1.11.10",
"svelte-i18n": "^4.0.0"
}
}
+1800
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+3
View File
@@ -0,0 +1,3 @@
@tailwind base
@tailwind components
@tailwind utilities
+69
View File
@@ -0,0 +1,69 @@
{
"navbar": {
"outputs": "Outputs",
"saves": "Saves",
"sources": "Sources",
"models": "Models"
},
"common": {
"emptyList": "No data",
"loading": "Loading...",
"rootDir": "Root",
"btn": {
"load": "Load",
"save": "Save",
"delete": "Delete",
"add": "Add",
"refresh": "Refresh"
}
},
"collectionsTab": {
"searchInput": {
"placeholder": "Filter by filename or memos"
},
"btn": {
"save": "Save",
"sync": "Sync",
"syncing": "Syncing"
},
"syncInput": {
"placeholder": "Git URL likes 'git@github.com:tzwm/comfyui-workflows.git'"
},
"collection": {
"memoPlaceholder": "write some memos..."
},
"toast": {
"synced": "Synced",
"syncFailed": "Failed to sync. Please check the ComfyUI server.",
"configUpdated": "Updated config",
"configUpdatedFailed": "Failed to update config. Please check the ComfyUI server.",
"deleteConfirm": "You want to delete this file: ",
"deleteSuccess": "Deleted the file: ",
"deleteFailed": "Failed to delete the file. Please check the ComfyUI server.",
"updated": "Updated",
"updatedFailed": "Failed to update. Please check the ComfyUI server.",
"Invalid filename": "Invalid filename"
}
},
"filesList": {
"searchInput": {
"placeholder": "Filter by filename"
},
"Added to Saves": "Added to Saves",
"Failed to add to Saves": "Failed to add to Saves. Please check the ComfyUI server.",
"You want to delete this file?": "You want to delete this file?",
"Deleted the file": "Deleted the file ",
"Failed to delete the file": "Failed to delete the file. Please check the ComfyUI server."
},
"downloadHistory": {
"Auto Refresh": "Auto Refresh"
},
"sourcesTab": {
"You want to delete this source": "You want to delete this source? ",
"Deleted this source": "Deleted this source.",
"Failed to delete this source": "Failed to delete this source.",
"Add new source": "Add new source",
"Subscribe": "Subscribe",
"Subscribing...": "Subscribing..."
}
}
+30
View File
@@ -0,0 +1,30 @@
import { browser } from '$app/environment';
import { locale, register, init, getLocaleFromNavigator } from 'svelte-i18n';
register('en-US', () => import('./en-US.json'));
register('zh-CN', () => import('./zh-CN.json'));
const langKey = 'lang';
const defaultLocale = 'en-US';
let initialLocale: string | null = browser ? window.navigator.language : defaultLocale;
if (getLocaleFromNavigator()) {
initialLocale = getLocaleFromNavigator();
}
if (browser && localStorage.getItem(langKey)) {
initialLocale = localStorage.getItem(langKey);
}
init({
fallbackLocale: defaultLocale,
initialLocale: initialLocale,
handleMissingMessage: (input: any) => input.id.split('.').pop(),
});
locale.subscribe((value: any) => {
//console.log('changed lang to ', value);
if (browser) {
localStorage.setItem(langKey, value);
}
});
+69
View File
@@ -0,0 +1,69 @@
{
"navbar": {
"outputs": "输出",
"saves": "收藏",
"sources": "订阅",
"models": "模型"
},
"common": {
"emptyList": "暂无数据",
"loading": "加载中...",
"rootDir": "根目录",
"btn": {
"load": "加载",
"save": "收藏",
"delete": "删除",
"add": "新增",
"refresh": "刷新"
}
},
"collectionsTab": {
"searchInput": {
"placeholder": "根据文件名和备忘搜索"
},
"btn": {
"save": "保存",
"sync": "同步",
"syncing": "同步中"
},
"syncInput": {
"placeholder": "Git 地址,比如 git@github.com:tzwm/comfyui-workflows.git"
},
"collection": {
"memoPlaceholder": "写个备忘,方便搜索"
},
"toast": {
"synced": "同步完成",
"syncFailed": "同步失败。请在控制台查看详情。",
"configUpdated": "保存成功",
"configUpdatedFailed": "保存失败。请在控制台查看详情。",
"deleteConfirm": "确认删除这个文件:",
"deleteSuccess": "删除成功:",
"deleteFailed": "删除失败。请在控制台查看详情。",
"updated": "更新成功",
"UpdatedFailed": "更新失败。请在控制台查看详情。",
"Invalid filename": "不是个有效文件名"
}
},
"filesList": {
"searchInput": {
"placeholder": "根据文件名过滤"
},
"Added to Saves": "收藏成功",
"Failed to add to Saves": "收藏失败。请在控制台查看详情。",
"You want to delete this file?": "确认删除这个文件?",
"Deleted the file": "删除成功:",
"Failed to delete the file": "删除失败。请在控制台查看详情。"
},
"downloadHistory": {
"Auto Refresh": "自动刷新"
},
"sourcesTab": {
"You want to delete this source": "确认删除这个订阅吗?",
"Deleted this source": "删除成功",
"Failed to delete this source": "删除失败。请在控制台查看详情。",
"Add new source": "订阅新的源",
"Subscribe": "订阅",
"Subscribing...": "订阅中"
}
}
+53
View File
@@ -0,0 +1,53 @@
import Dexie, { type Table } from 'dexie';
export interface User {
uuid: string;
name: string;
ctime: number;
mtime: number;
}
export interface Score {
uuid: string;
score: number;
link?: string;
ctime: number;
mtime: number;
}
export class ScoreDB extends Dexie {
user!: Table<User>;
scoreboard!: Table<Score>;
constructor() {
super('scores');
this.version(1).stores({
user: 'uuid, name, ctime, mtime',
scoreboard: 'uuid, score, link, ctime, mtime',
});
}
}
export const db = new ScoreDB();
export function getUser() {
return db.user.toArray().then((d) => (d.length > 0 ? d[0] : null));
}
export function getScore(uuid: string) {
return db.scoreboard
.where('uuid')
.equals(uuid)
.toArray()
.then((d) => (d.length > 0 ? d[0] : null));
}
export function updateScore(
uuid: string,
value: Partial<Omit<Score, 'ctime' | 'mtime' | 'uuid'>>
) {
return db.scoreboard.where('uuid').equals(uuid).modify({
link: value.link,
score: value.score,
mtime: Date.now(),
});
}
+15
View File
@@ -0,0 +1,15 @@
<script>
export let width = 34;
export let height = 34;
</script>
<svg
viewBox="0 0 1024 1024"
{height}
{width}
class="inline-block stroke-current fill-current"
><path
stroke-width="2"
d="M509.92 795.84c157.904 0 285.92-128.016 285.92-285.92C795.84 352 667.808 224 509.92 224 352 224 224 352 224 509.92c0 157.904 128 285.92 285.92 285.92z m0 48C325.504 843.84 176 694.32 176 509.92 176 325.504 325.504 176 509.92 176c184.416 0 333.92 149.504 333.92 333.92 0 184.416-149.504 333.92-333.92 333.92z m-2.448-400.704h16a16 16 0 0 1 16 16v201.728a16 16 0 0 1-16 16h-16a16 16 0 0 1-16-16V459.136a16 16 0 0 1 16-16z m0-100.176h16a16 16 0 0 1 16 16v23.648a16 16 0 0 1-16 16h-16a16 16 0 0 1-16-16v-23.648a16 16 0 0 1 16-16z"
></path></svg
>
@@ -0,0 +1,110 @@
<script lang="ts">
import { TableMode, imageWidth, mode } from './store';
export let extraItems:
| undefined
| {
label: string;
onClick?: () => void;
}[] = undefined;
export let sidebarItems: typeof extraItems = undefined;
let _imageWidth = 180;
let _mode = TableMode.View;
mode.subscribe((v) => {
_mode = v;
});
$: {
imageWidth.set(_imageWidth);
}
</script>
<div class="drawer absolute h-full w-full overflow-y-hidden">
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
<!-- Navbar -->
<div class="w-full navbar p-0 bg-base-300 min-h-0">
<div class="flex-none">
{#if sidebarItems}
<label
for="my-drawer-3"
aria-label="open sidebar"
class="btn btn-square btn-ghost"
>
<svg
fill="none"
viewBox="0 0 24 24"
class="inline-block w-6 h-6 stroke-current"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</label>
{/if}
</div>
<div class="flex-1 px-2 mx-2">
<slot name="title" />
</div>
<div class="flex-none">
{#if extraItems}
<ul class="menu menu-horizontal items-center">
<li>
<div>
<input
type="range"
min="50"
max="500"
bind:value={_imageWidth}
class="range"
/>
</div>
</li>
<li>
<button
on:click={() => {
if (_mode === TableMode.Score) {
mode.set(TableMode.View);
} else {
mode.set(TableMode.Score);
}
}}
>
{_mode === TableMode.Score ? 'Score Mode' : 'View Mode'}
</button>
</li>
{#each extraItems as item}
<li>
<button on:click={item.onClick}>
{item.label}
</button>
</li>
{/each}
</ul>
{/if}
</div>
</div>
<slot />
</div>
<div class="drawer-side z-50">
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"
></label>
{#if sidebarItems}
<ul class="menu p-4 w-80 min-h-full bg-base-200">
{#each sidebarItems as item}
<li>
<a on:click={item.onClick}>
{item.label}
</a>
</li>
{/each}
</ul>
{/if}
</div>
</div>
+35
View File
@@ -0,0 +1,35 @@
export interface Payload {
workflow: Workflow;
annotations: Annotation[];
result: Axis[];
}
export interface Workflow {
url: string;
}
export interface Annotation {
axis: string;
key: string;
type: string;
}
export interface Axis {
type: 'axis';
value: string;
children: AxisValue[];
}
export type AxisValue = Axis | ImgResult;
export interface ImgResult {
type: 'img';
uuid: string;
src: string;
}
export interface AxisScore {
type: 'axis';
score: number;
children?: AxisScore[];
}
@@ -0,0 +1,7 @@
<script lang="ts">
export let score: number;
</script>
<span class="text-accent">
({score})
</span>
+19
View File
@@ -0,0 +1,19 @@
import { writable } from 'svelte/store';
import PubSub from 'pubsub-js';
export const imageWidth = writable(50);
export const comfyUrl = writable('');
export enum TableMode {
Score,
View,
}
export const mode = writable(TableMode.View);
export function createRefetchStatisticPublisher() {
return () => PubSub.publish('refetch-statistics');
}
export function createRefetchStatisticSubscriber(fn: () => void) {
return PubSub.subscribe('refetch-statistics', fn);
}
@@ -0,0 +1,39 @@
<script lang="ts">
import Score from './score.svelte';
import type { Axis, AxisScore } from './models';
import TableHolder from './table-holder.svelte';
import { zip } from './utils';
export let axis: Axis;
export let score: AxisScore | undefined;
</script>
<tr class="p-0 even:bg-stone-800 odd:bg-base-100">
<!-- prompts -->
<th class="z-10 p-0 max-w-44 bg-inherit">
{#if score}
<Score score={score.score} />
{/if}
{axis.value}
</th>
{#if score && score.children}
{#each zip(axis.children, score.children) as [dim2, score2]}
{#if dim2.type === 'axis' && score2.children}
<td class="p-0">
{#each zip(dim2.children, score2.children) as [dim3, score3]}
<TableHolder value={dim3} score={score3}></TableHolder>
{/each}
</td>
{/if}
{/each}
{:else}
{#each axis.children as dim2}
{#if dim2.type === 'axis'}
<td class="p-0">
{#each dim2.children as dim3}
<TableHolder value={dim3} score={undefined}></TableHolder>
{/each}
</td>
{/if}
{/each}
{/if}
</tr>
@@ -0,0 +1,21 @@
<script lang="ts">
import type { Axis, AxisScore } from './models';
import Score from './score.svelte';
import TableHolder from './table-holder.svelte';
export let value: Axis;
export let score: AxisScore | undefined;
</script>
<div class="border box-border inline-block">
<div class="text-center font-bold">
{#if score}
<Score score={score.score} />
{/if}
{value.value}
</div>
<div class="flex justify-around">
{#each value.children as item}
<TableHolder value={item} {score} />
{/each}
</div>
</div>
@@ -0,0 +1,120 @@
<script lang="ts">
import type { ImgResult } from './models';
import { TableMode, comfyUrl, imageWidth, mode } from './store';
import { db, getScore, getUser, updateScore, type User } from '$lib/db';
export let value: ImgResult;
export let width = 100;
import { createRefetchStatisticPublisher } from './store';
let refetch = createRefetchStatisticPublisher();
let api = '';
comfyUrl.subscribe((d) => (api = d));
let like = false;
let _mode = TableMode.View;
imageWidth.subscribe((d) => {
width = d;
});
mode.subscribe((v) => {
_mode = v;
if (v === TableMode.Score && value.uuid)
db.scoreboard
.where('uuid')
.equals(value.uuid)
.toArray()
.then((arr) => {
if (arr.length > 0 && arr[0].score > 0) {
like = true;
} else {
like = false;
}
});
});
function updateSelfScore(score: number) {
return updateScore(value.uuid, { score });
}
async function onLike() {
const { score } = (await getScore(value.uuid)) ?? {};
if (score === undefined) {
await db.scoreboard.add({
uuid: value.uuid,
score: 1,
link: value.src,
ctime: Date.now(),
mtime: Date.now(),
});
} else if (score === 1) {
return;
} else {
await updateSelfScore(1);
}
return apiUpdateScore(1)
.then(async (res) => {
if (res && res.ok) {
like = true;
refetch();
} else await updateSelfScore(0);
})
.catch(() => {
updateSelfScore(0);
});
}
async function onDislike() {
const { score } = (await getScore(value.uuid)) ?? {};
if (score === undefined || score === 0) {
return;
}
await updateSelfScore(0);
return apiUpdateScore(0)
.then(async (res) => {
if (res && res.ok) {
like = false;
refetch();
} else await updateSelfScore(1);
})
.catch(() => {
updateSelfScore(1);
});
}
async function apiUpdateScore(score: number) {
const user = await getUser();
if (!user) return;
return fetch(api + '/browser/xyz_plot/score', {
method: 'PUT',
body: JSON.stringify({
user: user.name,
source: value.uuid,
score,
}),
});
}
</script>
<div class="px-0.5 box-border inline-block relative" style={`width:${width}px`}>
<a href={value.src} target="_blank">
<img
class="w-full"
src={value.src}
alt={value.uuid}
style={`width:${width}px`}
/>
</a>
{#if _mode === TableMode.Score}
<button
class="absolute w-full h-full top-0 left-0 flex justify-end items-end p-1"
tabindex="0"
on:click={like ? onDislike : onLike}
>
<svg class="inline-block w-1/4" viewBox="0 0 1024 1024">
<path
d="M725.333333 192c-89.6 0-168.533333 44.8-213.333333 115.2C467.2 236.8 388.266667 192 298.666667 192 157.866667 192 42.666667 307.2 42.666667 448c0 253.866667 469.333333 512 469.333333 512s469.333333-256 469.333333-512c0-140.8-115.2-256-256-256z"
fill={like ? '#F44336' : 'grey'}
/>
</svg>
</button>
{/if}
</div>
@@ -0,0 +1,14 @@
<script lang="ts">
import type { AxisScore, AxisValue } from './models';
import TableCellAxis from './table-cell-axis.svelte';
import TableCellImg from './table-cell-img.svelte';
export let value: AxisValue;
export let score: AxisScore | undefined;
</script>
{#if value.type === 'img'}
<TableCellImg {value} />
{:else if value.type === 'axis'}
<TableCellAxis {value} bind:score />
{/if}
@@ -0,0 +1,71 @@
<script lang="ts">
import { el } from '@faker-js/faker';
import type { AxisScore, Payload } from './models';
import Score from './score.svelte';
import TableAxis from './table-axis.svelte';
import { zip } from './utils';
export let height: number = 0;
export let payload: Payload;
export let scores: AxisScore[] | undefined = undefined;
let headScore: AxisScore[] | undefined = undefined;
$: headScore = scores
? scores[0]
.children!.map(
(d) =>
JSON.parse(
JSON.stringify({ ...d, children: undefined }),
) as AxisScore,
)
.map((axis, idx) => ({
...axis,
score:
scores
?.map((axis) => axis.children![idx].score)
.reduce((a, b) => a + b) ?? 0,
}))
: undefined;
</script>
<div
class="overflow-x-auto max-w-full inline-block"
style={`max-height: ${height}px`}
>
<table class="table table-sm table-pin-rows table-pin-cols">
<thead>
<tr class="p-0">
<th></th>
{#if headScore}
{#each zip(payload.result[0].children, headScore) as [axis, score]}
{#if axis.type === 'axis'}
<td class="p-0">
<p class="max-h-16 text-wrap overflow-auto hover:overflow-y-scroll">
<Score score={score.score} />{axis.value}
</p>
</td>
{/if}
{/each}
{:else}
{#each payload.result[0].children as axis}
{#if axis.type === 'axis'}
<td class="p-0">
<p class="max-h-12 text-wrap overflow-auto hover:overflow-y-scroll">{axis.value}</p>
</td>
{/if}
{/each}
{/if}
</tr>
</thead>
<tbody>
{#if scores}
{#each zip(payload.result, scores) as [axis, score]}
<TableAxis {axis} {score} />
{/each}
{:else}
{#each payload.result as axis}
<TableAxis {axis} score={undefined} />
{/each}
{/if}
</tbody>
</table>
</div>
+3
View File
@@ -0,0 +1,3 @@
export function zip<T = unknown, U = unknown>(arr1: T[], arr2: U[]): [T, U][] {
return Array.from({ length: arr1.length }).map((_, i) => [arr1[i], arr2[i]]);
}
+7
View File
@@ -0,0 +1,7 @@
import { faker } from '@faker-js/faker';
export function fakeUsername() {
const now = Date.now();
const suf = now.toString(32);
return `${faker.person.lastName()}_${suf}`;
}
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import "../app.css";
import '../app.sass';
</script>
<slot />
+6 -1
View File
@@ -1,7 +1,12 @@
import '../i18n/index';
import { waitLocale } from 'svelte-i18n';
export const prerender = true;
/** @type {import('./$types').PageLoad} */
export async function load({ url }) {
await waitLocale();
let comfyUrl = '';
if (typeof process === 'undefined') {
comfyUrl = url.searchParams.get('comfyUrl') || '';
@@ -9,5 +14,5 @@ export async function load({ url }) {
return {
comfyUrl,
}
};
}
+5 -2
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import Navbar from './Navbar.svelte';
import FilesTab from "./FilesTab.svelte";
import FilesTab from './FilesTab.svelte';
import CollectionsTab from './CollectionsTab.svelte';
import SourcesTab from './SourcesTab.svelte';
import ModelsTab from './ModelsTab.svelte';
import { getLocalConfig } from './utils';
import { onMount } from 'svelte';
@@ -20,12 +21,14 @@
});
</script>
<Navbar bind:activeTab={activeTab} />
<Navbar bind:activeTab />
{#if activeTab === 'collections'}
<CollectionsTab {comfyUrl} />
{:else if activeTab === 'sources'}
<SourcesTab {comfyUrl} />
{:else if activeTab === 'models'}
<ModelsTab {comfyUrl} />
{:else}
<FilesTab {comfyUrl} />
{/if}
+108 -59
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { fetchFiles, onLoadWorkflow, onScroll } from './utils';
import MediaShow from "./MediaShow.svelte";
import Toast from "./Toast.svelte";
import MediaShow from './MediaShow.svelte';
import Toast from './Toast.svelte';
export let comfyUrl: string;
@@ -20,30 +21,41 @@
let searchRegex = new RegExp('');
$: if (folderPath != undefined) {
fetchFiles(folderType, comfyUrl, folderPath)
.then(res => {
files = res;
loaded = true;
});
refresh();
}
$: try {
searchRegex = new RegExp(searchQuery.toLowerCase());
} catch {
searchRegex = new RegExp('');
searchRegex = new RegExp('');
}
$: tt = function(key: string) {
return $t('collectionsTab.' + key);
}
async function refresh() {
loaded = true;
files = await fetchFiles(folderType, comfyUrl, folderPath);
loaded = true;
}
onMount(async () => {
//@ts-ignore
comfyApp = window.top.app;
//@ts-ignore
window.top.addEventListener("comfyuiBrowserShow", () => {
refresh();
});
folderPath = '';
config = await fetchConfig() || {};
config = (await fetchConfig()) || {};
configGitRepo = config?.git_repo;
window.addEventListener('scroll', () => { showCursor = onScroll(showCursor, files.length); });
window.addEventListener('scroll', () => {
showCursor = onScroll(showCursor, files.length);
});
});
async function fetchConfig() {
@@ -54,18 +66,18 @@
async function onClickSyncCollections(e: Event) {
const btn = e.target as HTMLButtonElement;
btn.disabled = true;
btn.innerHTML = 'Syncing...';
btn.innerHTML = tt('btn.syncing');
const res = await fetch(comfyUrl + '/browser/collections/sync', {
method: 'POST',
method: 'POST',
});
toast.show(
res.ok,
'Synced',
'Failed to sync. Please check the ComfyUI server.'
tt('toast.synced'),
tt('toast.syncFailed'),
);
btn.disabled = false;
btn.innerHTML = 'Sync';
btn.innerHTML = tt('btn.sync');
folderPath = '';
files = await fetchFiles(folderType, comfyUrl);
@@ -79,15 +91,16 @@
}),
});
fetchConfig();
toast.show(
res.ok,
'Updated config',
'Failed to update config. Please check the ComfyUI server.'
tt('toast.configUpdated'),
tt('toast.configUpdatedFailed'),
);
}
async function onDelete(file: any) {
const ret = confirm('You want to delete this file? ' + file.name);
const ret = confirm(tt('toast.deleteConfirm') + file.name);
if (!ret) {
return;
}
@@ -101,12 +114,12 @@
}),
});
refresh();
toast.show(
res.ok,
'Deleted the file ' + file.name,
'Failed to delete the file. Please check the ComfyUI server.'
tt('toast.deleteSuccess') + file.name,
tt('toast.deleteFailed'),
);
files = files.filter(f => f != file);
}
async function updateFile(file: any, payload: any) {
@@ -120,12 +133,17 @@
}),
});
refresh();
return res.ok;
}
async function updateFilename(e: Event, file: any) {
//@ts-ignore
const value = e.target.value;
const value = (e.target as HTMLInputElement).value;
if (! isFilenameValid(value)) {
toast.show(false, '', tt('toast.Invalid filename'));
return;
}
if (value === file.name) {
return;
}
@@ -138,8 +156,8 @@
toast.show(
ret,
'Memos Updated',
'Failed to update. Please check the ComfyUI server.'
tt('toast.updated'),
tt('toast.updatedFailed'),
);
}
@@ -158,8 +176,8 @@
toast.show(
ret,
'Updated',
'Failed to update. Please check the ComfyUI server.'
tt('toast.updated'),
tt('toast.updatedFailed'),
);
}
@@ -173,43 +191,63 @@
return;
}
folderPath = folderPath.split('/').slice(0, index + 1).join('/');
folderPath = folderPath
.split('/')
.slice(0, index + 1)
.join('/');
}
function isFilenameValid(filename: string) {
// Regular expression to match valid filename patterns
var validFilenameRegex = /^[a-zA-Z0-9-_|\u4E00-\u9FFF]+(\.[a-zA-Z0-9]+)?$/;
return validFilenameRegex.test(filename);
}
</script>
<div class="flex border-b border-base-content pb-2">
<a class="btn pr-2"
<a
class="btn pr-2"
target="_blank"
href="https://github.com/talesofai/comfyui-browser/wiki/How-to-use-'Sync'-in-the-'Saves'">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" data-slot="icon" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
href="https://github.com/talesofai/comfyui-browser/wiki/How-to-use-Sync-in-the-Saves-tab"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
data-slot="icon"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
/>
</svg>
</a>
<input
type="text"
placeholder="Input your git repository address"
placeholder={tt('syncInput.placeholder')}
bind:value={configGitRepo}
class="input input-bordered w-full max-w-lg"
/>
{#if configGitRepo != config?.git_repo}
<button
class="btn btn-outline btn-accent"
on:click={onClickSaveConfig}
>
Save
<button class="btn btn-outline btn-accent" on:click={onClickSaveConfig}>
{tt('btn.save')}
</button>
{/if}
<button
class="btn btn-outline btn-accent"
on:click={onClickSyncCollections}
>
Sync
<button class="btn btn-outline btn-accent" on:click={onClickSyncCollections}>
{tt('btn.sync')}
</button>
</div>
<div class="max-w-full text-sm breadcrumbs flex flex-row ml-4">
<ul class="basis-2/3">
<li><button on:click={() => onClickPath(-1)}>Root</button></li>
<li>
<button on:click={() => onClickPath(-1)}>{$t('common.rootDir')}</button>
</li>
{#each (folderPath || '').split('/') as path, index}
<li><button on:click={() => onClickPath(index)}>{path}</button></li>
{/each}
@@ -217,21 +255,18 @@
<input
type="text"
placeholder="Filter by filename or memos"
placeholder={tt('searchInput.placeholder')}
bind:value={searchQuery}
class="input input-bordered border-slate-600 w-full h-full rounded-none text-sm basis-1/3"
/>
</div>
<ul class="space-y-2">
{#each files.filter(f => searchRegex.test(f.name.toLowerCase()) || searchRegex.test(f.notes.toLowerCase()))
{#each files
.filter((f) => searchRegex.test(f.name.toLowerCase()) || searchRegex.test(f.notes.toLowerCase()))
.slice(0, showCursor) as file}
<li class="flex h-16 sm:h-28 border-0 space-x-4 p-2 bg-info-content">
<MediaShow
file={file}
styleClass="w-16 sm:w-28"
onClickDir={onClickDir}
/>
<MediaShow {file} styleClass="w-16 sm:w-28" {onClickDir} />
<div class="space-y-2 w-96 relative">
<input
type="text"
@@ -248,19 +283,25 @@
<button
class="btn btn-link btn-sm p-0 no-underline text-accent"
on:click={async () => await onLoadWorkflow(file, comfyApp, toast)}
>Load</button>
>{$t('common.btn.load')}</button
>
{/if}
<button
class="btn btn-link btn-sm p-0 no-underline text-error"
class="btn btn-link btn-sm p-0 no-underline text-error ml-52"
on:click={async () => await onDelete(file)}
>Remove</button>
>
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 2v4h6v2h-2v14H4V8H2V6h6V2h8zm-2 2h-4v2h4V4zm0 4H6v12h12V8h-4zm-5 2h2v8H9v-8zm6 0h-2v8h2v-8z" fill="#f77"/>
</svg>
{$t('common.btn.delete')}
</button>
</div>
</div>
<div>
<textarea
name="notes"
placeholder="write some memos..."
placeholder={tt('collection.memoPlaceholder')}
on:blur={(e) => updateFileNotes(e, file)}
class="resize-none textarea hidden md:block md:w-72 max-w-72 h-14 sm:h-24"
value={file.notes}
@@ -270,13 +311,21 @@
{/each}
</ul>
<div class="flex justify-center">
{#if files.length > showCursor}
<button on:click={() => showCursor += 10} class="btn btn-neutral btn-outline">Load more</button>
{:else}
<p class="text-neutral-content">No more content.</p>
{/if}
</div>
{#if files.length === 0}
<div class="w-full h-full flex items-center justify-center">
<span class="font-bold text-4xl">
{#if loaded}
It's empty here.
{$t('common.emptyList')}
{:else}
Loading ...
{$t('common.rootDir')}
{/if}
</span>
</div>
+87
View File
@@ -0,0 +1,87 @@
<script lang="ts">
import dayjs from 'dayjs';
import { onMount, onDestroy } from "svelte";
import { formatFileSize } from './utils';
import { t } from 'svelte-i18n';
export let comfyUrl: string;
let downloads: Array<any> = [];
let autoRefresh = true;
$: if (autoRefresh) {
autoRefreshInterval = setInterval(refreshHistory, 1000);
} else {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
}
let autoRefreshInterval: number;
async function refreshHistory() {
const res = await fetch(comfyUrl + '/browser/downloads');
const ret = await res.json();
let newDownloads: Array<any> = [];
ret.download_logs.forEach((dl: any) => {
let newDl = dl;
const timeFormat = 'YYYY-MM-DD HH:mm:ss';
newDl['formattedCreatedAt'] = dayjs.unix(dl.created_at).format(timeFormat);
newDl['formattedUpdatedAt'] = dayjs.unix(dl.updated_at).format(timeFormat);
newDl['percentStr'] = `${Math.floor(dl.downloaded_size/dl.total_size * 100)}%`;
newDl['formattedDownloadedSize'] = formatFileSize(dl.downloaded_size);
newDl['formattedTotalSize'] = formatFileSize(dl.total_size);
newDownloads.push(newDl);
});
downloads = newDownloads;
}
onMount(async () => {
refreshHistory();
});
onDestroy(() => {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
});
</script>
<div class="flex flex-row justify-between">
<button
class="btn btn-accent btn-outline"
on:click={refreshHistory}
>
{$t('common.btn.refresh')}
</button>
<div class="flex items-center">
<span class="label-text">{$t('downloadHistory.Auto Refresh')}</span>
<input type="checkbox" bind:checked={autoRefresh} class="checkbox checkbox-secondary" />
</div>
</div>
<ul class="space-y-2">
{#each downloads as dl}
<li class="bg-info-content">
<p class="font-bold text-lg">{dl.filename ? dl.filename : dl.uuid}</p>
<p class="text-gray-500 text-xs">
<span>Created at: {dl.formattedCreatedAt}</span>
<span>Updated at: {dl.formattedUpdatedAt}</span>
</p>
<p class="text-gray-500 text-xs">URL: {dl.download_url}</p>
<p class="text-gray-500 text-xs">Save in: {dl.save_in}</p>
{#if dl.total_size !== 0}
<div>
<span>{dl.formattedDownloadedSize}/{dl.formattedTotalSize}</span>
<span>{dl.percentStr}</span>
<progress
class="progress progress-primary"
value={dl.downloaded_size}
max={dl.total_size} >
</progress>
</div>
{:else}
<p>Result: {dl.result}</p>
{/if}
</li>
{/each}
</ul>
+87 -34
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import { onMount } from "svelte";
import { onLoadWorkflow, onScroll, fetchFiles } from './utils';
import { onMount } from 'svelte';
import { onLoadWorkflow, onScroll, fetchFiles, WHITE_EXTS } from './utils';
import { t } from 'svelte-i18n';
import type { FOLDER_TYPES } from './utils';
import MediaShow from "./MediaShow.svelte";
import type Toast from "./Toast.svelte";
import MediaShow from './MediaShow.svelte';
import type Toast from './Toast.svelte';
export let comfyUrl: string;
export let folderType: FOLDER_TYPES;
@@ -16,7 +17,7 @@
$: try {
searchRegex = new RegExp(searchQuery.toLowerCase());
} catch {
searchRegex = new RegExp('');
searchRegex = new RegExp('');
}
let comfyApp: any;
@@ -25,9 +26,14 @@
let showCursor = 20;
let searchQuery = '';
let searchRegex = new RegExp('');
let scrollTop = 0;
$: tt = function(key: string) {
return $t('filesList.' + key);
}
export async function refresh() {
loaded = true;
loaded = false;
files = await fetchFiles(folderType, comfyUrl, folderPath);
loaded = true;
}
@@ -36,8 +42,18 @@
//@ts-ignore
comfyApp = window.top.app;
//@ts-ignore
window.top.addEventListener("comfyuiBrowserShow", () => {
refresh();
});
folderPath = '';
window.addEventListener('scroll', () => { showCursor = onScroll(showCursor, files.length); });
window.addEventListener('scroll', (e) => {
showCursor = onScroll(showCursor, files.length);
//@ts-ignore
scrollTop = (e.target.scrollingElement as HTMLElement).scrollTop;
});
});
async function onCollect(file: any) {
@@ -52,13 +68,18 @@
toast.show(
res.ok,
'Added to Saves',
'Failed to add to Saves. Please check the ComfyUI server.'
tt('fileList.Added to Saves'),
tt('fileList.Failed to add to Saves'),
);
}
async function scrollToTop() {
window.scrollTo(0, 0);
}
async function onDelete(file: any) {
const ret = confirm('You want to delete this file? ' + file.name);
const ret = confirm(tt('You want to delete this file?') + ' ' + file.name);
if (!ret) {
return;
}
@@ -72,12 +93,12 @@
}),
});
refresh();
toast.show(
res.ok,
'Deleted the file ' + file.name,
'Failed to delete the file. Please check the ComfyUI server.'
tt('Deleted the file') + file.name,
tt('Failed to delete the file'),
);
files = files.filter(f => f != file);
}
async function onClickDir(dir: any) {
@@ -90,13 +111,18 @@
return;
}
folderPath = folderPath.split('/').slice(0, index + 1).join('/');
folderPath = folderPath
.split('/')
.slice(0, index + 1)
.join('/');
}
</script>
<div class="max-w-full text-sm breadcrumbs flex flex-row ml-4">
<ul class="basis-3/4">
<li><button on:click={() => onClickPath(-1)}>Root</button></li>
<li>
<button on:click={() => onClickPath(-1)}>{$t('common.rootDir')}</button>
</li>
{#each (folderPath || '').split('/') as path, index}
<li><button on:click={() => onClickPath(index)}>{path}</button></li>
{/each}
@@ -104,57 +130,84 @@
<input
type="text"
placeholder="Filter by filename"
placeholder={tt('searchInput.placeholder')}
bind:value={searchQuery}
class="input input-bordered w-full h-full rounded-none border-slate-600 text-sm basis-1/4"
/>
</div>
<div class="grid grid-cols-4 lg:grid-cols-6 gap-2">
{#each files.filter(f => searchRegex.test(f.name.toLowerCase())).slice(0, showCursor) as file}
{#if ['dir', 'image', 'video', 'json'].includes(file.fileType)}
{#each files
.filter((f) => searchRegex.test(f.name.toLowerCase()))
.slice(0, showCursor) as file}
{#if WHITE_EXTS.includes(file.fileType)}
<div class="p-2 bg-info-content">
<div class="flex items-center">
<MediaShow
file={file}
styleClass="w-full h-16 sm:h-36"
onClickDir={onClickDir}
/>
<MediaShow {file} styleClass="w-full h-16 sm:h-36" {onClickDir} />
</div>
<p class="font-bold max-h-12 leading-6 overflow-auto mt-1">{file.name}</p>
<p class="hidden sm:block text-gray-500 text-xs">{file.formattedDatetime}</p>
<p class="hidden sm:block text-gray-500 text-xs">{file.formattedSize}</p>
<p class="font-bold max-h-12 leading-6 overflow-auto mt-1">
{file.name}
</p>
<p class="hidden sm:block text-gray-500 text-xs">
{file.formattedDatetime}
</p>
<p class="hidden sm:block text-gray-500 text-xs">
{file.formattedSize}
</p>
<div class="">
{#if comfyApp && file.type != 'dir'}
<button
class="btn btn-link btn-sm p-0 no-underline text-accent"
on:click={async () => await onLoadWorkflow(file, comfyApp, toast)}
>Load</button>
>{$t('common.btn.load')}</button
>
{/if}
<button
class="btn btn-link btn-sm p-0 no-underline text-accent"
on:click={async () => await onCollect(file)}
>Save</button>
>{$t('common.btn.save')}</button
>
<button
class="btn btn-link btn-sm p-0 no-underline text-error"
class="btn btn-link btn-sm p-0 no-underline text-error float-right"
on:click={async () => await onDelete(file)}
>Delete</button>
>
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 2v4h6v2h-2v14H4V8H2V6h6V2h8zm-2 2h-4v2h4V4zm0 4H6v12h12V8h-4zm-5 2h2v8H9v-8zm6 0h-2v8h2v-8z" fill="#f77"/>
</svg>
{$t('common.btn.delete')}
</button>
</div>
</div>
{/if}
{/each}
</div>
<div class="flex justify-center">
{#if files.length > showCursor}
<button on:click={() => showCursor += 10} class="btn btn-neutral btn-outline">Load more</button>
{:else}
<p class="text-neutral-content">No more content.</p>
{/if}
</div>
{#if files.length === 0}
<div class="w-full h-full flex items-center justify-center">
<span class="font-bold text-4xl">
{#if loaded }
It's empty here.
{:else }
Loading ...
{#if loaded}
{$t('common.emptyList')}
{:else}
{$t('common.loading')}
{/if}
</span>
</div>
{/if}
{#if scrollTop >= 300}
<button class="fixed right-10 bottom-10 w-10 h-10 flex justify-center items-center rounded-full bg-slate-50" on:click={scrollToTop}>
<svg class="w-5 h-5 icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path fill="#333333" d="M572.235 205.282v600.365a30.118 30.118 0 11-60.235 0V205.282L292.382 438.633a28.913 28.913 0 01-42.646 0 33.43 33.43 0 010-45.236l271.058-288.045a28.913 28.913 0 0142.647 0L834.5 393.397a33.43 33.43 0 010 45.176 28.913 28.913 0 01-42.647 0l-219.618-233.23z"/>
</svg>
</button>
{/if}
+2 -3
View File
@@ -1,11 +1,10 @@
<script lang="ts">
import Toast from "./Toast.svelte";
import FilesList from "./FilesList.svelte";
import Toast from './Toast.svelte';
import FilesList from './FilesList.svelte';
export let comfyUrl: string;
let toast: Toast;
</script>
<FilesList folderType="outputs" {comfyUrl} {toast} />
+9 -2
View File
@@ -28,17 +28,24 @@
</span>
</div>
{/if}
{#if file.fileType === 'html'}
<div class="w-full h-full flex items-center justify-center">
<span class="font-bold text-xl">
HTML
</span>
</div>
{/if}
{#if file.fileType === 'image'}
<img
class="h-full w-full object-contain"
loading="lazy"
src={file.url}
src={file.previewUrl}
alt={file.name} />
{/if}
{#if file.fileType === 'video'}
<video
class="h-full w-full object-contain pb-0.5 border-0.5 border-black"
src={file.url}
src={file.previewUrl}
loop={true}
autoplay={true}
muted={true}
+51
View File
@@ -0,0 +1,51 @@
<script lang="ts">
import DownloadHistory from "./DownloadHistory.svelte";
import NewDownload from "./NewDownload.svelte";
export let comfyUrl: string;
let activeTab = 'downloadNewModel';
$: activeTabClass = function(tab: string) {
return tab === activeTab ? 'active' : '';
}
async function onClickTab(tab: string) {
activeTab = tab;
}
</script>
<div class="drawer md:drawer-open">
<input type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col pl-2">
{#if activeTab === 'downloadHistory'}
<DownloadHistory {comfyUrl} />
{:else}
<NewDownload
comfyUrl={comfyUrl}
afterStartingDownload={() => onClickTab('downloadHistory')}
/>
{/if}
</div>
<div class="drawer-side border-r border-base-content pr-2">
<ul class="menu bg-base-200 w-56 p-0 [&_li>*]:rounded-none">
<li class="h-10">
<button
class="{activeTabClass('downloadNewModel')} pl-2 btn-outline btn-accent"
on:click={() => onClickTab('downloadNewModel')}
>
Download new model
</button>
</li>
<li class="h-10">
<button
class="{activeTabClass('downloadHistory')} pl-2 btn-outline btn-accent"
on:click={() => onClickTab('downloadHistory')}
>
Download History
</button>
</li>
</ul>
</div>
</div>
+102 -17
View File
@@ -1,60 +1,145 @@
<script lang="ts">
import { setLocalConfig } from "./utils";
import { setLocalConfig } from './utils';
import { t, locales, locale } from 'svelte-i18n';
export let activeTab = 'outputs';
$: activeTabClass = function(tab: string) {
$: activeTabClass = function (tab: string) {
return tab === activeTab ? 'active' : '';
}
};
async function onClickTab(tab: string) {
activeTab = tab;
setLocalConfig('lastTab', tab);
}
function showLocale(locale: string) {
switch (locale) {
case 'en-US':
return 'En';
case 'zh-CN':
return '中文';
default:
return 'En';
}
}
</script>
<div class="flex">
<ul class="menu menu-horizontal bg-base-200 rounded-box flex-1">
<li>
<button
class="{activeTabClass('outputs')}"
class={activeTabClass('outputs')}
on:click={() => onClickTab('outputs')}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
Outputs
{$t('navbar.outputs')}
</button>
</li>
<li>
<button
class="{activeTabClass('collections')}"
class={activeTabClass('collections')}
on:click={() => onClickTab('collections')}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
/>
</svg>
Saves
{$t('navbar.saves')}
</button>
</li>
<li>
<button
class="{activeTabClass('sources')}"
class={activeTabClass('sources')}
on:click={() => onClickTab('sources')}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-.75a7.5 7.5 0 00-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12.75 19.5v-.75a7.5 7.5 0 00-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
/>
</svg>
Sources
{$t('navbar.sources')}
</button>
</li>
<li>
<button
class="{activeTabClass('models')}"
on:click={() => onClickTab('models')}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
{$t('navbar.models')}
</button>
</li>
</ul>
<div class="dropdown dropdown-hover">
<div tabindex="0" role="button" class="m-1 btn btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
</svg>
<svg width="12px" height="12px" class="hidden h-2 w-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path>
</svg>
</div>
<ul class="p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box w-24">
{#each $locales as l}
<li>
<button class={l == $locale ? 'active' : ''} on:click={() => locale.set(l)}>
{showLocale(l)}
</button>
</li>
{/each}
</ul>
</div>
<div class="hidden sm:flex flex-0 items-center justify-center p-2">
<a href="https://github.com/talesofai/comfyui-browser" target="_blank">
<img src="https://img.shields.io/github/stars/talesofai/comfyui-browser?style=social" alt="stars - comfyui-browser">
</a>
<img
src="https://img.shields.io/github/stars/talesofai/comfyui-browser?style=social"
alt="stars - comfyui-browser"
/>
</a>
</div>
<div class="hidden sm:flex flex-0 items-center justify-center p-2">
<a href="https://discord.gg/f49R9dTKwM" target="_blank">
<svg class="hidden h-6 w-6 fill-current opacity-60 sm:inline-block " viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5764f2" fill-rule="nonzero"></path>
</svg>
</a>
</div>
</div>
+103
View File
@@ -0,0 +1,103 @@
<script lang="ts">
import Toast from "./Toast.svelte";
export let comfyUrl: string;
export let afterStartingDownload: Function;
const modelTypes = [
{ text: 'Checkpoint', value: 'checkpoints' },
{ text: 'LoRA', value: 'loras' },
{ text: 'VAE', value: 'vae' },
{ text: 'Embedding', value: 'embeddings' },
{ text: 'ControlNet', value: 'controlnet' },
];
let toast: Toast;
let downloadUrl = '';
let modelType = modelTypes[0].value;
let filename = '';
let overwrite = false;
async function onClickDownload() {
if (! downloadUrl) {
toast.show(false, '', 'Invalid download URL');
return;
}
const res = await fetch(comfyUrl + '/browser/downloads', {
method: 'POST',
body: JSON.stringify({
download_url: downloadUrl,
save_in: modelType,
filename: filename,
overwrite: overwrite,
}),
});
toast.show(
res.ok,
'Download started',
'Failed'
);
if (afterStartingDownload) {
afterStartingDownload();
}
}
</script>
<div class="space-y-4">
<div class="form-control w-full max-w-2xl">
<div class="label">
<span class="label-text">* Download URL</span>
</div>
<input
type="text"
placeholder="https://civitai.com/api/download/models/35516"
class="input input-bordered w-full max-w-2xl"
bind:value={downloadUrl}
/>
</div>
<div class="form-control w-full max-w-2xl">
<div class="label">
<span class="label-text">Filename(leave this blank if you want to auto detect this)</span>
</div>
<input
type="text"
class="input input-bordered w-full max-w-2xl"
bind:value={filename}
/>
</div>
<div class="form-control w-full">
<div class="label">
<span class="label-text">* Select the modal type</span>
</div>
<select
class="select select-bordered max-w-xs"
bind:value={modelType}
>
{#each modelTypes as mt}
<option value={mt.value}>{mt.text}</option>
{/each}
</select>
</div>
<div class="form-control w-full">
<label class="cursor-pointer label max-w-xs">
<span class="label-text">Overwrite the exist file?</span>
<input type="checkbox" bind:checked={overwrite} class="checkbox checkbox-secondary" />
</label>
</div>
<button
class="btn btn-secondary btn-outline"
on:click={onClickDownload}
>
Download
</button>
</div>
<Toast bind:this={toast} />
+107 -39
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from "svelte";
import Toast from "./Toast.svelte";
import FilesList from "./FilesList.svelte";
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import Toast from './Toast.svelte';
import FilesList from './FilesList.svelte';
export let comfyUrl: string;
@@ -15,6 +16,10 @@
let fileList: FilesList;
let syncingSouce: any;
$: tt = function(key: string) {
return $t('sourcesTab.' + key);
}
async function refreshSources() {
const res = await fetch(comfyUrl + '/browser/sources');
const ret = await res.json();
@@ -37,12 +42,11 @@
});
function onClickSource(source: any) {
console.log(folderPath);
folderPath = source.name;
}
async function onClickDeleteSource(source: any) {
const ret = confirm('You want to delete this source? ' + source.name);
const ret = confirm(tt('You want to delete this source') + source.name);
if (!ret) {
return;
}
@@ -57,8 +61,8 @@
}
toast.show(
res.ok,
'Deleted this source',
'Failed to delete this source. Please check the ComfyUI server.',
tt('Deleted this source'),
tt('Failed to delete this source'),
);
}
@@ -85,7 +89,7 @@
}
async function addSource(url: string) {
if (! url) {
if (!url) {
toast.show(false, '', 'missing Git URL');
return;
}
@@ -123,55 +127,105 @@
<div class="drawer-content flex flex-col pl-2">
<FilesList
folderType="sources"
bind:folderPath={folderPath}
comfyUrl={comfyUrl}
toast={toast}
bind:folderPath
{comfyUrl}
{toast}
bind:this={fileList}
/>
</div>
<div class="drawer-side border-r border-base-content pr-2">
<ul class="menu bg-base-200 w-56 p-0 [&_li>*]:rounded-none">
<li class="h-10">
<li class="h-10">
<button class="pl-2 btn-outline btn-accent" on:click={openEditModal}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" data-slot="icon" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
data-slot="icon"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
Add new source
{tt('Add new source')}
</button>
</li>
{#each sources as source}
<li class="h-14">
{#if source.homepage }
{#if source.homepage}
<a
class="h-14 px-0.5 flex items-center justify-center"
href={source.homepage}
target="_blank"
>
<!--TODO: using more general icon instead of GitHub-->
<svg class="w-6 h-6 fill-accent" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 30 30">
<path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"></path>
<svg
class="w-6 h-6 fill-accent"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="100"
height="100"
viewBox="0 0 30 30"
>
<path
d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"
></path>
</svg>
</a>
{:else}
<div class="h-14 px-0.5 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" data-slot="icon" class="w-6 h-6 text-accent">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 0 0 4.5 4.5H18a3.75 3.75 0 0 0 1.332-7.257 3 3 0 0 0-3.758-3.848 5.25 5.25 0 0 0-10.233 2.33A4.502 4.502 0 0 0 2.25 15Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
data-slot="icon"
class="w-6 h-6 text-accent"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 0 0 4.5 4.5H18a3.75 3.75 0 0 0 1.332-7.257 3 3 0 0 0-3.758-3.848 5.25 5.25 0 0 0-10.233 2.33A4.502 4.502 0 0 0 2.25 15Z"
/>
</svg>
</div>
{/if}
<button
class="pl-2 h-14 line-clamp-2 overflow-hidden {source.name == folderPath.split('/')[0] ? 'active' : ''}"
class="pl-2 h-14 line-clamp-2 overflow-hidden {source.name ==
folderPath.split('/')[0]
? 'active'
: ''}"
on:click={() => onClickSource(source)}
><p class="w-40 truncate whitespace-normal">{source.name}</p></button>
><p class="w-40 truncate whitespace-normal">
{source.name}
</p></button
>
<button
class="h-14 right-0 fixed flex items-center justify-center"
on:click={() => onClickSyncSource(source)}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="{syncingSouce == source ? 'text-accent' : ''} w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="{syncingSouce == source ? 'text-accent' : ''} w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</li>
@@ -196,32 +250,38 @@
class="btn btn-outline btn-primary"
on:click={() => addSource(inputRepoUrl)}
disabled={waitingForUrl.length > 0 || !inputRepoUrl}
>{waitingForUrl === inputRepoUrl && inputRepoUrl ? 'Subscribing...' : 'Subscribe'}</button>
>{waitingForUrl === inputRepoUrl && inputRepoUrl
? tt('Subscribing...')
: tt('Subscribe')}</button
>
</div>
<div>
{#if allSources.length === 0}
<div class="w-full h-full flex items-center justify-center">
<span class="font-bold text-4xl">
Loading ...
</span>
<span class="font-bold text-4xl"> {$t('common.loading')} </span>
</div>
{/if}
<ul class="space-y-1">
{#each allSources as s}
<li class="flex flex-nowrap space-x-2 h-20 border-b border-b-base-content">
<img
src={`https://github.com/${s.author}.png`}
alt={s.author} />
<li
class="flex flex-nowrap space-x-2 h-20 border-b border-b-base-content"
>
<img src={`https://github.com/${s.author}.png`} alt={s.author} />
<div class="w-80">
<a class="link link-warning text-lg no-underline" href={s.url} target="_blank">
<p>{s.author}/{s.title}</p>
<p class="line-clamp-2">{s.author}/{s.title}</p>
</a>
<a href={s.url} target="_blank">
<img src={`https://img.shields.io/github/stars${(new URL(s.url)).pathname}?style=flat-square`} alt="stars">
<img
src={`https://img.shields.io/github/stars${
new URL(s.url).pathname
}?style=flat-square`}
alt="stars"
/>
</a>
</div>
<div class="w-72 grow">
<div class="w-72 grow overflow-auto">
<p>{s.description}</p>
</div>
<div class="flex items-center justify-center">
@@ -230,7 +290,7 @@
on:click={() => addSource(s.url)}
disabled={waitingForUrl.length > 0}
>
{waitingForUrl === s.url ? 'Subscribing...' : 'Subscribe'}
{waitingForUrl === s.url ? tt('Subscribing...') : tt('Subscribe')}
</button>
</div>
</li>
@@ -239,11 +299,19 @@
</div>
<p class="mt-1 text-xs text-gray-500">
You could open
<a class="text-accent" target="_blank" href="https://github.com/talesofai/comfyui-browser/edit/main/data/sources.json">
<a
class="text-accent"
target="_blank"
href="https://github.com/talesofai/comfyui-browser/edit/main/data/sources.json"
>
a pull request
</a>
or submit
<a class="text-accent" target="_blank" href="https://github.com/talesofai/comfyui-browser/issues/new?assignees=tzwm&labels=workflow-repo&projects=&template=new-workflow-repository.md&title=New+workflow+repo%3A">
<a
class="text-accent"
target="_blank"
href="https://github.com/talesofai/comfyui-browser/issues/new?assignees=tzwm&labels=workflow-repo&projects=&template=new-workflow-repository.md&title=New+workflow+repo%3A"
>
an issue
</a>
to add your workflow repository here.<br />
+68 -13
View File
@@ -3,32 +3,73 @@ import type Toast from './Toast.svelte';
export type FOLDER_TYPES = 'outputs' | 'collections' | 'sources';
export const IMAGE_EXTS = ['png', 'webp', 'jpeg', 'jpg', 'gif'];
export const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv'];
export const JSON_EXTS = ['json'];
export const WHITE_EXTS = ['html', 'image', 'video', 'json', 'dir'];
const localStorageKey = 'comfyui-browser';
function getFileUrl(comfyUrl: string, folderType: string, file: any) {
if (file.folder_path) {
return `${comfyUrl}/browser/s/${folderType}/${file.folder_path}/${file.name}`;
} else {
return `${comfyUrl}/browser/s/${folderType}/${file.name}`;
}
}
function findFile(filename: string, exts: Array<string>, files: Array<any>) {
let fn: any = filename.split('.');
fn.pop();
fn = fn.join('.');
return files.find((f: any) => {
const fa = f.name.split('.');
const extname = fa.pop().toLowerCase();
const name = fa.join('.');
return name === fn && exts.includes(extname);
});
}
function processFile(
file: any,
folderType: FOLDER_TYPES,
comfyUrl: string,
files: Array<any>,
) {
const extname = file.name.split('.').pop().toLowerCase();
file['fileType'] = 'json';
if (['png', 'webp', 'jpeg', 'jpg', 'gif'].includes(extname)) {
if (WHITE_EXTS.includes(extname)) {
file['fileType'] = extname;
if (extname === 'json') {
if (findFile(file.name, IMAGE_EXTS.concat(VIDEO_EXTS), files)) {
return;
}
}
}
if (IMAGE_EXTS.includes(extname)) {
file['fileType'] = 'image';
}
if (['mp4', 'webm', 'mov', 'avi', 'mkv'].includes(extname)) {
if (VIDEO_EXTS.includes(extname)) {
file['fileType'] = 'video';
}
if (! file['fileType']) {
return;
}
file['url'] = `${comfyUrl}/browser/files/view?folder_type=${folderType}&filename=${file.name}&folder_path=${file.folder_path}`;
file['url'] = getFileUrl(comfyUrl, folderType, file);
if (['image', 'video'].includes(file['fileType'])) {
file['previewUrl'] = getFileUrl(comfyUrl, folderType, file);
let jsonFile = findFile(file.name, JSON_EXTS, files);
if (jsonFile) {
file['url'] = getFileUrl(comfyUrl, folderType, jsonFile);
}
}
const d = dayjs.unix(file.created_at);
file['formattedDatetime'] = d.format('YYYY-MM-DD HH-mm-ss');
file['formattedDatetime'] = d.format('YYYY-MM-DD HH:mm:ss');
file['formattedSize'] = formatFileSize(file['bytes']);
if (file['bytes'] / 1024 / 1024 > 1) {
file['formattedSize'] = (file['bytes'] / 1024 / 1024).toFixed(2) + ' MB';
} else {
file['formattedSize'] = Math.round(file['bytes'] / 1024) + ' KB';
}
return file;
}
@@ -59,15 +100,21 @@ export async function fetchFiles(
const ret = await res.json();
let files = ret.files;
let newFiles: Array<any> = [];
files.forEach((f: any) => {
let newFile;
if (f.type === 'dir') {
f = processDir(f);
newFile = processDir(f);
} else {
f = processFile(f, folderType, comfyUrl);
newFile = processFile(f, folderType, comfyUrl, files);
}
if (newFile) {
newFiles.push(newFile);
}
});
return files;
return newFiles;
}
export function onScroll(showCursor: number, filesLen: number) {
@@ -120,3 +167,11 @@ export function setLocalConfig(key: string, value: any) {
localConfig[key] = value;
localStorage.setItem(localStorageKey, JSON.stringify(localConfig));
}
export function formatFileSize(size: number) {
if (size / 1024 / 1024 > 1) {
return (size / 1024 / 1024).toFixed(2) + ' MB';
} else {
return Math.round(size / 1024) + ' KB';
}
}
+14
View File
@@ -0,0 +1,14 @@
import { waitLocale } from 'svelte-i18n';
/** @type {import('./$types').PageLoad} */
export async function load({ url }) {
await waitLocale();
if (typeof process === 'undefined') {
const path = url.searchParams.get('path');
return {
path,
};
}
return {};
}
+119
View File
@@ -0,0 +1,119 @@
<script>
import { onMount } from 'svelte';
import MultiDimTableLayout from '$lib/multi-dim-table/layout.svelte';
import AnalyzeTable from '$lib/multi-dim-table/table.svelte';
import {
comfyUrl,
createRefetchStatisticPublisher,
} from '$lib/multi-dim-table/store';
import InfoIcon from '$lib/icons/info.svelte';
import { db, getUser } from '$lib/db';
import { v1 as uuid } from 'uuid';
import { fakeUsername } from '$lib/random';
import { createRefetchStatisticSubscriber } from '$lib/multi-dim-table/store';
/** @type {import('./$types').PageData} */
export let data;
/** @type {string | null} */
export let path;
/** @type {import('$lib/multi-dim-table/models').AxisScore[]} */
let scores;
$: ({ path } = data);
$: comfyUrl.set(data.comfyUrl);
let mounted = false;
/** @type {import("$lib/multi-dim-table/models").Payload}*/
let payload;
let loading = false;
/**@type {HTMLElement | undefined}*/
let tableContainerRef;
createRefetchStatisticSubscriber(() => {
fetch(data.comfyUrl + '/browser/xyz_plot/statistic?path=' + data.path)
.then(async (d) => {
if (d.ok) {
scores = await d.json().then((d) => d.result);
}
})
.catch(() => {});
});
const refetch = createRefetchStatisticPublisher();
$: {
if (mounted && path) {
loading = true;
fetch(path)
.then((d) => d.json())
.then((d) => (payload = d))
.finally(() => {
loading = false;
});
}
}
$: {
if (mounted && path) {
refetch();
}
}
onMount(async () => {
mounted = true;
const user = await getUser();
if (!user) {
db.user.add({
uuid: uuid(),
name: fakeUsername(),
ctime: Date.now(),
mtime: Date.now(),
});
}
});
</script>
{#if typeof data.path === 'string' && data.path.length > 0}
{#if loading}
<div class="flex flex-col w-screen h-screen justify-center items-center">
<span class="loading loading-dots loading-lg"></span>
</div>
{:else if !payload}
null
{:else}
<MultiDimTableLayout
extraItems={[
{
label: 'Open Workflow',
onClick: () => {
open(payload.workflow.url);
},
},
]}
>
<div slot="title">
XYZ Plot
<div
class="tooltip before:pl-20 before:text-left before:max-w-96 tooltip-bottom z-40 before:whitespace-pre-wrap"
data-tip={payload.annotations
.map((d) => `${d.axis}: ${d.key} - ${d.type}`)
.join('\n')}
>
<InfoIcon />
</div>
</div>
<div class="w-full h-full" bind:this={tableContainerRef}>
<AnalyzeTable
bind:payload
bind:scores
height={tableContainerRef?.clientHeight ?? 0}
/>
</div>
</MultiDimTableLayout>
{/if}
{:else}
Invalid Path
{/if}
+13 -13
View File
@@ -1,17 +1,17 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
kit: {
adapter: adapter({
// default options are shown. On some platforms
// these options are set automatically — see below
pages: '../web/build',
assets: '../web/build',
fallback: undefined,
precompress: false,
strict: true
}),
},
kit: {
adapter: adapter({
// default options are shown. On some platforms
// these options are set automatically — see below
pages: '../web/build',
assets: '../web/build',
fallback: undefined,
precompress: false,
strict: true,
}),
},
preprocess: vitePreprocess(),
};
}
+8 -1
View File
@@ -2,5 +2,12 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
server: {
proxy: {
'/browser': {
target: 'http://127.0.0.1:8188',
},
},
},
});
+207
View File
@@ -0,0 +1,207 @@
import functools
import json
from os import path, scandir, makedirs
import subprocess
import time
from typing import TypedDict, List
import requests
from requests.adapters import HTTPAdapter, Retry
import folder_paths
from comfy.cli_args import args
SERVER_BASE_URL = f'http://{args.listen}:{args.port}'
# To support IPv6
if ':' in args.listen:
SERVER_BASE_URL = f'http://[{args.listen}]:{args.port}'
browser_path = path.dirname(__file__)
config_path = path.join(browser_path, 'config.json')
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv']
white_extensions = ['.json', '.html'] + image_extensions + video_extensions
info_file_suffix = '.info'
git_remote_name = 'origin'
def http_client():
adapter = HTTPAdapter(max_retries=Retry(3, backoff_factor=0.1))
http = requests.session()
http.mount('http://', adapter)
http.mount('https://', adapter)
return http
@functools.cache
def get_config():
return {
"collections": path.join(browser_path, 'collections'),
"download_logs": path.join(browser_path, 'download_logs'),
"outputs": output_directory_from_comfyui(),
"sources": path.join(browser_path, 'sources'),
} | load_config()
def load_config():
if not path.exists(config_path):
return {}
else:
with open(config_path, 'r') as f:
return json.load(f)
@functools.cache
def collections_path():
return get_config()['collections']
@functools.cache
def download_logs_path():
return get_config()['download_logs']
@functools.cache
def outputs_path():
return get_config()['outputs']
@functools.cache
def sources_path():
return get_config()['sources']
class FileInfoDict(TypedDict):
type: str
name: str
bytes: int
created_at: float
folder_path: str
notes: str
def log(message):
print('[comfyui-browser] ' + message)
def run_cmd(cmd, run_path, log_cmd=True, log_code=True, log_message=True):
if log_cmd:
log(f'running: {cmd}')
ret = subprocess.run(
f'cd {run_path} && {cmd}',
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="UTF-8"
)
if log_code:
if ret.returncode == 0:
log('successed')
else:
log('failed')
if log_message:
if (len(ret.stdout) > 0 or len(ret.stderr) > 0):
log(ret.stdout + ret.stderr)
return ret
# folder_type = 'outputs', 'collections', 'sources'
def get_parent_path(folder_type: str):
if folder_type == 'collections':
return collections_path()
if folder_type == 'sources':
return sources_path()
# outputs
return outputs_path()
# folder_type = 'outputs', 'collections', 'sources'
def get_target_folder_files(folder_path: str, folder_type: str = 'outputs'):
if '..' in folder_path:
return None
parent_path = get_parent_path(folder_type)
files: List[FileInfoDict] = []
target_path = path.join(parent_path, folder_path)
if not path.exists(target_path):
return []
folder_listing = scandir(target_path)
folder_listing = sorted(folder_listing, key=lambda f: (f.is_file(), -f.stat().st_ctime))
for item in folder_listing:
if not path.exists(item.path):
continue
name = path.basename(item.path)
ext = path.splitext(name)[1].lower()
if name == '' or name[0] == '.':
continue
if item.is_file():
if not (ext in white_extensions):
continue
created_at = item.stat().st_ctime
info_file_path = get_info_filename(item.path)
info_data = {}
if path.exists(info_file_path):
with open(info_file_path, 'r') as f:
info_data = json.load(f)
if item.is_file():
bytes = item.stat().st_size
files.append({
"type": "file",
"name": name,
"bytes": bytes,
"created_at": created_at,
"folder_path": folder_path,
"notes": info_data.get("notes", "")
})
elif item.is_dir():
files.append({
"type": "dir",
"name": name,
"bytes": 0,
"created_at": created_at,
"folder_path": folder_path,
"notes": info_data.get("notes", "")
})
return files
def get_info_filename(filename):
return path.splitext(filename)[0] + info_file_suffix
def add_uuid_to_filename(filename):
name, ext = path.splitext(filename)
return f'{name}_{int(time.time())}{ext}'
def output_directory_from_comfyui():
if args.output_directory:
return path.abspath(args.output_directory)
else:
return folder_paths.get_output_directory()
def git_init():
if not path.exists(path.join(collections_path(), '.git')):
run_cmd('git init', collections_path())
ret = run_cmd('git config user.name', collections_path(),
log_cmd=False, log_code=False, log_message=False)
if len(ret.stdout) == 0:
ret = run_cmd('whoami', collections_path(),
log_cmd=False, log_code=False, log_message=False)
username = ret.stdout.rstrip("\n")
run_cmd(f'git config user.name "{username}"', collections_path())
ret = run_cmd('git config user.email', collections_path(),
log_cmd=False, log_code=False, log_message=False)
if len(ret.stdout) == 0:
ret = run_cmd('hostname', collections_path(),
log_cmd=False, log_code=False, log_message=False)
hostname = ret.stdout.rstrip("\n")
run_cmd(f'git config user.email "{hostname}"', collections_path())
for dir in [
collections_path(),
sources_path(),
download_logs_path(),
outputs_path(),
]:
makedirs(dir, exist_ok=True)
+1
View File
@@ -0,0 +1 @@
export const env={}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
var o=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function l(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}export{o as c,l as g};
@@ -0,0 +1 @@
function e(n){return n?.length!==void 0?n:Array.from(n)}export{e};
@@ -0,0 +1 @@
const e={outputs:"Outputs",saves:"Saves",sources:"Sources",models:"Models"},t={emptyList:"No data",loading:"Loading...",rootDir:"Root",btn:{load:"Load",save:"Save",delete:"Delete",add:"Add",refresh:"Refresh"}},o={searchInput:{placeholder:"Filter by filename or memos"},btn:{save:"Save",sync:"Sync",syncing:"Syncing"},syncInput:{placeholder:"Git URL likes 'git@github.com:tzwm/comfyui-workflows.git'"},collection:{memoPlaceholder:"write some memos..."},toast:{synced:"Synced",syncFailed:"Failed to sync. Please check the ComfyUI server.",configUpdated:"Updated config",configUpdatedFailed:"Failed to update config. Please check the ComfyUI server.",deleteConfirm:"You want to delete this file: ",deleteSuccess:"Deleted the file: ",deleteFailed:"Failed to delete the file. Please check the ComfyUI server.",updated:"Updated",updatedFailed:"Failed to update. Please check the ComfyUI server.","Invalid filename":"Invalid filename"}},s={searchInput:{placeholder:"Filter by filename"},"Added to Saves":"Added to Saves","Failed to add to Saves":"Failed to add to Saves. Please check the ComfyUI server.","You want to delete this file?":"You want to delete this file?","Deleted the file":"Deleted the file ","Failed to delete the file":"Failed to delete the file. Please check the ComfyUI server."},d={"Auto Refresh":"Auto Refresh"},l={"You want to delete this source":"You want to delete this source? ","Deleted this source":"Deleted this source.","Failed to delete this source":"Failed to delete this source.","Add new source":"Add new source",Subscribe:"Subscribe","Subscribing...":"Subscribing..."},a={navbar:e,common:t,collectionsTab:o,filesList:s,downloadHistory:d,sourcesTab:l};export{o as collectionsTab,t as common,a as default,d as downloadHistory,s as filesList,e as navbar,l as sourcesTab};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{n as f,l as w,r as m,s as q,k as x}from"./scheduler.DD_VFgMU.js";const a=[];function z(e,i){return{subscribe:A(e,i).subscribe}}function A(e,i=f){let r;const n=new Set;function u(t){if(q(e,t)&&(e=t,r)){const o=!a.length;for(const s of n)s[1](),a.push(s,e);if(o){for(let s=0;s<a.length;s+=2)a[s][0](a[s+1]);a.length=0}}}function l(t){u(t(e))}function b(t,o=f){const s=[t,o];return n.add(s),n.size===1&&(r=i(u,l)||f),t(e),()=>{n.delete(s),n.size===0&&r&&(r(),r=null)}}return{set:u,update:l,subscribe:b}}function B(e,i,r){const n=!Array.isArray(e),u=n?[e]:e;if(!u.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const l=i.length<2;return z(r,(b,t)=>{let o=!1;const s=[];let d=0,p=f;const h=()=>{if(d)return;p();const c=i(n?s[0]:s,b,t);l?b(c):p=x(c)?c:f},y=u.map((c,g)=>w(c,_=>{s[g]=_,d&=~(1<<g),o&&h()},()=>{d|=1<<g}));return o=!0,h(),function(){m(y),p(),o=!1}})}export{B as d,A as w};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
const v="modulepreload",y=function(a,i){return new URL(a,i).href},d={},p=function(i,l,u){let f=Promise.resolve();if(l&&l.length>0){const r=document.getElementsByTagName("link"),n=document.querySelector("meta[property=csp-nonce]"),h=n?.nonce||n?.getAttribute("nonce");f=Promise.all(l.map(e=>{if(e=y(e,u),e in d)return;d[e]=!0;const s=e.endsWith(".css"),m=s?'[rel="stylesheet"]':"";if(!!u)for(let o=r.length-1;o>=0;o--){const c=r[o];if(c.href===e&&(!s||c.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${e}"]${m}`))return;const t=document.createElement("link");if(t.rel=s?"stylesheet":v,s||(t.as="script",t.crossOrigin=""),t.href=e,h&&t.setAttribute("nonce",h),document.head.appendChild(t),s)return new Promise((o,c)=>{t.addEventListener("load",o),t.addEventListener("error",()=>c(new Error(`Unable to preload CSS for ${e}`)))})}))}return f.then(()=>i()).catch(r=>{const n=new Event("vite:preloadError",{cancelable:!0});if(n.payload=r,window.dispatchEvent(n),!n.defaultPrevented)throw r})};export{p as _};
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
function x(){}function w(t,n){for(const e in n)t[e]=n[e];return t}function j(t){return t()}function F(){return Object.create(null)}function E(t){t.forEach(j)}function P(t){return typeof t=="function"}function S(t,n){return t!=t?n==n:t!==n||t&&typeof t=="object"||typeof t=="function"}let i;function U(t,n){return t===n?!0:(i||(i=document.createElement("a")),i.href=n,t===i.href)}function A(t){return Object.keys(t).length===0}function q(t,...n){if(t==null){for(const r of n)r(void 0);return x}const e=t.subscribe(...n);return e.unsubscribe?()=>e.unsubscribe():e}function B(t,n,e){t.$$.on_destroy.push(q(n,e))}function C(t,n,e,r){if(t){const o=m(t,n,e,r);return t[0](o)}}function m(t,n,e,r){return t[1]&&r?w(e.ctx.slice(),t[1](r(n))):e.ctx}function D(t,n,e,r){if(t[2]&&r){const o=t[2](r(e));if(n.dirty===void 0)return o;if(typeof o=="object"){const l=[],_=Math.max(n.dirty.length,o.length);for(let s=0;s<_;s+=1)l[s]=n.dirty[s]|o[s];return l}return n.dirty|o}return n.dirty}function G(t,n,e,r,o,l){if(o){const _=m(n,e,r,l);t.p(_,o)}}function H(t){if(t.ctx.length>32){const n=[],e=t.ctx.length/32;for(let r=0;r<e;r++)n[r]=-1;return n}return-1}let f;function h(t){f=t}function y(){if(!f)throw new Error("Function called outside component initialization");return f}function I(t){y().$$.on_mount.push(t)}function J(t){y().$$.after_update.push(t)}const a=[],g=[];let u=[];const p=[],k=Promise.resolve();let b=!1;function v(){b||(b=!0,k.then(M))}function K(){return v(),k}function O(t){u.push(t)}function L(t){p.push(t)}const d=new Set;let c=0;function M(){if(c!==0)return;const t=f;do{try{for(;c<a.length;){const n=a[c];c++,h(n),z(n.$$)}}catch(n){throw a.length=0,c=0,n}for(h(null),a.length=0,c=0;g.length;)g.pop()();for(let n=0;n<u.length;n+=1){const e=u[n];d.has(e)||(d.add(e),e())}u.length=0}while(a.length);for(;p.length;)p.pop()();b=!1,d.clear(),h(t)}function z(t){if(t.fragment!==null){t.update(),E(t.before_update);const n=t.dirty;t.dirty=[-1],t.fragment&&t.fragment.p(t.ctx,n),t.after_update.forEach(O)}}function N(t){const n=[],e=[];u.forEach(r=>t.indexOf(r)===-1?n.push(r):e.push(r)),e.forEach(r=>r()),u=n}export{J as a,g as b,C as c,D as d,B as e,U as f,H as g,L as h,F as i,M as j,P as k,A as l,O as m,x as n,I as o,N as p,f as q,E as r,S as s,K as t,G as u,h as v,j as w,a as x,v as y};
@@ -0,0 +1 @@
function x(){}function w(t,n){for(const e in n)t[e]=n[e];return t}function j(t){return t()}function A(){return Object.create(null)}function E(t){t.forEach(j)}function D(t){return typeof t=="function"}function F(t,n){return t!=t?n==n:t!==n||t&&typeof t=="object"||typeof t=="function"}let i;function P(t,n){return t===n?!0:(i||(i=document.createElement("a")),i.href=n,t===i.href)}function S(t){return Object.keys(t).length===0}function q(t,...n){if(t==null){for(const r of n)r(void 0);return x}const e=t.subscribe(...n);return e.unsubscribe?()=>e.unsubscribe():e}function U(t,n,e){t.$$.on_destroy.push(q(n,e))}function B(t,n,e,r){if(t){const o=m(t,n,e,r);return t[0](o)}}function m(t,n,e,r){return t[1]&&r?w(e.ctx.slice(),t[1](r(n))):e.ctx}function C(t,n,e,r){if(t[2]&&r){const o=t[2](r(e));if(n.dirty===void 0)return o;if(typeof o=="object"){const l=[],_=Math.max(n.dirty.length,o.length);for(let s=0;s<_;s+=1)l[s]=n.dirty[s]|o[s];return l}return n.dirty|o}return n.dirty}function G(t,n,e,r,o,l){if(o){const _=m(n,e,r,l);t.p(_,o)}}function H(t){if(t.ctx.length>32){const n=[],e=t.ctx.length/32;for(let r=0;r<e;r++)n[r]=-1;return n}return-1}let f;function h(t){f=t}function g(){if(!f)throw new Error("Function called outside component initialization");return f}function I(t){g().$$.on_mount.push(t)}function J(t){g().$$.after_update.push(t)}function K(t){g().$$.on_destroy.push(t)}const a=[],y=[];let u=[];const p=[],k=Promise.resolve();let b=!1;function v(){b||(b=!0,k.then(z))}function L(){return v(),k}function O(t){u.push(t)}function N(t){p.push(t)}const d=new Set;let c=0;function z(){if(c!==0)return;const t=f;do{try{for(;c<a.length;){const n=a[c];c++,h(n),M(n.$$)}}catch(n){throw a.length=0,c=0,n}for(h(null),a.length=0,c=0;y.length;)y.pop()();for(let n=0;n<u.length;n+=1){const e=u[n];d.has(e)||(d.add(e),e())}u.length=0}while(a.length);for(;p.length;)p.pop()();b=!1,d.clear(),h(t)}function M(t){if(t.fragment!==null){t.update(),E(t.before_update);const n=t.dirty;t.dirty=[-1],t.fragment&&t.fragment.p(t.ctx,n),t.after_update.forEach(O)}}function Q(t){const n=[],e=[];u.forEach(r=>t.indexOf(r)===-1?n.push(r):e.push(r)),e.forEach(r=>r()),u=n}export{v as A,J as a,y as b,B as c,C as d,U as e,P as f,H as g,N as h,K as i,O as j,D as k,q as l,A as m,x as n,I as o,z as p,S as q,E as r,F as s,L as t,G as u,Q as v,f as w,h as x,j as y,a as z};
@@ -1 +0,0 @@
import{n as d,s as E}from"./scheduler.89bf212d.js";const u=[];function p(e,t=d){let n;const o=new Set;function r(s){if(E(e,s)&&(e=s,n)){const c=!u.length;for(const l of o)l[1](),u.push(l,e);if(c){for(let l=0;l<u.length;l+=2)u[l][0](u[l+1]);u.length=0}}}function i(s){r(s(e))}function a(s,c=d){const l=[s,c];return o.add(l),o.size===1&&(n=t(r,i)||d),s(e),()=>{o.delete(l),o.size===0&&n&&(n(),n=null)}}return{set:r,update:i,subscribe:a}}var g;const w=((g=globalThis.__sveltekit_1n5kjen)==null?void 0:g.base)??"";var k;const A=((k=globalThis.__sveltekit_1n5kjen)==null?void 0:k.assets)??w,R="1703046965654",I="sveltekit:snapshot",x="sveltekit:scroll",O="sveltekit:index",_={tap:1,hover:2,viewport:3,eager:4,off:-1},v=location.origin;function U(e){let t=e.baseURI;if(!t){const n=e.getElementsByTagName("base");t=n.length?n[0].href:e.URL}return t}function L(){return{x:pageXOffset,y:pageYOffset}}function f(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const b={..._,"":_.hover};function m(e){let t=e.assignedSlot??e.parentNode;return(t==null?void 0:t.nodeType)===11&&(t=t.host),t}function N(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=m(e)}}function j(e,t){let n;try{n=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI)}catch{}const o=e instanceof SVGAElement?e.target.baseVal:e.target,r=!n||!!o||T(n,t)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),i=(n==null?void 0:n.origin)===v&&e.hasAttribute("download");return{url:n,external:r,target:o,download:i}}function P(e){let t=null,n=null,o=null,r=null,i=null,a=null,s=e;for(;s&&s!==document.documentElement;)o===null&&(o=f(s,"preload-code")),r===null&&(r=f(s,"preload-data")),t===null&&(t=f(s,"keepfocus")),n===null&&(n=f(s,"noscroll")),i===null&&(i=f(s,"reload")),a===null&&(a=f(s,"replacestate")),s=m(s);function c(l){switch(l){case"":case"true":return!0;case"off":case"false":return!1;default:return null}}return{preload_code:b[o??"off"],preload_data:b[r??"off"],keep_focus:c(t),noscroll:c(n),reload:c(i),replace_state:c(a)}}function h(e){const t=p(e);let n=!0;function o(){n=!0,t.update(a=>a)}function r(a){n=!1,t.set(a)}function i(a){let s;return t.subscribe(c=>{(s===void 0||n&&c!==s)&&a(s=c)})}return{notify:o,set:r,subscribe:i}}function S(){const{set:e,subscribe:t}=p(!1);let n;async function o(){clearTimeout(n);try{const r=await fetch(`${A}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const a=(await r.json()).version!==R;return a&&(e(!0),clearTimeout(n)),a}catch{return!1}}return{subscribe:t,check:o}}function T(e,t){return e.origin!==v||!e.pathname.startsWith(t)}function V(e){e.client}const Y={url:h({}),page:h({}),navigating:p(null),updated:S()};export{O as I,_ as P,x as S,I as a,j as b,P as c,Y as d,w as e,N as f,U as g,V as h,T as i,v as o,L as s};
@@ -0,0 +1 @@
import{w as u}from"./index.BlOrB6Vx.js";const b=globalThis.__sveltekit_sim1g4?.base??"",v=globalThis.__sveltekit_sim1g4?.assets??b,k="1714203421307",R="sveltekit:snapshot",T="sveltekit:scroll",w="sveltekit:states",I="sveltekit:pageurl",S="sveltekit:history",y="sveltekit:navigation",f={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},_=location.origin;function N(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){const n=document.getElementsByTagName("base");t=n.length?n[0].href:document.URL}return new URL(e,t)}function U(){return{x:pageXOffset,y:pageYOffset}}function c(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const d={...f,"":f.hover};function g(e){let t=e.assignedSlot??e.parentNode;return t?.nodeType===11&&(t=t.host),t}function L(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=g(e)}}function O(e,t){let n;try{n=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI)}catch{}const a=e instanceof SVGAElement?e.target.baseVal:e.target,r=!n||!!a||E(n,t)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),l=n?.origin===_&&e.hasAttribute("download");return{url:n,external:r,target:a,download:l}}function Y(e){let t=null,n=null,a=null,r=null,l=null,o=null,s=e;for(;s&&s!==document.documentElement;)a===null&&(a=c(s,"preload-code")),r===null&&(r=c(s,"preload-data")),t===null&&(t=c(s,"keepfocus")),n===null&&(n=c(s,"noscroll")),l===null&&(l=c(s,"reload")),o===null&&(o=c(s,"replacestate")),s=g(s);function i(h){switch(h){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:d[a??"off"],preload_data:d[r??"off"],keepfocus:i(t),noscroll:i(n),reload:i(l),replace_state:i(o)}}function p(e){const t=u(e);let n=!0;function a(){n=!0,t.update(o=>o)}function r(o){n=!1,t.set(o)}function l(o){let s;return t.subscribe(i=>{(s===void 0||n&&i!==s)&&o(s=i)})}return{notify:a,set:r,subscribe:l}}function m(){const{set:e,subscribe:t}=u(!1);let n;async function a(){clearTimeout(n);try{const r=await fetch(`${v}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const o=(await r.json()).version!==k;return o&&(e(!0),clearTimeout(n)),o}catch{return!1}}return{subscribe:t,check:a}}function E(e,t){return e.origin!==_||!e.pathname.startsWith(t)}function x(e){e.client}const P={url:p({}),page:p({}),navigating:u(null),updated:m()};export{S as H,y as N,I as P,T as S,w as a,R as b,Y as c,P as d,b as e,L as f,O as g,f as h,E as i,x as j,_ as o,N as r,U as s};
@@ -0,0 +1 @@
const e={outputs:"输出",saves:"收藏",sources:"订阅",models:"模型"},t={emptyList:"暂无数据",loading:"加载中...",rootDir:"根目录",btn:{load:"加载",save:"收藏",delete:"删除",add:"新增",refresh:"刷新"}},o={searchInput:{placeholder:"根据文件名和备忘搜索"},btn:{save:"保存",sync:"同步",syncing:"同步中"},syncInput:{placeholder:"Git 地址,比如 git@github.com:tzwm/comfyui-workflows.git"},collection:{memoPlaceholder:"写个备忘,方便搜索"},toast:{synced:"同步完成",syncFailed:"同步失败。请在控制台查看详情。",configUpdated:"保存成功",configUpdatedFailed:"保存失败。请在控制台查看详情。",deleteConfirm:"确认删除这个文件:",deleteSuccess:"删除成功:",deleteFailed:"删除失败。请在控制台查看详情。",updated:"更新成功",UpdatedFailed:"更新失败。请在控制台查看详情。","Invalid filename":"不是个有效文件名"}},s={searchInput:{placeholder:"根据文件名过滤"},"Added to Saves":"收藏成功","Failed to add to Saves":"收藏失败。请在控制台查看详情。","You want to delete this file?":"确认删除这个文件?","Deleted the file":"删除成功:","Failed to delete the file":"删除失败。请在控制台查看详情。"},d={"Auto Refresh":"自动刷新"},l={"You want to delete this source":"确认删除这个订阅吗?","Deleted this source":"删除成功","Failed to delete this source":"删除失败。请在控制台查看详情。","Add new source":"订阅新的源",Subscribe:"订阅","Subscribing...":"订阅中"},a={navbar:e,common:t,collectionsTab:o,filesList:s,downloadHistory:d,sourcesTab:l};export{o as collectionsTab,t as common,a as default,d as downloadHistory,s as filesList,e as navbar,l as sourcesTab};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{s as r,c as l,u as i,g as u,d as f}from"../chunks/scheduler.89bf212d.js";import{S as c,i as _,d as p,t as d}from"../chunks/index.6b8f49fb.js";const m=!0;async function g({url:o}){let s="";return typeof process>"u"&&(s=o.searchParams.get("comfyUrl")||""),{comfyUrl:s}}const h=Object.freeze(Object.defineProperty({__proto__:null,load:g,prerender:m},Symbol.toStringTag,{value:"Module"}));function y(o){let s;const a=o[1].default,t=l(a,o,o[0],null);return{c(){t&&t.c()},l(e){t&&t.l(e)},m(e,n){t&&t.m(e,n),s=!0},p(e,[n]){t&&t.p&&(!s||n&1)&&i(t,a,e,e[0],s?f(a,e[0],n,null):u(e[0]),null)},i(e){s||(p(t,e),s=!0)},o(e){d(t,e),s=!1},d(e){t&&t.d(e)}}}function $(o,s,a){let{$$slots:t={},$$scope:e}=s;return o.$$set=n=>{"$$scope"in n&&a(0,e=n.$$scope)},[e,t]}class v extends c{constructor(s){super(),_(this,s,$,y,r,{})}}export{v as component,h as universal};
@@ -0,0 +1 @@
import{_ as c}from"../chunks/preload-helper.Dch09mLN.js";import{r as u,g as n,i as _,$ as f,w as m}from"../chunks/runtime.CA0Siqq1.js";import{s as p,c as g,u as d,g as $,d as L}from"../chunks/scheduler.DD_VFgMU.js";import{S as y,i as S,d as v,t as b}from"../chunks/index.m7aY60kV.js";u("en-US",()=>c(()=>import("../chunks/en-US.3ZgkfkIk.js"),[],import.meta.url));u("zh-CN",()=>c(()=>import("../chunks/zh-CN.BEOSgdra.js"),[],import.meta.url));const r="lang",h="en-US";let i=window.navigator.language;n()&&(i=n());localStorage.getItem(r)&&(i=localStorage.getItem(r));_({fallbackLocale:h,initialLocale:i,handleMissingMessage:o=>o.id.split(".").pop()});f.subscribe(o=>{localStorage.setItem(r,o)});const w=!0;async function I({url:o}){await m();let a="";return typeof process>"u"&&(a=o.searchParams.get("comfyUrl")||""),{comfyUrl:a}}const j=Object.freeze(Object.defineProperty({__proto__:null,load:I,prerender:w},Symbol.toStringTag,{value:"Module"}));function P(o){let a;const s=o[1].default,t=g(s,o,o[0],null);return{c(){t&&t.c()},l(e){t&&t.l(e)},m(e,l){t&&t.m(e,l),a=!0},p(e,[l]){t&&t.p&&(!a||l&1)&&d(t,s,e,e[0],a?L(s,e[0],l,null):$(e[0]),null)},i(e){a||(v(t,e),a=!0)},o(e){b(t,e),a=!1},d(e){t&&t.d(e)}}}function E(o,a,s){let{$$slots:t={},$$scope:e}=a;return o.$$set=l=>{"$$scope"in l&&s(0,e=l.$$scope)},[e,t]}class z extends y{constructor(a){super(),S(this,a,E,P,p,{})}}export{z as component,j as universal};
@@ -0,0 +1 @@
import{s as E,n as b,e as x}from"../chunks/scheduler.DD_VFgMU.js";import{S,i as j,g as _,m as f,s as q,h as d,j as g,n as h,f as p,c as y,a as l,x as v,o as $}from"../chunks/index.m7aY60kV.js";import{d as C}from"../chunks/singletons.Bc_UULDX.js";const H=()=>{const s=C;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},P={subscribe(s){return H().page.subscribe(s)}};function k(s){let t,r=s[0].status+"",o,n,i,c=s[0].error?.message+"",u;return{c(){t=_("h1"),o=f(r),n=q(),i=_("p"),u=f(c)},l(e){t=d(e,"H1",{});var a=g(t);o=h(a,r),a.forEach(p),n=y(e),i=d(e,"P",{});var m=g(i);u=h(m,c),m.forEach(p)},m(e,a){l(e,t,a),v(t,o),l(e,n,a),l(e,i,a),v(i,u)},p(e,[a]){a&1&&r!==(r=e[0].status+"")&&$(o,r),a&1&&c!==(c=e[0].error?.message+"")&&$(u,c)},i:b,o:b,d(e){e&&(p(t),p(n),p(i))}}}function w(s,t,r){let o;return x(s,P,n=>r(0,o=n)),[o]}let D=class extends S{constructor(t){super(),j(this,t,w,k,E,{})}};export{D as component};
@@ -1 +0,0 @@
import{s as x,n as _,e as S}from"../chunks/scheduler.89bf212d.js";import{S as j,i as q,g as f,m as d,s as y,h as g,j as h,n as v,f as u,c as C,a as m,x as $,o as E}from"../chunks/index.6b8f49fb.js";import{d as H}from"../chunks/singletons.21e9392d.js";const P=()=>{const s=H;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},k={subscribe(s){return P().page.subscribe(s)}};function w(s){var b;let t,r=s[0].status+"",o,n,i,c=((b=s[0].error)==null?void 0:b.message)+"",l;return{c(){t=f("h1"),o=d(r),n=y(),i=f("p"),l=d(c)},l(e){t=g(e,"H1",{});var a=h(t);o=v(a,r),a.forEach(u),n=C(e),i=g(e,"P",{});var p=h(i);l=v(p,c),p.forEach(u)},m(e,a){m(e,t,a),$(t,o),m(e,n,a),m(e,i,a),$(i,l)},p(e,[a]){var p;a&1&&r!==(r=e[0].status+"")&&E(o,r),a&1&&c!==(c=((p=e[0].error)==null?void 0:p.message)+"")&&E(l,c)},i:_,o:_,d(e){e&&(u(t),u(n),u(i))}}}function z(s,t,r){let o;return S(s,k,n=>r(0,o=n)),[o]}let F=class extends j{constructor(t){super(),q(this,t,z,w,x,{})}};export{F as component};
@@ -0,0 +1 @@
import{w as r}from"../chunks/runtime.CA0Siqq1.js";import{s as l,c as i,u,g as f,d as c}from"../chunks/scheduler.DD_VFgMU.js";import{S as _,i as p,d as m,t as d}from"../chunks/index.m7aY60kV.js";async function g({url:o}){return await r(),typeof process>"u"?{path:o.searchParams.get("path")}:{}}const v=Object.freeze(Object.defineProperty({__proto__:null,load:g},Symbol.toStringTag,{value:"Module"}));function $(o){let s;const n=o[1].default,e=i(n,o,o[0],null);return{c(){e&&e.c()},l(t){e&&e.l(t)},m(t,a){e&&e.m(t,a),s=!0},p(t,[a]){e&&e.p&&(!s||a&1)&&u(e,n,t,t[0],s?c(n,t[0],a,null):f(t[0]),null)},i(t){s||(m(e,t),s=!0)},o(t){d(e,t),s=!1},d(t){e&&e.d(t)}}}function h(o,s,n){let{$$slots:e={},$$scope:t}=s;return o.$$set=a=>{"$$scope"in a&&n(0,t=a.$$scope)},[t,e]}class w extends _{constructor(s){super(),p(this,s,h,$,l,{})}}export{w as component,v as universal};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":"1703046965654"}
{"version":"1714203421307"}
+19 -17
View File
File diff suppressed because one or more lines are too long
+52
View File
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en" data-theme="forest">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="./_app/immutable/assets/0.yjAznyz0.css" rel="stylesheet">
<link rel="modulepreload" href="./_app/immutable/entry/start.BUrJroT3.js">
<link rel="modulepreload" href="./_app/immutable/chunks/scheduler.DD_VFgMU.js">
<link rel="modulepreload" href="./_app/immutable/chunks/singletons.Bc_UULDX.js">
<link rel="modulepreload" href="./_app/immutable/chunks/index.BlOrB6Vx.js">
<link rel="modulepreload" href="./_app/immutable/entry/app.D5ml1A7d.js">
<link rel="modulepreload" href="./_app/immutable/chunks/preload-helper.Dch09mLN.js">
<link rel="modulepreload" href="./_app/immutable/chunks/index.m7aY60kV.js">
<link rel="modulepreload" href="./_app/immutable/nodes/0.BLschTxR.js">
<link rel="modulepreload" href="./_app/immutable/chunks/runtime.CA0Siqq1.js">
<link rel="modulepreload" href="./_app/immutable/chunks/_commonjsHelpers.Cpj98o6Y.js">
<link rel="modulepreload" href="./_app/immutable/nodes/2.-Y4AVwMq.js">
<link rel="modulepreload" href="./_app/immutable/nodes/4.DteQa3LM.js">
<link rel="modulepreload" href="./_app/immutable/chunks/each.-gASlQSi.js">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents"> Invalid Path
<script>
{
__sveltekit_sim1g4 = {
base: new URL(".", location).pathname.slice(0, -1),
env: null
};
const element = document.currentScript.parentElement;
const data = [null,null,null];
Promise.all([
import("./_app/immutable/entry/start.BUrJroT3.js"),
import("./_app/immutable/entry/app.D5ml1A7d.js")
]).then(([kit, app]) => {
kit.start(app, element, {
node_ids: [0, 2, 4],
data,
form: null,
error: null
});
});
}
</script>
</div>
</body>
</html>
+113 -20
View File
@@ -59,8 +59,11 @@ class BrowserDialog extends ComfyDialog {
height: "100%",
},
}, [
$el("div", {
$: (e) => (this.textElement = e), style: { height: "inherit",
$el("iframe", {
src: browserUrl + "?timestamp=" + Date.now(),
style: {
width: "100%",
height: "100%",
},
}),
...this.createButtons(),
@@ -101,6 +104,16 @@ class BrowserDialog extends ComfyDialog {
closeBtn,
browseBtn,
toggleSidePanelBtn,
/*$el("span", {*/
/*textContent: "Tips: press 'B' to toggle me",*/
/*style: {*/
/*color: "var(--input-text)",*/
/*right: 0,*/
/*position: "absolute",*/
/*lineHeight: "28.5px",*/
/*marginRight: "2px",*/
/*},*/
/*}),*/
]),
];
}
@@ -145,13 +158,18 @@ class BrowserDialog extends ComfyDialog {
this.element.style.display = "none";
}
show(html) {
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
}
show() {
this.element.style.display = "flex";
dispatchEvent(new Event('comfyuiBrowserShow'));
}
toggle() {
const e = this.element;
if (e.style.display === "none") {
this.show();
} else {
this.close();
}
}
}
@@ -189,16 +207,6 @@ function showToast(text, onClick) {
}, 2000);
}
function showBrowserDialog(browserDialog) {
browserDialog.show($el("iframe", {
src: browserUrl + "?timestamp=" + Date.now(),
style: {
width: "100%",
height: "100%",
},
}));
}
app.registerExtension({
name: "ComfyUI.Browser",
init() {
@@ -206,6 +214,23 @@ app.registerExtension({
async setup() {
const browserDialog = new BrowserDialog();
document.addEventListener('keydown', (event) => {
if (event.key === 'b') {
if (event.target.matches('input, textarea')) {
return;
}
browserDialog.toggle();
event.preventDefault();
}
});
// add event listener for ctrl+i
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'i') {
browserDialog.toggle();
event.preventDefault();
}
})
app.ui.menuContainer.appendChild(
$el("div.comfy-list", {
style: {
@@ -224,7 +249,7 @@ app.registerExtension({
//color: "var(--descrip-text) !important",
width: "80%",
},
onclick: () => { showBrowserDialog(browserDialog); },
onclick: () => { browserDialog.show() },
}),
$el("button", {
id: "comfyui-browser-collect-button",
@@ -263,7 +288,7 @@ app.registerExtension({
saveBtn.style = originBtnStyle + "border-color: green;";
showToast(
'Saved. Click me to open.',
() => { showBrowserDialog(browserDialog); },
() => { browserDialog.show() },
);
} else {
saveBtn.style = originBtnStyle + "border-color: red;";
@@ -276,5 +301,73 @@ app.registerExtension({
}),
])
);
try{
// new menu based features
// browser and save to collection button into new style menu
let cbGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
action: () => {
if(browserDialog)
browserDialog.show();
},
tooltip: "Browse and manage your outputs and collections",
content: "📚",
// content: "🪟",
// content: "Browser",
// icon: "table",// cloud, folder, folder-open, table, database, server
// classList: "comfyui-button comfyui-menu-mobile-collapse primary"
classList: "comfyui-button comfyui-menu-mobile-collapse "
}).element,
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
action: (e) => {
const saveBtn = e.target;
const originBtnStyle = saveBtn.style.cssText;
let filename = "workflow.json";
const promptFilename = app.ui.settings.getSettingValue(
"Comfy.PromptFilename",
true,
);
if (promptFilename) {
filename = prompt("Collect workflow as:", filename);
if (!filename) return;
if (!filename.toLowerCase().endsWith(".json")) {
filename += ".json";
}
}
app.graphToPrompt().then(async p => {
const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string
const res = await api.fetchApi("/browser/collections/workflows", {
method: "POST",
body: JSON.stringify({
filename: filename,
content: json,
}),
});
if (res.ok) {
saveBtn.style = originBtnStyle + "border-color: green;";
showToast(
'Saved. Click me to open.',
() => { browserDialog.show() },
);
} else {
saveBtn.style = originBtnStyle + "border-color: red;";
}
setTimeout(() => {
saveBtn.style = originBtnStyle;
}, 1000);
});
},
tooltip: "Save workflow to collections",
content: "💾",
classList: "comfyui-button comfyui-menu-mobile-collapse "
}).element
);
app.menu?.settingsGroup.element.before(cbGroup.element);
}catch(exception){
console.log('ComfyUI-Browser could not load new menu based features.');
}
},
});
+90
View File
@@ -0,0 +1,90 @@
import { app } from "../../../scripts/app.js";
const selectInputsNodeType = 'SelectInputs //Browser';
const SPLITTER = '::';
function getGraphInputs(graph) {
let inputs = [];
graph._nodes?.forEach(n => {
n.widgets?.forEach(w => {
inputs.push([`#${n.id}`, n.title, w.name].join(SPLITTER));
});
});
return inputs;
}
function refreshPreview(node) {
let values = [];
node.widgets.forEach(w => {
if (w.type === 'combo' && w.name.startsWith('input_')) {
const v = w.value.split(SPLITTER);
values.push({
node_id: v[0].substring(1),
node_title: v[1],
widget_name: v[2],
});
}
});
const preview = node.widgets.find(w => w.name === 'preview');
preview.value = JSON.stringify(values);
}
function refreshInputs(node, app) {
const inputs = getGraphInputs(app.graph);
let index = -1;
for (const w of node.widgets) {
index += 1;
if (w.type != 'combo') {
return;
}
w.options.values = inputs;
if (node.widgets_values) {
w.value = node.widgets_values[index]
} else {
w.value = inputs[0];
}
}
const size = node.computeSize();
node.setSize([size[0] * 1.5, size[1]]);
refreshPreview(node);
}
app.registerExtension({
name: "Browser.Nodes.SelectInputs",
nodeCreated(node, app) {
if (node.constructor.type != selectInputsNodeType) {
return;
}
node.widgets.forEach(w => {
if (w.name === 'preview') {
if (w.element) {
w.element.disabled = true;
}
}
if (w.type === 'combo' && w.name.startsWith('input_')) {
const oriCallback = w.callback;
w.callback = () => {
oriCallback?.apply(arguments);
refreshPreview(node)
};
}
});
node.addWidget(
"button", 'Refresh', '',
() => refreshInputs(node, app)
);
refreshInputs(node, app);
},
loadedGraphNode(node, app) {
if (node.type != selectInputsNodeType) {
return;
}
refreshInputs(node, app);
}
});
+31
View File
@@ -0,0 +1,31 @@
import { app } from "../../../scripts/app.js";
const xyzPlotNodeType = 'XyzPlot //Browser';
app.registerExtension({
name: "Browser.Nodes.XyzPlot",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name != xyzPlotNodeType) {
return;
}
const orig = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
const ret = orig?.apply(this, arguments);
const resultPath = message.result_path[0];
if (!resultPath) {
return ret;
}
let button = this.widgets.find(w => w.type === 'button');
const callback = () => { window.open(window.location.origin + resultPath, "_blank"); };
if (!button) {
this.addWidget("button", 'Open the result', '', callback);
} else {
button.callback = callback;
}
return ret;
};
},
});